Using the Banner API
by Eric Eve
Note: the customBanner.t source file described in this article can be found in the extensions folder under your adv3Lite folder.
Important Note: the Banner API cannot be used with games compiled for the Web UI; if you want to use the Banner API you must compile your game for use with a traditional HTML-TADS interpreter.
Introduction
The Banner API in TADS 3 is the feature that allows the interpreter screen to be divided into a number of different windows during game execution. Normally a TADS 3 game uses two windows, the main window in which commands are entered and the game's responses are displayed, and the status line banner at the top, which typically shows the current room, the current turn count and score, and a list of currently available exits. This is more often than not quite sufficient for most text adventures/masterly works of Interactive Fiction, but occasionally an author may want something more elaborate.
Suppose, for example, we were writing a game in which we wanted to display a picture of every room the player character visits, along with pictures of some of the objects he or she examines. Rather than having our pictures incorporated into the text shown in the main window, where they would soon scroll out of sight, we might want them displayed in a separate window where they remain in view until replaced by another picture. With this arrangement we might also want a separate caption window to display some text describing what the current picture is a picture of. To implement this scheme, we might want to divide the available screen space up something like this:
Status | |
Picture | Caption |
Main |
The TADS 3 library already takes care of the status line for us, but we would need to implement the picture banner window and the caption banner window. Not only would we need to arrange these banners correctly on screen and update them with the right contents, but we should also need to ensure that our code took care of issues such as the following:
- Ensuring that the banner window layout is still correct after an UNDO, RESTART or RESTORE
- Ensuring that the contents of each banner window is shown correctly after an UNDO, RESTART or RESTORE
- Catering for the possibility that our game might be run on an interpreter which lacks the capability to display graphics
The basic banner window model is described in the System Manual article on The Banner Window Display Model. If you are not already reasonably familiar with this, you might like to read it now, since the present article assumes some understanding of the screen layout model it describes. This might also be a good point to take a look at the the System Manual's description of the low-level banner API functions in the section on the tads-io Function Set. Although these low-level functions are not the best way to manipulate banners in a TADS 3 game, some understanding of what they do may nevertheless be helpful for seeing the range of banner functions available. In particular it is useful to refer to the bannerCreate() function, and in particular its argument list, since even if we end up using alternatives to bannerCreate(), our alternatives will still use much the same list of arguments.
Using the tadsio Banner API Functions
Although you would probably not want to use the low-level Banner API functions in a real game, it's worth taking a quick look at how one might use them to start implementing the banner layout shown above, since this at least introduces some important principles.
For ease of reference, let's repeat that layout diagram here:
Status | |
Picture | Caption |
Main |
The main thing to notice about this layout is that the space between the status line banner is divided into two windows, one for the picture, and the other for the caption. To create such a layout we first need to create a horizontal division within the main window, with the upper area being used for our two new banners. We then need to divide that new area vertically into left-hand and right-hand regions. The way the banner display model works, we do that by creating one new child window from our new window, and assigning it to (say) half the available space.
It doesn't much matter whether we create the picture window or the caption window first, but there is perhaps some logic in starting with the picture window (since the caption window is in some sense subservient to it in function, so that if we decided to remove the picture window at some point, we wouldn't want to retain the caption window).
We could create the picture window using the low level function bannerCreate():
picWin = bannerCreate(nil, BannerAfter, statuslineBanner.handle_, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder);
The first argument here is the parent window of the new window we're creating. In this case the parent is the main window so we use the value nil. The second and third arguments define where in the parent window we want our new banner created; in this case, we want it after the status line banner. There's a slight complication here, in that bannerCreate expects us to specify not the sibling banner object here but the sibling banner's handle, which is simply an integer that the system uses internally to keep track of which banner is which.
The fifth argument, BannerAlignTop, stipulates that we want our new banner to appear at the top of the available space we're carving out of the main window, but after the statusline banner. The sixth argument is the size (in this case depth) of our new banner window (at this stage its width is simply the full screen width), the seventh argument, BannerSizeAbsolute stipulates that this is an absolute size, the unit being lines of text in the default font of the window. Finally we give our new banner a border with the last argument.
At this stage, our window layout will look something this this:
Status | |
Picture | |
Main |
The next job is to create the Caption banner window. We want this to occupy the right hand half of the space currently occupied by the picture banner, so we make it a child of picWin, align it to the right, and assign it 50% of the space:
captionWin = bannerCreate(picWin, BannerFirst, nil, BannerTypeText, BannerAlignRight, 50, BannerSizePercent, BannerStyleBorder);
This time the second and third arguments are BannerFirst and nil respectively, since we want this new banner to be the first (in this case rightmost) child of its parent, which means in turn that we don't need to name a sibling for it to come before or after (in any case, it has no siblings).
At this point we need to stop and ask the question, what kind of values are pcWin and captionWin? They are, in fact, banner handles, which as we've already seen, are simply integers. This being so we need some place we can store a reference to them so we can refer to them later; that place will almost certainly need to be a pair of object properties, which means in turn that we need to create an object in which to store them. While we're at it, we could make it an InitObject so that it can automatically create our banner layout at startup:
banners:InitObject picWin = nil captionWin = nil initLayout() { picWin = bannerCreate(nil, BannerAfter, statuslineBanner.handle_, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder); captionWin = bannerCreate(picWin, BannerFirst, nil , BannerTypeText, BannerAlignRight, 50, BannerSizePercent, BannerStyleBorder); } execute() { initLayout(); } ;
Now, when we want to display anything in our new banner windows, we can simply use the bannerSay() method; e.g. to display a picture of a banner with a caption:
bannerSay(banners.picWin, '<img src="pics/sheldonian.jpg">'); bannerSay(banners.captionWin, 'A view of Oxford\'s famous Sheldonian Theatre, designed by Sir Christopher Wren');
So far, this works reasonably well, and it serves to illustrate some of the main principles of working with banners in TADS 3, but there a great many issues we have not dealt with. For example, if the player issues an UNDO command just after we have updated our banner windows with new content, the new content will remain on screen, and won't automatically roll back to what was shown on the previous turn. There's also nothing to handle what should happen when a game is restored, or when it's played on an interpreter that can't handle graphics. It would no doubt be possible to add further sophisications to the basic code shown above to handle all these situations, but this may not be the best method to proceed, so from now on we'll look at an alternative: controlling banner windows through the BannerWindow class.
Using the BannerWindow class
The main difference from the previous approach is that instead of calling low-level tadsio methods, we define objects to represent the various banner windows we want to create, and then use their methods to manipulate them. In essence we create:
picWindow: BannerWindow ; captionWindow: BannerWindow ;
Of course the above definitions will not actually do anything, since they don't define where our two new banners are to appear, or do anything to make them appear. But they do illustrate one point: whereas before picWindow and captionWindow were properties of a separate object we had to create for the purpose, now they are objects in their own right. The identifiers picWindow and captionWindow thus refer to objects and not to integers representing handles. The handle values we used before would now be given by picWindow.handle_ and captionWindow.handle_, but in fact we probably won't need to refer to these handles any more, since we can now refer to the objects instead. Indeed, if we did refer to the handle_ properties at this point, we'd find they were both nil, since we've done nothing to create the actual banner windows in the screen layout.
To make a BannerWindow object actually do something to the screen layout, we need to do the equivalent of invoking the bannerCreate() function, which is to evoke the BannerWindow's showBanner() method (which in fact does itself call bannerCreate(), after carrying out lot of intermediate background busy-work to help keep track of what's going on). A good place to call this method if we want our banners to be included in the screen layout at startup is their initBannerWindow() method, since this is called during game initialization. Following the same logic we used before, as a first attempt we might try:
picWindow: BannerWindow initBannerWindow() { showBanner(nil, BannerAfter, statuslineBanner, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder); /* * inherited() here simply sets inited_ to true, to show that we have * now initialized this banner. */ inherited(); } ; captionWindow: BannerWindow initBannerWindow() { showBanner(picWindow, BannerFirst, nil , BannerTypeText, BannerAlignRight, 50, BannerSizePercent, BannerStyleBorder); inherited(); } ;
As you may have noticed, the arguments to showBanner() are almost identical to those of bannerCreate(), with one important difference: in showBanner() the first and third arguments (if present), representing the parent and sibling of this window, are now BannerWindow objects, not integers representing handles.
If you actually tried to run the above code, however, there's a good chance you'd encounter a run-time error. The problem is that there's now nothing in our code to control the order in which our banner windows are created (we can't rely on source text order to determine it), with the result that the VM may attempt to initialize captionWindow before picWindow; this will cause an error because you must create the parent banner before any of its children; in fact you must create it before any of the other banners listed among the arguments it uses in showBanner(), whether a parent banner, or a sibling banner we're to be placed before or after.
The trick is to ensure that initBannerWindow() calls the initBannerWindow() methods of any banners that must already exist. So as a second attempt we might try:
picWindow: BannerWindow initBannerWindow() { statusLineBanner.initBannerWindow(); showBanner(nil, BannerAfter, statuslineBanner, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder); inherited(); } ; captionWindow: BannerWindow initBannerWindow() { picWindow.initBannerWindow(); showBanner(picWindow, BannerFirst, nil , BannerTypeText, BannerAlignRight, 50, BannerSizePercent, BannerStyleBorder); inherited(); } ;
Now if the initialization routine happens upon captionWindow before picWindow, captionWindow.initBannerWindow() will invoke picWindow.initBannerWindow() before calling its own showBanner() method, thereby ensuring that the banner created for picWindow exists before we try to give it a child. Unfortunately, this at once leads us to another problem, namely that if captionWindow.initBannerWindow() runs first, picWindow.initBannerWindow() will be called a second time when the initializer reaches picWindow, with the result that we could end up with two picture banners displayed on screen.
To avoid that, we make use of the inited_ property set by the inherited method. If inited_ is true, we know we have already been initialized, so we don't need to be initialized again. We can therefore check the value of inited_ at the start of the initBannerWindow() method:
picWindow: BannerWindow initBannerWindow() { if(inited_) return; statuslineBanner.initBannerWindow(); showBanner(nil, BannerAfter, statuslineBanner, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder); inherited(); } ; captionWindow: BannerWindow initBannerWindow() { if(inited_) return; picWindow.initBannerWindow(); showBanner(picWindow, BannerFirst, nil , BannerTypeText, BannerAlignRight, 50, BannerSizePercent, BannerStyleBorder); inherited(); } ;
At this point, you may be getting the impression that it was more straightforward to use the low level banner API functions, except that we no longer need to create a special object to hold references to the banner handles. But in fact the BannerWindow class, and the classes associated with it in banner.t, are taking care of a lot of the complexity for us. In particular, although they don't address the issue of keeping the banner contents in sync with any UNDO, RESTORE or RESTART operations the player may perform, they do look after the business of ensuring that the banner layout (which is all we have handled so far) is properly maintained through these operations. This may not seem much of an issue in this example, which envisages a banner layout that remains constant throughout the game, but would be more of an issue in a game in which the banner layout changed according to circumstance.
In order to display actual content in these banner windows, we simply need to call their writeToBanner() method; for example:
picWindow.writeToBanner('<img src="pics/sheldonian.jpg">'); captionWindow.writeToBanner('A view of Oxford\'s famous Sheldonian Theatre, designed by Sir Christopher Wren');
This method is perfectly workable if either of the following conditions is true:
- Our game updates the banner contents each turn
- Our game updates the banner contents each time the room description is displayed (e.g. because we want to show a picture of the location as well as describing it)
Because the library automatically performs a look around after a RESTORE, RESTART or UNDO command, issuing any of these commands will accordingly automatically update our banner contents for us if they are updated with each look around.
However, if neither of the above conditions is met, then we still have to find some means of updating our banner contents after a RESTORE or UNDO (though our initialization code will probably take care of RESTART). One way to do this is to define a currentContents property on each of our banners that gets updated with whatever we write to the banners, so that we can arrange for it to be displayed again after a RESTORE or UNDO. That's not too hard to do, but we also have to make sure that we do things in the right order if the layout might change, that if we UNDO or RESTORE from a point in the game where, say, our picture banner isn't displayed to one where it is, we must ensure that we redisplay the picture banner before we try to write to it.
And we still haven't dealt with the issue of the not-HTML interpreter. To be sure, if the interpreter our game runs on can't display graphics, say, our attempts to display graphics on it won't do too much harm, since they'll simply be ignored. The problem is rather that we may have a banner window taking up space on a text-only display while doing nothing useful; if we only created the banner in order to display pictures on it, then there's simply no point in having it created when our game runs on an interpreter that can't display pictures. On the other hand, if test for the interpreter type and then don't create our graphics banner, our code may well run into difficulties when it tries to write to it.
In fact, dealing with the banner content on RESTORE/UNDO problem and the reasonable compatibility with different interpreter types problem can prove more than a little tricky, not least because these two problems can impact on each other. Consider the case when the player of your Multimedia Masterpiece starts on his desk-top Windows PC with a full HTML interpeter. He then saves the game and copies the save file to a portable device which runs a text-only interpreter, so he can carry on playing on the train or bus on the way to work. When the game is restored on the handheld, which can't display graphics, we may decide we don't want the graphics window to appear, even though it was part of the layout saved in the save file. Now, when our intrepid adventurer returns at the end of his long hard day in the office to relax with your Epic Extravaganza once more, having played on a few dozen turns on his portable device, he saves the game in his text-only interpreter, copies the saved file to his desk-top machine, restores the game to his HTML interpreter, and carries on playing. What the restore on the HTML interpreter ought to achieve is to show the banner layout and content appropriate to the stage of the game now reached, even though these weren't being shown on the text-only interpreter in the interim. This is possible to arrange, but it is also quite tricky.
In the next section, we'll look at using a class that takes care of most of these issues for you.
Using the CustomBannerWindow Class
Note: to use the CustomBannerWindow class you'll need to include the cuatomBanner.t extension in your project. This can be found in the extensions folder under you adv3Lite folder.
Using CustomBannerWindow, the banners we defined in the previous section could be defined simply with:
picWindow: CustomBannerWindow bannerArgs = [nil, BannerAfter, statuslineBanner, BannerText, BannerTop, 10, BannerSizeAbsolute, BannerStyleBorder] ; captionWindow: CustomBannerWindow bannerArgs = [picWindow, BannerFirst, nil , BannerText, BannerRight, 50, BannerSizePercent, BannerStyleBorder] ;
This code in fact does much the same as the code we ended up with using BannerWindow. In particular, CustomBannerWindow.initBannerWindow() follows the coding pattern we used above, but works out from the bannerArgs property which other BannerWindows it needs to initialize first. Using CustomBannerWindow thus saves both quite a bit of typing and the danger of some errors.
But it can do quite a bit more for us besides. CustomBannerWindow is a subclass of BannerWindow and inherits all its methods, which you can still use, but it also defines a number of methods and properties of its own. So, for example, while you can still call writeToBanner() on a CustomBannerWindow(), to take advantage of this class it's more useful to update its contents with its updateContents() method. This does two things: first it stores the banner's new contents (as defined by the string argument of updateContents()) in the currentContents property, and then displays the contents of the currentContents property in the banner. By itself this storing of the banner's output in a banner property may not seem very exciting, but the CustomBannerWindow also takes care of displaying its currentContents property on RESTORE, UNDO or RESTART (and, indeed, on startup). This means that as long as you consistently use the updateContents() method to display content in a CustomBannerWindow, the RESTORE/UNDO/RESTART issue is automatically taken care of for you. It also means that you can use the currentContents property to stipulate what you want a banner to play at the start of the game, for example:
picWindow: CustomBannerWindow bannerArgs = [nil, BannerAfter, statuslineBanner, BannerText, BannerTop, 10, BannerSizeAbsolute, BannerStyleBorder] currentContents = '<img src="welcome.jpg">' ;
Apart from initialization, however, your game code should not modify currentContents directly (unless for some very peculiar purpose), but should allow updateContents() to keep the currentContents property in sync with what the banner actually displays.
We said above that updateContents() method does two things; actually it normally does three: normally it clears the banner window before displaying the new content. This is typically what you'd want for a banner that displays a picture that reflects the current situation, say. But it may not always be what you want, e.g. for a scrolling window to which text is added cumulatively. So if you don't want a particular banner to be cleared before displaying new content in it via updateContents(), you need to override its clearBeforeUpdate property to nil.
CustomBannerWindow can also help us with the varying interpreter type issue. To recapitulate, the problem is not that trying to display a picture (say) on a non-HTML interpreter will of itself cause an error, but that displaying a picture banner on a non-HTML interpreter will take up screen space for no good purpose, making your game look rather kludgy on the text-only 'terp. Ideally, it would be best if, in this case, the picture banner simply wasn't part of the screen layout on the non-HTML interpreter, but if our game doesn't even initialize the banner on the non-HTML interpreter, then attempting to display a picture (or anything else) in it will cause a run-time error. What we'd ideally like is a means of deciding whether we want a particular banner to display on a particular class of interpreter, and have attempts to write to the banner simply ignored if the banner isn't displayed.
CustomBannerWindow provides the canDisplay property for this purpose. If canDisplay evaluates to nil, initBannerWindow() will not add the banner to the screen layout, but we can nevertheleess call any of the BannerWindow or CustomBannerWindow methods without causing a run-time error, since they'll then all be ignored (except that updateContents() will still update the currentContents property, even though nothing will be shown on screen).
The canDisplay property is meant to be used with in conjunction with the SystemInfo() function, to determine the kind of interpreter we're running on. So, for example, to suppress the display of a banner on an interpreter than can't display JPEGs we'd define:
canDisplay = (systemInfo(SysInfoJpeg))
The main thing we need to be careful about here ensuring that we don't define canDisplay methods that can evaluate to nil on a parent window and true on one or more of its child windows at the same time. So if we are designing a banner layout in which one window (a text-diplay window, say) is always required, but another (a graphics window, say) is only required if our game is running on an HTML interpreter, we must be careful not to make the text-display window a child of the graphics window.
In the example we have been using, our caption banner is a child of our picture banner, but this is okay since we don't want either banner to display if we can't display graphics: there's no point in showing the picture caption without the picture. To ensure that both our custom banners either do or don't display together, it's slightly less work and slightly less error-prone to override the CustomBannerWindow class than to override the canDisplay property separately on each window:
modify CustomBannerWindow canDisplay = (systemInfo(SysInfoJpeg)) ;
It's safe to do this since the only CustomBannerWindows that exist in our game are the ones we create ourselves. The banner windows defined in the library, such as the statusline banner and various banners used in displaying menus are of class BannerWindow, and so won't be affected by any changes we make to CustomBannerWindow.
The canDisplay property is intended purely for testing the interpreter type. We can use a different property, isActive, to add or remove a CustomBannerWindow from the display during the course of our game. More accurately, we can define the isActive property as nil on a CustomBannerWindow we don't want displayed at game start-up, and then use the activate() and deactivate() methods to add or remove custom banners to or from the screen. Once again, it's our responsibility to respect the dependency order of any parent, child or sibling banners involved.
An Example
Suppose we want to use the banner layout described above in the following way:
- Some of the rooms and some of the objects in our game have a picture.
- When an object that has a picture is examined, we display its picture together with a caption describing that picture.
- When the player character enters a room that has a picture, we display that picture together with its caption.
- When the player character enters a room that doesn't have a picture, we remove our custom banners from the screen layout.
- If a room has a picture, we display it in response to an explicit LOOK command.
- It follows that when a picture is displayed, it remains on screen until a movement, look or examine command changes it.