Previous | Contents | Next

Chapter 4: The API provided by the mid-end

This chapter documents the API provided by the mid-end to be called by the front end. You probably only need to read this if you are a front end implementor, i.e. you are porting Puzzles to a new platform. If you're only interested in writing new puzzles, you can safely skip this chapter.

All the persistent state in the mid-end is encapsulated within a midend structure, to facilitate having multiple mid-ends in any port which supports multiple puzzle windows open simultaneously. Each midend is intended to handle the contents of a single puzzle window.

4.1 midend_new()

midend *midend_new(frontend *fe, const game *ourgame,
                   const drawing_api *drapi, void *drhandle)

Allocates and returns a new mid-end structure.

The fe argument is stored in the mid-end. It will be used when calling back to functions such as activate_timer() (section 4.37), and will be passed on to the back end function colours() (section 2.8.6).

The parameters drapi and drhandle are passed to drawing_new() (section 3.3.1) to construct a drawing object which will be passed to the back end function redraw() (section 2.8.10). Hence, all drawing-related function pointers defined in drapi can expect to be called with drhandle as their first argument.

The ourgame argument points to a container structure describing a game back end. The mid-end thus created will only be capable of handling that one game. (So even in a monolithic front end containing all the games, this imposes the constraint that any individual puzzle window is tied to a single game. Unless, of course, you feel brave enough to change the mid-end for the window without closing the window...)

4.2 midend_free()

void midend_free(midend *me);

Frees a mid-end structure and all its associated data.

4.3 midend_tilesize()

int midend_tilesize(midend *me);

Returns the ‘tilesize’ parameter being used to display the current puzzle (section 2.8.3).

4.4 midend_set_params()

void midend_set_params(midend *me, game_params *params);

Sets the current game parameters for a mid-end. Subsequent games generated by midend_new_game() (section 4.8) will use these parameters until further notice.

The usual way in which the front end will have an actual game_params structure to pass to this function is if it had previously got it from midend_fetch_preset() (section 4.16). Thus, this function is usually called in response to the user making a selection from the presets menu.

4.5 midend_get_params()

game_params *midend_get_params(midend *me);

Returns the current game parameters stored in this mid-end.

The returned value is dynamically allocated, and should be freed when finished with by passing it to the game's own free_params() function (see section 2.3.5).

4.6 midend_size()

void midend_size(midend *me, int *x, int *y, int user_size);

Tells the mid-end to figure out its window size.

On input, *x and *y should contain the maximum or requested size for the window. (Typically this will be the size of the screen that the window has to fit on, or similar.) The mid-end will repeatedly call the back end function compute_size() (section 2.8.4), searching for a tile size that best satisfies the requirements. On exit, *x and *y will contain the size needed for the puzzle window's drawing area. (It is of course up to the front end to adjust this for any additional window furniture such as menu bars and window borders, if necessary. The status bar is also not included in this size.)

Use user_size to indicate whether *x and *y are a requested size, or just a maximum size.

If user_size is set to TRUE, the mid-end will treat the input size as a request, and will pick a tile size which approximates it as closely as possible, going over the game's preferred tile size if necessary to achieve this. The mid-end will also use the resulting tile size as its preferred one until further notice, on the assumption that this size was explicitly requested by the user. Use this option if you want your front end to support dynamic resizing of the puzzle window with automatic scaling of the puzzle to fit.

If user_size is set to FALSE, then the game's tile size will never go over its preferred one, although it may go under in order to fit within the maximum bounds specified by *x and *y. This is the recommended approach when opening a new window at default size: the game will use its preferred size unless it has to use a smaller one to fit on the screen. If the tile size is shrunk for this reason, the change will not persist; if a smaller grid is subsequently chosen, the tile size will recover.

