Created: around 09-07-2001
Last update: 10-07-2021
Back Previous Next

Beginners howto 6 - version testing

This howto quotes a code example from Chad Randall's KiSS .cnf layout, FKiSS/FKiSS2 command reference, and various PlayFKiSS notes. It has been updated on 10-07-2021 with an additional safeguard due to a quirk of UltraKiSS, discovered while writing the jigsaw puzzle how-i.

FKiSS syntax was implemented not simultaneously but sequentially, meaning, some commands were defined and viewers were programmed to understand them, then some more commands were defined and newer viewers were programmed to understand those too, and so on. This means that a viewer which is FKiSS-capable may still fail to understand the FKiSS code in a set, with two possible results: the viewer either ignores the unknown code, or starts to malfunction. Code that causes the viewer to malfunction (eg. freeze, ignore code that it should understand, or any other strange behaviour) is said to "break" the viewer. To warn the user that the viewer may not understand the FKiSS syntax in a set, FKiSS2 introduced the event version(), used with the number of a FKiSS level.

The currently existing FKiSS syntax has five levels: FKiSS, FKiSS2, FKiSS2.1, FKiSS3 and FKiSS4. (FKiSS5 is unique to the viewer UltraKiSS, and not widely supported.) The version() event works like this: put the level number of the highest FKiSS level used by the cnf in the brackets (for FKiSS3, that would be version(4), as FKiSS2 and FKiSS2.1 are separate levels) and if the viewer understands that level, it will carry out the actions under the event.

As version() is an event and is triggered only once, shortly after initialize(), it can't be used for "If viewer understands command X then do command X else do command Y" constructions; its main use is to warn the user when the set is opened in the viewer, and so it should be put above any other FKiSS code. Typically, it unmaps a warning cel that would stay mapped if the FKiSS level number was too high for the viewer. Since version() is a FKiSS2 event, a FKiSS-only viewer won't even recognize it.

In the original version of this howto, I stated that the version() event was processed as soon as the parser encountered it in the cnf, so it should therefore be put above initialize(). This is not true. It is carried out after initialize(), no matter where you put it. It is even supposed to be carried out after begin(), but UltraKiSS always processes version() before begin(). What set me on the wrong foot is the general habit that FKiSS viewers have, of stringing the actions of an event they don't recognize, under a higher event that they do recognize. The example in Chad's FKiSS2 document goes like this:

;@never() ; This tricks older viewers into ignoring the next 2 lines.
;@version(2) ; If the viewer supports FKiSS2,
; ; revision 2, run the following commands.
;@ unmap("warning.cel")
; ; warning.cel is a huge poster saying
; ; "You need a FKiSS2 viewer to play this set."

The event never(), meaning an event that is literally never triggered, acts like a wrapper to the version() event, making the viewer believe that the unknown event is an action that belongs to never(), and is consequently never executed. At least, in most viewers. WKiSS doesn't recognize never() either, or else ignores it completely, because the actions under version() are triggered by initialize(), or begin(), or whatever event happens to be just above both version() and never(); even EventHandler itself, which means that the actions are executed as soon as the set opens. So, since WKiSS is (or was) a generally available viewer, it's better to replace never() with a wrapper that WKiSS understands.

The best option is the so-called "dummy alarm"; never() is replaced by alarm(n), where timer(n) is never set. Another option is replacing never() with press(#n), where object #n can never be pressed because it's under another (locked) object. (Referring to a non-existent object #n is a bad idea, however, as the viewer may report the object as missing every time the set is loaded.)

Depending on the viewer, version() can be used once for each level number, or simply only once. It's safe to assume the latter. To unmap more warning cels, you can use version-specific actions under never(), if you don't mind error messages when loading the set in a viewer that doesn't understand them. See the rather pointless example below:

;@alarm(0) ; This dummy alarm tricks older viewers into ignoring version().
;@  unmap("warning.cel")
; warning.cel is a huge poster saying
; "You need a FKiSS2 viewer to play this set."
; FKiSS2.1 code
;@  ifmapped("warning2.cel",1,1)
; warning2.cel is a huge poster saying
; "You need a FKiSS2.1 viewer to play this set."
; FKiSS3 code
;@  letmapped(A,"warning3.cel")
;@  ifequal(A,1) timer(2,1) endif()
; warning3.cel is a huge poster saying
; "You need a FKiSS3 viewer to play this set."

;@alarm(1) ; called by FKiSS2.1 code
;@  unmap("warning2.cel")

;@alarm(2) ; called by FKiSS3 code
;@  unmap("warning3.cel")

And of course if you don't want the warning cel to stop the user from playing with the set, you can add:

;@ unmap("anywarning.cel")
so the user can get rid of it with a mouseclick.

But even with simple FKiSS, the artist may want something other than the usual "if version = X then unmap warning cel Y". Since version() follows initialize(), it can selectively undo actions under initialize(), even per FKiSS version if more than one version() event is allowed (which, I repeat, it is usually NOT). The following mini-version set takes three approaches: on the first screen, the usual unmapping of a warning cel (which reveals a "nothing to see here" screen) on page 0; a tick signifying that the viewer can handle the set's FKiSS version, which is unmapped under initialize() and mapped under version(), on page 1; and an unfixed black rectangle obscuring the word "not" in "This viewer is not FKiSS2-capable" on page 2, which is unmapped under initialize() and altmapped under version(), but it could also be moved out of view under initialize() and put back in place under version(), or unmapped via an alarm set under initialize(), which would be set to 0 under version()... It's even possible to set three alarms using randomtimer(), each mapping a different warning cel, where the first alarm to go off stops the other two timers, so that unless version() is triggered and sets all three timers to 0, the player sees a different warning every time the set is opened.

NB. When I wrote this, I didn't realize yet that setting alarms under initialize() is a Very Bad Idea, although it should be safe for small sets that load quickly. It is a better idea to set them under begin(), which happens after the set is loaded and before version, except in the case of UltraKiSS, the only viewer I know where version() happens before begin(). That may not be a big deal, but consider the following:

A set has a "This set needs a FKiSS-x-capable viewer" warning cel overlaying the rest of the set. What is more, begin() sets an alarm that will use quit() to shut down the set in two seconds. (That doesn't work in most viewers, but it works ruthlessly well in UltraKiSS.) Unless version() happens, and unmaps the warning cel, and sets the alarm to 0. But if version() is processed before begin(), the warning cel may be unmapped, but the set will shut down in two seconds sharp anyway. Because begin() starts the alarm after version() stops it.

Since version() has a whole two seconds to prevent this shutdown happening, that dummy alarm(0) above version() gets a use after all. It will now be set by version() itself, to a time shorter than 2 seconds. The sequence of triggered events will be:

  1. Under version(), if the viewer is right for the set, a warning cel is unmapped and an alarm(1), which shuts down the set, is stopped. Its own dummy wrapper alarm(0) is set to go off in 1 second.
  2. Under begin(), alarm(1) is set to go off in 2 seconds.
  3. If version() was triggered, alarm(0) goes off, stopping alarm(1).

This is coded as follows:

;@  timer(1,2000) ; quitting alarm
;@alarm(0) ; dummy wrapper for FKiSS-only viewers
;@  timer(1,0) ; in case begin() started timer(2) after version() stopped it
;@  timer(1,0) ; no need to quit
;@  timer(0,1000) ; forestall alarm1 (if version() is processed before begin())
;@  unmap("warning3.cel")
;@alarm(1) ; if FKiSS2.1 not supported
;@  quit()

In a viewer where version() does get processed after begin(), this code will simply stop the alarm set under begin() twice; once directly under version(), once via alarm(0).

Back Previous Next