The mid-end will try as hard as it can to return a size which is less than or equal to the input size, in both dimensions. In extreme circumstances it may fail (if even the lowest possible tile size gives window dimensions greater than the input), in which case it will return a size greater than the input size. Front ends should be prepared for this to happen (i.e. don't crash or fail an assertion), but may handle it in any way they see fit: by rejecting the game parameters which caused the problem, by opening a window larger than the screen regardless of inconvenience, by introducing scroll bars on the window, by drawing on a large bitmap and scaling it into a smaller window, or by any other means you can think of. It is likely that when the tile size is that small the game will be unplayable anyway, so don't put too much effort into handling it creatively.

If your platform has no limit on window size (or if you're planning to use scroll bars for large puzzles), you can pass dimensions of INT_MAX as input to this function. You should probably not do that and set the user_size flag, though!

The midend relies on the frontend calling midend_new_game() (section 4.8) before calling midend_size().

4.7 midend_reset_tilesize()

void midend_reset_tilesize(midend *me);

This function resets the midend's preferred tile size to that of the standard puzzle.

As discussed in section 4.6, puzzle resizes are typically 'sticky', in that once the user has dragged the puzzle to a different window size, the resulting tile size will be remembered and used when the puzzle configuration changes. If you don't want that, e.g. if you want to provide a command to explicitly reset the puzzle size back to its default, then you can call this just before calling midend_size() (which, in turn, you would probably call with user_size set to FALSE).

4.8 midend_new_game()

void midend_new_game(midend *me);

Causes the mid-end to begin a new game. Normally the game will be a new randomly generated puzzle. However, if you have previously called midend_game_id() or midend_set_config(), the game generated might be dictated by the results of those functions. (In particular, you must call midend_new_game() after calling either of those functions, or else no immediate effect will be visible.)

You will probably need to call midend_size() after calling this function, because if the game parameters have been changed since the last new game then the window size might need to change. (If you know the parameters haven't changed, you don't need to do this.)

This function will create a new game_drawstate, but does not actually perform a redraw (since you often need to call midend_size() before the redraw can be done). So after calling this function and after calling midend_size(), you should then call midend_redraw(). (It is not necessary to call midend_force_redraw(); that will discard the draw state and create a fresh one, which is unnecessary in this case since there's a fresh one already. It would work, but it's usually excessive.)

4.9 midend_restart_game()

void midend_restart_game(midend *me);

This function causes the current game to be restarted. This is done by placing a new copy of the original game state on the end of the undo list (so that an accidental restart can be undone).

This function automatically causes a redraw, i.e. the front end can expect its drawing API to be called from within a call to this function. Some back ends require that midend_size() (section 4.6) is called before midend_restart_game().

4.10 midend_force_redraw()

void midend_force_redraw(midend *me);

Forces a complete redraw of the puzzle window, by means of discarding the current game_drawstate and creating a new one from scratch before calling the game's redraw() function.

The front end can expect its drawing API to be called from within a call to this function. Some back ends require that midend_size() (section 4.6) is called before midend_force_redraw().

4.11 midend_redraw()

void midend_redraw(midend *me);

Causes a partial redraw of the puzzle window, by means of simply calling the game's redraw() function. (That is, the only things redrawn will be things that have changed since the last redraw.)

The front end can expect its drawing API to be called from within a call to this function. Some back ends require that midend_size() (section 4.6) is called before midend_redraw().

4.12 midend_process_key()

int midend_process_key(midend *me, int x, int y, int button);

The front end calls this function to report a mouse or keyboard event. The parameters x, y and button are almost identical to the ones passed to the back end function interpret_move() (section 2.7.1), except that the front end is not required to provide the guarantees about mouse event ordering. The mid-end will sort out multiple simultaneous button presses and changes of button; the front end's responsibility is simply to pass on the mouse events it receives as accurately as possible.

(Some platforms may need to emulate absent mouse buttons by means of using a modifier key such as Shift with another mouse button. This tends to mean that if Shift is pressed or released in the middle of a mouse drag, the mid-end will suddenly stop receiving, say, LEFT_DRAG events and start receiving RIGHT_DRAGs, with no intervening button release or press events. This too is something which the mid-end will sort out for you; the front end has no obligation to maintain sanity in this area.)

The front end should, however, always eventually send some kind of button release. On some platforms this requires special effort: Windows, for example, requires a call to the system API function SetCapture() in order to ensure that your window receives a mouse-up event even if the pointer has left the window by the time the mouse button is released. On any platform that requires this sort of thing, the front end is responsible for doing it.

Calling this function is very likely to result in calls back to the front end's drawing API and/or activate_timer() (section 4.37).

The return value from midend_process_key() is non-zero, unless the effect of the keypress was to request termination of the program. A front end should shut down the puzzle in response to a zero return.

4.13 midend_colours()

float *midend_colours(midend *me, int *ncolours);

Returns an array of the colours required by the game, in exactly the same format as that returned by the back end function colours() (section 2.8.6). Front ends should call this function rather than calling the back end's version directly, since the mid-end adds standard customisation facilities. (At the time of writing, those customisation facilities are implemented hackily by means of environment variables, but it's not impossible that they may become more full and formal in future.)

4.14 midend_timer()

void midend_timer(midend *me, float tplus);

If the mid-end has called activate_timer() (section 4.37) to request regular callbacks for purposes of animation or timing, this is the function the front end should call on a regular basis. The argument tplus gives the time, in seconds, since the last time either this function was called or activate_timer() was invoked.

One of the major purposes of timing in the mid-end is to perform move animation. Therefore, calling this function is very likely to result in calls back to the front end's drawing API.

4.15 midend_num_presets()

int midend_num_presets(midend *me);

Returns the number of game parameter presets supplied by this game. Front ends should use this function and midend_fetch_preset() to configure their presets menu rather than calling the back end directly, since the mid-end adds standard customisation facilities. (At the time of writing, those customisation facilities are implemented hackily by means of environment variables, but it's not impossible that they may become more full and formal in future.)

4.16 midend_fetch_preset()

void midend_fetch_preset(midend *me, int n,
                         char **name, game_params **params);

Returns one of the preset game parameter structures for the game. On input n must be a non-negative integer and less than the value returned from midend_num_presets(). On output, *name is set to an ASCII string suitable for entering in the game's presets menu, and *params is set to the corresponding game_params structure.

Both of the two output values are dynamically allocated, but they are owned by the mid-end structure: the front end should not ever free them directly, because they will be freed automatically during midend_free().

4.17 midend_which_preset()

int midend_which_preset(midend *me);

Returns the numeric index of the preset game parameter structure which matches the current game parameters, or a negative number if no preset matches. Front ends could use this to maintain a tick beside one of the items in the menu (or tick the ‘Custom’ option if the return value is less than zero).

4.18 midend_wants_statusbar()

int midend_wants_statusbar(midend *me);

This function returns TRUE if the puzzle has a use for a textual status line (to display score, completion status, currently active tiles, time, or anything else).

Front ends should call this function rather than talking directly to the back end.

4.19 midend_get_config()

config_item *midend_get_config(midend *me, int which,
                               char **wintitle);

Returns a dialog box description for user configuration.

On input, which should be set to one of three values, which select which of the various dialog box descriptions is returned:

CFG_SETTINGS
Requests the GUI parameter configuration box generated by the puzzle itself. This should be used when the user selects ‘Custom’ from the game types menu (or equivalent). The mid-end passes this request on to the back end function configure() (section 2.3.8).
CFG_DESC
Requests a box suitable for entering a descriptive game ID (and viewing the existing one). The mid-end generates this dialog box description itself. This should be used when the user selects ‘Specific’ from the game menu (or equivalent).
CFG_SEED
Requests a box suitable for entering a random-seed game ID (and viewing the existing one). The mid-end generates this dialog box description itself. This should be used when the user selects ‘Random Seed’ from the game menu (or equivalent).

The returned value is an array of config_items, exactly as described in section 2.3.8. Another returned value is an ASCII string giving a suitable title for the configuration window, in *wintitle.

Both returned values are dynamically allocated and will need to be freed. The window title can be freed in the obvious way; the config_item array is a slightly complex structure, so a utility function free_cfg() is provided to free it for you. See section 5.2.6.

(Of course, you will probably not want to free the config_item array until the dialog box is dismissed, because before then you will probably need to pass it to midend_set_config.)

4.20 midend_set_config()

char *midend_set_config(midend *me, int which,
                        config_item *cfg);

Passes the mid-end the results of a configuration dialog box. which should have the same value which it had when midend_get_config() was called; cfg should be the array of config_items returned from midend_get_config(), modified to contain the results of the user's editing operations.

This function returns NULL on success, or otherwise (if the configuration data was in some way invalid) an ASCII string containing an error message suitable for showing to the user.

If the function succeeds, it is likely that the game parameters will have been changed and it is certain that a new game will be requested. The front end should therefore call midend_new_game(), and probably also re-think the window size using midend_size() and eventually perform a refresh using midend_redraw().

4.21 midend_game_id()

char *midend_game_id(midend *me, char *id);

Passes the mid-end a string game ID (of any of the valid forms ‘params’, ‘params:description’ or ‘params#seed’) which the mid-end will process and use for the next generated game.

This function returns NULL on success, or otherwise (if the configuration data was in some way invalid) an ASCII string containing an error message (not dynamically allocated) suitable for showing to the user. In the event of an error, the mid-end's internal state will be left exactly as it was before the call.

If the function succeeds, it is likely that the game parameters will have been changed and it is certain that a new game will be requested. The front end should therefore call midend_new_game(), and probably also re-think the window size using midend_size() and eventually case a refresh using midend_redraw().

4.22 midend_get_game_id()

char *midend_get_game_id(midend *me)

Returns a descriptive game ID (i.e. one in the form ‘params:description’) describing the game currently active in the mid-end. The returned string is dynamically allocated.

4.23 midend_get_random_seed()

char *midend_get_random_seed(midend *me)

Returns a random game ID (i.e. one in the form ‘params#seedstring’) describing the game currently active in the mid-end, if there is one. If the game was created by entering a description, no random seed will currently exist and this function will return NULL.

The returned string, if it is non-NULL, is dynamically allocated.

4.24 midend_can_format_as_text_now()

int midend_can_format_as_text_now(midend *me);

Returns TRUE if the game code is capable of formatting puzzles of the currently selected game type as ASCII.

If this returns FALSE, then midend_text_format() (section 4.25) will return NULL.

4.25 midend_text_format()

char *midend_text_format(midend *me);

Formats the current game's current state as ASCII text suitable for copying to the clipboard. The returned string is dynamically allocated.

If the game's can_format_as_text_ever flag is FALSE, or if its can_format_as_text_now() function returns FALSE, then this function will return NULL.

If the returned string contains multiple lines (which is likely), it will use the normal C line ending convention (\n only). On platforms which use a different line ending convention for data in the clipboard, it is the front end's responsibility to perform the conversion.

4.26 midend_solve()

char *midend_solve(midend *me);

Requests the mid-end to perform a Solve operation.

On success, NULL is returned. On failure, an error message (not dynamically allocated) is returned, suitable for showing to the user.

The front end can expect its drawing API and/or activate_timer() to be called from within a call to this function. Some back ends require that midend_size() (section 4.6) is called before midend_solve().

4.27 midend_status()

int midend_status(midend *me);

This function returns +1 if the midend is currently displaying a game in a solved state, -1 if the game is in a permanently lost state, or 0 otherwise. This function just calls the back end's status() function. Front ends may wish to use this as a cue to proactively offer the option of starting a new game.

(See section 2.8.9 for more detail about the back end's status() function and discussion of what should count as which status code.)

4.28 midend_can_undo()

int midend_can_undo(midend *me);

Returns TRUE if the midend is currently in a state where the undo operation is meaningful (i.e. at least one position exists on the undo chain before the present one). Front ends may wish to use this to visually activate and deactivate an undo button.

4.29 midend_can_redo()

int midend_can_redo(midend *me);

Returns TRUE if the midend is currently in a state where the redo operation is meaningful (i.e. at least one position exists on the redo chain after the present one). Front ends may wish to use this to visually activate and deactivate a redo button.

4.30 midend_serialise()

void midend_serialise(midend *me,
                      void (*write)(void *ctx, void *buf, int len),
                      void *wctx);

Calling this function causes the mid-end to convert its entire internal state into a long ASCII text string, and to pass that string (piece by piece) to the supplied write function.

Desktop implementations can use this function to save a game in any state (including half-finished) to a disk file, by supplying a write function which is a wrapper on fwrite() (or local equivalent). Other implementations may find other uses for it, such as compressing the large and sprawling mid-end state into a manageable amount of memory when a palmtop application is suspended so that another one can run; in this case write might want to write to a memory buffer rather than a file. There may be other uses for it as well.

This function will call back to the supplied write function a number of times, with the first parameter (ctx) equal to wctx, and the other two parameters pointing at a piece of the output string.

4.31 midend_deserialise()

char *midend_deserialise(midend *me,
                         int (*read)(void *ctx, void *buf, int len),
                         void *rctx);

This function is the counterpart to midend_serialise(). It calls the supplied read function repeatedly to read a quantity of data, and attempts to interpret that data as a serialised mid-end as output by midend_serialise().

The read function is called with the first parameter (ctx) equal to rctx, and should attempt to read len bytes of data into the buffer pointed to by buf. It should return FALSE on failure or TRUE on success. It should not report success unless it has filled the entire buffer; on platforms which might be reading from a pipe or other blocking data source, read is responsible for looping until the whole buffer has been filled.

If the de-serialisation operation is successful, the mid-end's internal data structures will be replaced by the results of the load, and NULL will be returned. Otherwise, the mid-end's state will be completely unchanged and an error message (typically some variation on ‘save file is corrupt’) will be returned. As usual, the error message string is not dynamically allocated.

If this function succeeds, it is likely that the game parameters will have been changed. The front end should therefore probably re-think the window size using midend_size(), and probably cause a refresh using midend_redraw().

Because each mid-end is tied to a specific game back end, this function will fail if you attempt to read in a save file generated by a different game from the one configured in this mid-end, even if your application is a monolithic one containing all the puzzles. See section 4.32 for a helper function which will allow you to identify a save file before you instantiate your mid-end in the first place.

4.32 identify_game()

char *identify_game(char **name,
                    int (*read)(void *ctx, void *buf, int len),
                    void *rctx);

This function examines a serialised midend stream, of the same kind used by midend_serialise() and midend_deserialise(), and returns the name field of the game back end from which it was saved.

You might want this if your front end was a monolithic one containing all the puzzles, and you wanted to be able to load an arbitrary save file and automatically switch to the right game. Probably your next step would be to iterate through gamelist (section 4.34) looking for a game structure whose name field matched the returned string, and give an error if you didn't find one.

On success, the return value of this function is NULL, and the game name string is written into *name. The caller should free that string after using it.

On failure, *name is NULL, and the return value is an error message (which does not need freeing at all).

(This isn't strictly speaking a midend function, since it doesn't accept or return a pointer to a midend. You'd probably call it just before deciding what kind of midend you wanted to instantiate.)

4.33 midend_request_id_changes()

void midend_request_id_changes(midend *me,
                               void (*notify)(void *), void *ctx);

This function is called by the front end to request notification by the mid-end when the current game IDs (either descriptive or random-seed) change. This can occur as a result of keypresses ('n' for New Game, for example) or when a puzzle supersedes its game description (see section 2.11.2). After this function is called, any change of the game ids will cause the mid-end to call notify(ctx) after the change.

This is for use by puzzles which want to present the game description to the user constantly (e.g. as an HTML hyperlink) instead of only showing it when the user explicitly requests it.

This is a function I anticipate few front ends needing to implement, so I make it a callback rather than a static function in order to relieve most front ends of the need to provide an empty implementation.

4.34 Direct reference to the back end structure by the front end

Although most things the front end needs done should be done by calling the mid-end, there are a few situations in which the front end needs to refer directly to the game back end structure.

The most obvious of these is

There are a few other back end features which are not wrapped by the mid-end because there didn't seem much point in doing so:

In order to find the game back end structure, the front end does one of two things:

4.35 Mid-end to front-end calls

This section describes the small number of functions which a front end must provide to be called by the mid-end or other standard utility modules.

4.36 get_random_seed()

void get_random_seed(void **randseed, int *randseedsize);

This function is called by a new mid-end, and also occasionally by game back ends. Its job is to return a piece of data suitable for using as a seed for initialisation of a new random_state.

On exit, *randseed should be set to point at a newly allocated piece of memory containing some seed data, and *randseedsize should be set to the length of that data.

A simple and entirely adequate implementation is to return a piece of data containing the current system time at the highest conveniently available resolution.

4.37 activate_timer()

void activate_timer(frontend *fe);

This is called by the mid-end to request that the front end begin calling it back at regular intervals.

The timeout interval is left up to the front end; the finer it is, the smoother move animations will be, but the more CPU time will be used. Current front ends use values around 20ms (i.e. 50Hz).

After this function is called, the mid-end will expect to receive calls to midend_timer() on a regular basis.

4.38 deactivate_timer()

void deactivate_timer(frontend *fe);

This is called by the mid-end to request that the front end stop calling midend_timer().

4.39 fatal()

void fatal(char *fmt, ...);

This is called by some utility functions if they encounter a genuinely fatal error such as running out of memory. It is a variadic function in the style of printf(), and is expected to show the formatted error message to the user any way it can and then terminate the application. It must not return.

4.40 frontend_default_colour()

void frontend_default_colour(frontend *fe, float *output);

This function expects to be passed a pointer to an array of three floats. It returns the platform's local preferred background colour in those three floats, as red, green and blue values (in that order) ranging from 0.0 to 1.0.

This function should only ever be called by the back end function colours() (section 2.8.6). (Thus, it isn't a midend-to-frontend function as such, but there didn't seem to be anywhere else particularly good to put it. Sorry.)