You are on page 1of 112

Dragonflight Design

FS2K+/CFS+ C-Language Gauge Creation

Table of Contents

Revision List ..........................................................................................................................................................3


Getting the SDK and VC++ to work together ........................................................................................................5
General Points ..................................................................................................................................................5
Master Source files ...........................................................................................................................................5
More on .rc files….. .........................................................................................................................................7
Header (.h) files…… ........................................................................................................................................9
To Build or Rebuild? ......................................................................................................................................11
Gauge Macros ......................................................................................................................................................12
Structural Breakdown .....................................................................................................................................12
ELEMENT STATIC IMAGE ...................................................................................................................12
ELEMENT NEEDLE ...............................................................................................................................13
ELEMENT STRING ................................................................................................................................14
ELEMENT SLIDER .................................................................................................................................15
ELEMENT MOVING IMAGE ................................................................................................................16
ELEMENT ICON .....................................................................................................................................17
ELEMENT SPRITE .................................................................................................................................18
Image Flags ....................................................................................................................................................19
Functions And Callbacks .....................................................................................................................................21
Working with Token Variables ......................................................................................................................21
Display Priority and Plist ................................................................................................................................21
SHOW/HIDE Macros .....................................................................................................................................24
Non-linearity Tables .......................................................................................................................................26
Updating String Displays................................................................................................................................29
Sequential Selection in String Displays ..........................................................................................................34
Autopilot Manually Tuneable Flags ...............................................................................................................35
Mouse Callback Functions .............................................................................................................................36
The Dirty Mouse Callback Problem .........................................................................................................42
Animating Icons .............................................................................................................................................43
Masks .............................................................................................................................................................45
Make Moving .................................................................................................................................................48
Make Slider ....................................................................................................................................................48
Make Sprite ....................................................................................................................................................49
Re-using Callbacks and Tables .......................................................................................................................52
Multiple HelpIDs ............................................................................................................................................52
Creating Static Tooltips ..................................................................................................................................53
Creating Dynamic Tooltips ............................................................................................................................54
Retrieving and Writing XML Data .................................................................................................................57
The Event Handler ..........................................................................................................................................59
Time Rollover .................................................................................................................................................60
Extending the Environment ..................................................................................................................................61
Sharing Variables ...........................................................................................................................................61
Client Gauge code lines ............................................................................................................................61
Server Gauge code lines ............................................................................................................................62
Panel Lighting ................................................................................................................................................68
Native System ...........................................................................................................................................68
Created System .........................................................................................................................................69
Adding Alpha Channels .................................................................................................................................73
How to directly control Flight Simulator gauges via Keyboard input ............................................................76
Multigauges ..........................................................................................................................................................79
Global Declarations ........................................................................................................................................79

Dragonflight Design Page 1 of 112


Multigauges and Sharing Variables ................................................................................................................80
Multiple Gauge Initialisation ..........................................................................................................................82
Using The Fifth Gauge Parameter in the Panel.cfg File .................................................................................83
Adding Gauge Sounds ....................................................................................................................................85
Adding the Code to your Gauge ...............................................................................................................85
Using the Code..........................................................................................................................................85
Debug Facilities ........................................................................................................................................86
Lateral Thinking Corner .......................................................................................................................................87
100% Realistic? ..............................................................................................................................................87
The FS2K+ Electrical System ........................................................................................................................89
Flight Models .................................................................................................................................................91
Creation of Superchargers and Turbochargers ...............................................................................................92
Modifying the Autopilot Turn Rate ................................................................................................................92
Getting Lazy .........................................................................................................................................................94
Running Visual C++ as a Gauge Debugger .........................................................................................................96
Conversion Factors and Oddities .........................................................................................................................97
Additional Macros ..........................................................................................................................................97
Image Manipulation ..................................................................................................................................97
Bit Manipulation .......................................................................................................................................97
Conversion Factors ...................................................................................................................................97
Conversion Macros ...................................................................................................................................98
Simulator-Related Conversion Macros .....................................................................................................98
Math Functions .........................................................................................................................................98
The OBI Course-Setting Bug .........................................................................................................................98
Token Variable Returns ..................................................................................................................................99
Aircraft Altitude ...........................................................................................................................................102
Navigation Radio Frequencies ......................................................................................................................102
Navigation Radio Identities ..........................................................................................................................103
ADF and Transponder Radio Frequencies ...................................................................................................104
Mach Speed Conversion ...............................................................................................................................106
VOR Reception and Ranges .........................................................................................................................106
Fuel Tanks ....................................................................................................................................................108
Creating Reusable Functions ..............................................................................................................................109
Source Codes......................................................................................................................................................110
Final Comments .................................................................................................................................................111
Related Documents and Archives .................................................................................................................111
Additional grateful thanks to:- ...........................................................................................................................111
Updates:- ......................................................................................................................................................111
Required Legal Bit:- .....................................................................................................................................111
Smiley Bit:- ..................................................................................................................................................111
Revision List

14th April 2004 Removes any references to XML code but otherwise is identical to the sd2gau17 tutorial.

17th June 2006 “Creating Static Tooltips” added.


“Creating Dynamic Tooltips” added.

29th April 2007 More information on the IMAGE_USE_STATIC flag added


“Retrieving XML Data” added.
Removal of all legacy information (i.e. references to FS98 and FS2000) except where absolutely still needed.

26th May 2007 FSX SDK SP1 gauges.h file corrected and added to archive (fsxgauges_sp2.h). Comment added to introduction.

29th June 2007 “Retrieving XML Data” changed to “Retrieving and Writing XML Data”.
RELEASE v23?

1st July 2007 “The Event Handler” added.


“Panel Lighting” added.
“Retrieving and Writing XML Data” ADF2 example code corrected.

2nd Dec 2007 FSX Acceleration SDK gauges.h file corrected and added to archive (fsxgauges_sp2.h).
“Header (.h) files” section revised – resource numbering information added.
“The FS2K+ Electrical System” section considerably updated.
“Source Codes” – “Panel Switches” non-working fssound note added.
“The OBI Course Setting Bug” legacy note added.

3rd Dec 2007 “Image Flags” updated.


“Adding Alpha Channels” added.
“Gauge Callbacks” section deleted because Microsoft have finally fixed the problem.
All references to “fsxgauges_sp1” corrected to read “fsxgauges_sp2”.
“Multigauge and Sharing Variables” revised and updated.
“Time Rollover” section clarified.
RELEASE v24.

1st June 2008 Line 7 in the ELEMENT_STRING description clarified.


Line13 in the ELEMENT_STRING description clarified.

30th June 2008 “Time Rollover” section updated.

7th July 2008 “Conversion factors” section removed and replaced by “Additional Macros”.
The original “Creating Resuable Code” section renamed to “Re-using Callbacks and Tables”.
New section “Creating Reusable Functions” added.
New header file (fs_functions.h ) added to the archive.

21st July 2008 “The Dirty Mouse Callback Problem” section added.
RELEASE v26 (no v25 – it was a repeat of v24.)

6th Aug 2008 “Adding Gauge Sounds” section updated and moved to belowthe main „Multiauges‟ header.
RELEASE v27.

Dragonflight Design Page 3 of 112


Introduction

We are back up over 100 pages again!

This is a very much shortened edition of the old introduction for the sd2gauXX tutorials. The examples will still
be familiar as they are the ones always used in the sd2gau series and each one has been tried and tested. Where
I have not tested something but I believe you need to know about it, I have said so. With the release of FS9 and
FSX I have also taken the time to rationalise the folder structures and to remove some out-of-date samples and
information.

Assumption: the SDK and Visual C++ are installed in separate directories and that you are not a C or C++
programmer!! To make any sense of the SDK you must have a grasp of at least one mainstream computer
language. I am not a professional C programmer which actually is to your advantage because I had to fight my
way through this lot and make it work! However, I have programmed in various dialects of BASIC. I have also
appended information on a series of topics that are essential to getting gauges to work; information that is
obvious to C programmers but not to the rest of us. Lastly, you must have access to and know how to use a
good paint program such as JASC Paintshop Pro, The Gimp or Adobe Photoshop.

Throughout this document information is in 10-point Times New Roman and code snippets are in 8-point Courier
New. Code sections are separated by //--------------. I suggest you print out this document so that the code snippets
are visible in their entirety while you‟re studying it; otherwise view it in Page Layout View set to 120% to be
certain of seeing all of the illustrations. Although I try to list all the major changes from issue to issue of this
document in the revision list, there may be times when the change is so small it might be a subtle changing of
the wording within a section. If you are having problems with understanding a section from a previous issue of
this document, it may be worth re-reading that same section again just in case one of those very minor changes
has been made in the interests of better clarity.

For those of you that are or will be Visual C++ programmers, this tutorial treats the Visual C++ working area as
simply an editor for writing C code, not C++ code. If you want to set up the Visual C++ Workspace and use
that, then please download Bryan Kostick‟s IDEHOWTO.ZIP from FlightSim.com and use it alongside the
information in this document. I strongly recommend that you go the path of using the IDE.

Included in this archive is a debugged version of the latest gauges.h file (fsxgauges_sp2.h) created by Arne
Bartels and myself. Copy it to the \inc folder created when you unzipped the FS Panel SDK. You now have a
small choice; you can either delete the original gauges.h file and rename fsxgauges_sp2.h to gauges.h, or where
I say "gauges.h" in the samples then substitute "fsxgauges_sp2.h". I'd rename the file as it will cause less grief
in the short term when you are learning this. The current version of the gauges header file is one corrected and
created from the FSX SDK Service Pack 2 gauges.h release. You‟d have thought by now that Microsoft would
have caught up with all of the errors that we‟ve been correcting since FS2000!

I must also make it clear that I am using Visual Studio 6a and also Visual Studio 2003, so if you are using GNU
C or Borland C, the wording of any compiler error examples may be slightly different. Finally, the code snip
samples are written in a very "open" format similar to the structures in Visual Basic. This is simply to make
them easier to read and easier to trace program flow; it is possible in C to write program code that works
properly but is impossible to read (even for the original programmer at times!).

-Dai Griffiths

with major help from Arne Bartels.


Getting the SDK and VC++ to work together
General Points
The SDK consists of macros and for a lot of the time you‟ll find you‟ll be bastardising those macros to do
things you want. The best thing you can do is to learn the basic mathematical side of C as you'll use it quite a
lot (the ||, &&, = =, !, +, -, /, =, * operators), plus how to create and use arrays and even how to create, read and
write text files. You‟ll also need to know how to use and convert the various numerical types; frequently the
return from a Token Variable is a Float64 type but needs converting to (e.g.) UINT32 before the result can be
used within a macro or displayed on a gauge string output. If you really haven‟t understood the above sentence
or don‟t have any idea what it means then you need to go away and study basic C before going any further with
gauge programming. Which book(s)? For pure reference it has to be the granddaddy of them all – “The C
Programming Language Second Edition” by Brian W. Kernighan and Dennis M. Ritchie (ISBN 0-13-
110362-8). Bear in mind what I said earlier – if you have a good grounding in another widely-used
programming language then understanding the basics in Kernighan & Ritchie will not be a problem. For a
reasonably easy introduction to C-structures that you can apply to gauge programming then I'd also recommend
"C For Dummies" (vols. 1 and 2) by Dan Gookin (ISBN not known). Gookin leads you by the hand but never
patronises and before you know it, you have a firm grip on the basic C-structures. It also makes a marvellous
memory-refresh book. Finally, I have also found “Visual Quickstart Guide: C Programming” by Larry
Ullman and Marc Liyange (ISBN 0-321-28763-0) to be a useful reference. And it‟s cheap compared to the
others…

If you don‟t understand another programming language then you are going to struggle badly; in truth, I don‟t
really think you‟ll stand a chance.

You'll also need to watch out for the few token variables that come back as a fraction instead of a whole unit. I
suggest that you could create a set of blank template files with just the bare information in them for every
different available function and remove the unwanted sections when starting a new gauge. It‟s also a very good
idea to print out the entire contents of the gauges.h file included here as it contains far more Token Variables
and Key_Events than are listed in the SDK itself. Being able to read the header file is also a great help!

To make life a lot easier for yourself I suggest you do the following for each gauge directory you create:- create
separate sub-directories for the sub-gauges and bitmaps. You will need to add the path information to the
bitmaps in the .rc file and to the sub-gauges in the master source file – see "Master Source Files" and "More on
.rc Files" below. This will help keep your working directories tidy. Remember that the only files that you need
to call into the IDE workspace are the master source file ("source files" folder) and the resource file[s]
("resources" folder). The " #include <gaugename>.c" lines in the master source file ensure that the individual
sub-gauges are called during compilation; if you do list each sub-gauge source file individually then you will
see a lot of "already initialised" errors when you first Build the gauge. Once you have run a successful Compile
(or Build) you will see all of the sub-gauge source files and all of the bitmaps called in the .rc files listed under
"External Resources" in the Workspace.

Master Source files


In FS2K+ gauge programming you have as many [source files plus one] as you have sub-gauges in the
multigauge. It is this additional source file that for lack of a better term I am going to refer to as the master
source file.

The master source file tells the compiler how many sub-gauges are included, which header files are to be called
and what the sub-gauges are to be named. This name is the one that you will need to extract the gauge in the
panel.cfg file, so take a little care to decide what it will be. This is a simple two-gauge master source file for a
twin-engine combined temperature and pressure gauge:-

#include "..\inc\gauges.h"
#include "..\inc\eventid.h"
#include "oilgauge.h"

//Included gauges list

Dragonflight Design Page 5 of 112


#define GAUGE_NAME "portoil\0"
#define GAUGEHDR_VAR_NAME gaugehdr_portoilstate
#define GAUGE_W 100

#include "subgauges\port_oil.c"

#define GAUGE_NAME "staroil\0"


#define GAUGEHDR_VAR_NAME gaugehdr_staroilstate
#define GAUGE_W 100

#include "subgauges\star_oil.c"

// Gauge table entries

GAUGE_TABLE_BEGIN()
GAUGE_TABLE_ENTRY(&gaugehdr_portoilstate)
GAUGE_TABLE_ENTRY(&gaugehdr_staroilstate)
GAUGE_TABLE_END()

Look at the #include lines. The first two lines are giving the path to where I keep my common header files. The
third line is the header file containing the preprocessor and resource definitions specifically for this gauge.
Ignore the preprocessor information because you will never change it.

After that you then come to the definitions and inclusions for the individual sub-gauges which make up the
header for the sub-gauge. GAUGE_NAME is the name that appears after the exclamation mark in the panel.cfg file
i.e.

gauge10=oilgauge!portoil, 10, 100, 60


gauge11=oilgauge!staroil, 70, 100, 60

The next line

#define GAUGEHDR_VAR_NAME gaugehdr_portoilstate

defines a variable name required by the sub-gauge header and also the gauge table to recognise the sub-gauge
so again, be careful what you call it. I rapidly regretted calling this one portoilstate the more times I had to type it
out…. but you do want it to be recognisable for what it is and what it does in amongst a long list. The GAUGE_W
line is the width in pixels of the sub-gauge if it is declared as 100% size in the panel.cfg file – here it is 100
pixels wide.

Finally

#include "subgauges\port_oil.c"

calls the sub-gauge source file that you have just been creating the header information for. Note that these
source files are held in a folder called "subgauges" that is in the same directory as the master source file.

This sequence is repeated as many times as there are sub-gauges in the multigauge. Once the definitions and
inclusions are declared for each sub-gauge, you need to assemble a gauge table. Exactly how the gauge table
works I'm not sure – you're into "C pointer" territory here and that's quicksand as far as I am concerned…..
What you must do is ensure that each entry in the gauge table has a matching GAUGEHDR_VAR_NAME.

Other functions and variables that are used in all sub-gauges can be declared in the master source file and I'll be
giving you some very useful ones under "Functions and Callbacks" later.

Just for illustration on where some of those variable and pointer names end up, here is the gauge header
information from port_oil.c:

// Set up gauge header


char portoilstate_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER portoilstate_list;
extern MOUSERECT portoilstate_mouse_rect[];
GAUGE_HEADER_FS700(GAUGE_W, portoilstate_gauge_name, &portoilstate_list,
portoilstate_mouse_rect,0,0,0 );

MOUSE_BEGIN(portoilstate_mouse_rect,0,0,0)
MOUSE_END

Now you see why I regretted that looonng name! More to the point, this is a complete gauge header as required
by all sub-gauges. Unlike FS98 where if you didn't want a mouse rectangle you just ignored it, in FS2K and
CFS you have two choices; you can declare the function for it and then just not use it or the alternative is to
declare a zero (0) in the gauge header instead of <gaugename>_mouse_rect.

More on .rc files…..


If you open the SDK.rc file with Wordpad, you‟ll see that the path to the bitmaps starts “res\\xxxxxx”. All the
SDK bitmaps are kept in the \SDK\Sample\res sub-directory, but it‟s not such a good idea to put your creations
there too. Changing those lines to point to where you want is simple; this example is the .rc file for my Collins
comms radio:-

#include "CollinsCom.h"

/////////////////////////////////////////////////////////////////////////////
//
// Version
//

VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_MAJOR,VERSION_MINOR,0,VERSION_BUILD
PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,0,VERSION_BUILD
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x10004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Dragonflight Design\0"
VALUE "FileDescription", "Flight Simulator 2000 Gauge\0"
VALUE "FileVersion", VERSION_STRING
VALUE "LegalCopyright", "Dragonflight Design 2000.\0"
VALUE "ProductName", "Collins VHF21A Com1 Radio\0"
VALUE "ProductVersion", VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

/////////////////////////////////////////////////////////////////////////////
//
// Bitmaps
//

BMP_BACKGROUND BITMAP DISCARDABLE "collins radios\\radio_com1_bg.bmp"


BMP_XFR BITMAP DISCARDABLE "collins radios\\radio_switch_up.bmp"
BMP_MEM BITMAP DISCARDABLE "collins radios\\radio_switch_dn.bmp"
BMP_PLATE BITMAP DISCARDABLE "collins radios\\radio_sw_bg.bmp"
BMP_STORE BITMAP DISCARDABLE "collins radios\\radio_store_dn.bmp"

As you can see, the .rc file is where all your copyright and legal info resides. You may add as many

VALUE "xxxxx", "xxxxx\0"

lines as you like as each one is written into the compiled code, thus acting as an anti-piracy measure.

The double-backslash (\\) in the bitmap lines defines where the compiler can find the bitmaps. Have a look

Dragonflight Design Page 7 of 112


again at the path to my Collins radios:-

h:\fs2ksdk\collins radios\CollinsCom.c

If I run the compiler from the h:\fs2ksdk\ directory then that .rc file is telling the compiler to look one sub-folder
further down under \collins radios. If you copy the bitmaps to the \fs2ksdk directory then you can remove the path
marker (but I suggest you don't – it can get very messy!). In C or C++ code a double-backslash is a path
direction – a single backslash has an entirely different meaning. The crunch factor is this:- the program will
assume that the directory it is running in is the starting point for a path search. In this respect as far as the
compiler is concerned, both

BMP_STORE BITMAP DISCARDABLE "collins radios\\radio_store_dn.bmp"

and

BMP_STORE BITMAP DISCARDABLE "h:\\fs2ksdk\\sample\\collins radios\\radio_store_dn.bmp"

are valid path directions as long as it is being run from the h:\fs2ksdk directory.

The .rc file structure does not change when you are declaring resources for a multigauge. The illustration above
is for a single gauge but if I wanted all of my Collins radios to be included in one multigauge then the .rc file
would have to contain references to all of the bitmaps needed by the .c source files. This has some serious
advantages in that you can use a single common bitmap across a number of gauges. Let's convert the above
single radio .rc file into the full avionics suite held in one .rc file:-

#include "Radios.h"

// Versioning info

VS_VERSION_INFO VERSIONINFO
FILEVERSION VERSION_MAJOR,VERSION_MINOR,0,VERSION_BUILD
PRODUCTVERSION VERSION_MAJOR,VERSION_MINOR,0,VERSION_BUILD
FILEFLAGSMASK 0x3fL
FILEFLAGS 0x0L
FILEOS 0x10004L
FILETYPE 0x1L
FILESUBTYPE 0x0L
BEGIN
BLOCK "StringFileInfo"
BEGIN
BLOCK "040904b0"
BEGIN
VALUE "CompanyName", "Dragonflight Design\0"
VALUE "FileDescription", "Flight Simulator 2000 Gauge\0"
VALUE "FileVersion", VERSION_STRING
VALUE "LegalCopyright", "Dragonflight Design 2000.\0"
VALUE "ProductName", "Collins Avionics\0"
VALUE "ProductVersion", VERSION_STRING
END
END
BLOCK "VarFileInfo"
BEGIN
VALUE "Translation", 0x409, 1200
END
END

//Individual radio bitmaps

RADIO_COM_BACKGROUND BITMAP DISCARDABLE "bitmaps\\radio_com1_bg.bmp"


RADIO_NAV1_BACKGROUND BITMAP DISCARDABLE "bitmaps\\radio_nav1_bg.bmp"
RADIO_NAV2_BACKGROUND BITMAP DISCARDABLE "bitmaps\\radio_nav2_bg.bmp"
RADIO_ADF_BACKGROUND BITMAP DISCARDABLE "bitmaps\\radio_adf_bg.bmp"
RADIO_XPDR_BACKGROUND BITMAP DISCARDABLE "bitmaps\\radio_xpndr_bg.bmp"
RADIO_XPDR_PLATE BITMAP DISCARDABLE "bitmaps\\radio_xpdr_sw.bmp"

//Common avionics bitmaps used in all radios

RADIO_BMP_XFR BITMAP DISCARDABLE "bitmaps\\radio_switch_up.bmp"


RADIO_BMP_MEM BITMAP DISCARDABLE "bitmaps\\radio_switch_dn.bmp"
RADIO_BMP_PLATE BITMAP DISCARDABLE "bitmaps\\radio_sw_bg.bmp"
RADIO_BMP_STORE BITMAP DISCARDABLE "bitmaps\\radio_store_dn.bmp"

And now you've just seen the big problem with multigauges; the need to have instantly recognisable resource
names as the list grows longer and longer, particularly if you are creating a complete panel to be extracted from
a single gauge…… It's also fairly obvious from this .rc file that I am holding all of the radio bitmaps in a
separate sub-directory called "bitmaps".

There are not that many bitmaps for these radios so it will be fairly easy to find the one I've suddenly decided to
re-edit. You'll find though that as you start building bigger multigauges, even the directory structure I suggested
above in "General Points" can start to get unwieldly with too many bitmaps in the one folder. I have created a
Collins EFIS-84 and it had gotten to the point where I need to sort out my bitmaps (there were already 130 of
them!) and although each one has a recognisable name header (e.g. "eadi_pitch.bmp" for the pitch indicator on
the EADI), I needed to make it clearer still. So at that point I sub-divided each instruments' bitmaps into a new
sub-directory. This meant the new directory structure looked like this:-

and the line in the .rc file for the eadi pitch indicator example above now looks like this:-

EADI_PITCH BITMAP DISCARDABLE "bitmaps\\eadi\\eadi_pitch.bmp"

It's not that much of a change and it does not affect any other files but it can make life a lot easier and tidier for
you when you get round to creating the more complicated types of flight instruments.

Header (.h) files……


There must be at least one header (.h) file in every gauge project; this file is referenced by the resource (.rc) file.
In the example in the above section it is called “radios.h”. This header file tells the compiler what number to
attach to each resource (bitmap), so that FS can „know‟ which bitmap to display when it finds that particular
resource number being called. This is my Radios.h file bitmap definition, minus the preprocessor information:-

Dragonflight Design Page 9 of 112


#define RADIO_COM_BACKGROUND 0x1000
#define RADIO_NAV1_BACKGROUND 0x1003
#define RADIO_NAV2_BACKGROUND 0x1006
#define RADIO_ADF_BACKGROUND 0x1009
#define RADIO_XPDR_BACKGROUND 0x100C
#define RADIO_XPDR_PLATE 0x100F
#define RADIO_BMP_XFR 0x1002
#define RADIO_BMP_MEM 0x1005
#define RADIO_BMP_PLATE 0x1008
#define RADIO_BMP_STORE 0x100B

You‟ll see that the bitmap name reflects exactly what is in the corresponding .rc file. If one of these bitmaps is a
mask, then the difference between the mask (see "Masks" below) and its associated bitmap should always be 1.
Also, don‟t forget that those resource ids are in Hex notation. You do not need to append the 0x to the front of
each number as long as you don‟t forget that e.g. 4009 and 4010 are actually separated by six digits (400A,
400B, 400C, 400D, 400E and 400F).

There is a possibility that on occasions, having a moving part of a gauge (such as MAKE_SLIDER) next to a
MAKE_STATIC or MAKE_ICON in the .h file can create visual problems. If you are absolutely certain that there is
no problem with your code, try increasing the numerical separation between these items.

The SDK also misleads you when referring to the number spacing between bitmaps. It leads you to believe that
you can have consecutive numbers for the bitmaps. This may or may not work, although in my experience it
usually does work. The gauge will always compile successfully but it may or may not appear on your PC and
may or may not appear on someone else's PC. In my case the „failed‟ gauges always appeared and always ran
successfully but the moment they went to another PC they either did not appear at all or only one or two of the
gauges would extract properly. I finally cured the problem by reverting to a minimum number spacing between
bitmaps of 3, always observing the requirement for consecutive numbers for a bitmap and associated mask and
also for consecutive bitmaps that form part of a multi-image icon.

There is also another „gotcha‟ with the numbering of bitmap resources in FSX. Bill Leaming gave permission
for me to quote the information below:

One of the major shortcomings of the SDK is that absolutely no mention is made of the requirements for
resource numbers, including the various auto-offsets that the panel system will look for.

Every bitmap used in a C gauge's .rc file requires a resource number allocated in the gauge's header file. Each
such resource allocation actually reserves FOUR resource numbers (in FSX) although only TWO in FS9 and
earlier gauges. For example (using decimal numbers), the background image for a gauge might be 1000.
Therefore, the following table applies:

1000 - background image

1500 - optional hi-resolution version of background image

41000 - optional night version of background image (FSX+ only)

41500 - optional night version of hi-res background image (FSX+ only)

So, even if we don't provide any of the "optional" bitmaps, the resource numbers are already "reserved," hence
unavailable for use by any other bitmaps!

Some months ago, Jean-Luc (of Reality XP) suggested one possible scheme that at least has the virtue of being
guaranteed "safe," since it takes into account all of the conditions placed by the panel system on resource
numbers. I've since expanded the list to take advantage of the full dynamic range of allowable numbers. Below is
the complete list of "Safe Ranges:"

0x0100-0x02F3 0x0500-0x07F3 0x0900-0x0AF3


0x1100-0x12F3 0x1500-0x16F3 0x1900-0x1AF3
0x2100-0x22F3 0x2500-0x26F3 0x2900-0x2AF3
0x3100-0x32F3 0x3500-0x36F3 0x3900-0x3AF3
0x4100-0x42F3 0x4500-0x46F3 0x4900-0x4AF3
0x5100-0x52F3 0x5500-0x56F3 0x5900-0x6AF3
0x6100-0x62F3
NOTE: A handy site for hex-decimal-binary conversion is: http://flor.nl/dec2hex.html

It's critical to recognize that, even though the "old system" of dual-resolution bitmaps is no longer canon for FSX
gauge code, because of the necessity for "backwards compatibility" the auto-offset of 500(decimal) in resource
numbers is still respected.

Of course, this is only one possible scheme, but it has the advantage of being rational and consistent. As long as
you allocate resources within these ranges, you will NEVER run into a problem with contention. :)

The complete range of allowable resources is of course 0x0000 to 0xffff. The reason the above table stops at
0x62f3 is because in FSX, if one takes into account the 40,000 (decimal) auto-offset that the night time bitmap
"swap code" looks for, then the highest possible number would be 0x63bf, which of course one could use, but it
"breaks" the consistency of the tabular range scheme. ;)

Aside from which, with 9,500 allowable id's, I cannot conceive of any multigauge cluster that would come close
to exhausting the list of numbers provided by restricting one's choices to the table's range...

...at least not one which would actually load and run efficiently!

Now, as I‟ve confessed before, I can get quite lazy at times and I never use the hex numbers themselves. The
reason is simply that for some reason best known to itself, the VC compiler complains that hex numbers are not
allowed as resource numbers when you load the .h resource file to edit it. It does this for every hex number it
finds and dismissing many tens of error messages gets… a little wearing at times… so I use 1-500, 1000-1500,
2000-2500 etc.

As for any other C program, you can create your own header files for the SDK (perhaps containing math
calculations that you are using in different gauges or something simple like just containing colours as a name
rather than keep having to find the RGB notations) and the compiler will accept them (see "Getting Lazy"
below). Just add your header file name to the #include structure at the top of the master source file for the
gauge. Also, don‟t forget that you can call on all of the header files available within the version of Visual C++
you are using. A word of caution though; to be absolutely sure that the gauge you are creating will work
correctly, it would be wise to stick to the purely C functions. Unless or until you are a proficient C++
programmer!

To Build or Rebuild?
Within the IDE workspace it is best to use the "Rebuild All" rather than the straight "Build" command as this
forces the compiler to rebuild all resources from scratch. If you do this then the IDE should build clean gauges
every time; otherwise you can end up with old bitmaps in the gauge instead of the ones you‟ve just edited.

Dragonflight Design Page 11 of 112


Gauge Macros
The information contained in the FS2K+ SDK on the structure of gauge macros will still benefit from a more
detailed explanation. A print out of the gauges.h file gets you better information but even so, for a beginner it is
still very obscure.

Structural Breakdown
The first thing to be aware of is that all of the information required for that particular part of the gauge to
display (e.g. a needle) is passed across to the gauges.h file via a macro. All the macro contains is a set of
parameters and nothing more; the heading to the macro (e.g. MAKE_NEEDLE) tells the gauge what the
information is to be used for.

In this section I will be taking all of the SDK gauge macros and breaking them down into an explanation of
which line does what. Each part of this section will begin with the SDK name followed by an example of the
macro and a line-by-line breakdown of the macro; I will not repeat definitions but add references to the place
where something is first defined.

ELEMENT STATIC IMAGE

You will always use this macro at least once in a gauge; it will be used as the call to the gauge‟s background
bitmap if nothing else.

MAKE_STATIC( otp_background,
OTP_BMP_BACKGROUND,
&plist8,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
0, 0 )

PELEMENT_HEADER portoilstate_list = otp_background.header;

Line 1. MAKE_STATIC is the name of the macro and otp_background is a unique name assigned to the macro.
Every macro must have a unique name so that it can be recognised by any other code you write that
wants to manipulate that macro. A gauge can have multiple instances of the same macro (e.g. three
needles on an altimeter) but each macro must have a different name. If you are creating a multigauge
then every single macro across all of the sub-gauge source files must have a unique name.

Line 2. OTP_BMP_BACKGROUND is the name identifying the bitmap to be displayed by the macro. See the
section on .rc and .h files above for a greater explanation of the naming process. I tend to use names
that are descriptive of the function that the bitmap is serving; also bear in mind that any bitmap can be
used by any macro in any sub-gauge.

Line 3. The plist line helps define the order in which the parts of the gauge are drawn. See “Display Priority and
Plist” under Functions and Callbacks for an explanation. This line does not need to have a plist
command as it can also be set to NULL; this would happen in simple gauges that contain perhaps one or
two moving parts that do not overlap each other.

Line 4. This line contains a reference to the failure mode number that the gauge is to react to. In this case the
macro forms the background of the gauge so it does not need to react to a failure mode and is set to
NULL.

Line 5. The image flags line defines how this particular bitmap is to display on screen. Please see "Image
Flags" below for a full explanation of both documented and un-documented flags.

Line 6. Please see "Autopilot Manually Tuneable Flags" in "Functions and Callbacks". If not used for the
autopilot callback then this line is always set to zero.
Line 7. These are the x,y co-ordinates that define where on the gauge the macro is to display the bitmap
defined in line 2. If the bitmap is being used for the gauge background then they will always be set to
0,0. See other macro topics for non-zero positioning; it would only confuse if I tried to explain it here!

The final line

PELEMENT_HEADER portoilstate_list = &background.header;

defines that this macro forms the gauge background for this sub-gauge. See “Display Priority and Plist” for
more information on this – it has changed considerably from FS98. portoilstate is a header variable declared in the
master source file; this macro sample was taken from the port_oil.c source file for my oil temperature and
pressure gauge (see "More on .rc Files" above).

Warning: Do NOT use ELEMENT_STATIC for anything else except the background if you are programming for
CFS. If you need another static element to the gauge then use ELEMENT_ICON (MAKE_ICON) with one single
bitmap and MODULE_VAR_NONE as the callback function (see "Callback Functions" below). If you use more
than one ELEMENT_STATIC in a CFS gauge then some of the non-background bitmaps will not show up.

ELEMENT NEEDLE

MAKE_NEEDLE( seconds,
BMP_SECOND,
NULL,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
158, 153,
0, 2,
CLOCK_SECOND,
second_cb,
second_hand,
0 )

PELEMENT_HEADER plist1[] =
{
&seconds.header,
NULL
};

The example is the second hand from a clock.

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 4.
Line 5. See ELEMENT_STATIC_IMAGE line 5.
Line 6. See ELEMENT_STATIC_IMAGE line 6.

Line 7. The x,y co-ordinates for the position on the background image that the needle is to rotate around.
In this example the “pin” carrying the second hand is at pixel 158, 153 on the clock face.

Line 8. The x,y co-ordinates on the needle image itself that the needle is to rotate around. Again, think of it
as the point where the pin that drives the needle is fixed to the needle. Please note that generally all
bitmaps of needles must be drawn horizontally, otherwise they will be displayed 90 degrees out on
the gauge. There is the odd occasion when the needle needs to be painted vertically but it doesn‟t
happen very often.

Line 9. The Token Variable that you wish to read is entered on this line. As I will explain later in "Functions
and Callbacks", this line can also be set to MODULE_VAR_NONE if you want to supply your own
information to drive the needle and can be modified further by use of callback functions and non-
linearity tables.

Line 10. The name of the callback function that Line 9 reacts to. See “Mouse Callback Functions” in

Dragonflight Design Page 13 of 112


“Functions and Callbacks” for one way of providing a callback function to drive a needle. Another
way would be to provide a return from (say) a constant mathematical calculation if line 9 was set to
MODULE_VAR_NONE. See “Non-linearity Tables” for an example of how to use a callback driven
from a Token Variable (AIRSPEED).

Line 11. The name of the non-linearity table that the needle is constrained by – see “Non-linearity Tables” in
“Functions and Callbacks”.

Line 12. The figure here defines how many cycles are skipped before the macro is updated again. A gauge is
refreshed 18 times a second (the old DOS clock speed) so a figure of zero says “update every cycle”
whereas a figure of nine would say “update every half-second”. The greater the figure you can put in
here and not affect the gauge‟s display, the more you will help the overall panel framerate. The
smaller the figure, the smoother the display but the greater the hit on framerate. Sometimes known as
the “lag” figure.

The last four lines are the plist and header definition; see “Display Priority and Plist” in Functions and
Callbacks.

ELEMENT STRING

MAKE_STRING(dme_dist,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
71, 46,
80, 30,
4,
DME2_DISTANCE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,128,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
update_dist )

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 5.
Line 5. See ELEMENT_STATIC_IMAGE line 6.

Line 6. This defines the x,y co-ordinates where the string is to be displayed at on the face of the gauge.

Line 7. This defines the width and height in pixels of each individual character in the string that will be
printed. If the height information exceeds the height required by the font width to display the character
fully then FS will allocate that height and will centre the character within it. I strongly suggest that
when aligning strings on a gauge, you do not use transparency flag and you do use a background
colour until you have the final position set. This will allow you to see exactly where the string is and
how much space it is occupying.

Line 8. Defines the number of characters to be displayed.

Line 9. A Token Variable that the string display is derived from. The display string can contain numbers,
characters or a mixture of both. See “Updating String Displays” in “Functions and Callbacks” for more
information.

Line 10 and Line 11: As line 9.


Line 12. Defines the colour for the string display in RGB components. This example shows Orange and the
colour does not change from day to night.

Line 13. Defines the background colour for the string display in RGB components. This example shows
Black. Note that if you have the IMAGE_USE_TRANPARENT flag enabled then anything you
enter here will be ignored and the string will always be displayed on a transparent background.

Line 14. Defines the colour for highlighting the selected part of the frequency e.g. Nav1 whole numbers. This
example shows Yellow.

Line 15. The font name for the string display.

Line 16. The font weight for the string display.

Line 17. The font character set for the string display. See “Updating String Variables” in Functions and
Callbacks for how to use the font styles in string displays.

Line 18. Tells FS how to display the font. If you set it to zero (default) it scales the font according to the
window size available defined in line 7. If you set it to any other number then that number describes
a fixed font size and the display becomes dependant on the flags in line 19.

Line 19. If line 18 is set to zero then this line has no effect. Otherwise the flags are:-

DT_CENTER: Centralise the string horizontally in the window.


DT_VCENTER: Centralise the string vertically in the window.
DT_SINGLELINE: Do not wrap the string display if it over-runs the window area.

Line 20. Usually set to NULL but can also contain the reference to a SEQ_SEL function. See “Sequential
Selection in String Displays” for more information.

Line 21. This is the callback function that modifies the display called by lines 9, 10 and 11. See “Updating
String Displays” in “Functions and Callbacks” for a greater explanation of how to use it.

The colours defined by lines 12, 13 and 14 are a good candidate for inclusion in a separate header file called at
the top of the gauge code along with all the other .h files. See "Getting Lazy" below.

Just in passing, although this string display works perfectly to show the distance to a DME I don‟t recommend
using it. For some unknown reason the update is marginally less slow than a drunken snail; a long time after
creating this DME gauge I did another one with all of the read and callback work done in the Update section of
the gauge using exactly the same Token Variable. This time, the display reaction time of the gauge was in the
order of thousands of times better than the above code. Go figure….. because again, I can‟t!!

ELEMENT SLIDER

MAKE_SLIDER( turnco_night,
BMP_BALL,
plist18,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
140, 253,
TURN_COORDINATOR_BALL_POS, NULL, 0.47,
MODULE_VAR_NONE, NULL, 0)

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 4.
Line 5. See ELEMENT_STATIC_IMAGE line 5.
Line 6. See ELEMENT_STATIC_IMAGE line 6.

Dragonflight Design Page 15 of 112


Line 7. Defines the x,y co-ordinates of the slider against the gauge‟s background bitmap when the slider is
either at the end point of its travel (this can be at zero or at maximum depending on the return from
the Token Variable) or at the centre point of its travel (if the return from the Token Variable runs
from a negative figure to a positive figure). In the latter case the x,y co-ordinates define the zero or
centre position. See “Sliders” in “Functions and Callbacks” for more information.

Line 8. Defines the movement of the bitmap in line 2 in the X-direction i.e. left – right. The first part of the
line is the Token Variable to be read, the second part is any modifying callback function required and
the last part is a scaling factor. See “Sliders” in “Functions and Callbacks” for more information.

Line 9. Defines the movement of the bitmap in line 2 in the Y-direction i.e. up – down. Line functions are as
line 8. Again, the movement of the bitmap can be done via a user-defined function using
MODULE_VAR_NONE rather than reading a Token Variable. See “Mouse Callback Functions” for
more information on this.

ELEMENT MOVING IMAGE

MAKE_MOVING(vbars,
BMP_VBARS,
&plist11,
fail2,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY,
0,
52,76,
MODULE_VAR_NONE, OBIX,
-20, 20,
MODULE_VAR_NONE, GlideY,
0, 240)

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 4.
Line 5. See ELEMENT_STATIC_IMAGE line 5.
Line 6. See ELEMENT_STATIC_IMAGE line 6.

Line 7. The x,y co-ordinates of the position of the mask on the gauge background. The top left pixel
corresponds to the minimum value of both the X and Y directions. The bottom right pixel of the
mask corresponds to the maximum values of both the X and Y directions. However, because the
movement is calculated from the centre of the moving bitmap, this does not mean that the minimum
and maximum value points are the furthest that the edges of the moving bitmap will go. See “Make
Moving” and “Masks” for a greater explanation.

Line 8. The Token Variable to be read plus a callback function to control the moving bitmap in the X
direction.

Line 9. The minimum and maximum values expected by the macro from the callback routine in line 8 for
use in the X direction.

Line 10. The Token Variable to be read plus a callback function to control the moving bitmap in the Y
direction.

Line 11. The minimum and maximum values expected by the macro from the callback routine in line 10 for
use in the Y direction.

See “Masks” for an explanation of how to use lines 8 through 11.


ELEMENT ICON
MAKE_ICON(radalt_fail_night,
BMP_RADALT_FLAG,
plist10,
NULL,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT | IMAGE_USE_TRANSPARENCY | IMAGE_HIDDEN,
0,
247,83,
MODULE_VAR_NONE, NULL,
ICON_SWITCH_TYPE_SET_CUR_ICON,
1,
0,
0)

MAKE_ICON is probably the most commonly used macro in gauge design. It is used at a static location on a
gauge where an image changes only occasionally e.g. a switch but it is capable of cycling through a number of
bitmaps at that location e.g. the Cessna magneto key.

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 4.
Line 5. See ELEMENT_STATIC_IMAGE line 5.
Line 6. See ELEMENT_STATIC_IMAGE line 6.

Line 7. Defines the x,y co-ordinates where the top lefthand pixel of the icon bitmap is to be placed on the
gauge background.

Line 8. This line contains the Token Variable to be read plus a callback modifier. In this case the callback
contains the number of the icon to be displayed.

Line 9. This is the “switch type” for the icon. There are four switch types as listed below.

1. ICON_SWITCH_TYPE_SET_CUR_ICON. Use this for a simple on-off function for two or more bitmaps
where you specify by number the bitmap you wish to display.

2. ICON_SWITCH_TYPE_STEP_TO. Use this for stepping through or animating a set of icons such as the
key in the Cessna magneto. The number returned by the callback function will display the
corresponding icon in the stack; this may mean a change in code after compilation when you find the
icons appear in the wrong order! You can also try changing the bitmap order as specified in the .h
file. The bitmap specified by line 2 is the first bitmap (bitmap 0) in the display order. The last bitmap
is number (no. of bitmaps minus 1). See “Mouse Callback Functions” for a way of using this switch
type.

3. ICON_SWITCH_TYPE_SET_CUR_USING_RANGE.

4. ICON_SWITCH_TYPE_STEP_TO_USING_RANGE.

The SDK defines switch type 3 being the same as switch type 1 and switch type 4 being the same as switch type
2 except that “the value is subtracted from offset and divided by scale to get the current index.” No, I don‟t understand
it either. The icon bitmaps do not all have to be the same size but they will all have the same upper left co-
ordinate when displayed onscreen.

Line 10. Specifies the number of icon bitmaps in the gauge.

Line 11. Specifies the scale of the icon.

Line 12. Specfies the offset of the icon.

Line 11 and line 12 appear to be related to switch types 3 and 4. I haven‟t experimented to try and see how they
affect the switch type so maybe you can tell me!

Dragonflight Design Page 17 of 112


ELEMENT SPRITE

MAKE_SPRITE(deviation,
BMP_DEVIATION,
&plist8,
fail2,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
72, 73,
149, 150,
1.5, 1.5,
VOR1_NEEDLE, NULL, -0.632813,
MODULE_VAR_NONE, NULL, 0.000000,
VOR1_OBI, OBI_deviation, -1.000000)

Line 1. See ELEMENT_STATIC_IMAGE line 1.


Line 2. See ELEMENT_STATIC_IMAGE line 2.
Line 3. See ELEMENT_STATIC_IMAGE line 3.
Line 4. See ELEMENT_STATIC_IMAGE line 4.
Line 5. See ELEMENT_STATIC_IMAGE line 5.
Line 6. See ELEMENT_STATIC_IMAGE line 6.

Line 7. The x,y co-ordinates of the position of top left pixel of the mask on the gauge background.

Line 8. The x,y co-ordinates of the point on the background bitmap that the centre of this bitmap is to
revolve around. This is entirely separate from line 7 above.

Line 9. The x,y scale factor for the moving bitmap. The X-scale and the Y-scale can be two different figures;
the bigger the figure the smaller the image in that direction. With judicious use of this line you can
make the moving element fit the mask just the way you want.

Line 10. Defines the Token Variable to be read, the callback and the scale factor for the X direction.

Line 11. Defines the Token Variable to be read, the callback and the scale factor for the Y direction.

Line 12. Defines the Token Variable to be read, the callback and the scale factor for the O direction.

See “Sprites” in Functions and Callbacks for a greater explanation of how lines 10, 11, and 12 are used and see
also the SDK's Attitude.c source code.

Where a macro is expecting information returned from a callback function then most times that information can
be a negative figure as well as a positive one. As an example, this is the code that draws the no.1 engine throttle
control on my SD3-60:-

MODULE_VAR lpos = {ENGINE1_THROTTLE_LEVER_POS};


MOVING_IMAGE_UPDATE_CALLBACK engine1_cb;

MAKE_MOVING (left,
BMP_LKNOB,
&plist1,
NULL,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY,
0,
12,0,
MODULE_VAR_NONE, CommonX_var,
0, 0,
MODULE_VAR_NONE, engine1_cb,
16383, -1638)
//--------------------------------------------------------------
FLOAT64 FSAPI engine1_cb( PELEMENT_MOVING_IMAGE pelement)
{
lookup_var(&lpos);
return lpos.var_value.n;
}

On the last line of the macro 16383 is the figure returned for full throttle and –1638 is full reverse thrust. If you
ever find that your moving bitmap is moving in the wrong direction then try reversing the order of the
maximum and minimum figures. Also bear in mind that 0,0 is always the top lefthand corner whatever the
macro.

Image Flags
You can use multiple flags to control the behaviour of the bitmap in the macro; all the flags must be on the
same line and each flag must be separated from the rest by use of the pipe symbol ( | ). This is the small double-
dash symbol next to the left shift key on a UK keyboard and needs the shift key to invoke it. These are the most
common image flags:-

IMAGE_USE_TRANSPARENCY: Do not display any area of the gauge set to colour 0,0,0.

IMAGE_HIDDEN: Do not show this bitmap until called by code in the main body of the gauge.

IMAGE_USE_ERASE: Generally you‟ll have this flag in every macro. This forces FS to redraw that gauge
element whenever the element control changes; if it is not there, then if for example you have turned a light
on, even if you command the light to go to off it will not switch off until the entire screen is refreshed.

IMAGE_USE_BRIGHT: Retains the daylight colours of the gauge element at night i.e. the gauge element is
never dimmed.

IMAGE_USE_ALPHA:Tells FS that the current bitmap has an alpha channel in it. Applies to
ELEMENT_STATIC_IMAGE (MAKE_STATIC) only – see “Adding Alpha Channels” below.

IMAGE_USE_LUMINOUS: Turns the white and light grey parts of the image pink at night, assuming that you
have not added the [Color] section to the panel.cfg file. Otherwise the night lighting colour is controlled by
the panel.cfg file.

IMAGE_USE_LUMINOUS_PARTIAL: As above but can be used with a macro with a mask and the
IMAGE_USE_ALPHA flag without problems.

IMAGE_HIDDEN_TREE: Hide every single bitmap in this gauge in one go!!

IMAGE_NO_STATIC_BLENDING: recommended to be used with every static image but again, I have no idea
why and it doesn‟t seem to matter if it is missing.

In addition there is one undocumented flag and another use for one of the documented ones. Each of them are
in the gauges.h file but neither has an explanation anywhere.

BIT7: Using this flag anti-aliases needles and sprites i.e it smooths the edges of the bitmaps.

Bit7 Needle Anti-Aliasing – the righthand image has Bit7 set.


(CFS2 screenshot courtesy Jorge Alsina)

BIT15: Use with the MAKE_STRING macro only. If line 13 is set to RGB 0,0,0 (transparent black) then the

Dragonflight Design Page 19 of 112


background of the string will also be transparent. The night colour of the string then depends very much on
the day colour (line 12); I haven't worked out a definite "this is where it changes" sequence but the lighter
day colours will result in a pink night colour and the darker day colours will give a black night colour. It
also seems that as the sim changes to night time some colours will go dark but as soon as you update the
string creating them, it changes to pink. This flag MUST NOT be used with IMAGE_HIDDEN as the
background will then become a solid colour. This flag is also aliased as IMAGE_USE_LUMINOUS_PARTIAL.
Functions And Callbacks
This section deals with the “glue” of gauges; the parts that interpret the information available from the Token
Variables and the various methods that are available for modifying that information. Again, this is information I
have been given or discovered along the way while making gauges; so although I can guarantee that it will
work, I don‟t always guarantee I have an answer for any known problems!

Working with Token Variables


Generally you will be able to do a simple lookup_var(&<token_variable>) and be able to act directly on the result.
Sometimes though you will get a zero return even when you know that the variable has changed; this happens
because you are not reading the variable directly but are being shown the variable through a pointer. Pointers
are the most useful and most reviled things in the C language and also one of the most difficult to explain. Try
this though in relation to FS:-

A pointer contains an address that points to an area in memory where the real variable is stored. (All
explanations of pointers start like this!!). The token variables in the SDK are all pointers so if you want to read
the contents of a token variable, your gauge puts up a pointer of its own that says "okay - tell me what's in that
token variable" and then acts on the answer. Similarly, in shared variables (see below) you put up a pointer that
says "this is the new value" and the pointer in the gauge you are talking to looks at its internal variable of the
same name and says "hey! that's different to the current value that I have - I'd better change it". You can never
act directly on a token (or shared) variable because it is an internal variable in another gauge. You can never act
directly on the pointer either; if you try to do that the compiler will throw a set of warnings but the message
itself will vary from compiler to compiler.

So what do you do??

If you are trying to read the contents of a token variable yourself rather than as part of a macro eventually you
will need to read it into an internal variable. The act of making an internal variable equal to the pointer e.g.

SINT16 nVsi = 0;
MODULE_VAR curr_vsi = {VERTICAL_SPEED};

//-----------------------------------

lookup_var(&curr_vsi);
nVsi = curr_vsi.var_value.n

forces the pointer to pass the actual value of the variable into nVsi so you can then work on it within your gauge.
Look at these:-

curr_vsi.var_value.n = curr_vsi.var_value.n + 100;


nVsi = curr_vsi.var_value.n + 100;

The first one is not acceptable because you are trying to act directly on the pointer but the second one is okay
because you are taking the contents of curr_vsi.var_value.n, putting them into nVsi and then adding 100 to the
contents of nVsi. It‟s the order that things happen in that makes the difference.

To avoid potential problems and until you get familiar with the FS interface, it‟s my advice to immediately put
the contents of a lookup_var action into a local variable before attempting to do anything with the result.

Display Priority and Plist


I have to credit Bryan Kostick of the Flightsim.com Panel Design Forum for initially sorting me out on plist for
the FS98 SDK - although he didn‟t know he was doing so at the time! He was answering a question from
someone else. Jorge Alsina later passed me more information on the list and plist keywords. The usage of plist
has not changed from FS98 days although the way that SDK.Attitude.c is laid out, you may be forgiven for
thinking it has!

Dragonflight Design Page 21 of 112


The order in which the parts of the gauge display is governed by the “previous list” order. Display order is
governed from top to bottom i.e. the top named icon will display on top. The simple plist where everything is
crammed together under one header e.g.

PELEMENT_HEADER plist1[] =
{
&light_on.header,
&gyro_failed_flag.header,
&switch_up.header,
&switch_dn.header,
NULL
};

is fine as long as the bitmaps that are being displayed in the gauge do not overlap each other. Every macro that
is declared must have a corresponding plist entry somewhere; if you fail to declare a plist entry the gauge will
compile without error but the associated macro will not display or work. If the gauge has a single needle (like
an airspeed indicator) then the simple plist is also okay. It‟s when you get to gauges with multiple needles (or
multiple bitmaps that overlap) you‟ll get problems; if you use a simple plist order like the one shown above I
can guarantee that the needles will not display correctly. This can take the form of erratic display, not showing
at all or one needle repeating exactly the path of another even if being driven by a totally different token
variable. To get around the problem you have to give each section of the gauge its own plist header and link
each plist back to the previous one. There is a “sort of” illustration of this in SDK.Attitude.c but it‟s not at all
clear, hence my comment above. The lines of code that follow are taken from my Collins RMI gauge and show
very clearly how the linking is done. Three things to note; the top section has no link (third line says NULL);
when linking, display order is governed by plist number with the lowest plist displaying on top (the VOR
needle here) and finally, even the static background image has to be linked. Each time the gauge updates it does
so in this order:-

VOR needle
ADF needle
ADF compass rose
RMI face
Static background image.

// Set up gauge header


char rmi_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER rmi36_list;
extern MOUSERECT rmi36_mouse_rect[];
GAUGE_HEADER_FS700(GAUGE_W, rmi36_gauge_name, &rmi36_list, rmi36_mouse_rect,0,0,0 );

MOUSE_BEGIN(rmi36_mouse_rect,0,0,0)
MOUSE_END

MAKE_NEEDLE(vor2,
RMI36_BMP_VOR_NEEDLE,
NULL,
&fail_nav,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
105, 103,
70, 13,
VOR2_BEARING_DEGREES,
NULL,
vor_needle,
0 )

PELEMENT_HEADER vor2_plist[] =
{
&vor2.header,
NULL
};

MAKE_NEEDLE(adf,
RMI36_BMP_ADF_NEEDLE,
&vor2_plist,
&fail_nav,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
105, 103,
70, 11,
ADF_NEEDLE,
NULL,
adf_needle,
0 )

PELEMENT_HEADER adf_plist[] =
{
&adf.header,
NULL
};

MAKE_SPRITE(adf_rose,
RMI36_BMP_CARD,
&adf_plist,
&fail_gyro,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
25, 22,
107, 104,
1.6, 1.6,
MODULE_VAR_NONE, NULL, 0.000000,
MODULE_VAR_NONE, NULL, 0.000000,
PLANE_HEADING_DEGREES_MAGNETIC, NULL, 1.000000 )

PELEMENT_HEADER rose_plist[] =
{
&adf_rose.header,
NULL
};

MAKE_ICON( face,
RMI36_BMP_FACE,
&rose_plist,
NULL,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY | IMAGE_USE_BRIGHT,
0,
0,0,
MODULE_VAR_NONE, NULL,
ICON_SWITCH_TYPE_SET_CUR_ICON,
1,
0,
0)

PELEMENT_HEADER night_plist[] =
{
&face.header,
NULL
};

MAKE_STATIC( rmi_background,
RMI36_BMP_BACKGROUND,
&night_plist,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
0, 0 )

PELEMENT_HEADER rmi36_list = &rmi_background.header;

In the example above I have given each plist item a name which clearly links it back to the previous macro but
if you want, you can just as easily use plist1, plist2 etc. The important part is the PELEMENT_HEADER; the rest is
just a reference name. I‟ll give you a good reason why not to do this though; in a large gauge with many display
macros it is easy to skip linking a macro if using numbered plists. Suppose your plist should have been plist31
but your fingers decided to type plist30 and not tell you; the gauge will compile perfectly (after all, it is a valid
instruction) but you will have one macro that will never display and it will take you hours to find out why. Trust
me…. I‟ve been there.

The last line

Dragonflight Design Page 23 of 112


PELEMENT_HEADER rmi36_list = &rmi_background.header;

is linked back to the gauge header information which helps tell FS2K+/CFS+ which sub-gauge to display.

I said I‟d give you more information about the list and plist keywords; in fact, I‟m just going to quote Jorge
Alsina from the old FS98 Gauhlp series because I couldn‟t put it any better and the explanation still stands!

„You also may use a plist for a MAKE_STATIC. The list is the first reference of the macro set (normally the last in
your macro list). The gauge when compiling will initialise the macros starting with the one that is called with
list. The element that has it is the first to be considered (the deepest in your drawing order). Then this one will
call the "previous list" element according to plist order you use. The element that has the list may be an icon, a
moving image or whatever. It will normally be the gauge “support”, that is, a static image. Another static image
could be present above and use a plist element (not a list).‟

SHOW/HIDE Macros
In FS98 it was relatively easy to show and hide any of the drawing macros (with the exception of
MAKE_STRING!). You could do it simply by declaring

SHOW_IMAGE(&light_on);
HIDE_IMAGE(&light_off);

This no longer works in the FS2K+ SDK; it now involves you in a lot more coding and you must be using
fsxgauges_sp2.h. The simple plist structure isn't too much of a problem:

PELEMENT_HEADER plist1[] =
{
&switch_on.header,
&gyro_fail.header,
&fail_light_on.header,
&light_off.header,
NULL
};

MAKE_STATIC( background,
BMP_BACKGROUND,
&plist1,
... )

PELEMENT_HEADER rmi36_list = &background.header;

To hide and show the background use this code structure:


HIDE_IMAGE(pgauge->elements_list[0]); //&background.header
SHOW_IMAGE(pgauge->elements_list[0]); //&background.header

To hide and show the drawing elements in the simple plist use:
HIDE_IMAGE(pgauge->elements_list[0]->next_element[1]); //&switch_on.header
HIDE_IMAGE(pgauge->elements_list[0]->next_element[2]); //&gyro_fail.header
HIDE_IMAGE(pgauge->elements_list[0]->next_element[3]); //&fail_light_on.header
HIDE_IMAGE(pgauge->elements_list[0]->next_element[4]); //&light_off.header

SHOW_IMAGE(pgauge->elements_list[0]->next_element[1]); //&switch_on.header
SHOW_IMAGE(pgauge->elements_list[0]->next_element[2]); //&gyro_fail.header
SHOW_IMAGE(pgauge->elements_list[0]->next_element[3]); //&fail_light_on.header
SHOW_IMAGE(pgauge->elements_list[0]->next_element[4]); //&light_off.header

in the GAUGE_CALLBACK structure. It must go in PANEL_SERVICE_PRE_UPDATE which is where continuous


updates are performed. The ->next_element[] numbers start from zero and zero is the bottom-most listed .header in
the plist structure, so the background MAKE_STATIC image will always be zero. If you look at the SHOW/HIDE
for the background and then compare it to the plist structure you can see how the code is built on the
background.
When you come to the back-linked plist structure which is probably the most common (see the RMI example in
"Display Priority and Plist" above) it becomes a lot more difficult because you have to declare exactly where in
the entire plist structure the macro is that you wish to manipulate. Have a look at this:-

SHOW_IMAGE(pgauge->elements_list[0]->next_element[0]->next_element[0]->next_element[0] -
>next_element[0]); //&night.header

This would be the structure needed to show and hide the night lighting bitmap in the RMI36 above (drawing
four in the list). Okay, so it doesn't look too bad (only four links); now, there are 126 images in the Engine
Services gauge of my SD3-60-200 - imagine what that would look like if I was trying to Show or Hide the last
icon……. It doesn't bear thinking about, does it?

Fortunately Arne had another look at the problem at my request and he has come up with two functions that
brought the problem right down to a simple short line. These are the two functions:-

/***************** SHOW/HIDE functions ***********************/

#define HIDE_LISTELEMENT(pelement,pos_element) \
add_imagedata_to_listelement(pelement,pos_element,IMAGE_HIDDEN)
#define SHOW_LISTELEMENT(pelement,pos_element) \
remove_imagedata_from_listelement(pelement,pos_element,IMAGE_HIDDEN)

The required macro changes have been already made in the fsxgauges_sp2.h file. This means that the example
line above can be simplified to:-

HIDE_LISTELEMENT(pgauge->elements_list[0],1); //&night.header
SHOW_LISTELEMENT(pgauge->elements_list[0],1); //&night.header

where the number to the right of the [0] brackets at the end is the position in the plist stack starting from zero.
Now, be very careful here. Zero is always the background bitmap and one is the macro directly above it, two is
the macro above that etc. So in the RMI-36 above although face.header is the fourth macro down it is number one
in the show/hide listings:

SHOW_LISTELEMENT(pgauge->elements_list[0],1); //&face.header

as shown above and as there are five macros in the gauge the vor2 needle, the very first macro, is number four
in the elements_list.

SHOW_LISTELEMENT(pgauge->elements_list[0],4); //&vor2.header

If for example you wanted to show and hide a switch you need to change a variable on a mouse-click:-

//---------------------------------------------------------------------------

BOOL FSAPI panel_switch( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
if (nSwitch == 0 )
{
nSwitch = 1;
}
else
{
nSwitch = 0;
}

return FALSE;
}

then use the setting of the variable (nSwitch) to show and hide the image in the gauge_callback.

//-----------------------------------------------------------------

case PANEL_SERVICE_PRE_UPDATE:

if (nSwitch == 0)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],1);

Dragonflight Design Page 25 of 112


}
else
if (nSwitch == 1)
{
SHOW_LISTELEMENT(pgauge->elements_list[0],1);
}
break;

Non-linearity Tables
You‟ll use a non-linearity table to force a needle to display a return that is non-linear or you may also want to
start the display from a point on the gauge that is not at the twelve-o‟clock default position; for example, an
airspeed indicator that reads from 50 knots to 260 knots and starts at the four-o‟clock position. This also
introduces the use of GAUGE_MAX_xxx and GAUGE_MIN_xxx variables to crop the returning callback to a range
that you want. GAUGE_MAX_xxx and GAUGE_MIN_xxx are the variable names I use; you can use whatever you
like. Using the actual numerical value in the code is allowed (but messy); I can guarantee that coming back to a
page of code after quite a while you‟ll find that GAUGE_MIN_50 is far easier to understand than a simple 50…

This example is from the airspeed indicator on my SD360 panel. The token variable being read is AIRSPEED and
the callback function to get the airspeed is needle_src_cb. The returned information is then processed by the non-
linearity table asi_needle before being displayed on the gauge.

This is the really important bit: the co-ordinates on the left side of the table are the x,y co-ordinates that the
needle should be pointing at when the value on the right is returned by the callback function.

Highlight the picture of the ASI below and copy it to your image editor; you can then check the needle co-
ordinates for yourself by moving the editor pointer over the picture and comparing the x,y figures to the non-
linearity table below.

#define GAUGE_MAX_AIRSPEED 260


#define GAUGE_MIN_SET 50

//airspeed indication

NONLINEARITY asi_needle[] =

{
{{253, 216}, 0.000000, 0},
{{226, 245}, 50.000000, 0},
{{192, 267}, 60.000000, 0},
{{150, 272}, 70.000000, 0},
{{105, 262}, 80.000000, 0},
{{67, 237}, 90.000000, 0},
{{38, 199}, 100.000000, 0},
{{29, 165}, 110.000000, 0},
{{29, 132}, 120.000000, 0},
{{39, 100}, 130.000000, 0},
{{59, 70}, 140.000000, 0},
{{84, 48}, 150.000000, 0},
{{114, 32}, 160.000000, 0},
{{150, 26}, 170.000000, 0},
{{180, 31}, 180.000000, 0},
{{210, 43}, 190.000000, 0},
{{231, 59}, 200.000000, 0},
{{248, 75}, 210.000000, 0},
{{260, 97}, 220.000000, 0},
{{270, 122}, 230.000000, 0},
{{271, 145}, 240.000000, 0},
{{269, 174}, 250.000000, 0},
{{260, 203}, 260.000000, 0},
};

NEEDLE_UPDATE_CALLBACK needle_src_cb

MAKE_NEEDLE( asi,
ASI_BMP_NEEDLE,
NULL,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

//---------------------------------------------------------------------------
FLOAT64 FSAPI needle_src_cb(PELEMENT_NEEDLE pelement)
{

//Restrict displayed airspeed from 50 knots to 260 knots

FLOAT64 val = pelement->source_var.var_value.n;


if ( val > GAUGE_MAX_AIRSPEED ) val = GAUGE_MAX_AIRSPEED;
if ( val < GAUGE_MIN_AIRSPEED ) val = GAUGE_MIN_AIRSPEED;
return val;
}

Non-linearity tables will also accept negative figures. However, if you are driving an anti-clockwise needle in
FS2K+ then you will encounter problems if you are trying to drive the needle from a callback created by
yourself – the needle will insist on rotating clockwise and will completely ignore any non-linearity table telling
it to do otherwise. There is no problem if you trigger the callback using a token variable.

Below is a code snip taken from an oil pressure gauge where the oil pressure needle works anti-clockwise. To
get the anti-clockwise movement from a callback created by the programmer we must resort to the old trick of
calling via a "dummy variable"; in this case I have used the AIRSPEED token variable to correctly trigger the oil
pressure callback and then run a lookup on the oil pressure in the callback code. For comparison I have
illustrated both callback types here but the token variable-triggered one is commented out. I've also changed the
callback names to make it more obvious which one is doing what.

One thing to note is that whether the anti-clockwise callback is created from a token variable or by the
programmer, the anti-clockwise movement non-linearity table must be reversed (upside-down, if you like!).

#define FOP1_MAX_OIL 10000

NONLINEARITY fop1_set_oil[] =
{
{{53, 12}, 16384.000000, 0},
{{72, 30}, 10000.000000, 0},
{{73, 35}, 9500.000000, 0},
{{74, 39}, 9000.000000, 0},
{{75, 40}, 8500.000000, 0},
{{77, 46}, 7500.000000, 0},
{{78, 48}, 7000.000000, 0},
{{76, 52}, 6000.000000, 0},
{{74, 57}, 5000.000000, 0},
{{71, 59}, 4000.000000, 0},

Dragonflight Design Page 27 of 112


{{70, 63}, 3000.000000, 0},
{{68, 64}, 2500.000000, 0},
{{67, 65}, 2000.000000, 0},
{{65, 65}, 1500.000000, 0},
{{62, 70}, 1000.000000, 0},
{{59, 72}, 500.000000, 0},
{{55, 74}, 00.000000, 0},
};

NEEDLE_UPDATE_CALLBACK programmer_oil_cb;
// NEEDLE_UPDATE_CALLBACK token_variable_oil_cb;

MODULE_VAR fop1_oil = {GENERAL_ENGINE1_OIL_PRES};

MAKE_NEEDLE(fop1_op_ind,
BMP_NEEDLE,
NULL,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
44, 44,
16, 6,
AIRSPEED,
programmer_oil_cb,
fop1_set_oil,
0 )

PELEMENT_HEADER fop1_plist1[] =
{
&fop1_op_ind.header,
NULL
};

/* For contrast this macro setup will work correctly without any "fiddling"

MAKE_NEEDLE(fop1_op_ind,
BMP_NEEDLE,
NULL,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
44, 44,
16, 6,
GENERAL_ENGINE1_OIL_PRES,
token_variable_oil_cb,
fop1_set_oil,
0 )

PELEMENT_HEADER fop1_plist1[] =
{
&fop1_op_ind.header,
NULL
};

*/

MAKE_STATIC(fop1_background,
BMP_BG1,
&fop1_plist1,
NULL,
IMAGE_USE_TRANSPARENCY,
0,
0, 0 )

PELEMENT_HEADER fop1_list = &fop1_background.header;

//---------------------------------------------------------------------------
FLOAT64 FSAPI programmer_oil_cb (PELEMENT_NEEDLE pelement )
{
//Trigger the callback lookup for airspeed

FLOAT64 val = pelement->source_var.var_value.n;


//Now substitute our own information into val

lookup_var(&fop1_oil);
val = fop1_oil.var_value.n;

if (val > FOP1_MAX_OIL)


{
val = FOP1_MAX_OIL;
}

return val;
}

//---------------------------------------------------------------------------
/* FLOAT64 FSAPI token_variable_oil_cb (PELEMENT_NEEDLE pelement )

{
//This token-variable driven callback works directly without any problems

FLOAT64 val = pelement->source_var.var_value.n;

if (val > FOP1_MAX_OIL)


{
val = FOP1_MAX_OIL;
}

return val;
}
*/

Interestingly, the problem does not exist in CFS or CFS2: you can call anti-clockwise needle movements via a
token variable or your own callback and the needle will behave perfectly……. guess what? - go figure!

Updating String Displays


Following the gauge header definitions you must add the three lines that define the font type to be used in the
string:-

#define GAUGE_CHARSET DEFAULT_CHARSET


#define GAUGE_FONT_DEFAULT "Courier New"
#define GAUGE_WEIGHT_DEFAULT FW_NORMAL

The first line is the default character set for UK/US English; as most aircraft instruments outside of the old
Soviet Union are labelled in English you‟d very rarely want to change this line. If you are using Cyrillic
characters then you could use RUSSIAN_CHARSET instead. I just hope you can use a Russian keyboard.....!

The second line defines the font set name and as you don‟t know what fonts or operating system the end user of
your gauge has on his PC, I suggest you stick to the following four fonts that are supplied with Windows95
(even if you are using Windows NT, Windows98, Windows2000, WindowsXP or Windows Millenium) plus
the three supplied with and installed by FS2K+:

Courier New
Arial
Times New Roman
Helvetica
Quartz
Glass Gauge
Arial Narrow Bold Italic.

The last line defines how the font is to be displayed and has the flags listed below:-

FW_THIN
FW_EXTRALIGHT
FW_LIGHT
FW_NORMAL

Dragonflight Design Page 29 of 112


FW_MEDIUM
FW_SEMIBOLD
FW_BOLD
FW_EXTRABOLD
FW_HEAVY

No useful italic, I‟m afraid!

Sooner or later you are going to want to update a string display with information that comes from a calculation
of your own. Declare all three token variable lines as MODULE_VAR_NONE and then force the string update by
simply flipping an unused variable on a mouse-click or whatever……. thanks to Daniel Steiner for coming up
with this one.

This is an example from my Collins Avionics Radio package. The first part is the string information:- note that
active_units_update line is the part that gets the updated information (the “callback”).

STRING_UPDATE_CALLBACK active_units_update;

MAKE_STRING(active_whole,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
13, 19,
70, 30,
3,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
active_units_update)

Now, I want to return a display that, depending on the action, either shows the currently active comm frequency
(whole numbers) or displays “Ch.”. memory is a variable set from a mouse-click on the gauge.

FLOAT64 FSAPI active_units_update( PELEMENT_STRING pelement )

{
FLOAT64 val=pelement->source_var[0].var_value.n;
val = !val;

if ( memory == 1)
{
wsprintf(pelement->string, "%3s", "Ch.");
}
else
{
wsprintf(pelement->string, "%3d", active_units);
}
return val;
}

The first line in the body of the structure is an FS2K+ requirement even if you are not actually reading a Token
Variable. It seems to be needed to trigger FS to check if the string needs updating. The next line does the dirty
deed and forces FS to run an update; each time it runs through this line it simply inverts the previous value of
val thus creating the change necessary to trigger the update function. What's good about this method is that the
update is only done when you require it, thus freeing up resources to increase the framerate. Note the change
from decimal to string display too, depending on what needs to be shown…..

To print decimal values use SPRINTF (see the VC help). This is an example used in a gauge to update a
Kohlsman millibars variable:-
STRING_UPDATE_CALLBACK update_press;

MAKE_STRING(millibars,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
13, 19,
70, 30,
6,
KOLLSMAN_SETTING_MILLIBARS, //Note the mis-spelling of Kohlsman
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
update_press)

//-------------------------------------------------

FLOAT64 FSAPI update_press(PELEMENT_STRING pelement )


{
FLOAT64 val=pelement->source_var[0].var_value.n;
val = !val;

if (val >= 948.2 && val <= 1049.5)


{
sprintf(pelement->string, "%6.1f", (FLOAT64)val);
}
else
if(val < 948.2 || val > 1049.5)
{
wsprintf(pelement->string, "%6 ", (UINT32)val);
}
return val;
}

For decimal value always use the "f" flag. In the example, "%6.1f" means a total of six digits with one decimal
(so four integers, the decimal point and then the decimal digit). The second part of the routine prints a blank
string when the value is out of range. In this case the string display really is using the first line of the update_press
callback routine to get and return the pressure value.

If you look at the SDK.Temperature.c source file included with the SDK samples you will see that the code is
using line 10 (token variable DISPLAY_UNITS) to modify the string display created by line 9 (token variable
TOTAL_AIR_TEMP). This opens up a whole new ballgame for string displays as you can now read up to three
token variables using

FLOAT64 val=pelement->source_var[0].var_value.n; //Token var 1


FLOAT64 val=pelement->source_var[1].var_value.n; //Token var 2
FLOAT64 val=pelement->source_var[2].var_value.n; //Token var 3

and then work on the results before sending the final answer to be displayed. Calculating time/distance/speed
etc. for navigational instruments seems to be the obvious use.

FS calculates the best font size for legibility based on the height and width specified in line 7 and the number of
characters specified in line 8 of the ELEMENT_STRING macro. Unlike a normally-generated C program, it
appears that there is no way to specify the justification of a string display (i.e. start from the left margin, centre
the line or start from the right margin) except to the limited extent allowed by the DT_xxxx flags.

There also does not appear to be a way of directly updating a string from e.g. a mouse-click. FS2K+ requires
that you do it through the callback routine as all string calls are the same i.e.

Dragonflight Design Page 31 of 112


wsprintf(pelement->string, "%6 ", val)

This goes to make life a little more awkward but not impossible. As an example this is part of my Collins
DME-42; the test is started by pushing a button and the instrument then sets all displays to "-" followed by "8"
to test all display filaments. It then reverts to the current display. This illustrates string updates being controlled
from a mouse-click, a callback routine and a continuous update and all being displayed through the callback
routine. Although at first this method of putting everything through the callback routine looks awkward, the end
result is tidier in that if something fails to display you know that the command to display is only coming from
one area and thus debugging becomes easier.

//Knots readout

MAKE_STRING(dme_knots,
&dme_plist5,
dme_elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
71, 108,
80, 30,
3,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,128,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
dme_update_knots)

PELEMENT_HEADER dme_plist6[] =
{
&dme_knots.header,
NULL
};

//---------------------------------------------------------------------------

BOOL FSAPI dme_test( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
lookup_var(&dme_time1);
dme_time2 = dme_time1.var_value.n + 18;
dme_do_test = 1;

return FALSE;
}

//---------------------------------------------------------------------------

FLOAT64 FSAPI dme_update_knots(PELEMENT_STRING pelement )


{
FLOAT64 val = pelement->source_var[0].var_value.n;
val = !val;

//Only do something if power is applied

if (power_on == 1)
{

//First part of test

if (dme_do_test == 1)
{
wsprintf(pelement->string, "%3s", "---");
}
else
//Second part of test

if (dme_do_test == 2)
{
wsprintf(pelement->string, "%3s", "888");
}
else

//If DME is turned on and station is out of range then display lines
//otherwise display time or distance as demanded

if (dme_knob_set != 0 && dme_do_test == 0)


{
if (dme_no_data == 0)
{
wsprintf(pelement->string, "%3s", "---");
}
else
{
wsprintf(pelement->string, "%3d", dme_knots_mins);
}
}
else

//If power is applied and DME is turned off or the test is finished then blank the display

if (dme_knob_set == 0 && dme_do_test == 0)


{
wsprintf(pelement->string, "%3s", "");
}
}
else

//Power off so kill the DME display

if (power_on == 0)
{
wsprintf(pelement->string, "%3s", "");
}
return val;
}

//---------------------------------------------------------------------------
void FSAPI dme_update (PGAUGEHDR pgauge, int service_id, UINT32 extra_data)
{
switch(service_id)
{

case PANEL_SERVICE_PRE_UPDATE:

lookup_var(&dme1_dist);
lookup_var(&dme1_speed);
lookup_var(&dme_time1);

dme_distance = dme1_dist.var_value.n;
dme_miles = dme1_dist.var_value.n;
dme_tenths = (dme_miles * 10.0) - (dme_miles * 10);
dme_speed = dme1_speed.var_value.n;

//Main updates

if (dme_do_test == 0)
{

//Calculate time to DME to the nearest minute

if (dme_knob_set == 1)
{
dme_distance = dme_distance * 60;
dme_knots_mins = (dme_distance/dme_speed) % 60;
}
else

Dragonflight Design Page 33 of 112


if (dme_knob_set == 2)
{
dme_knots_mins = dme_speed;
}

//Switch display to lines if out of range

if (dme_knob_set == 1 || dme_knob_set == 2)
{
if (dme_miles == -1)
{
dme_no_data = 0;
}
else
{
dme_no_data = 1;
}
}
}
else

//Test routine called: 1 second of lines followed by 2 seconds of "8"

if (dme_do_test != 0 && power_on == 1)


{
if (dme_time1.var_value.n == dme_time2 && dme_do_test == 1)
{
dme_do_test = 2;
dme_time2 = dme_time1.var_value.n + 36;
}

if (dme_time1.var_value.n == dme_time2 && dme_do_test == 2)


{
dme_do_test = 0;
dme_time2 = -1;
}
}

//No power so kill DME test if in action

if (power_on == 0)
{
dme_do_test = 0;
}

break;

Do you remember my comment about the speed of DME function direct callbacks in ELEMENT_STRING
above? Well, this is the fast way of doing it!!!

Sequential Selection in String Displays


On the default panels you‟ll have already found that the item that you are trying to change by mouse or
keyboard e.g. a radio frequency, may highlight in yellow. This is done by the SEQ_REC command which is the
last line of a MAKE_STRING macro. This example shows how to highlight the two halves of the radio frequency
but please note one very important point: the SEQ_REC tables must come before the MAKE_STRING macros,
otherwise it will not work.

SEQ_REC seqrec1[] =
{
{SELECT_COM_WHOLE, 0, 2},
{SELECT_NONE, 0, 0},
};

SEQ_REC seqrec2[] =
{
{SELECT_COM_FRACTION, 0, 1},
{SELECT_NONE, 0, 0},
};

MAKE_STRING( com_frequency_major,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
13, 19,
70, 30,
3,
COM_FREQUENCY,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
3,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
seqreq1,
frequency_update)

MAKE_STRING( com_frequency_minor,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
13, 19,
70, 30,
2,
COM_FREQUENCY,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
2,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
seqrec2,
frequency_update)

PELEMENT_HEADER com_plist[] =
{
&com_frequency_major.header,
&com_frequency_minor.header,
NULL
};

When the com radio major frequency is mouse-clicked or selected by keyboard, seqrec1 tells FS2K+ to highlight
the first three digits of the frequency (0,1,2) according to the colour listed in line 15 of the MAKE_STRING macro
com_freq_major. When the fractional part of the frequency is called, seqrec2 tells it to highlight the two digits (0, 1)
of the frequency in the MAKE_STRING macro com_freq_minor. Remember that the count starts from zero. In theory
both seq_rec tables can be combined but in practice it tends to fail. It‟s as if FS gets confused as to exactly which
section it is looking at so if you‟re programming information that is split as whole units and fractional units, I‟d
stick to separate tables and macros.

The fsxgauges_sp2.h file contains a complete listing of all available SEQ_REC sequel selection variables.

Autopilot Manually Tuneable Flags


There are two calls to the autopilot that can be set manually by the pilot – these are to set the target altitude and
to set the target heading. If you do not add these flags to the MAKE_STRING macro and are using line 21 directly
(i.e. by using a Token Variable rather than by code in the Update section) then FS will lock to the current
altitude and current heading respectively. I can't prove this but I guess the update callback (in this case

Dragonflight Design Page 35 of 112


update_display), which always has to be an integer, is passed by the macro into line 5 and then FS acts on it.

MAKE_STRING( string_alt_lock,
NULL,
fail_alt_lock,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
ASI_ALT_MANUALLY_TUNABLE,
115, 7,
100, 14,
5,
AUTOPILOT_ALTITUDE_LOCK_VAR,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
3,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
update_display)

MAKE_STRING( string_head_lock,
NULL,
fail_head_lock,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
ASI_HEADING_MANUALLY_TUNABLE,
47, 7,
40, 14,
3,
AUTOPILOT_HEADING_LOCK,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,0,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
3,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
update_display)

Mouse Callback Functions


The information in the SDK is another area that is not so clear as it could be. Most of the available cursor flags
do not work – you are restricted to CURSOR_HAND, CURSOR_DOWNARROW and CURSOR_UPARROW only because
the others are simply repeats of these under different names. At one point any MOUSE_RIGHTxxx flag was
unusable, but this was corrected in FS2K2.

The mouse function macro is always required in FS2K+/CFS+ even if you do not want a mouse function in
your gauge.

Again it is possible to use the callback function to create your own events. This is a section taken from my
SD3-60 ASI again and here the mouse is being used to increment/decrement the position of the airspeed bug.
You can also see that the movement for the needle is being restricted to above 50 knots and below 260 knots
and that asi selection is in 10-knot increments/decrements. The actual arc of movement as displayed on the asi
gauge is defined by a non-linearity table (set_airspeed_bug).

// Set up gauge header


char asi_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER asi_list;
extern MOUSERECT asi_mouse_rect[];
GAUGE_HEADER_FS700(GAUGE_W, asi_gauge_name, &asi_list, asi_mouse_rect,0,0,0 );
MOUSE_FUNCTION set_speed_down;
MOUSE_FUNCTION set_speed_up;

MOUSE_BEGIN(asi_mouse_rect,0,0,0)
MOUSE_CHILD_FUNCT(35,255,25,34,CURSOR_DOWNARROW,MOUSE_LEFTSINGLE,set_speed_down )
MOUSE_CHILD_FUNCT(60,255,25,34,CURSOR_UPARROW,MOUSE_LEFTSINGLE,set_speed_up )
MOUSE_END

NEEDLE_UPDATE_CALLBACK needle_set_src_cb;

MAKE_NEEDLE(asi_set,
BMP_ASI_SET,
NULL,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
0, 12,
MODULE_VAR_NONE,
needle_set_src_cb,
set_airspeed_bug,
0 )

//----------------------------------------------------------------------------

BOOL FSAPI set_speed_up( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
airspeed = airspeed + 10;
if ( airspeed > 260 )
{
airspeed = 260; //Crop return to a maximum of 260
}
return FALSE;
}

//----------------------------------------------------------------------------

BOOL FSAPI set_speed_down( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
airspeed = airspeed - 10;
if ( airspeed < 50 )
{
airspeed = 50; //Crop return to a minimum of 50
}
return FALSE;
}

//----------------------------------------------------------------------------

FLOAT64 FSAPI needle_set_src_cb ( PELEMENT_NEEDLE pelement)

{
return airspeed;
}

A much simpler thing to do is just to turn an indicator on and off: this is taken from my YG-7500 Radar
Altimeter and is the decision height lamp.
MOUSE_FUNCTION ap_set;
MOUSE_FUNCTION ap_dis;

MOUSE_BEGIN(radalt_mouse_rect,0,0,0)
//Set Radalt active
MOUSE_CHILD_FUNCT( 30, 270, 26, 15, CURSOR_UPARROW, MOUSE_LEFTSINGLE, ap_set )

//Set Radalt inactive


MOUSE_CHILD_FUNCT( 43, 258, 26, 15, CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, ap_dis )
MOUSE_END

//Track the state of the light

UINT32 light_state = 0;

Dragonflight Design Page 37 of 112


MAKE_ICON(greenlight,
BMP_GREENON,
NULL,
fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT | IMAGE_HIDDEN,
0,
215,130,
MODULE_VAR_NONE, NULL,
ICON_SWITCH_TYPE_SET_CUR_ICON,
1,
0,
0, )

//---------------------------------------------------------------------------

BOOL FSAPI ap_set( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
light_state = 1; //Turn the Green LED on
return FALSE;
}
//---------------------------------------------------------------------------

BOOL FSAPI ap_dis( PPIXPOINT relative_point, FLAGS32 mouse_flags )

{
light_state = 0; //Turn the Green LED off
return FALSE;
}

//-----------------------------------------------------------------
void FSAPI yg7500_update( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{
case PANEL_SERVICE_PRE_UPDATE:

if (light_state == 0)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],1);
}
else
if (light_state == 1)
{
SHOW_LISTELEMENT(pgauge->elements_list[0],1);
}
break;
}
}

The FS2K+/CFS+ SDKs are all totally lacking in information about the use of mouse dragging. Again, as with
the SHOW/HIDE macros, the information is in the fsxgauges_sp2.h file but does not work at all….. Rolf Dieter
Buckmann came up with the solution below.

pstat is the name that Rolf gave to the variable that holds the mouse position but it can be anything you want;
this only works with a MAKE_STATIC macro though. Normally this won‟t be a problem as you will almost
always have a MAKE_STATIC as the background image of the sub-gauge.

In the sub-gauge add the following line to the initialise routine:-

case PANEL_SERVICE_PRE_INITIALIZE:

//Get mouse position

pstat = (PELEMENT_STATIC_IMAGE)pgauge->elements_list[0]->next_element[0];

break;

See the similarity to the Show/Hide background macro solution? You now have the elements in place to read
the relative position of the mouse cursor on the background of your gauge. To actually get the relative position
you need this (where new_pos is a UINT variable):-
new_pos = pstat->image_data.final->dim.x - relative_point->x;

or
new_pos = pstat->image_data.final->dim.y - relative_point->y;

The obvious use of the mouse drag is engine control levers but as with the old gauhlp series, as an example I‟m
going to give you the code to build a steering tiller!

This is the master source file 360.tiller.c:-

#include "..\inc\gauges.h"
#include "..\inc\eventid.h"
#include "360.tiller.h"

/***************** Global Variables **************************/

//Included gauges list

#define GAUGE_NAME "tiller\0"


#define GAUGEHDR_VAR_NAME gaugehdr_tiller
#define GAUGE_W 100

#include "tiller.c"

//---------------------------------------------------------------------------
// Gauge table entries

GAUGE_TABLE_BEGIN()
GAUGE_TABLE_ENTRY(&gaugehdr_tiller)
GAUGE_TABLE_END()

This is the sub-gauge source file (tiller.c).

// Set up gauge header


char tiller_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER tiller_list;
extern MOUSERECT tiller_mouse_rect[];
GAUGE_CALLBACK tiller_update;
GAUGE_HEADER_FS700(GAUGE_W,tiller_gauge_name,&tiller_list,tiller_mouse_rect,tiller_update,0,0
,0);

MOUSE_FUNCTION tiller_move;
MOUSE_BEGIN( tiller_mouse_rect, 0, 0, 0 )
MOUSE_CHILD_FUNCT(0,0,170,40,CURSOR_HAND, MOUSE_LEFTDRAG|MOUSE_LEFTSINGLE,tiller_move )
MOUSE_END

/*Lever movement - relative_point reads from right to left so a mouse box always has 0 as the
far right figure and your SCREEN_DIVISIONS figure as the far left figure. See mouse callback
routine */

NONLINEARITY rudder_readout[] =
{
{{0, 10}, 128.000000, 0},
{{86, 0}, 64.000000, 0},
{{162, 10}, 0.000000, 0},
};

FLOAT64 rudder = 64.000000; //Return for callback initialised as the central pos
FLOAT64 old_pos = 0; //Comparison
FLOAT64 new_pos = 0; //Current nosewheel position
FLOAT64 centre_pos = 0; //Central point of the mouse box
FLOAT64 back_size = 0; //Relative size of the mouse box depending on screen
UINT32 rudder_pos = 0; //Nosewheel position
UINT32 x = 0;

NEEDLE_UPDATE_CALLBACK tiller_cb;

FAILURE_RECORD fail[] =
{
{FAIL_SYSTEM_ELECTRICAL_PANELS, FAIL_ACTION_ZERO},
{FAIL_NONE, FAIL_ACTION_NONE}

Dragonflight Design Page 39 of 112


};

MAKE_NEEDLE(tiller_needle,
TILLER_BMP_NEEDLE,
NULL,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
85, 178,
37, 69,
MODULE_VAR_NONE,
tiller_cb,
rudder_readout,
1 )

PELEMENT_HEADER tiller_plist1[] =
{
&tiller_needle.header,
NULL
};

MAKE_STATIC(tiller_background,
TILLER_BMP_BACKGROUND,
&tiller_plist1,
NULL,
IMAGE_USE_TRANSPARENCY,
0,
0, 0 )

PELEMENT_HEADER tiller_list = &tiller_background.header;

//------------------------------------------------------------------------

BOOL FSAPI tiller_move ( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{

//Get current mouse position relative to the size of the mousebox vs. screen resolution

new_pos = pstat->image_data.final->dim.x - relative_point->x;

/*Divide the distance that the mouse moves over by 128 <SCREEN_DIVISIONS> – this is a purely
arbitrary figure because it makes later calculations easier */

back_size = pstat->image_data.final->dim.x;
back_size = back_size / 128;

old_pos = new_pos / back_size;

//Ensure that the division figure is never out of limits

if (old_pos < 0)
{
old_pos = 0;
}

if (old_pos > 128)


{
old_pos = 128;
}

//Is the handle in the null zone?

if (old_pos > 55 && old_pos < 73)


{
rudder_pos = 0;
}

//Calculate turn nosewheel

else
{

//Compensate for null zone

if (old_pos < 56)


{
old_pos = old_pos + 8;
}
else
if (old_pos > 72)
{
old_pos = old_pos - 8;
}

rudder_pos = old_pos * 128;

if (rudder_pos > 16383)


{
rudder_pos = 16383;
}

//Invert the rudder position to turn left

if (old_pos > 65)


{
x = rudder_pos *2;
rudder_pos = rudder_pos - x;
}
}

//Now turn the nosewheel and set current nosewheel position

trigger_key_event(KEY_RUDDER_SET, rudder_pos);
rudder = old_pos;

return FALSE;
}

//---------------------------------------------------------------------------

void FSAPI tiller_update( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )


{
switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
break;

case PANEL_SERVICE_PRE_INITIALIZE:

//Get mouse position on background image

pstat = (PELEMENT_STATIC_IMAGE)pgauge->elements_list[0]->next_element[0];

break;

case PANEL_SERVICE_PRE_UPDATE:
break;

case PANEL_SERVICE_PRE_DRAW:
break;

case PANEL_SERVICE_PRE_KILL:
break;
}
}

//---------------------------------------------------------------------------
FLOAT64 FSAPI tiller_cb (PELEMENT_NEEDLE var)
{
return rudder;
}

//----------------------------------------------
#undef GAUGE_NAME
#undef GAUGEHDR_VAR_NAME
#undef GAUGE_W

Now, I have done one other thing with the mouse_flags; I've allowed the user to use a left-click as well as
dragging the tiller. This gives your hand a rest on a long taxiway! There is also nothing to stop you
implementing mouse_drag in both the x and y directions at the same time.

Dragonflight Design Page 41 of 112


Finally, you can change the cursor icon dynamically if you require. The following information came from
Claude Troncy; all I have done is clean up the presentation slightly. In certain conditions he wanted the hand
cursor to be displayed above the rectangle associated with mouse_callback2 but at other times he required the
standard cursor arrow to display in the same area.

MOUSE_BEGIN( Gauge_mouse_rect, HELPID_SOPWITH_MAG, 0, 0 )


MOUSE_CHILD_FUNCT( 0, 0, 18, 18,CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, mouse_callback1) //
this is mouse_rect[1]
MOUSE_CHILD_FUNCT( 19, 46, 8, 9, CURSOR_HAND, MOUSE_LEFTSINGLE,mouse_callback2) // this is
mouse_rect[2]
MOUSE_END

In the Gauge Callback function just execute:


case PANEL_SERVICE_PRE_UPDATE:
if (Change_state == TRUE) // Boolean indicate that we have to change the cursor
{
if (New_Cursor==hand) // Variable which takes value „hand‟ or „none‟
pgauge->mouse_rect[2].cursor = CURSOR_HAND;
else
pgauge->mouse_rect[2].cursor = CURSOR_NONE;
}

So finally, how do you use the MOUSE_RIGHTXXXX macros? Surprisingly easily, as it turns out. I can‟t
remember who I got this piece of code from, so please accept my apologies, whoever you may be.

BOOL FSAPI YourMouseCb ( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
if ( mouse_flags & MOUSE_LEFTSINGLE )
{
//
// Left Mouse Button Functionality here
//
}
else if ( mouse_flags & MOUSE_RIGHTSINGLE )
{
//
// Right Mouse Button Functionality here
//
}

return FALSE;
}

The same applies for MOUSE_LEFTRELEASE and MOUSE_RIGHTRELEASE flags.

The Dirty Mouse Callback Problem

Under certain, rather common circumstances, the mouse callback function does not deliver clean results.
Suppose we have this declaration for the mouse function:

MOUSE_CHILD_FUNCT(0,0,18,18,CURSOR_UPARROW, MOUSE_LEFTSINGLE | MOUSE_LEFTRELEASE |


MOUSE_DOWN_REPEAT, setfuel_mcb)

In the mouse callback function itself, we want to use MOUSE_LEFTSINGLE to increment a counter and
MOUSE_LEFTRELEASE to indicate that we have finished incrementing the counter.

BOOL FSAPI setfuel_mcb (PPIXPOINT relative_point, FLAGS32 mouse_flags)


{
if (mouse_flags & MOUSE_LEFTSINGLE)
{
iRefuelCounter++;
iRefuelInProgress=1;
}
else if(mouse_flags & MOUSE_LEFTRELEASE)
iRefuelInProgress=0;

return FALSE;
}
At the point that MOUSE_LEFTRELEASE is actioned our variable iRefuelCounter will be filled with random
rubbish. I have traced this to the use of MOUSE_DOWN_REPEAT but have not been able to find a way to stop
it. It does not occur if the MOUSE_DOWN_REPEAT flag is not used and it does not affect a variable that is being set
absolutely i.e. in our case the variable iRefuelInProgress. It also does not happen if you use different buttons
e.g.

MOUSE_CHILD_FUNCT(0,0,18,18,CURSOR_UPARROW, MOUSE_LEFTSINGLE | MOUSE_RIGHTSINGLE |


MOUSE_DOWN_REPEAT, setfuel_mcb)

BOOL FSAPI setfuel_mcb (PPIXPOINT relative_point, FLAGS32 mouse_flags)


{
if (mouse_flags & MOUSE_LEFTSINGLE)
{
iRefuelCounter++;
iRefuelInProgress=1;
}
else if(mouse_flags & MOUSE_RIGHTSINGLE)
iRefuelInProgress=0;

return FALSE;
}

Animating Icons
Another method of returning a result to a MAKE_ICON macro is particularly important when you use the
ICON_SWITCH_TYPE_STEP_TO method of animating icons. Although there are only two states for an LED (on and
off), you might have a succession of icons to display (such as the five keys that appear in the default Cessna
magneto switch). This is a fragment from my Engine Starter Pack and is the magneto switch using the default
Cessna keys. It also illustrates usage of the GAUGE_CALLBACK function.

// Set up gauge header


char rmi_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER mag_list;
extern MOUSERECT mag_mouse_rect[];
GAUGE_CALLBACK mag_switches;
GAUGE_HEADER_FS700(GAUGE_W,mag_gauge_name,&mag_list,mag_mouse_rect,mag_switches,0,0,0 );

UINT32 startup = 0;
UINT32 time_check = 0;
UINT32 pos = 0;

MOUSE_FUNCTION mag_plus;
MOUSE_FUNCTION mag_minus;

MOUSE_BEGIN( mag_mouse_rect, HELPID_SOPWITH_MAG, 0, 0 )


MOUSE_CHILD_FUNCT( 2, 10, 40, 80, CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, mag_minus )
MOUSE_CHILD_FUNCT( 42, 10, 40, 80, CURSOR_UPARROW, MOUSE_LEFTSINGLE, mag_plus )
MOUSE_END

MAKE_ICON(mag_off,
BMP_MAG_OFF,
NULL,
fail,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY,
0,
19, 34,
MODULE_VAR_NONE, switchpos,
ICON_SWITCH_TYPE_STEP_TO,
5,
0,
0)

It sets the magneto off position as the starting point ( BMP_MAG_OFF) and defines five icons as being in the
sequence (line 10). The callback function switchpos defines which icon in that sequence is being displayed (from
0 to 4) at any one time. Remember the resource ids in the header file?

Dragonflight Design Page 43 of 112


#define BMP_MAG_OFF 0x2000
#define BMP_MAG_RIGHT 0x2001
#define BMP_MAG_LEFT 0x2002
#define BMP_MAG_BOTH 0x2003
#define BMP_MAG_START 0x2004

The work of calling the individual icons looks like this:-

//--------------------------------------------------------------

FLOAT64 FSAPI switchpos( PELEMENT_ICON pelement )


{
return pos;
}
//--------------------------------------------------------------------
BOOL FSAPI mag_plus( PPIXPOINT relative_point, FLAGS32 mouse_flags )
{
pos = pos + 1;
if (pos > 4) //Trap multiple key clicks
{
pos = 4;
}

if (pos == 1 && engine_start == 1)


{
trigger_key_event(KEY_MAGNETO1_RIGHT,0);
}

if (pos == 2 && engine_start == 1)


{
trigger_key_event(KEY_MAGNETO1_LEFT,0);
}

if (pos == 3 && engine_start == 1)


{
trigger_key_event(KEY_MAGNETO1_BOTH,0);
}

if (pos == 4)
{
lookup_var(&ticks); //Get current state of TICK18
time = ticks.var_value.n;
time_check = time + 36; //Add 36 to make a 2 second delay
}

return TRUE;
}
//--------------------------------------------------------------
void FSAPI mag_switches( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
break;

case PANEL_SERVICE_PRE_UPDATE:

lookup_var(&ticks);
time = ticks.var_value.n; //Get the current time

if (pos == 4 && time == time_check) //If current time matches the delay time
{ //and key position is “Start”
engine_start = 1; //then allow the mags to fire
pos = 3; //Move key back to “Both”
time_check = -1; //Set delay time to something impossible
trigger_key_event(KEY_MAGNETO1_START,0); //Startup!!
}
if (!startup) //Ensure mags are off when panel loads
{
startup = 1;
trigger_key_event(KEY_MAGNETO1_OFF,0);
}

break;

case PANEL_SERVICE_PRE_DRAW:
// "draw_routine()
break;

case PANEL_SERVICE_PRE_KILL:
// "kill_routine()
break;
}
}

The missing anti-clockwise movement of the key you can work out for yourselves! The variable engine_start
prevents the magnetos from being activated until the key has been turned to the Start position.

Masks
Microsoft describe these as an area which “hides part of an image” and which a “texture slides and rotates
underneath”. They‟re used for the ELEMENT_MOVING_IMAGE (MAKE_MOVING) and ELEMENT_SPRITE
(MAKE_SLIDER) macros. A far easier way of thinking of them is an area inside which another element of the
gauge (such as a lever) can move and display; outside of this area the image will not display. The top left corner
of the mask defines the top and left area that the internal image will move to and the bottom right of the mask is
the maximum right and down that the internal image will move to and still be visible. A mask has a special
colour - RGB values of 1,1,1. I keep a special Paintshop palette specifically for masks which consists of three
colours; 0,0,0 (transparency), 1,1,1 (mask) and 255,0,0 (red). The last colour I use for checking the mask
boundaries by flood fill before finally flooding the red areas with transparency. The example below is taken
from the SDK.Attitude.c supplied with the FS2K SDK. Imagine the pitch and bank bitmap being masked by the
red area and the central artificial horizon but showing in the black area and you begin to get the idea. In reality
the red area will be transparent to show the background bitmap through but transparency acts in the same way.

SDK.Attitude.mask2.bmp as supplied Transparency flooded to reveal the mask

The easiest way to create a simple mask is to simply to cut a chunk out of the background bitmap which is the
size and position of the “moving area”, then repaint as necessary with 0,0,0 and 1,1,1 colours. While cutting the
bitmap, make a note of the top-left x,y co-ordinates (these are shown on the bottom status bar in PaintShop) as
these will be the co-ordinates you‟ll need in the display macro. Now paint everything except the mask area in
red, then paint the mask area itself in mask-black (1,1,1). Now flood-fill the red with transparent-black (0,0,0)
and the mask is ready for use. Bear in mind that you can draw something else over the top of the mask by being
careful with plist order; the artificial horizon shown above isn‟t necessary on both masks as the SDK.Attitude
suggests they are. You can prove it by modifying the original SDK masks and removing the artificial horizon
from mask2; re-compile the gauge and it will still work.

Dragonflight Design Page 45 of 112


So you‟ve got the mask; now how do you use it? You know where on the background bitmap the mask is
positioned but where does the moving image go? Insofar as the moving part of the image is concerned, the
centre of the moving image is the part that FS2K+ calculates the movement distance from. Very important,
that! If you think about it, that makes it possible to have part of the moving image disappear off the edge of the
mask if you don‟t get your callback calculations right. Or maybe you wanted that to happen?

ADI84A background and V-bars

For an example of mask usage in two dimensions I‟m going to take a chunk out of the code for my Collins
ADI84A attitude direction indicator. This deals with the motion of the V-bars, a small yellow indicator that tells
the aircraft flight director (not the pilot!) to turn left or right to maintain the heading and up or down to maintain
the glideslope. The figures listed below the MODULE_VAR_NONE lines are the range that the preceding line
callback function deals with (none in my case!) and will depend entirely on what you are trying to achieve. In
this case, -20,20 refers to the maximum number of degrees left or right of the current heading and 0,240 is the
range I have allowed to deal with the return from the glideslope capture. As you can see, MAKE_MOVING will
accept both positive and negative figures. ADI84A background has the mask area marked on it in red; the
centre circular area of the mask is painted in 1,1,1 and the outside edges are transparent 0,0,0 to show the
background through.

MOVING_IMAGE_UPDATE_CALLBACK OBIX; //V-bars – OBI hdg


MOVING_IMAGE_UPDATE_CALLBACK GlideY; //V-bars – glideslope return

MODULE_VAR Heading = {VOR1_OBI};


MODULE_VAR Nose = {PLANE_HEADING_DEGREES_MAGNETIC};
MODULE_VAR VbarSlope = {VOR1_GS_NEEDLE};

MAKE_MOVING(vbars,
BMP_VBARS,
&plist11,
fail2,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY,
0,
52,76,
MODULE_VAR_NONE, OBIX,
-20, 20,
MODULE_VAR_NONE, GlideY,
0, 240)

PELEMENT_HEADER plist12[] =
{
&vbars.header,
NULL
};

//---------------------------------------------------------------------------
FLOAT64 FSAPI OBIX(PELEMENT_MOVING_IMAGE pelement)
{

//Required course

lookup_var(&Heading);
Course = Heading.var_value.n;

//Aircraft heading

lookup_var(&Nose);
Hdg = Nose.var_value.n;

//Calculate left or right turn; negative Result = turn left

Result = Course - Hdg;


if (Result < -180)
{
Result = 360 + Result;
}
else
if (Result > 180)
{
Result = -360 + Result;
}

//Now fit answer to display by cropping Result

if (Result < -20)


{
Result = -20;
}

if (Result > 20)


{
Result = 20;
}

return Result;
}

//---------------------------------------------------------------------------
FLOAT64 FSAPI GlideY(PELEMENT_MOVING_IMAGE pelement)
{

//Modify return so that it doesn't *exactly* follow the glideslope indicator


//by subtracting 30 from the final return. This is just visual cosmetics.
//As glideslope return is always a positive figure (0 – 240) also add 120 to the
//figure to allow the V-bars to pitch around the centre of the mask.

if (GlideFlag == 1) //Glideslope captured


{
lookup_var(&VbarSlope);
Slope = VbarSlope.var_value.n;
Slope = Slope + 120;

if (Slope > 190)


{
Slope = 190;
}
if (Slope < 70)
{
Slope = 70;
}
Slope = Slope - 30;
}
else
{
Slope = 90; //Smack on glideslope!!
}

return Slope;
}

(I must also add that the V-bars on a flight director don‟t actually work quite like this – there is a lot more to the

Dragonflight Design Page 47 of 112


behaviour of them and this is only part of the command input. What I‟ve described is better applied to a VOR
equipped with a horizontal needle glideslope indicator).

Make Moving
Moving tapes for altitude readouts are probably the most common use of MAKE_MOVING. In that case however,
the callback only needs to function in one direction (usually y – up and down) but you still need to supply a
return for the other direction. The mask on an altitude readout would normally just be big enough to display one
complete number from the tape. If you have more than one number then you‟ll need a mask per tape. The
ADI84A contains a radar altimeter that works in one direction only and uses a dummy callback variable to
control the x direction.

MOVING_IMAGE_UPDATE_CALLBACK commonX_var;
MOVING_IMAGE_UPDATE_CALLBACK Alt; //Radalt tape callback

MODULE_VAR PHeight = {PLANE_ALTITUDE};


MODULE_VAR GHeight = {GROUND_ALTITUDE};

MAKE_MOVING(radalt,
BMP_RADALT_TAPE,
&tape_list,
fail_power,
IMAGE_USE_ERASE,
0,
248,84,
MODULE_VAR_NONE, commonX_var,
0, 0,
MODULE_VAR_NONE, Alt,
0, 225)

//---------------------------------------------------------------------------
FLOAT64 FSAPI commonX_var(PELEMENT_MOVING_IMAGE pelement)
{
return 1.0; //Dummy callback for y-direction on radalt tape
}
//---------------------------------------------------------------------------

FLOAT64 FSAPI Alt(PELEMENT_MOVING_IMAGE pelement)


{

lookup_var(&PHeight); //Current plane altitude


Plane = PHeight.var_value.n;

lookup_var(&GHeight); //Current ground level


Ground = GHeight.var_value.n; //Return is 1/256 metre/unit so
Ground = Ground /256; //divide by 256 to get actual metres
Feet = Plane - Ground; //Subtract Ground from Plane gets alt metres
Feet = Feet * 3.28083989501312; //Multiply to convert from metres to feet

return Feet; //Radalt tape movement


}

This tape only moves from top to bottom once and then displays a blank; it will reverse as you come down
again. If you‟re using a scrolling tape then you need to overlap the ends (how far depends purely on the size of
the mask – some experimentation is nearly always necessary). Take a look at the SDK whiskey compass sample
for an example of both tape overlap and the use of the dummy callback variable in the x direction.

Make Slider
MAKE_SLIDER and MAKE_MOVING appear to be very similar with the exception that MAKE_MOVING needs a
mask to function. So what is the difference? Both a simple one and a vital one is the answer: the simple one is
that MAKE_MOVING moves under a mask and cuts off parts of the image that are not supposed to show (e.g. a
pitch and bank indicator) whereas MAKE_SLIDER moves its image on top of whatever‟s underneath and the
entire sliding image shows all of the time, subject to plist order. The vital difference is that it is not possible to
create a user input to drive a MAKE_SLIDER; it can only be driven from a Token Variable. The oversight on
Microsoft's part is that you cannot declare a range return as you can with lines 9 and 11 of a MAKE_MOVING
macro.

This is a snip from the ADI84A (again!) and if you look at the previous section you‟ll see just where the turnco
ball sits on the background. Now, the turn co-ordinator return runs from –128 to + 128 with 0 being the centre
position and you need to set the ball up in the centre of the tube at Return 0. So the starting offset is the centre
position minus half of the ball bitmap; the ADI84A bitmap is 298 pixels wide and the ball bitmap is 12 pixels
wide so I get

(298 / 2) – 6 = 143

for my x offset. Calculating the y offset (number of pixels down) can be done by simply reading the co-
ordinates in your image editing program.

MAKE_SLIDER(turnco,
BMP_BALL,
vbar_list,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
143, 253,
TURN_COORDINATOR_BALL_POS, NULL, 0.47,
MODULE_VAR_NONE, NULL, 0)

PELEMENT_HEADER turnco_list[] =
{
&turnco.header,
NULL
};

The final figure on the TURN_COORDINATOR_BALL_POS, NULL, 0.47, line is a scaling factor to get the ball to
display only across the area I want; in this case within the turnco glass tube. Adding a negative sign (-) in front
of the scaling factor will reverse the movement on-screen. Quite frequently you‟ll only be able to work out the
scaling factor after you‟ve seen where the image displays. To finish this off properly I should have also added
code to the y-axis line to get the ball to slide upwards as it moves across the tube. A simple scaling factor would
not have the desired effect; I also need to add a callback function to take the place of that NULL marker.
Remember that the TURN_COORDINATOR_BALL_POS return runs from negative to positive so all a simple scaling
factor is going to do is to make the ball move up and down a diagonal. Which, at the end of the day, looks
rather silly as your turn co-ordinator ball moves out of the tube and wanders across the face of your
gauge………

Make Sprite
The main difference between MAKE_SLIDER and MAKE_MOVING and MAKE_SPRITE is that MAKE_SPRITE has a
third axis of movement. Whereas as the first two can move in the X and Y directions, MAKE_SPRITE can also
move in the O direction creating an orbital or circular movement. One place that this is obvious is the runway
deviation pointer in the centre of an HSI; the needle section can move left and right depending on the position
of the aircraft relative to the runway direction but it can also move round with the gyro to maintain a set
heading. This also gives the illusion of the needle having moved up and down too. It‟s important to note that the
MAKE_SPRITE macro processes callback information for the moving bitmap in the order X, Y, O. Like
MAKE_MOVING, MAKE_SPRITE requires a mask. So let‟s take a look at a code snip taken from my Collins HSI.

Dragonflight Design Page 49 of 112


HSI OBI Needle (left) and deviation needle (right)

// Set up gauge header


char asi_gauge_name[] = GAUGE_NAME;
extern PELEMENT_HEADER obi_list;
extern MOUSERECT obi_mouse_rect[];
GAUGE_CALLBACK obi_callback;
GAUGE_HEADER_FS700(GAUGE_W,obi_gauge_name,&obi_list,obi_mouse_rect,obi_callback,0,0,0 );

MOUSE_BEGIN(obi_mouse_rect,0,0,0)
MOUSE_END

UINT32 obi = 0;
UINT32 obi_gyro = 0;
UINT32 obi_1 = 0;
UINT32 obi_2 = 0;
UINT32 gyro_card = 0;

MODULE_VAR gyro_hdg = {PLANE_HEADING_DEGREES_MAGNETIC};

SPRITE_UPDATE_CALLBACK OBI_deviation;

MAKE_SPRITE( deviation,
BMP_DEVIATION,
&off_flag_list,
&fail_nav,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
72, 73,
149, 150,
1.5, 1.5,
VOR1_NEEDLE, NULL, -0.632813,
MODULE_VAR_NONE, NULL, 0.000000,
VOR1_OBI, OBI_deviation, -1.000000)

PELEMENT_HEADER deviation_list[] =
{
&deviation.header,
NULL
};

MAKE_NEEDLE( obi_needle,
BMP_OBI_NEEDLE,
&deviation_list,
&fail_nav,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
149, 148,
60, 83,
MODULE_VAR_NONE,
obi_src_cb1,
NULL,
0)
PELEMENT_HEADER obi_needle_list[] =
{
&obi_needle.header,
NULL
};

//---------------------------------------------------------------------------
FLOAT64 FSAPI obi_src_cb1 ( PELEMENT_SPRITE pelement )
{
return obi_2;
}
//---------------------------------------------------------------------------
FLOAT64 FSAPI OBI_deviation(PELEMENT_SPRITE pelement)
{
return obi_2;
}
//---------------------------------------------------------------------------
void FSAPI obi_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()" */

lookup_var(&gyro_hdg); //Lookup current aircraft heading


obi_gyro = gyro_hdg.var_value.n;

lookup_var(&obi_hdg); //Lookup current OBI bearing


obi = obi_hdg.var_value.n;

gyro_card = 359 - obi_gyro; //Get OBI variation from plane heading


obi_1 = obi + gyro_card;

if (obi_1 >= 0) //If the variation is greater or equal


{ //to zero then add 360 degrees
obi_2 = obi_1 + 360;
}
else
if (obi_1 <= 360) //If the variation is less than or equal
{ //to 360 then subtract 360 degrees
obi_2 = obi_1 - 360;
} //This returns the correct leading or
//trailing angle of the OBI needle setting
//versus aircraft heading
break;

case PANEL_SERVICE_PRE_DRAW:
// "draw_routine()
break;

case PANEL_SERVICE_PRE_KILL:
// "kill_routine()
break;
}
}

Processing the O-direction first; although I have VOR1_OBI set into the Token Variable section of the last line
I‟m not actually using it. All of the information on what angle the deviation pointer should be set to is derived
from the calculation in the update_routine section. The minus sign in front of the scaling factor at the end of the
line (-1.000000) forces FS2K to reverse the movement, thus keeping the deviation needle turning in the same
direction as the gyro but maintaining the indication of the OBI bearing it has been set to. Once this has been
processed, the X-direction is added to the movement and the deviation needle is then forced to the left or right
of the centreline. Bear in mind that the centerline is the direction of the OBI bearing; your aircraft could be at
ninety degrees to the runway heading and the deviation needle could be hard up against the top of the HSI
display. If you were then to turn your aircraft to match the OBI heading, suddenly it becomes obvious that you

Dragonflight Design Page 51 of 112


are left of the runway centreline. The scaling factor is designed to prevent the deviation needle from moving
outside of the mask. Again it has a minus sign in front of it to reverse the needle movement so that you get the
familiar “turn towards the needle to line up the runway” effect. As the callback return OBI_2 deals with the OBI
bearing angle, we can use it again to display the angle of the rest of the OBI needle.

In this case we have no Y-direction to add to the equation so the line is left as default. However, there is one
little fact that I haven‟t mentioned; to get the HSI to display correctly the OBI needle is painted in a vertical
direction as illustrated above instead of the more common horizontal direction.....!

See also the SDK.Attitude sample for another way of using a sprite; in this case it is used to display pitch and
bank.

Re-using Callbacks and Tables


On the face of it, it appears that you should be able to re-use callback functions and non-linearity tables within a
gauge. For example, you have a gauge that displays oil pressure for both engines of a twin and the needles
rotate in the same direction (I believe some Pipers have oil pressure gauges like this….?). The obvious thing to
do is to is to write one non-linearity table and have both needles reference it.

Unfortunately it doesn‟t always work.

To be absolutely certain of a gauge working ensure that every macro that references a non-linearity table or
callback function has its own individual non-linearity table and callback function, even if this means repeating
code. Each non-linearity table and callback function must also have an individual name as referenced by the
different macros. Although this looks horrific in terms of what it might do to gauge size, unless you have a very
large and very complex gauge the overhead of individual callbacks and tables is not that great and frequently is
only a few bytes after compilation.

How do you know whether you have this type of failure problem? Basically, nothing works that references the
same tables or callback functions. It‟s easy enough to check; if you suspect this is the problem, just rem out
(using //) all the macros except for one that calls that table or callback function. If this macro now works, you
have the multiple reference problem.

I have no idea why sometimes you can reference a non-linearity table or callback function multiple times and it
works, whereas at other times it simply doesn‟t.

Multiple HelpIDs
The Defining Mouse Rectangles section of the SDK again leaves much to be desired. It suggests that you can
have multiple HelpIDs on a gauge and then leaves you with a very misleading code snip. If you do try and
compile a test worked from the SDK info, you are also very likely to get compiler errors. The
MOUSE_CHILD_EVENT function does not appear to be implemented correctly so you must use
MOUSE_CHILD_FUNCT instead.

The very simplest mouse callback function and HelpID is

//Pressure adjustment by mouse

MOUSE_BEGIN( altimeter_mouse_rect, HELPID_CESS_ALTIMETER, 0, 0)


MOUSE_CHILD_FUNCT( 16, 257, 27, 53, CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, set_baro_down )
MOUSE_CHILD_FUNCT( 43, 257, 27, 53, CURSOR_UPARROW, MOUSE_LEFTSINGLE, set_baro_up )
MOUSE_END

but this will only give you the one HelpID which covers the whole gauge. To get multiple HelpIDs you need to
use the MOUSE_PARENT_BEGIN function. MOUSE_PARENT_BEGIN defines a sub-rectangle of the gauge within
which the mouse callback flags start again from pixel 0,0 (the upper left corner of the MOUSE_PARENT_BEGIN
sub-rectangle) to the maximum specified size of the MOUSE_PARENT_BEGIN sub-rectangle. Here's my newly-
functioning altimeter HelpID mouse code:-
//Pressure adjustment by mouse

MOUSE_BEGIN( altimeter_mouse_rect, HELPID_CESS_ALTIMETER, 0, 0)


MOUSE_PARENT_BEGIN(16, 257, 54, 54, HELPID_CESS_ALTIMETER_SET)
MOUSE_CHILD_FUNCT( 0, 0, 27, 54, CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, set_baro_down )
MOUSE_CHILD_FUNCT( 28, 0, 27, 54, CURSOR_UPARROW, MOUSE_LEFTSINGLE, set_baro_up )
MOUSE_PARENT_END
MOUSE_END

So the HELP_CESS_ALTIMETER HelpID covers the whole gauge except for the small area defined for the Pressure
Setting knob

MOUSE_PARENT_BEGIN (16, 257, 54, 54, HELPID_CESS_ALTIMETER_SET).

The mouse flags which change the barometric pressure are then defined as 28 x 54 pixel rectangles inside the
area covered by the HELPID_CESS_ALTIMETER_SET sub-rectangle, which itself is 54 x 54 pixels.

MOUSE_CHILD_FUNCT( 0, 0, 27, 54, CURSOR_DOWNARROW, MOUSE_LEFTSINGLE, set_baro_down )


MOUSE_CHILD_FUNCT( 28, 0, 27, 54, CURSOR_UPARROW, MOUSE_LEFTSINGLE, set_baro_up )

Unfortunately, you cannot add more HelpIDs to FS2K+; you will need to use Tooltips instead.

Creating Static Tooltips


FS9 introduced the ability to add user-defined static and dynamic tooltips for gauges. In the next two sections I
will take you through creating both types of tooltip. Of the two (and much as you would expect) static tooltips
are the easiest to do.

Assuming that you are using the fsxgauges_sp2.h file enclosed in the zipfile, adding a single static tooltip
requires only three more lines in the mouse definition area. You can add as many tooltips as you like within a
single gauge; this illustrates the use of two static tooltips, one indicating the main ADI and the other indicating
the turn co-ordinator.

MOUSE_BEGIN( adi_mouse_rect, 0, 0, 0 )

MOUSE_PARENT_BEGIN(123,272,70,30,HELP_NONE)
MOUSE_TOOLTIP_STRING(”Turn Co-ordinator”)
MOUSE_PARENT_END

MOUSE_PARENT_BEGIN(0,0,302,302,HELP_NONE)
MOUSE_TOOLTIP_STRING(“Attitude Direction Indicator”)
MOUSE_PARENT_END

//Other mouseclick functions here

MOUSE_END

Note that the order of the tooltips is important; small areas must be listed before the main gauge, otherwise the
small area (in this case, the turnco) will never display.

FS also allows the use of a header file to contain the tooltips, something which can be quite useful if you have
multiple gauges doing the same basic function (e.g. main and standby altimeters). If you decide that you don't
like the tooltip description, it only needs changing in one place before recompiling the gauges.

The header file (e.g. mygauges_tooltips.h):

#define DFD_ADI “Attitude Direction Indicator”


#define DFD_TURNCO “Turn Co-ordinator”

The modified mouse function:

MOUSE_BEGIN( adi_mouse_rect, 0, 0, 0 )
MOUSE_PARENT_BEGIN(123,272,70,30,HELP_NONE) //The turnco rectangle co-ordinates
MOUSE_TOOLTIP_STRING (DFD_TURNCO)

Dragonflight Design Page 53 of 112


MOUSE_PARENT_END
MOUSE_PARENT_BEGIN(0,0,302,302,HELP_NONE) //The full gauge size co-ordinates
MOUSE_TOOLTIP_STRING(DFD_ADI)
MOUSE_PARENT_END

//Other mouseclick functions here

MOUSE_END

Don't forget to #include the tooltip header file in the master gauge.

Creating Dynamic Tooltips


Dynamic tooltips require a lot more preparation and there are multiple ways of returning information. I am only
going to deal with the two ways with which I am familiar; contributions to expand this section will be very
welcome.

I am going to show the use of a dynamic tooltip on an altimeter that displays the altitude in feet, the barometric
pressure in US measurements (Hg) and the barometric pressure in metric measurements (Mb). Each of these
pieces of information requires its own callback function and the order in which they are displayed is defined by
the MOUSE_TOOLTIPS_ARGS function. This is an entirely separate function from the mouseclick/static tooltips
function shown above. Each tooltip callback is defined as follows:-

static FLOAT64 FSAPI <callback name>(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge);

The PanelSDK defines these parameters as follows:-

The numeric value of the module variable after scaling and lookup in the numeric lookup table.
The id of the gauge which is the value from the id lookup table.
A string pointer from the string lookup table.
The pointer to the MODULE_VAR structure initialized to the first argument of the MOUSE_TOOLTIP_ARGS
macro.
The pointer to the gauge header.

The callback name is used by the MOUSE_TOOLTIPS_ARG functions within the MOUSE_TOOLTIPS_ARGS macro.
Note the difference of only one letters (S) between the two names. Each MOUSE_TOOLTIPS_ARG function also
has a set of parameters, all of which are required even though they can all be set to NULL.
MOUSE_TOOLTIP_ARGS(macroname)

//First tooltip
MOUSE_TOOLTIP_ARG(MODULE_VAR,SCALING,LOOKUP_TABLE1,LOOKUP_TABLE2,LOOKUP_TABLE3,
CALLBACK1,CALLBACK2,CALLBACK3)

//Second tooltip
MOUSE_TOOLTIP_ARG(MODULE_VAR,SCALING,LOOKUP_TABLE1,LOOKUP_TABLE2,LOOKUP_TABLE3,
CALLBACK1,CALLBACK2,CALLBACK3)

//[etc.]
MOUSE_TOOLTIP_ARGS_END

Most of the time you will only be using the module_var, scaling and callback1 slots. All of the callback slots have the
callback name as the argument but each returns all of the information available on the tooltip callback as
defined above. In the three cases we are looking at here, we will be displaying the information contained in the
FLOAT64 number section. Our tooltip arguments will all look like this:-

MOUSE_TOOLTIP_ARG(MODULE_VAR_NONE,0,NULL,NULL,NULL,<CALLBACKNAME>,NULL,NULL)

Once we have gathered all the information required, it is then placed into the MOUSE_TOOLTIP_TEXT_STRING for
display. MOUSE_TOOLTIP_TEXT_STRING has a strictly-defined layout:
MOUSE_TOOLTIP_TEXT_STRING("<Info> %1!<display-type>!,
<Info> %2!<display-type>!,
[etc]”,<macroname>)

'Info' is a descriptive name for the information about to be displayed. Note that the %1, %2 etc must be in
numeric order and that they correspond to the same-numbered line in the MOUSE_TOOLTIP_ARGS macro. Finally,
<display-type> corresponds to the various C-language printf variables e.g. decimal (d), float (f), string (s) etc. The
float can be formatted; in our case we only want the first two decimal places of the US barometric pressure to
be displayed.

MOUSE_TOOLTIP_TEXT_STRING("Altimeter altitude (feet) %1!d!,


QNH (US) %2!2.2f!,
QNH (Metric) %3!df!,”,feedback)

Note that each <display-type> must be prefixed and suffixed by an exclamation mark(!).

This is the full dynamic tooltip code for our altimeter; please be aware that much of this code sample wraps
round onto two lines.

MODULE_VAR curr_alt = {ALT_FROM_BAROMETRIC_PRESSURE};


MODULE_VAR fBaroImp = {KOHLSMAN_SETTING_HG};
MODULE_VAR fBaroMetric = {KOHLSMAN_SETTING_MB};

FLOAT64 fAlt;
FLOAT64 fBaroUS;
FLOAT64 fBaroMet;

//Declare the tooltip callback functions


//Altitude in feet
static FLOAT64 FSAPI Get_Alt(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge);

//Barometric pressure in US units


static FLOAT64 FSAPI Get_BaroImp(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge);

//Barometric pressure in metric units


static FLOAT64 FSAPI Get_BaroMetric(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge);

//The mouse tooltip macro. 'feedback' is the name I have given to the macro.
MOUSE_TOOLTIP_ARGS(feedback)
MOUSE_TOOLTIP_ARG(MODULE_VAR_NONE,0,NULL,NULL,NULL,Get_Alt,NULL,NULL)
MOUSE_TOOLTIP_ARG(MODULE_VAR_NONE,0,NULL,NULL,NULL,Get_BaroImp,NULL,NULL)
MOUSE_TOOLTIP_ARG(MODULE_VAR_NONE,0,NULL,NULL,NULL,Get_BaroMetric,NULL,NULL)
MOUSE_TOOLTIP_ARGS_END

//---------------------------------------------------------------------------
//Get the current altitude in feet
static FLOAT64 FSAPI Get_Alt(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge)
{
lookup_var(&curr_alt);
fAlt=curr_alt.var_value.n;
return fAlt;
}

//---------------------------------------------------------------------------
//Get the current barometric pressure in inches of mercury
static FLOAT64 FSAPI Get_BaroImp(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge)
{
lookup_var(&fBaroImp);
fBaroUS=fBaroImp.var_value.n;
return fBaroUS;
}

Dragonflight Design Page 55 of 112


//---------------------------------------------------------------------------
//Get the current barometric pressure in millibars
static FLOAT64 FSAPI Get_BaroMetric(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge)
{
lookup_var(&fBaroMetric);
fBaroMet=fBaroMetric.var_value.n;
return fBaroMet;
}
//---------------------------------------------------------------------------

//Name the barometric adjustment mouse callback


MOUSE_FUNCTION palt_set_baro;

MOUSE_BEGIN( palt_mouse_rect,0, 0, 0 )

//Static tooltip rectangle for the barometric pressure adjustment knob


MOUSE_PARENT_BEGIN(15,205,40,40,HELP_NONE)
MOUSE_TOOLTIP_STRING(“Set Barometric Pressure”)
MOUSE_PARENT_END

//Define the background rectangle for the dynamic tooltip


MOUSE_PARENT_BEGIN(0,0,256,256,HELP_NONE)

//Define the tooltip display. Here it is broken onto multiple lines for readability.
MOUSE_TOOLTIP_TEXT_STRING("Altitude (feet): %1!d!,
QNH (US): %2!2.2f!,
QNH (Metric): %3!d!",feedback)
MOUSE_PARENT_END

//Barometric adjustment function


MOUSE_CHILD_FUNCT( 15, 205, 40, 40, CURSOR_HAND,MOUSE_LEFTSINGLE | MOUSE_RIGHTSINGLE
| MOUSE_DOWN_REPEAT, palt_set_baro )
MOUSE_END

If you cut'n'paste this code into an altimeter it will work as-is, with the exception of the barometric adjustment.
You will also need to change the rectangle definitions to match your own altimeter.

Like the static tooltip, it is also possible to store the entire display string in a header file.

The header file:

//Static tooltips
#define DFD_BARO “Set Barometric Pressure”

//Dynamic tooltips
#define DFD_DYNALT "Altimeter Altitude (feet): %1!d!, QNH (US): %2!2.2f!, QNH (Metric):
%3!d!"

and the modified mouse macro:-

MOUSE_BEGIN( palt_mouse_rect,0, 0, 0 )

//Static tooltip rectangle for the barometric pressure adjustment knob


MOUSE_PARENT_BEGIN(15,205,40,40,HELP_NONE)
MOUSE_TOOLTIP_STRING(DFD_BARO)
MOUSE_PARENT_END

//Define the background rectangle for the dynamic tooltip


MOUSE_PARENT_BEGIN(0,0,256,256,HELP_NONE)

//Define the tooltip display.


MOUSE_TOOLTIP_TEXT_STRING(DFD_DYNALT,feedback)
MOUSE_PARENT_END

//Barometric adjustment function


MOUSE_CHILD_FUNCT( 15, 205, 40, 40, CURSOR_HAND,MOUSE_LEFTSINGLE | MOUSE_RIGHTSINGLE
| MOUSE_DOWN_REPEAT, palt_set_baro )
MOUSE_END
The above code made use of callback functions; in the following example we will use the module_var in the
MOUSE_TOOLTIP_ARG to return the altitude in metres.

//Declare the tooltip callback function


static FLOAT64 FSAPI Get_Alt(FLOAT64 number, ID id, PCSTRINGZ string, MODULE_VAR
*source_var, PGAUGEHDR gauge);

//The mouse tooltip macro. 'feedback' is the name I have given to the macro.
MOUSE_TOOLTIP_ARGS(feedback)
MOUSE_TOOLTIP_ARG(ALT_FROM_BAROMETRIC_PRESSURE,3.28084,NULL,NULL,NULL,NULL,NULL,NULL)
MOUSE_TOOLTIP_ARGS_END

//---------------------------------------------------------------------------
//Name the barometric adjustment mouse callback
MOUSE_FUNCTION palt_set_baro;

MOUSE_BEGIN( palt_mouse_rect,0, 0, 0 )

//Static tooltip rectangle for the barometric pressure adjustment knob


MOUSE_PARENT_BEGIN(15,205,40,40,HELP_NONE)
MOUSE_TOOLTIP_STRING(“Set Barometric Pressure”)
MOUSE_PARENT_END

//Define the background rectangle for the dynamic tooltip


MOUSE_PARENT_BEGIN(0,0,256,256,HELP_NONE)

//Define the tooltip display.


MOUSE_TOOLTIP_TEXT_STRING("Altitude (feet): %1!d!",feedback)
MOUSE_PARENT_END

//Barometric adjustment function


MOUSE_CHILD_FUNCT( 15, 205, 40, 40, CURSOR_HAND,MOUSE_LEFTSINGLE | MOUSE_RIGHTSINGLE
| MOUSE_DOWN_REPEAT, palt_set_baro )
MOUSE_END

Retrieving and Writing XML Data


The FSX SDK Service Pack 1 contains example code on how to read and write to XML variables. What we
will deal with here is a very much simpler read and write sequence; as to why we would need to do this, for
example the fsxgauges_sp2.h file contains a variable to read ADF1 but no ADF2, yet there is an XML variable
for returning ADF2. Bill Leaming and Doug Dowson provided the original code on grabbing XML variables.
There is also another reason why you may like to consider using XML data for all of the other navradio
information too; if you use C code then you will need to do a BCD conversion routine (this is dealt with in
Conversion Factors and Oddities) but if you call the XML data it is already presented in decimal format. We‟re
going to make use of the following part of the PANELS struct in the fsxgauges_sp2.h file:

(FSAPI *execute_calculator_code) (PCSTRINGZ code, FLOAT64* fvalue, SINT32* ivalue, PCSTRINGZ*


svalue);

In the following example (returning ADF2) we will be using fvalue to return the frequency value, but the
prinicipal is the same if the variable you wish to access is a SINT32 value or a PCSTRINGZ value. Note that for a
string you must declare the variable as a PCSTRINGZ; the normal string or char will not work

Dragonflight Design Page 57 of 112


//Frequency display
double adf2_current=0;
double adf2_current_freq=0;

//--snip

//5 - Decimals
MAKE_STRING(adf2_dec_string,
&adf2_test_plist,
Adf2_fail,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENT,
0,
280, 55,
20, 50,
1,
TICK18,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(208,208,208),
RGB(0,0,0),
RGB(255,255,0),
"TREBUCHET MS",
FW_BOLD,
DEFAULT_CHARSET,
0,
DT_VCENTER | DT_SINGLELINE,
NULL,
adf2_dec_string_cb )

PELEMENT_HEADER adf2_dec_string_plist[] =
{
&adf2_dec_string.header,
NULL
};

//4 - Units
MAKE_STRING(adf2_string,
&adf2_dec_string_plist,
Adf2_fail,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENT,
0,
199, 55,
75, 50,
4,
TICK18,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(208,208,208),
RGB(0,0,0),
RGB(255,255,0),
"TREBUCHET MS",
FW_BOLD,
DEFAULT_CHARSET,
0,
DT_VCENTER | DT_SINGLELINE,
NULL,
adf2_string_cb )

PELEMENT_HEADER adf2_string_plist[] =
{
&adf2_string.header,
NULL
};

//--snip

//---------------------------------------------------------------------------
//Get the ADF2 data and calculate the display values
void GetFrequencyADF2()
{
int adf2_hun_temp=-1;
int adf2_ten_temp=-1;
int adf2_unit_temp=-1;

execute_calculator_code("(A:ADF ACTIVE FREQUENCY:2, KHz)",&adf2_current,NULL,NULL);


adf2_hun_temp=(int)adf2_current/100;
adf2_ten_temp=((int)adf2_current-(adf2_hun_temp*100))/10;
adf2_unit_temp=(int)adf2_current-((adf2_hun_temp*100)+(adf2_ten_temp*10));
adf2_dec=(adf2_current-((adf2_hun_temp*100)+(adf2_ten_temp*10)+adf2_unit_temp))*10;
adf2_current_freq=(int)adf2_current;

return;
}

//---------------------------------------------------------------------------
FLOAT64 FSAPI adf2_string_cb(PELEMENT_STRING pelement)
{
GetFrequencyADF2();
sprintf(pelement->string, "%04.0f", adf2_current_freq);
return 0;
}
//---------------------------------------------------------------------------
FLOAT64 FSAPI adf2_dec_string_cb(PELEMENT_STRING pelement)
{
GetFrequencyADF2();
sprintf(pelement->string, "%1.0f", adf2_dec);
return 0;
}

For the use of the PCSTRINGZ code - in our case, "(A:ADF ACTIVE FREQUENCY:2, KHz)" - I would refer you to
the XML data supplied with the SDK of whichever version of FS you are currently using. I will say that every
single piece of XML data has to have a corresponding return type – here we are using KHz but I could have
used (for example) MHz and changed the calculations accordingly..

To reverse the procedure and write to the XML variables, use the following piece of code.

execute_calculator_code("(>K:<kvar>)", NULL, &return_value, NULL);

The return_value is zero for a successful write.

The Event Handler


The event handler registers all trigger_key_event(KEY_<event>) events i.e. both keyboard presses and
joystick/console key inputs. It is particularly useful for trapping keyboard and joystick button presses and using
the resulting information to trigger a further piece of code e.g. if you have coded a flight controls hydraulic
failure, then you don‟t want the user selecting flaps/slats from the keyboard and finding that they still operate.
Another reason for using the event_handler would be to trap a keyboard input for (say) extending the
spoilers/speedbrakes when you have also provided a mouseclick on the spoiler gauge. By trapping the event,
you can cause the spoiler handle to move on-screen even it has not been clicked on.

The prototype for the event_handler is this:

void FSAPI EventHandler (ID32 event, UINT32 evdata, PVOID userdata)

event = the name of the event e.g. KEY_OBI_SET


evdata = any data generated by the event
userdata = data supplied by the gauge or by user input

I have not yet found a use for userdata and I am not sure even if it works as it appears that it should do. For the
majority of the time you are most likely to be concerned with event only.

Microsoft do not draw your attention to this extremely useful macro, nor to many others! I regret that I cannot
remember who pointed it out to me but it was a long time ago and a long time until I ultimately made use of it.

Note that the event_handler must only be declared in one of the gauges in your multigauge and that all event
trapping must be done in that gauge. If you declare it in more than one place the compiler will throw a fit and if
you try to reference it in another subgauge, the compiler will again throw a fit. Because of this behaviour, I
would normally devote a single subgauge to doing nothing but handling events. I also prefer to take each event
and deal with it in an individual subroutine because I find that this makes the handling code easier to read. Also
please note that the event_handler must be declared in the PANEL_SERVICE_CONNECT_TO_WINDOW and

Dragonflight Design Page 59 of 112


PANEL_SERVICE_DISCONNECT events; the reason for this is simply that these are the only window events that can
be guaranteed to fire once-only during the panel load procedure. You will almost certainly have to add these
events to the case statements in the PANEL_UPDATE section of your gauge.

In the following example we will trap both of the above events (speedbrakes and flaps); in the case of the
speedbrakes we will supply the variable required to move the handle on screen and in the case of the flaps/slats,
we will prevent them from being selected in the event of a hydraulic failure. Note also that the subroutines
must be listed before they are called in the code structure.

//***************************************************************************
// Subroutines
//***************************************************************************
void Speedbrakes(ID32 event)
{
// Speedbrakes - trap the slash (/) key and ensure that the handle movement variable
// (speedbrake_pos) is correctly filled.
lookup_var(&left_spoilers);
if(event==KEY_SPOILERS_TOGGLE && left_spoilers.var_value.n>16000)speedbrake_pos=100;
if(event==KEY_SPOILERS_TOGGLE && !left_spoilers.var_value.n)speedbrake_pos=0;
return;
}

//***************************************************************************
//Hydraulic failures
void Hydfailure(ID32 event)
{
// Trap the flaps demand and ensure they remain at zero if both the a and c hydraulic
// systems have failed.
if ((event==KEY_FLAPS_INCR) && fail_hydsys_a && fail_hydsys_c)
trigger_key_event(KEY_FLAPS_SET,0);
return;
}

//***************************************************************************
//---------------------------------------------------------------------------
void FSAPI EventHandler (ID32 event, UINT32 evdata, PVOID userdata)
{
Speedbrakes(event);
Hydfailure(event);
}

//-------------------------------------------------------------------------------
void FSAPI dfdblind_update (PGAUGEHDR pgauge, int service_id, UINT32 extra_data)
{
switch(service_id)
{
case PANEL_SERVICE_CONNECT_TO_WINDOW:
register_key_event_handler((GAUGE_KEY_EVENT_HANDLER)EventHandler,0);
break;

case PANEL_SERVICE_DISCONNECT:
unregister_key_event_handler((GAUGE_KEY_EVENT_HANDLER)EventHandler,0);
break;
}
}

Time Rollover
The official SDK shows the TICK18 genreral purpose timer as being a UINT16 i.e. it will rollover at 65535 (a
little over an hour). According to sources within the ACES team the SDK is incorrect and the TICK18 timer has
been a 32-bit integer for at least the last three versions of FS. This means that its rollover period is in excess of
7.5 years; consequently all information in this section and also the corresponding Excel spreadsheets have now
been deleted as the information has been rendered obsolete.

Until the official SDK is corrected, this note will be left in for information.
Extending the Environment

This section will deal with extending the use of gauges outside of the information contained in the FS SDK.

Sharing Variables
I first saw this concept propounded by Rolf Dieter Buckmann and used it in my first "electrical panel" gauges
for FS98. Soon after it appeared in CFS as part of the engine-starting procedures but astonishingly, the code is
also in FS95 but not activated. Anyone who downloaded the original CFS SDK will have realised that it was a
badly re-badged version of the FS98 SDK; the only new addition was the section entitled “Using Named
Variables” and that section was a cut‟n‟paste of RDB's original files without further expansion or accreditation
of the author.

Take care – this section applies to gauges that are completely separate from each other i.e. not sub-gauges of
the same multigauge. For the same information on sharing variables within multigauges please see
"Multigauges and Sharing Variables" below.

The concept of shared variables back in FS98 days was quite exciting; finally having gauges communicate with
each other meant that we could start to weave panels together as a whole instead of as a bunch of unrelated
instruments. These days with the big multigauges it‟s all old hat but there are times when this method of
communication across separate gauges is still useful.

Note that both client and server gauges have to have the same named variable, otherwise inter-gauge
communication will not work. In the examples think of *pvar1 as the bit that actually carries the broadcast
variable information; this is not an accurate picture but quite good enough for here. xxxx is an integer that you
can specify directly (*pvar1 = 1) or via another variable as in my Altitude Preselector on the Flight Director
(*pvar = Height). Note also that pvar and pvar1 are arbitrary names and taken from Microsoft‟s example
naming conventions (pvar = “pointer to variable”); the leading asterisk (*) is the important part. This leading
asterisk defines the variable as one that is being sent external to the current gauge. Finally, the example deals
only with broadcasting and receiving an unsigned 32-bit integer (UINT32); generally this will suffice for
around 99% of gauge interactions. You can see exactly which variable types are available for use by checking
the typedef enum VAR_TYPE structure in the fsxgauges_sp2.h file.

Client Gauge code lines

MODULE_VAR new_var; // Variable for gauge to be controlled

//---------------------------------------------------------------------------
void FSAPI obi_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
PUINT32 pvar1 = (PUINT32)new_var.var_ptr;

switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
// Make variable accessible to other gauges

initialize_var_by_name (&new_var, "new_on");

break;

etc.

However you chose to do the update on the client variable(s), the line(s)

Dragonflight Design Page 61 of 112


PUINT32 pvar1 = (PUINT32)new_var.var_ptr;

must be the first line in that section as shown above, otherwise the compiler will throw an error.

Server Gauge code lines

UINT32 new_var = 0; //New shared variable

//---------------------------------------------------------------------------
void FSAPI obi_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()

register_var_by_name (&new_var, TYPE_UINT32, "new_var");

break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()" */
break;

case PANEL_SERVICE_PRE_DRAW:
// "draw_routine()
break;

case PANEL_SERVICE_PRE_KILL:
// "kill_routine()
unregister_var_by_name ("new_var");

break;
}
}

Somewhere in the code for the server gauge you will be doing something with the value carried by new_var.
new_var is purely an arbitary name for the example; you can call the variable anything you like but preferably
something that tells you what it is supposed to be doing in the code. This is where the above-mentioned pvar
comes in.

This is a fragment from the Altitude Preselector; note I‟ve chosen to keep both the internal and external variable
names the same.

MODULE_VAR Selheight; //Broadcast pre-selected height to flight controller

//---------------------------------------------------------------------------
void FSAPI preselector_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
PUINT32 pvar = (PUINT32)Selheight.var_ptr; //MUST be first line in this section

switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
initialize_var_by_name (&Selheight, "Selheight");

break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()"
Height = 0;
Height = (UINT32)(Call1 * 10000);
Height = Height + (UINT32)(Call2 * 1000);
Height = Height + (UINT32)(Call3 * 100);
Height = Height + (UINT32)(Call4 * 10);
*pvar = Height; //Broadcast height set on preselector to the FCP65

break;

case PANEL_SERVICE_PRE_DRAW:
// "draw_routine()
break;

case PANEL_SERVICE_PRE_KILL:
// "kill_routine()
unregister_var_by_name ("Selheight");

break;
}
}

At the other end of my Altitude Preselector is the FCP65 Flight Control Panel and this is where the broadcast
variable Selheight is used:-

UINT32 Selheight = 0; // Incoming height set by preselector

//---------------------------------------------------------------------------
// Altitude preselection

BOOL FSAPI set_alt_sel( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
//Set final altitude from preselector info
trigger_key_event(KEY_AP_ALT_VAR_SET_ENGLISH,Selheight);
}

//---------------------------------------------------------------------------
void FSAPI fcp65_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()

register_var_by_name (&Selheight, TYPE_UINT32, "Selheight");

break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()
break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()"
break;

case PANEL_SERVICE_PRE_DRAW:
// "draw_routine()
break;

case PANEL_SERVICE_PRE_KILL:
// "kill_routine()

unregister_var_by_name ("Selheight");

break;
}
}

Note also that you can mix Client and Server code in the same gauge – sometimes that's very useful!

Shared variables do not persist; that is once the variable has been given a value and fired off it ceases to exist
until the next time it is fired again, which can lead to problems in certain circumstances. One way around this

Dragonflight Design Page 63 of 112


problem is to continuously fire the shared variable using the PANEL_SERVICE_PRE_UPDATE.

UINT32 switch_on = 0;

//---------------------------------------------------------------------------

BOOL FSAPI light_switch( PPIXPOINT relative_point, FLAGS32 mouse_flags )


{
if (switch_on == 0)
{
switch_on = 1; //Tell update to broadcast
SHOW_IMAGE(&rocker_switch); //Show switch operated
}
else
{
switch_on = 0;
HIDE_IMAGE(&rocker_switch);
}
return FALSE;
}
//---------------------------------------------------------------------------
void FSAPI panel_lights_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
PUINT32 pvar = (PUINT32)light_on.var_ptr;

switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()

initialize_var_by_name (&light_on, "light_on");

break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()"
//Broadcast variable continuously

if (switch_on == 1)
{
*pvar = 1; //Turn light on
}
else
{
*pvar = 0; //Turn light off
}

break;

etc;

Now, it appears that multiple gauges cannot make simultaneous use of a single broadcast variable. The first
Server gauge to initialise will monopolise the Client variable and all of the other Server gauges will not react.
So if, for example, you are trying to send a command from a switch to light multiple indicators the first light to
"see" the command variable will grab it and hold it and no other indicators will light. The way round this
problem is to broadcast multiple individually-named variables from the Client gauge and then each Server
gauge will react to its unique variable.

Do not confuse this with multiple gauges not being able to change the same variable; they can, providing that
you don‟t want to change it by more than one gauge at the same time.

Each variable to be broadcast will require its own

PUINT32 pvar = (PUINT32)<variable name>.var_ptr;

line and I must stress very strongly that all of these lines must be the first lines in the section so that they
initialise before any code is executed. Failure to do this will lead to all sorts of funny error messages from the
compiler.
In the FS98 SDK debugging days Wade Chafe had the goodness to send me information on how to stop a GPF
from occurring in a gauge when the corresponding variable has not been initialised. I didn‟t fully understand it
for a while (a C-programmer would have got it immediately!) but I was trying to figure out a way round the
primary panel to secondary panel broadcasting restriction when suddenly it fell into place. In the FS2K SDK
Microsoft conceded that Wade's method is the way to stop gauge GPFs occurring when the Client has been
initialised but the Server has not. Again, there is no accreditation to the original source of the code, but
considering that the wording is identical, it‟s pretty unmistakeable.

Have a look at this code snip which is a modified version of the one above:-

//---------------------------------------------------------------------------
void FSAPI panel_lights_callback( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
PUINT32 pvar = (PUINT32)turn_on.var_ptr;

switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
// "install_routine()
break;

case PANEL_SERVICE_PRE_INITIALIZE:
// "initialize_routine()

initialize_var_by_name (&turn_on, "turn_on");

break;

case PANEL_SERVICE_PRE_UPDATE:
// "update_routine()"
//Broadcast variable continuously

if (turn_on.var_ptr != NULL)
{
if (switch_on == 1)
{
*pvar1 = 1;
}
else
{
*pvar1 = 0;
}
}

break;

etc;

It‟s a piece from a lamp test routine I created in FS98 (yes!) but the important line is

if (turn_on.var_ptr != NULL).

turn_on is the variable to be broadcast but before the information is sent, the code checks to see if the Server
gauge's corresponding turn_on variable has been initialised. It says “Has the area in memory that will receive my
information been created?” (var_ptr != NULL). If the answer is “Yes” then the Server gauge must be active so
execute the code, otherwise skip it. For me, the exciting thing about that simple piece of code was that the
switch was on a secondary panel, the lamp was on the primary panel and a second lamp on another secondary
panel. Up until then, this type of inter-gauge, inter-panel communication had never been successfully done
before!!!!

If you add a similar routine to all of your broadcast code then you‟ll never be rudely thrown out of FS with a
shared variable GPF again!

Dragonflight Design Page 65 of 112


File I/O
There was a suggestion that I do a section on reading and writing to text files. Initially I agreed this was a good
idea but the more I thought about it, the less certain I was. The problem is that code can vary wildly depending
on the gauge programmers requirements. My Collins Navpack does a lot of file I/O work but if you actually
open up the .dat files, you‟ll see that each line only contains a single number and each number is either only one
part of a radio frequency or a line identifier that says “the next frequency starts on the next line”. At the time I
first wrote this section I had a lot to learn in this area but even the simple I/O routine that I used then extends
FS2K+ immeasurably. As file I/O is fairly crucial to the use of any programming language I encourage you to
learn how to do it properly! But, for the sake of completeness, here is my routine that reads and writes to the
radio frequency files; if nothing else, it illustrates that you can do file I/O from inside a gauge.

Dat file for the Collins ADF-60A radio; the structure is


[new freq header]
[high digit]
[mid digit]
[low digit]

10
7
3
3
20
2
0
0
30
2
0
0
40
2
0
0
11
2
0
0

//-------------------------------------------------------------------------------

#include <stdio.h>

//Declare variables

FILE *old_freq; //Original frequency file I/O


FILE *new_freq; //New frequency file I/O
int w = 0; //General use variables
int z = 0;
char freq_string[2]; //Grab two characters maximum from each line

//-------------------------------------------------------------------------------
BOOL FSAPI store_freq( PPIXPOINT relative_point, FLAGS32 mouse_flags )
{
//Open current frequency file for read-only and a new temporary file for read-write

old_freq = fopen("adf.dat","r");
new_freq = fopen("test.dat","a");

//While there is a line to read in the frequency file then loop through code

while (TRUE)
{

//If end of file is reached then exit loop


//otherwise read the line into freq_string for analysis

if(fgets(freq_string,2,old_freq)==NULL)
break;

//Check for frequency header in the incoming string


//z carries the required frequency line number to read
//atoi is a C routine that converts from alpha to integer
//(count * 10) is the frequency line header to look for
//When count matches z then print the new frequency (left_, mid_ and right_units)
//to the new file (new_freq)

z = atoi(freq_string);
if (count * 10 == z)
{
w = 3;
fprintf(new_freq,"%d\n",count * 10);
fprintf(new_freq,"%d\n",left_units);
fprintf(new_freq,"%d\n",mid_units);
fprintf(new_freq,"%d\n",right_units);
}
else

//If w has been set to three then decrement it by one on each pass. This prevents
//the write routine from writing the next three lines to the new file as they are
//read from the old frequency file. These lines are the old frequency we want to discard.
//As soon as w equals zero again continue to copy lines across.

{
if (w == 0)
{
fprintf(new_freq,"%s",freq_string);
}
else
{
w = w - 1;
}
}
}

//Exited from loop so close the files

fclose(old_freq);
fclose(new_freq);

//Now rename the new file as the frequency file

remove("adf.dat");
rename("test.dat","adf.dat");
}

Incidently, declaring a file name without a path will automatically make the gauge look in the root directory of
FS2K+ for the named file. Note also that I have no code in there to check whether the .dat file actually exists;
on the face of it this looks to be pretty bad practice as it will cause a GPF in FS2K the first time you attempt to
read a non-existant file. As I always supply default .dat files with gauges this shouldn‟t be a problem as long as
the user remembers to follow the installation instructions and copy the files across. Secondly, if you did a test to
see that the file existed and it didn‟t, how would you alert the user? FS2K doesn‟t really lend itself to on-screen
messages, so you have two choices; crash the sim with a GPF or create a data file on the fly. Easy enough if it‟s
a dead simple file like my radio frequency .dat files – not so easy if it‟s (for example) a GPS data file.

Having said that, I suppose that if it was a radio you could flash an “ERR” message across the digital display.....

//Open current frequency file for read-only; check for its existance first
//and if the user has forgotten to copy file flash an error message, then exit the loop.
//If the file exists it will be opened and the rest of the code will follow.
//preset_xxx_display identifiers are the standby frequency digits on the ADF-60A gauge

if((old_freq = fopen("adf.dat","r")) == NULL)


{
wsprintf(preset_left_display.string, "%1s", “E”);
wsprintf(preset_mid_display.string, "%1s", “R”);
wsprintf(preset_right_display.string, "%1s", “R”);
}
else
{

//Move all the other code here

Dragonflight Design Page 67 of 112


Panel Lighting
Depending on how much time you have and how far you are willing to go, there are two methods of producing
gauge lighting. You can use the built-in lighting system or you can create your own; both have pros and cons.
Both work by adding and removing image flags „on the fly‟. To distinguish between them, I am going to call
them the „native‟ system and the „created‟ system.

Advantages Disadvantages
Native system Requires no additional bitmaps. Three levels of illumination only: off, dim
and bright.
Created system Can have as many levels of illumination as Requires one additional bitmap per lighting
you want. level for each gauge bitmap.
Can lead to a major increase in gauge size.
Can become a framerate hog if not managed
correctly.

Native System

For the sake of this example we‟ll revisit the airspeed indicator and illuminate the face and the needle at night.

int dimbrt=0;
int asi_pwr=0;

MAKE_NEEDLE(asi_needle,
ASI_BMP_NEEDLE,
NULL,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle_plist[] =
{
&asi_needle.header,
NULL
};

MAKE_ICON(asi_face,
ASI_FACE,
&asi_needle_plist,
asi_fail,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY,
0,
19, 34,
MODULE_VAR_NONE, NULL,
NULL,
1,
0,
0)

PELEMENT_HEADER asi_face_plist[] =
{
&asi_face.header,
NULL
};

MAKE_STATIC(asi_background,
ASI_BACKGROUND,
&asi_face_plist,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
0, 0 )

PELEMENT_HEADER asi_list = asi_background.header;

The background is the bezel of the ASI and is never illuminated. asi_pwr indicates that power is available to
the ASI and dimbrt sets the level of illumination to off, dim or bright. Although this code is set in the
PRE_UPDATE section of the callback routine and so gets called every cycle, surprisingly it causes no hit on the
framerate. The _LISTELEMENT macros are part of the corrected fsxgauges_sp2.h file included in this archive.

//---------------------------------------------------------------------------
void FSAPI asi_update( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{

case PANEL_SERVICE_PRE_UPDATE:

//If power is available


if(asi_pwr)
{
//Remove all the lighting image flags
DARKEN_LISTELEMENT(pgauge->elements_list[0],1);
DARKEN_LISTELEMENT(pgauge->elements_list[0],2);
DELUMINOUS_LISTELEMENT(pgauge->elements_list[0],1);
DELUMINOUS_LISTELEMENT(pgauge->elements_list[0],2);

//Set backlighting to dim by applying IMAGE_USE_LUMINOUS


if(dimbrt==1)
{
LUMINOUS_LISTELEMENT(pgauge->elements_list[0],1);
LUMINOUS_LISTELEMENT(pgauge->elements_list[0],2);
}
else
//Set backlighting to bright by applying IMAGE_USE_BRIGHT
if(dimbrt==2)
{
LIGHT_LISTELEMENT(pgauge->elements_list[0],1);
LIGHT_LISTELEMENT(pgauge->elements_list[0],2);
}
}
else
{
//If no power then turn everything off by removing the lighting flags
DARKEN_LISTELEMENT(pgauge->elements_list[0],1);
DARKEN_LISTELEMENT(pgauge->elements_list[0],2);
DELUMINOUS_LISTELEMENT(pgauge->elements_list[0],1);
DELUMINOUS_LISTELEMENT(pgauge->elements_list[0],2);
}

break;
}
}

Note that there is no dimbrt=0 section as zero is done by default when the image flags are turned off.

Created System

For the „created‟ system you will need one bitmap per lighting level; for this example we are going to create
five levels of illumination which means we need five faces and five needles.

Dragonflight Design Page 69 of 112


ASI – Five levels of illumination

The idea here is that the dimbrt variable will go from zero to 4 and call each face and needle as it does so. If
you are very careful with your shading you can build ten levels of illumination from just five bitmaps by
introducing the LUMINOUS and BRIGHT image flags at step 6. However, that‟s up to you – I‟m not going to go
there for this simple example!

The immediate problem is that as the face is an icon, we can use an icon callback routine to swap the images.
You can‟t do this with a needle; you need one needle macro per needle image. Secondly, you need to be careful
with the bitmap colours as you will need to blend them into daylight. I usually sidestep this problem by
ensuring that the lighting circuits can only work in dusk, night and dawn and automatically removing all
lighting flags during daylight hours.

int dimbrt=0;
int asi_pwr=0;

ICON_UPDATE_CALLBACK lightstate_icon_cb;

//6
MAKE_NEEDLE(asi_needle5,
ASI_BMP_NEEDLE5,
NULL,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle5_plist[] =
{
&asi_needle5.header,
NULL
};

//5
MAKE_NEEDLE(asi_needle4,
ASI_BMP_NEEDLE4,
& asi_needle5_plist,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle4_plist[] =
{
&asi_needle4.header,
NULL
};

//4
MAKE_NEEDLE(asi3_needle,
ASI_BMP_NEEDLE3,
& asi_needle4_plist,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle3_plist[] =
{
&asi_needle3.header,
NULL
};

//3
MAKE_NEEDLE(asi_needle2,
ASI_BMP_NEEDLE2,
& asi_needle3_plist,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle3_plist[] =
{
&asi_needle3.header,
NULL
};

//2
MAKE_NEEDLE(asi_needle1,
ASI_BMP_NEEDLE1,
& asi_needle1_plist,
fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
150, 150,
9, 9,
AIRSPEED,
needle_src_cb,
asi_needle,
0 )

PELEMENT_HEADER asi_needle1_plist[] =
{
&asi_needle1.header,
NULL
};

//1
MAKE_ICON(asi_face,
ASI_FACE,
&asi_needle1_plist,
asi_fail,
IMAGE_USE_ERASE | IMAGE_USE_TRANSPARENCY | IMAGE_USE_BRIGHT,
0,
19, 34,
MODULE_VAR_NONE, lightstate_icon_cb,
ICON_SWITCH_TYPE_STEP_TO,
5,
0,
0)

PELEMENT_HEADER asi_face_plist[] =
{

Dragonflight Design Page 71 of 112


&asi_face.header,
NULL
};

MAKE_STATIC(asi_background,
ASI_BACKGROUND,
&asi_face_plist,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
0, 0 )

PELEMENT_HEADER asi_list = asi_background.header;

//---------------------------------------------------------------------------
FLOAT64 FSAPI lightstate_icon_cb(PELEMENT_ICON pelement)
{
return dimbrt;
}

//---------------------------------------------------------------------------
void FSAPI asi_update( PGAUGEHDR pgauge, int service_id, UINT32 extra_data )
{
switch(service_id)
{

case PANEL_SERVICE_PRE_UPDATE:

//If power is available


if(asi_pwr)
{
//From dim to bright
if(dimbrt ==0)
{
SHOW_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
HIDE_LISTELEMENT(pgauge->elements_list[0],5);
HIDE_LISTELEMENT(pgauge->elements_list[0],6);
}
if(dimbrt ==1)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],2);
SHOW_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
HIDE_LISTELEMENT(pgauge->elements_list[0],5);
HIDE_LISTELEMENT(pgauge->elements_list[0],6);
}
if(dimbrt ==2)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
SHOW_LISTELEMENT(pgauge->elements_list[0],4);
HIDE_LISTELEMENT(pgauge->elements_list[0],5);
HIDE_LISTELEMENT(pgauge->elements_list[0],6);
}
if(dimbrt ==3)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
SHOW_LISTELEMENT(pgauge->elements_list[0],5);
HIDE_LISTELEMENT(pgauge->elements_list[0],6);
}
if(dimbrt ==4)
{
HIDE_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
HIDE_LISTELEMENT(pgauge->elements_list[0],5);
SHOW_LISTELEMENT(pgauge->elements_list[0],6);
}

}
else
{
//If no power then turn everything off by showing the most dim bitmap
dimbrt=0;
SHOW_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
HIDE_LISTELEMENT(pgauge->elements_list[0],5);
HIDE_LISTELEMENT(pgauge->elements_list[0],6);

break;
}
}

I understand that it‟s actually easier to do this in XML….

Adding Alpha Channels


Briefly, an alpha channel is an instruction to FS to shade the visible areas of any bitmaps in the z-order below
it. An alpha channel is most often used in an ADI to give a 3D feel to the artificial horizon globe; this is the
situation we will deal with here.

ADI Artificial Horizon

There are two pre-requisites; you will need a graphics program capable of outputting files in Adobe
Photoshop‟s .psd format and you will need Microsoft‟s ImageTool utility, available from their website. I am not
going to go through the step-by-step instructions of creating the alpha channel as this will depend on which
graphics program you are using, but I will give the outline of how I do it in PhotoshopCS.

What I require here is to create an alpha channel to shade the edges of the artificial horizon rotating globe and
also to create a shadow underneath the aircraft symbol. My experience has shown that it is easiest to create the
alpha channel in a transparent bitmap and then to add the original surround back in as a separate MAKE_ICON or
MAKE_STATIC as the next macro above the alpha channel macro.

1. In Photoshop I create the picture that will become the alpha channel using as many layers as needed in
the image file, as it is easier to edit in the layers palette than to edit the alpha channel itself within the
channels palette. Once that is finished, I save it as a new file.
2. I then flatten/delete any layers in the new image until I have just the picture that will create the alpha
channel.
3. I copy the entire flattened image to the clipboard.
4. I create a new alpha channel and paste the image into the channel; remember that FS can only cope
with one alpha channel per MAKE_STATIC macro.
5. Finally, I delete everything in the original layers palette so that the remaining layer consists entirely of
the RGB0,0,0 transparency colour.

Dragonflight Design Page 73 of 112


The Completed Alpha Channel

Remember that for an alpha channel, RGB255,255,255 (pure white) means block all light and RGB0,0,0 (pure
black) means let all light through. The shades of grey let varying amounts of light through. It isn‟t visible in the
reduced image but the aircraft symbol shadow is also shaded. Once you‟ve got this far, you will need to run the
.psd file through the ImageTool. There is no menu option for this, so it must be done from the command line.
The syntax is:

imagetool -gauge -gaugealpha -nogui -nobeep <imagename.psd>

I use a batch file called alpha.bat with the following command:

imagetool -gauge -gaugealpha -nogui -nobeep %1

This means that I only have to enter “alpha <imagename.psd>” on the command line. ImageTool will then
produce an extended bitmap with the same name as the .psd file. You can check the alpha channel by calling
the extended bitmap into ImageTool and selecting View – Alpha Channel.

The Alpha Channel in ImageTool

Note that ImageTool shows the shading as it will appear on screen i.e. reversed to the way you are required to
create it!
To get this into the ADI gauge, I used the following code. I have used a MAKE_ICON to get the aircraft symbol on
top of the alpha bitmap but I could just as easily have used another MAKE_STATIC. Old habits….

//7 - the aircraft symbol


MAKE_ICON(ladi_face,
LADI_FACE,
&padi_list4,
NULL,
IMAGE_USE_TRANSPARENCY |IMAGE_USE_ERASE,
0,
0, 0,
MODULE_VAR_NONE, NULL,
NULL,
1,
0,
0)

PELEMENT_HEADER padi_ladi_face[] =
{
&ladi_face.header,
NULL
};

//6 - alpha shading on the globe


MAKE_STATIC(ladi_alpha,
LADI_FACE_ALPHA,
&padi_ladi_face,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ALPHA,
0,
0, 0)

PELEMENT_HEADER padi_ladi_alpha[]=
{
&ladi_alpha.header,
NULL
};

The ADI in FS

A couple of comments about the finished item. This isn‟t the alpha channel shown above; that proved to not
have sufficient shading on it to create the illusion of a small globe. I also darkened the aircraft symbol shadow
as with it being close to the aircraft symbol, the light has less distance to disperse and soften the shadow. Trial
and error…. as always.

In her original tutorial blog on creating alpha channels in FS gauges, the ACES developer Engauged hinted that
alpha channels only work with the MAKE_STATIC macro, hence my comments above were limited to that.
However, I have had some limited success with alpha channels in MAKE_NEEDLE (see below) and I see no reason
at all why they shouldn‟t work successfully with MAKE_ICON.

Dragonflight Design Page 75 of 112


Alpha channel in a needle

The alpha channel here is created as part of a fully-transparent layer over the bank indicator (MAKE_NEEDLE)and
is used to shade the upper and lower edges of the drum-type artifical horizon (MAKE_SPRITE). As I said, limited
success.

How to directly control Flight Simulator gauges via Keyboard input


This very useful piece of information was supplied by Mathias Elsaesser with help from Arne Bartels. I have to
sound a note of caution though; if you‟re adding a new gauge to an existing panel then you can only use this to
replace a gauge that uses the same keyboard input; otherwise you‟ll have two gauges reacting at the same time.
If you‟re developing a panel from scratch, then it really doesn‟t matter as you can define what keystrokes do
whichever job you want. Secondly, this does NOT work in FSX and so this section may be removed in a future
update as FSX becomes the more popular sim.

I‟m going to simply quote the entire file that Mathias sent me:-

“In the course of developing my Helicopter Pitch Trim gauge for FS2002 I found it would be ideal if it could react
to the default FS elevator trim keystrokes Numpad 7 and Numpad 1. Indirect access via Flight Simulator was not
possible as Microsoft had not implemented the elevator trim in the still used FS98-style helicopter air file in
FS2002. Arne Bartels was so kind as to help me out with the possibilities of Microsoft’s DirectInput SDK so here
we go:

First make sure you store the following header and source code files in your Microsoft Visual
Studio\VC98\Include folder [these are found in the Keyboard input files folder]:

DInputKeyboard.cpp
DInputKeyboard.h

In your VC++ Workspace add

DInputKeyboard.cpp

to the Source Code files and

DInputKeyboard.h

to your Header files.

In your Project Settings in the Linker tab make sure the following files are in the window “Object/Library-
Modules” or add them if they aren’t: user32.lib dinput.lib dxguid.lib

In your main c file make sure you include the following code:

#include <dinput.h>
#include "DInputKeyboard.h"

HWND hFSMainWnd = NULL;

#define NO_KEY 0 //not absolutely necessary as a simple 0 will do the job also
int keypressed=NO_KEY;

In your subgauge c file: After the section

/* "initialize_routine()" */

case PANEL_SERVICE_PRE_INITIALIZE:
break;

insert this section:

/* "make access to keyboard variables possible()" */

case PANEL_SERVICE_CONNECT_TO_WINDOW:
hFSMainWnd= GetFSWindowHandle();
CreatDirectInputDevice( hFSMainWnd);
break;

After the section

/* "draw_routine()" */

case PANEL_SERVICE_PRE_DRAW:
break;

and before the

/* "kill_routine()" */

case PANEL_SERVICE_PRE_KILL:
break;

section insert this:

/* "disconnect keyboard access()" */

case PANEL_SERVICE_DISCONNECT:
FreeDirectInput();
break;

Now how do you make this do some work for you? First open the file dinput.h contained in the Microsoft Visual
Studio\VC98\Include folder and scroll down to the section

#ifndef DIJ_RINGZERO

/****************************************************************************
*
* DirectInput keyboard scan codes
*
****************************************************************************/

Below there is a complete list of all the keyboard strokes you can think of, i.e.

#define DIK_ESCAPE 0x01


#define DIK_1 0x02

#define DIK_NUMPAD7 0x47

The bit behind DIK_ is the actual key’s main value. As Flight Simulator uses a multitude of keyboard shortcuts
you will have to take care to pick one that is not yet in use or possibly one that IS in use already and will help
your cause…

In the update_routine section of your subgauge’s c file put what you want to happen:

/* "update_routine()" */

case PANEL_SERVICE_PRE_UPDATE:
keypressed=ReadDirectInputKeyboard( hFSMainWnd, NO_KEY);
if (keypressed==DIK_NUMPAD7)
{
… the code you want executed while the key defined above is pressed
}

The code below the if (keypressed==DIK_NUMPAD7) will be executed whenever and as long as the key
Numpad 7 is pressed. Instead of verbal definition from dinput.h you can directly insert the numerical hex value
assigned to it, i.e. 0x47 for DIK_NUMPAD7.

For key combinations with CTRL, ALT etc. things will only be slightly different. Here the example that will only

Dragonflight Design Page 77 of 112


work if both the right ALT key (DIK_RMENU) and Numpad 7 are pressed simultaneously:

/* "update_routine()" */

case PANEL_SERVICE_PRE_UPDATE:
keypressed=ReadDirectInputKeyboard( hFSMainWnd, DIK_RMENU);
if (keypressed==DIK_NUMPAD7)
{
… the code you want executed while the keys defined above are pressed
}

So simply ensure you place the “base” key in the brackets of ReadDirectInputKeyboard and any other key
pressed in unison with that “base” key will then trigger what you want it to.

Actually quite simple when you know how to do it!

Thanks to Arne Bartels who introduced me to the world of keyboard input in a chain of numerous emails. And to
make sure this info is made available to other not so proficient C++ programmers like me I decided to put this
little How-To together”.
Multigauges

Now we come to the hub of the FS2K+ gauge system - the single gauge that holds a number of smaller sub-
gauges. I have broken this section down into smaller topics as many of them add information to previous topics
above.

Global Declarations
Sooner or later your compiler is going to fall over with an error message that has "multiple initialisations"
written somewhere in the body of it. What this means is you have declared a variable with the same name in
two different sub-gauges; if you don't want this then simply change the name of one variable but be sure that
you change the name of all instances of that particular variable in that sub-gauge, otherwise the second sub-
gauge will also work with it!!

However, if you do want a variable to be seen by all gauges (as in sharing variables) then it must be declared in
only one place. The very best place for this is in the master source file as, although it should work if a variable
is declared in a sub-gauge, I've found that sometimes it doesn't.

The one declaration I can guarantee that you will fall foul of is the MAKE_STRING default font declarations i.e.

#define GAUGE_CHARSET DEFAULT_CHARSET


#define GAUGE_WEIGHT_DEFAULT FW_NORMAL
#define GAUGE_FONT_DEFAULT "COURIER_NEW"

Put this in the master source file and remove it from all sub-gauge files. It makes it immediately visible as a
global declaration and all sub-gauges that have strings in can see it. But what if you have one sub-gauge that
uses one font and another sub-gauge that uses a different font? Try this:-

#define GAUGE_CHARSET DEFAULT_CHARSET


#define GAUGE_WEIGHT_DEFAULT FW_NORMAL
#define GAUGE_FONT_ARIAL "ARIAL"
#define GAUGE_FONT_COURIER_NEW "COURIER_NEW"

then at the bottom of the MAKE_STRING macro replace the default line with the required font e.g.

MAKE_STRING( dme_dist,
NULL,
elec_fail,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
71, 46,
80, 30,
4,
DME2_DISTANCE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
RGB(255,128,0),
RGB(0,0,0),
RGB(92,92,92),
GAUGE_FONT_ARIAL, //<- New font type
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_CENTER | DT_VCENTER | DT_SINGLELINE,
NULL,
update_dist )

There is nothing to stop you declaring a set of font weights (bold, extra bold etc) using exactly the same form.
You can also declare MODULE_VAR-type variables in one source file and use them in another; each time I need
to read a new Token Variable I simply add it to my master source file template as well as to the master source
file of the gauge I am creating. All these small tricks help to speed up the creation of FS2K+ and CFS+ gauges.

Dragonflight Design Page 79 of 112


Multigauges and Sharing Variables
BIG NOTE!!!!! Although with FS2K+ you no longer really need to have day and night bitmaps as you did
with FS98, I’ve left the following section intact because of the principle of sharing variables that it illustrates.

Be very careful here. The "Sharing Variables" section above refers purely to sharing variables between two
unrelated gauges i.e. not called from or existing in the same multigauge. I am now talking about sharing
variables between subgauges of the same multigauge. Got that? Good! You have already seen me emphasising
the fact that all macro headers and all bitmaps in a multigauge must have unique names; this is because all
variable names declared in the master.c file of a multigauge are global – that is, any sub-gauge can see the
contents of the variables of any other sub-gauge. So to share variables inside a multigauge you no longer have
to bother with the setting up of MODULE_VAR, initialisation, installation or killing of client and server variables;
just simply declare the variable (e.g. UINT16 lights_on = 0) and use it. Any sub-gauge can operate on it and
all other sub-gauges will react to it.

That easy!!!

At this point it may be a good idea to describe the scope of FS variables within a multigauge.

1. Anything that is declared in a shared header file or the master.c file is global in scope i.e. it can be seen
and acted upon by all gauges in the multigauge.
2. Anything declared at the top of a subgauge.c file is modular in scope i.e. any subroutine within that
gauge can see and act on that variable.
3. Anything declared within a subroutine within a subgauge.c can only be seen by operations performed
within that subroutine. This applies to both „real‟ sub-routines and also to gauge callbacks (icon,
needle, string etc.).

The following illustrates the use of both internal and external shared variables. In the master.c file (sd360.c) I
have the following include declarations:-

#include "..\inc\fsxgauges_sp2.h"
#include "sd360.h"
#include "sd360_global_vars"

In the shared header file sd360_global_vars, I declare:-


/************** GLOBAL VARIABLES ***********************/
//Incoming power on signal for all gauges
UINT8 power_on = 0;

//Incoming lights on variable for all gauges


UINT8 lights_on = 0;

Now, power_on is the one that tells this gauge that power has been applied (1 = on) and is coming from an
external source – actually the Engine Services battery master switch. Of the sub-gauges in this gauge, plight1.c
is the one that is listening for this variable

switch(service_id)
{
case PANEL_SERVICE_PRE_INSTALL:
register_var_by_name (&power_on, TYPE_UINT32, "power_on");
break;

and as soon as it changes it uses that information to send the lighting command to the other sub-gauges in this
multigauge (nSwitch is a variable internal to this sub-gauge that indicates the lighting switch activated)

case PANEL_SERVICE_PRE_UPDATE:
if (power_on == 1 && nSwitch == 1)
{
lights_on = 1;
}
else
{
lights_on = 0;
}
lights_on is the command to turn the instrument backlighting on across all gauges in this multigauge; this is
the port engine fuel flow sub-gauge source file port_flow.c:-

MAKE_NEEDLE( port_flow,
FFG_BMP_NEEDLE,
NULL,
port_flow_fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
127, 127,
0, 5,
ENGINE1_FF_PPH,
port_flow_needle_src_cb,
port_flow_needle,
6 )

PELEMENT_HEADER portflow_plist1[] =
{
&port_flow.header,
NULL
};

MAKE_NEEDLE( port_flow_night,
FFG_BMP_NEEDLE,
&portflow_plist1,
port_flow_fail,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
127, 127,
0, 5,
ENGINE1_FF_PPH,
port_flow_needle_src_cb,
port_flow_needle_night,
6 )

PELEMENT_HEADER portflow_plist2[] =
{
&port_flow_night.header,
NULL
};

MAKE_ICON( port_flow_mask,
FFG_BMP_NIGHT,
&portflow_plist2,
NULL,
IMAGE_USE_TRANSPARENCY,
0,
0,0,
MODULE_VAR_NONE, NULL,
ICON_SWITCH_TYPE_SET_CUR_ICON,
1,
0,
0, )

PELEMENT_HEADER port_flow_plist3[] =
{
&port_flow_mask.header,
NULL
};

MAKE_ICON( port_flow_mask_night,
FFG_BMP_NIGHT,
&port_flow_plist3,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_BRIGHT,
0,
0,0,
MODULE_VAR_NONE, NULL,
ICON_SWITCH_TYPE_SET_CUR_ICON,
1,
0,
0, )

Dragonflight Design Page 81 of 112


PELEMENT_HEADER port_flow_plist4[] =
{
&port_flow_mask_night.header,
NULL
};

MAKE_STATIC( port_flow_background,
FFG_BMP_BACKGROUND,
&port_flow_plist4,
NULL,
IMAGE_USE_TRANSPARENCY,
0,
{0, 0} )

PELEMENT_HEADER portflow_list = &port_flow_background.header;

//----------------------------------------------------

case PANEL_SERVICE_PRE_UPDATE:

//Day-night switching

if (lights_on == 1)
{
SHOW_LISTELEMENT(pgauge->elements_list[0],1);
SHOW_LISTELEMENT(pgauge->elements_list[0],3);
HIDE_LISTELEMENT(pgauge->elements_list[0],2);
HIDE_LISTELEMENT(pgauge->elements_list[0],4);
}
else
{
HIDE_LISTELEMENT(pgauge->elements_list[0],1);
HIDE_LISTELEMENT(pgauge->elements_list[0],3);
SHOW_LISTELEMENT(pgauge->elements_list[0],2);
SHOW_LISTELEMENT(pgauge->elements_list[0],4);
}

break;

The first half of the if-else structure is commanding the IMAGE_BRIGHT bitmaps to display and hiding the non-
bright bitmaps. The second half reverses the process if the lighting is turned off.

Multiple Gauge Initialisation


One of the advantages of the multigauge system is that you can initialise the same gauge more than once to
appear on the same panel or on different sub-panels. There is however a small catch to this….. and it partially
explains the fifth parameter on each of the panel.cfg file's gauge initialisation lines. Look at this snip from one
of my panel.cfg files; here I am initialising a Collins EFIS-84 system both on the co-pilot's panel and also on a
large PIC panel that has just the primary instruments and nothing more (effectively an IFR panel):-

[Window07]
file=copilot.bmp
size_mm=640
position=7
visible=0
ident=COPILOT_PANEL

gauge00=1900d.efis!eadi, 210, 40, 126


gauge01=1900d.efis!power, 480, 275, 160
gauge02=1900d.efis!test, 200, 262, 180
gauge03=1900d.efis!dsp, 195, 285, 160
gauge04=1900d.efis!presel, 380, 290, 80
gauge05=1900d.efis!autopilot, 480, 225, 160
gauge06=1900d.efis!fdr, 480, 170, 160,,1
gauge07=1900d.efis!ehsi, 210, 162, 126,,1
gauge08=1900d.pri!vsi, 340, 150, 100,,1
gauge09=1900d.avionics!rmi, 102, 133, 100,,1
[Window08]
file=tscan.bmp
size_mm=640
position=7
visible=0
ident=TSCAN

gauge00=1900d.efis!eadi, 300, 123, 180,,2


gauge01=1900d.efis!ehsi, 300, 300, 180,,2
gauge02=1900d.pri!vsi, 480, 270, 135,,2
gauge03=1900d.avionics!rmi, 160, 260, 135,,2
gauge04=1900d.efis!fdr, 270, 40, 240,,2

You can see from the snip that there are three multigauges involved; the main EFIS (1900d.efis), the nav radios
(1900d.avionics) and the primary instruments (1900d.pri). Where I have extracted the same gauge on more than
one panel I have told FS2K that this is the nth-initialisation of this instrument. If you do not do this then one of
three things will happen:-

1. The instruments will all work quite happily!!


2. The instruments will only partially work.
3. The instruments will not work at all.

After a bit of experimentation I came up with the answer to why sometimes they work and other times they
don't. It depends on the internal programming; any instrument such as a VSI that relies solely on a token
variable callback from an SDK macro for its display will always work in multiple initialisations without the
trailing number. Any instrument that uses callbacks created by the programmer (i.e. token variables are not
inserted directly into a macro) will fail on that code (versions 2 and 3). As soon as you add the trailing
initialisation number FS2K pulls out its proverbial finger and starts displaying all multiple-initialisation
instruments correctly. It might be worth adding the above explanation to the readme of any gauge you release,
particularly if you believe that someone may want to display the same instrument more than once.

A minor problem with multigauge (not multiple gauge!) initialisation is that if a sub-gauge is set onto an
offscreen panel (i.e. one that is not yet shown) then any shared variables associated with that gauge will be
initialised but it will not react to any incoming shared variable information until the gauge is displayed.
Generally this will not be too much of a problem until you hit a similar problem to the one I did…… I spent a
lot of time very carefully designing the electrical system for a Beechcraft 1900D so that all the electrical buses
and invertors were represented. This meant that from the electrical buses it all went into the circuit breaker
panel where I had fifty-five circuit breakers, before then moving on to the various individual circuits. It looked
and worked beautifully – until I tried it all for "real" and as I powered up the aircraft nothing came on…. but as
soon as I opened the circuit breaker window the whole screen lit up like a Christmas tree as the various shared
variables were accepted, processed and sent on again. The answer is to initialise two versions of the same gauge
and put one on the primary panel in such a position and at such a size so that it cannot be seen or accessed. My
favourite position for this is 0,0 and at 1 percent of the designed size. This normally puts it somewhere in the
region of one pixel in size and completely invisible but all the variables behave as they should.

Using The Fifth Gauge Parameter in the Panel.cfg File


In some ways the above findings about multiple initialisation seems a little at odds with the Concorde engine
instrumentation used as an example in the SDK documentation; this appears to declare the same gauge without
reference to a specific engine and then tacks the engine number on as the trailing number. Aas usual the SDK
tells you nothing, confining itself to a terse

"Parameters – these are passed to the gauge as a text string argument".

However, Arne Bartels has come up with one way of passing the "text string argument" into a gauge and
making some use of it. Not unexpectedly, the work is again done by a pelement structure

pelement->gauge_header->parameters

where parameters is the fifth number on the "gaugeXX=" line. He has managed to duplicate the Concorde-type
engine setup in FS2K2 as follows.

Dragonflight Design Page 83 of 112


First of all, declare the function to switch engine numbers.

GAUGE_TOKEN SwitchRpmToken(UINT32 EngineNum)


{
switch(EngineNum)
{
case 2:
return TURB_ENGINE_2_N1;
case 3:
return TURB_ENGINE_3_N1;
case 4:
return TURB_ENGINE_4_N1;
default:
return TURB_ENGINE_1_N1;
}
}

Now return that information to a MAKE_SLIDER display

FLOAT64 FSAPI slider_cb( PELEMENT_SLIDER pelement )


{

//Initialise the MODULE_VAR rpm

MODULE_VAR rpm = {MODULE_VAR_NONE};

//Get the next engine parameter

PCHAR GaugeParameter=(PCHAR)pelement->gauge_header->parameters;

//Convert the GaugeParameter alpha character to an integer


//and pass it to the SwitchRpmToken function

rpm.id=SwitchRpmToken(atoi(GaugeParameter));

//Lookup the rpm for the new engine

lookup_var(&rpm);

//Do something with it to drive the display etc.

Arne comments that "During each cycle a check for the engine number is made, not too effective, but working".
You can also make the engine number decision in the gauge callback function but access to the gauge bitmap
display information is more difficult, involving a

pgauge->elements_list[0]->next_element[0]

type structure, much the same as required by the HIDE/SHOW macros. Again, in Arne's words: "In this
example I want to switch the module var of an ELEMENT_SLIDER that is the first element after the background. If
you use this code, the module var is set to the desired engine before each drawing cycle." For greater clarity I'd
also add that this code snip is calling to the GAUGE_TOKEN SwitchRpmToken function defined at the top of this
section and that the reading from each engine is read (in order) to the same display element in the MAKE_SLIDER
macro. As the functionality of this code snip is very similar to the one above, you should be able to figure out
exactly what is going on.
void FSAPI gauge_cb( PGAUGEHDR pgauge, int service_id, UINT32 extra_data)
{
PELEMENT_SLIDER pelement=NULL;
PCHAR GaugeParameter=NULL;
switch(service_id)

{
case PANEL_SERVICE_PRE_DRAW:

GaugeParameter=(PCHAR)pgauge->parameters;
pelement=(PELEMENT_SLIDER)pgauge->elements_list[0]->next_element[0];
pelement->source_var_y.id=SwitchRpmToken(atoi(GaugeParameter));

break;
}
}

Included in the archive \examples folder is a very much more advanced code sample for using the fifth
parameter from Arne Bartels (registration.zip). As it involves some in-depth and fairly serious knowledge of the
C language, I have chosen not to expand the contents further here. Any mistakes in the readme.txt file are my
fault – I wrote it from Arne's information! I would encourage you to study this code because I have made
extensive use of the fifth parameter to customise the appearance of gauges – it can be very useful indeed.

Adding Gauge Sounds


Programming Microsoft‟s DirectSound is well beyond the remit of this tutorial. However, included in this
package is the code for a gauge that allows you play sounds via the DirectSound interface. The code is designed
to be compiled as a part of your own gauge; this initial release has no volume control and no panning facilities
and will only accept sounds of a few seconds duration. This will be fine for around 99.5% of gauge
applications. At a later date I intend to expand the code to accept volume, panning and EAX processing as well
as the ability to stream files of a much longer duration.

The package is in the \Source Codes\DirectSound folder. I have also supplied a complete compiled gauge that
you can use for initial testing to ensure that it is calling DirectSound correctly on your PC. Copy the gauge in
\directsound\gauge into your \fs\gauges folder and add the following two lines to any panel:

gaugeXX=dfdsnd!msoundgau, 0,0,1,1
gaugeXX=dfdsnd!dsoundtest, 0,0,184,64

Now launch that aircraft. In the top left corner of the screen you will see three coloured squares (red, green and
yellow). Click on the red square for a play once sound and on the yellow square to set off a continuous
(looping) sound. The green square cancels the loop.

Adding the Code to your Gauge

I make no assumptions about how you have your IDE set up for coding, so you will need to figure out your own
paths and pathing. Check the structure of the \directsound\code folder and make changes as needed to match
your setup. Note that the dragonflight.h in the \sources codes\directsound\code\include folder is a modified
version of dragonflight.h found in the \source codes\inc folder and contains only the file calls required for
DirectSound. You can either rename this to whatever you choose and #include it from your main gauge or
incorporate its contents into the „other‟ dragonflight.h. In either case be careful: there are some duplicate
#includes which you will need to remove. You must have at least one header file that contains variables shared
across all gauges; to this add the line

char PlaySnd[256];

In this case it is in the \code\include\dragonflight.h. The panel.cfg syntax is:

gaugeXX=<your gauge name>!msoundgau, x,y,dx,dy

Using the Code

There are some minor restrictions on passing information to the gauge. The path to your sound folder should
not be more than 255 characters long, including the name of the wav file itself and the control parameters. The
maximum number of simultaneous sounds is fifty (50); sounds 51+ will be silently discarded. The syntax for
the variable PlaySnd is

PlaySnd(<path to sound>,<command>)

Dragonflight Design Page 85 of 112


where command can be once, loop or stop. e.g.

PlaySnd(sounds\barn.fx,loop)

<path to sound> is relative to the \gauges folder. The gauge will accept a path pointing to a custom sound
folder within the aircraft folder. If you are passing in a sound that is played once only (i.e. not looping) then you
can use the shorter syntax

PlaySnd(<path to sound>)

Passing in a stop command to a sound that was started with the once control command (or the short syntax
command) will be ignored.

The code within the gauge tracks the DirectSound secondary buffers in use in an attempt to reduce the amount
of memory overhead. If a sound has finished playing the gauge will reallocate that buffer to a new incoming
play command, rather than creating a new buffer. A new buffer is only created if all existing buffers are in use;
for some reason the unmanaged DirectSound interface makes no provision for destroying unused buffers and
freeing up the used memory. The buffers are only destroyed when DirectSound is released. A rough calculation
showed that if you did manage to set all fifty possible files playing at the same time you would occupy
approximately 5Mb of memory. Finally, if you pass in a stop command to a looping file and there is more than
one file of that name in loop mode, the gauge will stop the first one it finds. I can‟t do much about that.

Debug Facilities

If you open and study the code in both dfdsnd.c and sound.cpp, you will see that it is littered with code blocks
that start

debugdata=fopen(debuglog,”r”);

or

debugdata1=fopen(debuglog1,”r”);

Each of these is a debug block. To trigger debugging create a file in the \gauges folder called dxdebug.log. Just
the file; you do not need to enter anything into it. The code checks for the existence of that file and if it finds it,
automatically logs the debug data to it. Be careful; dxdebug.log can grow very big very quickly if you forget
about it. You can remove all of the debugging blocks if you wish but I would recommend leaving them in as a
troubleshooting aid both on your PC and on the PC of a user of your gauge. Dxdebug.log will log the
DirectSound initialization, all information that is passed to the gauge, every subroutine entered and every call
made within that subroutine. In general, any problems you get should be immediately visible as incorrect data
getting into the system or should be immediately traceable as the failure point will be after the last-logged
datum.
Lateral Thinking Corner
There are many useful functions missing from the list of Token Variables and key_events and still quite a few
problems with the way FS2K+ is actually written but with a little thought it is possible to get round a lot of
them and extend the environment still further. This section will normally contain only ideas; they will be ideas
that I have coded (or seen coded) and working, but for reasons of proprietry code or the awkwardness of
providing any example there will probably be very little in the way of code snips here. It is here simply to make
you think.

100% Realistic?
How many times have you seen “100% realistic” written down as part of the description of a panel? There are
some panels that come very, very close indeed to the operation of their real-world counterparts but the majority
of them, however well presented and however close visually they are to the originals, are missing some very
important systems. Just looking at the default Cessna; you can leap into your virtual cockpit, push home the
mixture control, twist the magneto key (which incorrectly fires the engine as soon as hits “R” instead of waiting
until it gets to “Start”), hit F4 for full throttle and away you go. What happened to the battery master switch?
What happened to the generator master switch? What happened to the low volts warning, the ammeter, the
voltmeter? What happened to all the pre-flight test procedures? I know FS2000 addressed some of these issues
but not enough of them. The default panels in particular in both sims are not a “panel” but simply a “collection
of gauges”. The pilot fires up, takes off and then just sits there, occasionally changing course but never
bothering to monitor any sub-systems because nothing ever really goes wrong. If it does it is of the FS “hey
look! all this has failed” variety and it promptly then drops you out of the virtual sky. Where‟s the airmanship
in flying with part of the system out? Where‟s the backups and standbys? Where‟s the badly behaved aircraft
because a bit of it‟s now missing?

With a bit of thought and some in-depth information on the aircraft and its sub-systems these missing items can
be addressed and the result is a much more realistic mode of operation. The downside is that you will have to
program almost every gauge on the panel to get it to work as an integrated whole. The key is the “Shared
Variables” sections above.

The simplest thing to add is probably the electrical systems. FS2K+ has greatly improved in this area over FS98
(see "The FS2K+ Electrical System" below); for FS98 I had been gradually releasing a set of gauges that I have
called (for want of a better reference!) the “Electrical Panel”. Despite the inclusion of some form of power
generation in FS2K+ it still does not go far enough if you are designing a complete panel from scratch, hence
the reason that I have chosen to leave this section in.

The Electrical Panel series was based on the premise that nothing will work until the battery master switch is
turned on. The battery master is programmed to broadcast a whole load of shared variables that simply say to
all the other instruments “the power has been turned on”. The other gauges will then react to those variables e.g.
the voltmeter then displays the battery voltage and the magneto switch, when turned to start, will engage the
“starter motor” and crank over the engine. Once the engine has fired and settled to idle, hitting the generator
switch will bring the “generator” on-line, turn out the “gen” light and broadcast a variable to activate the
ammeter. The ammeter is programmed to show a slight fluctuation depending on engine rpm (using the
ENGINEx_N2_RPM token variable). During the starting procedure as the magneto gets to the “start” position, it
broadcasts a variable to the voltmeter which then shows a needle deflection downwards (from “26 volts” to “21
volts”) to illustrate the current surge as the starter motor is engaged. Further on across the system, the OMI test
and the OMI will not work unless the battery is active and nor do the landing gear lights or flaps.

None of this was very difficult to achieve. The gauges were all written as if they were standalone and after
testing to make sure they worked correctly, the final move was to add the “power on” code. The code structure
will, in general, look like this for whatever section of the gauge it is in:-

Is the battery master turned on?


if yes
{
Put all the code for the gauge here
}
if no
{

Dragonflight Design Page 87 of 112


{
Do nothing if the gauge has never been powered up
}
else
{
if the gauge has been powered up
turn off all lights
return all needles to zero
return all internal variables to their default states
}
}

The last part (turning off the gauge) is as important as turning it on because you are allowing for both the
battery master being turned off and a power failure.

Which neatly brings me to the next problem – system failures. FS has a “block failure” mode in which
everything on a particular sub-system (electrical, hydraulic, vacuum etc) fails in one go. FS2K+ is improved
dramatically over FS98 in this respect but it is still very unrealistic – in real life a single instrument on one sub-
system will fail. Within that instrument (e.g. an ADI) there might be many separate sections of that instrument
that can fail for different reasons (loss of a navaid, loss of electrical power, loss of a radio). This, I‟m afraid, is a
whole new ballgame and can get incredibly complex. How far do you go? And how do you do it?

One method is to have a gauge that does nothing except generate random “failure variables”. When the panel
initialises, that gauge runs through its randomising procedure once and stores “fail/no fail” indications for each
gauge (and sub-section of that gauge) that you have coded to react to it. If the initial result is a “fail” then you
refine that to “how long into the flight before I broadcast the fail signal?”. So far, on a simple light aircraft
panel it‟s fairly easy – the instrument dies and that‟s that. However, if you are programming a commercial
aircraft panel then you have all the backup systems that are required by law and they, in turn, have to be
notified that a particular instrument has failed. So now you have to create all those backup systems too - as I
said, how far do you go?

In some degree it depends on how complex the real aircraft is and how willing you are. It can be done on a
small commercial turboprop such as an F27, ATR or SD3-60 but once you start hitting the big boys then you
have to start making decisions on what you are NOT going to do. I spent over a year programming one
turboprop panel with the direct help of a commercial aircraft manufacturer so that they could use it as part of a
pilot conversion course, so I know it can be done. Even so, I still only achieved 98% systems accuracy because
the circuit breaker panel was simply out of the question.... well, it wasn‟t really but I wasn‟t willing to do it!!! If
you decide to go this far at some stage in the future you will need as a bare minimum (in addition to panel
pictures) the following items;-

The FCOM (Flight Crew Operations Manual) for that aircraft


The aircraft manufacturer's reference manual showing all the systems (or similar as used by a flight school)
A full set of checklists and emergency procedures
Access to someone who flies the aircraft and is willing to test the panel as well as answer questions
Manufacturer‟s manuals for the more complex instruments (such as a flight director).

Coming back a little to reality (!), there are some easier ways to program problems especially if you are
working with an aircraft designer. Aircraft icing is one area that‟s annoyed me in the past; you hit 4 degrees
Centigrade or below, you are in cloud and where‟s the ice problem? Ice destroys the aerodynamics of the wings
- and so do spoilers. But what if your aircraft doesn‟t have spoilers? Get your designer to put them in the flight
model – it doesn‟t have to use them! (There is a minor problem here in FS9 in that you cannot recognise when
you are in cloud, so take the 4 degrees Centigrade point as a possible trouble point and run a randomiser for an
ice/no ice indication. In FSX you do have access to in cloud, atmospheric saturation and temperature variables
which will enable you to create a realistic icing scenario). Program the anti-ice panel with the randomiser as
above and work on (say) a twenty minute time period of being in “icing” conditions before trouble strikes. If,
after the time period has expired the pilot has taken no action then start to very gradually extend the spoilers.
The aircraft will steadily get more difficult to fly and eventually (if no remedial action is taken) fall out of the
sky – just like the real thing. Part of the preflight checklists in a commercial aircraft is to switch on the ice
detector if icing conditions are expected, so your virtual pilot should have no excuse not to do it if he is trying
to operate the aircraft correctly.

My last thought for you in this section concerns an equally serious aviation problem; that of engine fires. I have
seen fire extinguisher handles on panels put there as eye-candy because they are in line-of-sight of another,
working gauge. But you can make them work with a little thought. Again, we are back to the randomiser – is
there going to be a fire this flight (say randomise to one in thirty flights)? If yes, which engine and when?
During the flight the relevant engine catches fire and the pilot hits the first extinguisher. Randomise – is the fire
out? If yes, randomise for engine failure and if yes, shut the engine down and prevent any restarts. If fire out
equals no, the pilot hits the second shot. Is the fire out now? If no, give him a certain amount of time to get on
the ground (say twenty minutes to the nearest airfield). If the aircraft is still in the air after this time, then take
control of the aircraft via the “firegauge” and crash it. Even if the fire is out on the second shot, shut the engine
down and prevent restarts.

Using similar methods you can program almost any aircraft system failure. Another easy one is the landing gear
hydraulics – will you get three greens or not? However, in most cases you will also need to have programmed
the relevant backup system – in the case of the landing gear failure you need the emergency gear handle
somewhere.

Think about it – 100% realistic panels are possible and you can make the virtual pilot work for his/her flight!

The FS2K+ Electrical System


Unlike earlier versions of FS, Microsoft have made some attempt to create a set of aircraft power systems and
allow gauge designers to hook into them. This section applies to creating individual gauges as replacements or
additions to the existing default panels, rather than gauges that form part of a specific aircraft.

The FS2K+ token variables give you the following electrical systems to work with:

Main Buses

BATTERY_VOLTAGE
MAIN_BUS_VOLTAGE
MAIN_BUS_AMPS
AVIONICS_BUS_VOLTAGE
AVIONICS_BUS_AMPS
HOT_BATTERY_BUS_VOLTAGE
HOT_BATTERY_BUS_AMPS
BATTERY_BUS_VOLTAGE
BATTERY_BUS_AMPS
GENERATOR_ALTERNATOR_1_BUS_VOLTAGE
GENERATOR_ALTERNATOR_1_BUS_AMPS
GENERATOR_ALTERNATOR_2_BUS_VOLTAGE
GENERATOR_ALTERNATOR_2_BUS_AMPS
GENERATOR_ALTERNATOR_3_BUS_VOLTAGE
GENERATOR_ALTERNATOR_3_BUS_AMPS
GENERATOR_ALTERNATOR_4_BUS_VOLTAGE
GENERATOR_ALTERNATOR_4_BUS_AMPS

Individual Circuits

NAV_LIGHTS – turns the panel instrument lights on and off


AVIONICS_MASTER_SWITCH
FLAP_MOTOR_CIRCUIT_ON,
GEAR_MOTOR_CIRCUIT_ON,
AUTOPILOT_CIRCUIT_ON,
AVIONICS_CIRCUIT_ON, – turns some of the flight instruments on and off
PITOT_HEAT_CIRCUIT_ON,
PROP_SYNC_CIRCUIT_ON,
AUTO_FEATHER_CIRCUIT_ON,
AUTO_BRAKES_CIRCUIT_ON,
STANDBY_VACUUM_CIRCUIT_ON,
MARKER_BEACON_CIRCUIT_ON,
GEAR_WARNING_CIRCUIT_ON,
HYDRAULIC_PUMP_CIRCUIT_ON,

In the case of the individual circuits the return value is always a 1 = On (or active) and 0 = Off (or inactive).
Simply by checking these returns you can make your gauge behave according to what that system is supposed
to be doing e.g. if (PANEL_LIGHTS == 1) then turn on your gauge's backlighting (see “Gauge Night Lighting”
above). However and as usual, all is not what it seems. The only really usuable token variables in this new list

Dragonflight Design Page 89 of 112


are AVIONICS_CIRCUIT_ON and NAV_LIGHTS as the moment you turn on the battery master switch all of the
others go to 1 i.e. on, unless you have the realism turned up high. In this case (low realism) these circuits only
ever go to zero when an FS2K+-generated failure occurs or you turn the battery master off. Worse, if you‟ve
ever tapped the default avionics circuits just to see what happens when you start adding load; it doesn‟t add
anything at all. The avionics on circuit adds ten amps discharge and that‟s your lot. No discharge for lights on,
beacons on, engine start etc. Horrible system, because you always have to assume that the user is going to have
the realism settings left at default (low).

Okay…..let‟s see if we can make some use of it. For example: you may want to create a major gauge from a
B744 that you know is always powered from generator bus 3 but you may also want to make this gauge more
widely useable as the real one is also used on smaller aircraft. In this case, for safety you will always be
restricted to using generator bus 1 or generator bus 2. As a general rule though, if you create a gauge that
should only operate when the main buses are on and that gauge can be used in any aircraft (e.g. a VOR or
ADF), then use GENERATOR_ALTERNATOR_1_BUS_VOLTAGE in combination with the correct CIRCUIT_ON variable
to be able to create a reasonably good gauge power mode.

If you do not want to go to all that trouble but still would like your gauge to react to the default power system in
any panel then simply use AVIONICS_CIRCUIT_ON. If it equals one then apply power to your gauge. I‟d also
advise that you use the original PANEL_LIGHTS token variable in preference to the NAV_LIGHTS variable if
deciding whether to turn instrument lights on and off. NAV_LIGHTS does work for the panel lighting – until you
turn the realism up……

My bottom line to all this is that I never use the default electrical variables - I always create my own electrical
circuits (anyone remember the FS98/FS2000 „Electrical Panels‟ series?). This allows me to reach absolute
accuracy for the particular aircraft that I am working on; for example, this is the program flow for a civil
airliner from engine 1 powering AC bus 1 and DC bus 1:-

For AC bus 1:-

Is the engine running?


-If yes, is the generator online?
-If yes, is the generator breaker set?
-If yes, make the following available (anything that hangs off the main AC bus 1, including any sub-buses):
--Hydraulic pump
--Battery charger
--Flight station bus 1
--TR1
--etc

For DC bus 1:-


Is TR1 available?
-If yes, make available everything that hangs off DC bus 1 e.g.
--FE Indicator light test
--Rudder servo C
--Tank valve eng 1
--etc

Down at sub-bus level, I do some further checks:-

Is flight station bus 1 available?


-If yes, make available everything that hangs off flight station bus 1 e.g.
--INS3 Gyro 3
--Captain‟s Left Instrument Lighting
--Captain‟s Right Instrument Lighting
--Left alpha heat
--etc

Finally, I arrive down at instrument level. In this case I am checking to see if the ASI instrument backlighting is
available:-
if(fstation_bus1 && capt_instr_light) //Flight station bus 1 is active and the lights
have been selected on
{
LIGHT_LISTELEMENT(pgauge->elements_list[0],1); //Instrument face
LIGHT_LISTELEMENT(pgauge->elements_list[0],2); //ASI needle
}
else //turn the lights off if either the bus fails or the lights are selected off
{
DARKEN_LISTELEMENT(pgauge->elements_list[0],1); //Instrument face
DARKEN_LISTELEMENT(pgauge->elements_list[0],2); //ASI needle
}

If you want to get really smart, there is nothing preventing you from adding the circuit breakers into the
electrical circuits and making them work as part of a failures scenario. We can modify the above snippet to
read:

if(fstation_bus1 && CB1L20 && capt_instr_light) //Flight station bus 1 is active, circuit
breaker CB 1L20 [Capt Left Instrument Lighting] is okay and the lights have been selected on
{
LIGHT_LISTELEMENT(pgauge->elements_list[0],1); //Instrument face
LIGHT_LISTELEMENT(pgauge->elements_list[0],2); //ASI needle
}
else //turn the lights off if either the bus fails or the lights are selected off
{
DARKEN_LISTELEMENT(pgauge->elements_list[0],1); //Instrument face
DARKEN_LISTELEMENT(pgauge->elements_list[0],2); //ASI needle
}

You may also need to create a set of variables that will display generator voltage and amperage if required. I
would normally base these off the N2 reading for the engine concerned; if N2 is stable at or above idle then
allow full voltage and power. If N2 falls below idle, wind down available voltage and power proportionally and
pick a point at which everything goes offline for lack of volts.

Flight Models
The biggest bugbear in FS aircraft is the fact that you are restricted to five, not very good flight models. A
clever aircraft designer can achieve quite a lot with the .air file but an aircraft designer working along with a
gauge designer can achieve a lot more. In FS98 in particular, if someone wanted to simulate a turboprop they
had no choice but to use the LearJet jet flight model. However much the air file was tweaked, in the majority of
cases the resulting aircraft was still far too “hot” in much of the flight regime. If the aircraft designer
concentrates mostly on the manoeverability and the inertia of the aircraft, the gauge designer can regulate the
amount of power available to the flight model and thus cut out those nasty “jet rocket” tendencies when in
reality you have a pair of fans up front. This still applies to the FS2K+ "turboprop" model. Right...... but how?

By regulating how much power the throttle levers can command. The allowed power range for the throttle is
from 0 to 16383 with idle usually showing up at around 4000 (use the ENGINEx_THROTTLE_LEVER_POS token
variable to read this). Full throttle is 16383 but you do not have to go this far. What happens if you restrict the
power input to (say) 12000? The result is a longer takeoff roll and less power in the climb so a more realistic
performance. The one thing you have to catch is the pilot who hits F4 for full power; you have to intercept that,
reduce the power to your allowed maximum and prevent the throttle lever bitmaps from jumping momentarily
while that section of the code is processing. You also need some way of easily adjusting the amount of
available maximum power without having to continually re-compile the gauge. Neither of these problems are
difficult to overcome; take a look at the source code in throttle.zip. This is the source code for the throttle stack
on the SD3-60 FS2K conversion so as it is simply a sub-gauge it is not compilable for use. It shows three things
of interest; how to derive a realistic maximum power setting (max_throttle); how to restrict ground power for
taxying (MAX_TAXI) and how to set up and trigger reserve take-off power (RTOP).

It also exhibits a bit of a split personality as although I have illustrated the code to derive max_throttle, if you
look at the MAKE_MOVING macros you will see that the maximum throttle movement has been already been
set there from FS98 days! (12288).

The section you are really interested in is the update routine.

1. The test for checking maximum power is done from a simple text file. When the power level is achieved,

Dragonflight Design Page 91 of 112


then max_throttle can be hard-coded as an integer. As told above, this finally worked out to 12288.
2. One of the shared variables is control_lock. This restricts power still further so that the maximum ground
speed available is MAX_TAXI, a speed set by the aircraft manufacturer for ground manoeuvers. In the case of
the SD3-60 this worked out at a throttle variable setting of 5500 to give about 25 knots.
3. The difference between maximum allowed power and FS maximum power can be used to program an
RTOP (Reserve Power Take Off) system in case of an engine failure during the takeoff roll – more realism.
4. How to override that F4 keyboard or joystick throttle inputs so that it doesn't override either max_throttle
or MAX_TAXI and conceal the fact using the MAKE_ICON functions dummy_left and dummy_right to override
the moving bitmaps.

It‟s better commented than it used to be when in FS98 format but by now I guess you should be able to follow it
and in particular, extract and use the code that defines max_throttle. The code really isn‟t that difficult but the
results can be spectacular!

Creation of Superchargers and Turbochargers


Note: this no longer applies to turbochargers as the FS2K2 SDK introduced turbo boost token variables.

Here's another use for this type of throttle restriction too: creation of superchargers and turbochargers (thanks to
Frank Guder for asking the question and making me think about this one). This is simply an extension of the
RTOP system I developed for my SD3-60 but instead of adding the entire extra throttle at one go you are
applying it in controlled increments. Ideally you need to work with an aircraft designer for this one but as long
as you have access to a flight dynamics editor and know how to modify an .air file then you can still go ahead.

First of all tweak the .air file until full throttle gives the engine plus full-bore super/turbocharger performance.
Next, using similar code as for max_throttle above find out which throttle setting gives full normally-aspirated
performance (i.e. no supercharger or turbocharger input at all). The difference between this and full throttle
equals your boost performance. Let's work an example whereby the aircraft has 10psi boost available for a
turbocharger (okay so I'm going to make this example dead easy!):-

16383 = full turbocharger


12883 = max_throttle (normally-aspirated performance)
16383 – 12883 = 4100/10 = 410 increments per pound of boost.

Somewhere on the panel you create another sub-gauge that allows you to set the amount of boost you require.
Each time you increment or decrement by one pound of boost you add or subtract 410 to max_throttle and this
change applies over the entire visual throttle range onscreen. Don't forget that to make this convincing you have
to restrict the visual movement of the throttle levers in exactly the same fashion as I have done to create the
RTOP on the SD3-60. If you do not do this then each time you add a pound of boost the throttle lever will move
further off the track.

Modifying the Autopilot Turn Rate


FS insists on a rate one turn but there are times when a rate one turn is too steep e.g. when flying through heavy
weather. For a rate one-half turn you would need to control the entire process yourself. What follows is pure
theory but the process was prompted by a question from Jonathan Bishop.

1. Ascertain which way to turn the aircraft onto the new heading.
2. Disconnect the FS autopilot heading function.
3. Feed in bank using (KEY_AILERON_xxx,nnn) and check for bank using PLANE_BANK_DEGREES until the
required angle is achieved.
4. Lock the bank using (KEY_AP_ATT_HOLD_ON,0). If you want to maintain altitude then you will also need to
hit (KEY_AP_ALT_ON,0) as the nose will drop.
5. When within (say 10 degrees) of the new heading, disconnect attitude hold (KEY_AP_ATT_HOLD_OFF,0) and
reconnect (KEY_AP_HDG_HOLD_ON,<hdg_dir>).
6. Disconnect (KEY_AP_ALT_OFF,0) if necessary when the new heading is stabilised.
Step three will define how quickly the aircraft takes up the bank angle and step five will take a bit of trial and
error as you need to discover the point where you will get a smooth transition between the two autopilot
functions.

Dragonflight Design Page 93 of 112


Getting Lazy
Sooner or later you are going to start to think of ways to speed up gauge creation. This can range from creating
your own gauge templates to building header files containing "shortened" commands. I'm not going to worry
about templates as each programmer will have their own idea about what constitutes a gauge template. As a
starting point though, this is the part of my dragonflight.h file that contains both shortened macro commands
and colours specified as RGB numbers. The colours are there mainly for string displays; the file itself is kept in
the same directory as fsxgauges_sp2.h.

//dragonflight.h Dragonflight Design 2007

//Make_string colours
#define collins RGB(255,64,0)
#define bright_red RGB(255,0,0)
#define orange RGB(255,128,0)
#define bright_green RGB(0,255,0)
#define blue RGB(0,0,255)
#define bright_yellow RGB(255,255,0)
#define trans_black RGB(0,0,0)
#define cyan RGB(0,255,255)
#define white RGB(255,255,255)
#define magenta RGB(255,0,255)

//Shortened macro flags


#define transparent BIT0
#define erase BIT1
#define bright BIT2
#define no_static BIT4
#define anti_alias BIT7
#define alpha_mask BIT12
#define alpha BIT13
#define luminous BIT14
#define string_trans BIT15
#define p_luminous BIT15
#define hidden_tree BIT25
#define hidden BIT30

//Font flags
#define def_char DEFAULT_CHARSET
#define arial "ARIAL"
#define courier_new "COURIER_NEW"
#define tnr "TIMES_NEW_ROMAN"
#define helvetica "HELVETICA"
#define quartz "QUARTZ"
#define glass "GLASS GAUGE"
#define anbi "ARIAL NARROW BOLD ITALIC"

#define normal FW_NORMAL


#define bold FW_BOLD

Probably the first thing you notice is that oddly-named "collins" colour in the colour descriptions. This is the
colour shown by the LED displays on Collins avionics and can be seen on the nav radios on my SD3-60 panel;
if anything, these odd manufacturer-specific colours are a good argument for a colour description header file.

In the shortened macro flag section I have repeated one macro flag definition ( BIT15) simply to make the macros
easier to read. I have also bypassed the original SDK definitions and gone back to the gauges.h source to get the
BITx information. Notice the gaps in the BITx progression….. the functions carried out by these definitions (if
any!) still need to be discovered. I am fairly confident that there are still new and unknown flag functions as
BIT7 came a long time after the others.

The font flags are a mixture of sheer laziness (can't be bothered to hunt for the shift key for uppercase letters!!!)
and shortened comands. Why only two fontsize flags? Because the others don't seem to work correctly…….

The following code snip is from my Collins EFIS-84 EADI display; I have listed both the original Microsoft
style and my lazy style for comparison.
//Original
MAKE_STRING(eadi_trim,
&eadi_plist3,
NULL,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT | BIT15,
0,
213, 62,
40, 30,
4,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
255, 255, 255,
0, 0, 0,
255, 255, 0,
GAUGE_FONT_COURIER,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
0,
DT_VCENTER | DT_CENTER | DT_SINGLELINE,
NULL,
eadi_trim_cb )

//Lazy
MAKE_STRING(eadi_trim,
&eadi_plist3,
NULL,
erase| bright | string_trans,
0,
213, 62,
40, 30,
4,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
MODULE_VAR_NONE,
white,
black_trans,
bright_yellow,
courier_new,
normal,
def_char,
0,
DT_VCENTER | DT_CENTER | DT_SINGLELINE,
NULL,
eadi_trim_cb )

Perhaps I'm biased but I know which one is easier to read! Be careful though – it is too easy to write really
obfusticated code by being too lazy.

Dragonflight Design Page 95 of 112


Running Visual C++ as a Gauge Debugger

This section has been deprecated as both FS9 and FSX will refuse to start if you attach a debugger to them.
Microsoft giveth and Microsoft taketh away; they‟ve considerably improved the SDK for FS9 and FSX but
have made it far more difficult to debug the gauges you create.
Conversion Factors and Oddities
Additional Macros
The following macros have been added to the fs2kgauges_sp2.h file to make life easier. Some of these have
already been refered to above (the LIST_ELEMENT structures) but they are all shown here for completeness.

Image Manipulation

LIGHT_IMAGE( element )
DARKEN_IMAGE( element )

GET_USE_LUMINOUS( element )
LUMINOUS_IMAGE( element )
DELUMINOUS_IMAGE( element )

FORCE_LIGHT(element)
FORCE_DARKEN(element)
FORCE_LUMINOUS(element)
FORCE_DELUMINOUS(element)

TOGGLE_IMAGE_DATA( element ,IMAGE_FLAGS )


ADD_IMAGE_DATA( element ,IMAGE_FLAGS )
REMOVE_IMAGE_DATA( element ,IMAGE_FLAGS )

REDRAW_IMAGE( element )

HIDE_LISTELEMENT( pelement ,pos_element )


SHOW_LISTELEMENT( pelement,pos_element )
REDRAW_LISTELEMENT( pelement,pos_element )
LIGHT_LISTELEMENT(pelement,pos_element)
DARKEN_LISTELEMENT(pelement,pos_element)
LUMINOUS_LISTELEMENT(pelement,pos_element)
DELUMINOUS_LISTELEMENT(pelement,pos_element)

SET_OFFSCREEN_LISTELEMENT(pelement,pos_element)
SET_ONSCREEN_LISTELEMENT(pelement,pos_element)

Bit Manipulation

GET_BIT(value,filtermask)
CHECK_BIT(value,filtermask)
CHECK_BIT_STRICT(value,filtermask)
ADD_BIT(value,bit)
REMOVE_BIT(value,bit)

Conversion Factors

These are used with the conversion macros below.

PI 3.1415926535897932384626433832795
FS_LAT_FACTOR 111130.555557
FS_LON_FACTOR 781874935307.40
RADIANS_TO_DEGREE_FACTOR (180.0/PI)
METER_FEET_FACTOR 3.28084
KILOMETER_NM_MILE_FACTOR 0.54
POUND_KILOGRAM_FACTOR 0.453592
METER_PER_SECOND_KNOT_FACTOR 1.944
GALLON_LITRE_FACTOR 3.785
INCH_HG_PSI_FACTOR (.4912)

Dragonflight Design Page 97 of 112


Conversion Macros

FEET_TO_METER(val)
METER_TO_FEET(val) INCH_HG_TO_PSI(press)
PSI_TO_INCH_HG(press)
FPS_TO_MPH(val)
MPH_TO_FPS(val) INCH_HG_TO_PSF(press)
PSF_TO_INCH_HG(press)

FPS_TO_KNOT(val) GALLON_TO_KILO_JETA(val)
KNOT_TO_FPS(val) KILO_TO_GALLON_JETA(val)

NAUTIC_MILE_TO_METER(val) GALLON_TO_KILO_AVGAS(val)
METER_TO_NAUTIC_MILE(val) KILO_TO_GALLON_AVGAS(val)

POUND_TO_KILOGRAM(pound) FAHRENHEIT_TO_CELSIUS(fahr
KILOGRAM_TO_POUND(kilogram CELSIUS_TO_FAHRENHEIT(cels

POUND_TO_METRIC_TON(pound) CELSIUS_TO_KELVIN(celsius)
METRIC_TON_TO_POUND(ton) KELVIN_TO_CELSIUS(kelvin)

METER_PER_SECOND_TO_KNOT(val) RANKINE_TO_KELVIN( rankine )


KNOT_TO_METER_PER_SECOND(val) KELVIN_TO_RANKINE( kelvin )

LITRE_TO_GALLON(litre) RANKINE_TO_FAHRENHEIT(rankine )
GALLON_TO_LITRE(gallon) FAHRENHEIT_TO_RANKINE(fahr)

PSI_TO_PSF(press) CELSIUS_TO_RANKINE(celsius)
PSF_TO_PSI(press) RANKINE_TO_CELSIUS(rankine)

Simulator-Related Conversion Macros

FS_LATITUDE_DEG(val) FS_GROUND_ALT_FT(val)
FS_LONGITUDE_DEG(val) FS_PLANE_ALT_FT(val)
FS_VSPD_FT(val) FS_MACH(val)

Math Functions

DEG_SIN(val) DEG_ATAN2(val1,val2)
DEG_COS(val) RAD_TO_DEG(val)
DEG_TAN(val) DEG_TO_RAD(val)
DEG_ASIN(val) SIGNUM(val)
DEG_ACOS(val) ROUND(val)
DEG_ATAN(val)

In this section there are also included a couple of mathematically correct float modulo operations. The problem
is that fmod() does not work correctly for negative values.
MODULO(x,y)
BALANCED_MOD(x,y)

The OBI Course-Setting Bug


(This is a legacy section; this bug appears to have been fixed from FS9 onwards but I’ve left it here as it
illustrates how to read a variable and then place a new value back into the same system).

There appears to be an intermittent bug in some third-party aircraft or aircraft imported from FS2K where the
OBI course setting does not function correctly even though your code is correct. There are two available
key_events for each OBI function:

KEY_VOR1_OBI_INC
KEY_VOR1_OBI_FAST_INC

KEY_VOR1_OBI_DEC
KEY_VOR1_OBI_FAST_DEC
KEY_VOR2_OBI_INC
KEY_VOR2_OBI_FAST_INC

KEY_VOR2_OBI_DEC
KEY_VOR2_OBI_FAST_DEC

- but somewhere along the way both the standard and _FAST_ versions of each event get set to the _FAST_ event.
This means that you cannot increment or decrement the OBI CRS by anything less than 10 degrees by using the
key_events. The workaround is to read the current OBI course and increment/decrement the course by the
required amount (1) before loading it back into the OBI using the _SET_ event. The following
increment/decrement code are snips from my EFIS-84:

MODULE_VAR heading = {VOR1_OBI};


UINT32 vor1_hdg = 0;

//---------------------------------------------------------------------------
BOOL FSAPI ehsi_crs_up( PPIXPOINT relative_point, FLAGS32 mouse_flags )
{

lookup_var(&heading);
vor1_hdg = heading.var_value.n + 1;
if (vor1_hdg == 360) vor1_hdg = 0;

//Active arrow

trigger_key_event(KEY_VOR1_SET,vor1_hdg);

//---------------------------------------------------------------------------
BOOL FSAPI ehsi_crs_dn( PPIXPOINT relative_point, FLAGS32 mouse_flags )
{

lookup_var(&heading);
vor1_hdg = heading.var_value.n - 1;
if (vor1_hdg == 0) vor1_hdg = 360;

//Active arrow

trigger_key_event(KEY_VOR1_SET,vor1_hdg);

Note the two traps to catch the OBI passing through 360 degrees. If you are at all uncertain about the future use
of any VOR gauges then always code the single-increment steps in this way and the gauge will be guaranteed to
work on any panel.

Token Variable Returns


Most (if not all) returns from a token variable callback are in the "double" format. In most of the code snips
above I have forced them into a UINT32 "unsigned integer" and apart from the normal protesting message from
the complier saying

<path>\example.c (line no.) : warning C4244 '=' : conversion from 'double' to 'unsigned int',
possible loss of data

the gauges have all worked very well when compiled. You can remove the warning by casting the return from
double to UINT32 – look up „casting‟ in one of the recommended books. Generally you don't get any problems
doing this but I can think of certain times when it may give erratic results. Just as an example, the return for
flaps full down is 16383.000000. If you have a four position flap then each position is going to be

16834.000000 / 4 = 4096.000000.

(Remember that all returns start from zero). If you now convert that to an unsigned integer it will become 4906.
Now, suppose you have a five position flap:-

Dragonflight Design Page 99 of 112


16384.000000 / 5 = 3276.800000.

If that is forced to an integer it will be truncated to 3276. Usually FS will accept this and set the correct
position, but what if it doesn't? Simply add a bracketting line to your code e.g.

if (flap_return > 3275 and flap_return < 3278)


{
//Set flap position 1
}

Why not just read all returns as doubles instead of casting them to unsigned integers? Simply because most of
the time the raw data as a double just does not make sense. What should be a 1 or a 0 can easily show an ever-
changing figure such as 34519859 but as soon as you force it into a UINT or a FLOAT it displays the correct
value. Now, don't ask me why this happens because I quite honestly do not know!

I have never had any problems along these lines which leads me to suspect that internally FS truncates many
doubles to integers anyway. However, it was a comment by a contributor to this document that led me to feel
that I needed to bring a potential problem to your attention.

On top of this, it appears that many of the token variables are simply re-named (aliased) versions of others. In
one case I have identified three token variables that all do the same job and all return the same values. I will be
creating a list here of the token variables that have the same values; if the variables are grouped together then
they return identical values.

VOR1_GS_NEEDLE VOR1_NEEDLE VOR1_TF_FLAG


VOR1_NEEDLE_RADIO HSI_VERTICAL_NEEDLE HSI_TF_FLAG
HSI_HORIZONTAL_NEEDLE

VOR1_GS_FLAG DME1_SPEED DME1_DISTANCE


HSI_HORIZONTAL_VALID HSI_SPEED HSI_DISTANCE

DME1_MORSE_IDENT VOR1_OBI VOR1_BEARING


HSI_STATION_NAME HSI_OBI_NEEDLE HSI_BEARING

AUTOPILOT_HEADING_LOCK_DIR
HSI_DESIRED_HEADING_NEEDLE

In addition there is a misleading variable – HSI_SIGNAL_LOCALISER – that does not return exactly as expected.
Read this very carefully before using this variable:-

1. If you are tuned to a LOC station and are out of range of the localiser, then HSI_SIGNAL_LOCALISER returns a
ZERO.
2. If you are tuned to a LOC station and are in range of the localiser, then HSI_SIGNAL_LOCALISER returns a
ONE.
3. If you are NOT tuned to a LOC station, then HSI_SIGNAL_LOCALISER returns a ONE at all times.

Used in conjunction with VOR1_GS_FLAG it is possible to separate localiser and glideslope functions and get the
correct returns whatever frequency is set in nav1.

There are also a group of variables that in theory should be returning the same information but actually don't!!
There are two variables for any engine manifold pressure, for example engine 1:-
RECIP_ENGINE1_MANIFOLD_PRESSURE and ENGINE1_MANIFOLD_PRESSURE. You would expect them to return the
same value as they are reading the same part of the same engine but the RECIP version appears to have different
internal parameters and consequently a different return for the same throttle settings. After a series of tests it
appears that the figure returned by the RECIP_ENGINE1_MANIFOLD_PRESSURE version is a division of 14.15 of the
figure returned by the standard ENGINE1_MANIFOLD_PRESSURE for the same throttle settings. This holds true right
across the rev range. Interestingly, that figure of 14.15 is one atmosphere (one bar), so the RECIP version info is
being returned in bars rather than lbs/sq.in. The introduction of the RECIP_ token variables may very well have
to do with the original FS98 ENGINE1_MANIFOLD_PRESSURE having multiple functions depending on the flight
model type chosen, whereas RECIP_ENGINE1_MANIFOLD_PRESSURE is purely for reciprocating engines.
Now, there are also a couple of crazy things concerning the token variable ADF_NEEDLE. Never use a non-
linearity table as the format is a FLOAT64 return and using an nl table will stop it from displaying correctly. It
usually shows up by reading okay all the way round to 180 degrees and then suddenly skipping backwards by
90 degrees and staying there until 360 degrees is reached. At this point it flicks forward 90 degrees again!! If
you use it as a token variable in a needle macro e.g.

MAKE_NEEDLE( adf_needle,
ADF_BMP_NEEDLE,
NULL,
&fail_nav,
erase | transparent | anti-alias,
0,
105, 103,
70, 13,
ADF_NEEDLE,
NULL,
NULL,
0 )

PELEMENT_HEADER adf_plist[] =
{
&adf_needle.header,
NULL
};

and the ADF_BITMAP_NEEDLE picture is in the normal horizontal position then everything will display correctly.
Now, if you modify that so you are doing the call back yourself e.g. to add the avionics bus to turn the ADF on
and off:-

NEEDLE_UPDATE_CALLBACK adf_needle_cb;

FLOAT64 adf_degrees = 0;
UINT32 avionics_on = 0;

MODULE_VAR avionics = {AVIONICS_CIRCUIT_ON};


MODULE_VAR adf_curr = {ADF_NEEDLE};

MAKE_NEEDLE(adf,
ADF_BMP_NEEDLE,
NULL,
&fail_nav,
erase | bright | transparent | anti-alias,
0,
105, 103,
70, 13,
MODULE_VAR_NONE,
adf_needle_cb,
NULL,
0)

PELEMENT_HEADER adf_plist[] =
{
&adf.header,
NULL
};

//---------------------------------------------------------------------------
FLOAT64 FSAPI adf_needle_cb( PELEMENT_NEEDLE pelement )
{
FLOAT64 val = pelement->source_var.var_value.n;

//Get the state of the avionics bus

lookup_var(&avionics);
avionics_on = avionics.var_value.n;

//Get the current ADF needle position

lookup_var(&adf_curr);
adf_degrees = adf_curr.var_value.n;

Dragonflight Design Page 101 of 112


//Intercept ADF return and set needle to zero if the avionics bus is off

if (avionics_on == 0) adf_degrees = 0.0;

return adf_degrees;
}

You will find that the ADF needle is displaying plus 90 degrees… uh??? You need to rotate the needle bitmap
so that the head of the needle is pointing towards the top of the screen. As I said – crazy; but both
VOR1_BEARING_DEGREES and VOR2_BEARING_DEGREES also exhibit this behaviour!!!!

Aircraft Altitude
It appears that the inches and millibars conversions in the token variables KOLLSMAN_SETTING_MB and
KOLLSMAN_SETTING_HG are not quite in sync. The trick to keep them correct is to use KOLLSMAN_SETTING_MB
only and then use the following calculation from Gordon Small:-

1013.25 mb = 1.0 atmospheres and each atmosphere is equal to 29.92 inches of mercury, therefore:

atmospheres=X/1013.25 where X is the pressure in mb


pressure in inches = atmospheres*29.92.

He comments that, "I think the problem with FS is that the code takes 1013mb to equal 29.92 inches when it
obviously isn't."

Navigation Radio Frequencies


The radio frequencies for the FS2K comm and nav radios are returned as BCD; that is Binary-Coded-Decimal.
The following code snip illustrates a way of converting the incoming BCD number to the 3.2 display on a
frequency string.

//----------------------------------------------------------------------
FLOAT64 FSAPI update_nav1_freq (FLOAT64 val);

MAKE_STRING( nav1_freq,
NULL,
&fail6,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
{13, 211},
{37, 11},
6,
NAV1_FREQUENCY, NULL,
MODULE_VAR_NONE, NULL,
MODULE_VAR_NONE, NULL,
update_nav1_freq,
RGB(255,0,255),
RGB(0,0,0),
RGB(255,0,255),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
NULL)

//---------------------------------------------------------------------------

FLOAT64 FSAPI update_nav1_freq(FLOAT64 val)


{
UINT32 x;
x = (UINT32)nav1_freq.source_var[0].var_value.d;
wsprintf(nav1_freq.string, "1%d%d.%d%d", (x >> 12) & 0x000f, (x >> 8) & 0x000f, (x >> 4) &
0x000f, x & 0x000f);
return (FLOAT64)x;
}
Note that line 3 (wsprintf etc) has a comma and not a semi-colon at the end of it. This is because line 4 is the
end of line 3. (Courtesy Scott Macmillan, originally from Chuck Dome).

Alternatively, you can use this scheme supplied by Arne Bartels which allows BCD conversion in both
directions. Add the following to the master source file:-

#include <math.h>

#define Bcd2Dec(BcdNum) HornerScheme(BcdNum,0x10,10)


#define Dec2Bcd(DecNum) HornerScheme(DecNum,10,0x10)

UINT32 HornerScheme(UINT32 Num,UINT32 Divider,UINT32 Factor)


{
UINT32 Remainder=0,Quotient=0,Result=0;
Remainder=Num%Divider;
Quotient=Num/Divider;
if(!(Quotient==0&&Remainder==0))
Result+=HornerScheme(Quotient,Divider,Factor)*Factor+Remainder;
return Result;
}

Then for each sub-gauge that requires a BCD conversion use the following syntax:-

Bcd2Dec(bcd_number) for BCD to decimal and


Dec2Bcd(dec_number) for decimal to BCD conversions.

Navigation Radio Identities


Buried in the available token variable returns for a vor frequency is also the identity of the broadcasting station
(VORx_IDENTITY). This code snip (courtesy Bryan Kostick) illustrates how to extract that identity and display it
as a string. Although token variable VOR1_IDENTITY is second in the list it has an identifier of [1] as the list
starts from 0.

FLOAT64 FSAPI update_nav1_id (FLOAT64 val);

MAKE_STRING( nav1_id,
NULL,
&fail6,
IMAGE_USE_ERASE | IMAGE_USE_BRIGHT,
0,
{19, 200},
{28, 10},
4,
VOR1_SIGNAL_STRENGTH, NULL,
VOR1_IDENTITY, NULL,
MODULE_VAR_NONE, NULL,
update_nav1_id,
RGB(255,0,255),
RGB(0,0,0),
RGB(255,0,255),
GAUGE_FONT_DEFAULT,
GAUGE_WEIGHT_DEFAULT,
GAUGE_CHARSET,
NULL)

//--------------------------------------------------------------------------

FLOAT64 FSAPI update_nav1_id(FLOAT64 val)


{
wsprintf(nav1_id.string, "%s", nav1_id.source_var[1].var_value.p);
return val;
}

Dragonflight Design Page 103 of 112


ADF and Transponder Radio Frequencies
The returns for both the ADF and the transponder, despite the comment in the SDK that the transponder return
is in octal, are actually in hex so that "octal" may have been a reference to the real-world. I have (incorrectly)
referred to the transponder display as Thou, Hun, Ten and Unit in the comments but is does make clear which
digit of the display is being worked on. This returns four separate variables which you can then add together as
one variable before displaying or display them separately. This code may be inefficient but it does work and is
easy to follow!

MODULE_VAR trans = {TRANSPONDER_CODE};

UINT32 high_units_right = 0; //XPDR active fractions


UINT32 high_units_left = 0;
UINT32 low_units_right = 0;
UINT32 low_units_left = 0;
UINT32 xpdr = 0;

//-------------------------------------------------

case PANEL_SERVICE_PRE_UPDATE:

//Clear out the previous displayed transponder code

high_units_left = 0; //Thou
high_units_right = 0; //Hun
low_units_left = 0; //Ten
low_units_right = 0; //Unit

//Get the current code

lookup_var(&trans);
xpdr = trans.var_value.d;

//Run a continous loop to break down the return into decimal figures

while (xpdr > 4095)


{
high_units_left = high_units_left + 1;
xpdr = xpdr - 4096;
}
while (xpdr > 255)
{
high_units_right = high_units_right + 1;
xpdr = xpdr - 256;
}
while (xpdr > 15)
{
low_units_left = low_units_left + 1;
xpdr = xpdr - 16;
}
low_units_right = xpdr;
}

//At this point we have the new transponder code so go and display it

break;
}

Between clearing out the transponder display and sending the new one there is no screen update so there is no
sudden blanking of the display.

Fortunately, converting the ADF frequency return is easier even than the transponder! It can be broken down
into two sections; with ADF set to 100Khz increments and without.

With 100Khz increments set to OFF, at the end of the code execution you have three variables to display. Like
the transponder, you can set these into three separate strings or concatenate them to form one three-digit string.
As the lowest available legal ADF code is 200, any FS ADF return will always be greater than hexadecimal 511
(512 = 200) so the high digit always needs seeding to 1 before starting the loop to ensure the correct decoding
of it. This also means that as the high digit loop will never decrement to zero we need to subtract 256 (which
would have been the figure 1) between decoding the high digit and the mid and lower digits.

MODULE_VAR curr_adf = {ADF_FREQUENCY};


UINT32 adf = 0;
UINT32 left = 0; //ADF high digit
UINT32 mid = 0; //ADF mid digit
UINT32 right = 0; //ADF low digit

//--------------------------------------------------------------

case PANEL_SERVICE_PRE_UPDATE:

//Get current frequency – left must always start as 1 because the lowest
//ADF code is 200

lookup_var(&curr_adf);
adf = curr_adf.var_value.d;
left = 1;
mid = 0;
right = 0;

while (adf > 511)


{
left = left + 1;
adf = adf - 256;
}
adf = adf - 256;
while (adf > 15)
{
mid = mid + 1;
adf = adf - 16;
}
right = adf;

break;

etc;

For some reason Microsoft have called the 100Hz tuneable frequency ADF_500_HZ_TUNABLE!!! Use the
following additional code:-

MODULE_VAR adf1_high_freq = {ADF_EXTENDED_FREQUENCY};


MODULE_VAR adf1_ext_ok = {ADF_500_HZ_TUNABLE};

UINT32 adf1_ext = 0; //Extended frequency check


UINT32 adf1_ext_freq = 0; //Extended frequency value
UINT32 adf1_high_digit = 0; //One thousand digit
UINT32 adf1_low_digit = 0; //100Khz figure
UINT32 adf1_main_freq = 0; //Top four figures

//----------------------------------------------------

//Check for extended frequency

lookup_var(&adf1_ext_ok);
adf1_ext = adf1_ext_ok.var_value.n;

if (adf1_ext == 1) //Extended frequency active


{
lookup_var(&adf1_high_freq);
adf1_ext_freq = adf1_high_freq.var_value.d;

//Check to see if the high digit (1) is active and subtract 256 from the
//extended frequency return to get the 100Khz figure. At the end of this
//test adf1_ext_freq will contain the 100Khz figure to be displayed on screen.

if (adf1_ext_freq > 255)


{
adf1_high_digit = 1;

Dragonflight Design Page 105 of 112


adf1_ext_freq = adf1_ext_freq – 256;
}
else
{
adf1_high_digit = 0;
}
}

Although the return is still a BCD figure it is a lot easier to deal with than the main display. If the ADF
frequency is set to less than 1000 then the extended frequency return (adf1_ext_freq) will be 0 to 9. If it is
greater than 999 then the extended frequency return will be 256 plus 0 to 9 for the 100Khz increment e.g. a
return of 260 indicates a 1 in the thousands digit and 4 in the 100Khz field.

In both the transponder and ADF cases you can simply reverse the loop functions to arrive at the required figure
to set a new code. And yes, there are easier (i.e. less code) ways to deal with this problem…. 

Mach Speed Conversion


This has to be the oddest divisor return I have come across yet! Believe me, this code snip does give the correct
answer…..

MODULE_VAR mach = {MACH};


UINT32 mspeed = 0;

//---snip

lookup_var(&mach);
mspeed = mach.var_value.n;
mspeed = mspeed / 204;

VOR Reception and Ranges


FS models three types of VORs – Terminal VOR (TVOR), low-altitude VOR (LVOR) and high-altitude VOR
(HVOR). Each of these has its own range as shown in the table below:-

VOR Type Altitude (Feet) Range (Nautical Miles)


Terminal VOR 1000' – 12000' 25
Low Altitude VOR 1000' – 18000' 40
High Altitude VOR 1000' – 14500' 40
" 14500' – 18000' 100
" 18000' – 45000' 130
" 45000' – 60000' 100

Each FS2K+ VOR also has its own identity field (VORx_CODE) which can be decoded to indicate which type of
VOR is meant to be represented. For reasons best known to themselves Microsoft have declared all default
VORs as VORDMEs…… but third party addons may have the correct indentity type. Buried in the
fsxgauges_sp2.h file is a set of possible VOR types:-

// defines for VOR_INFO.CODE field

#define VOR_CODE_IS_LOCALIZER BIT7 // bit7 = 0= VOR 1= Localizer


#define VOR_CODE_GLIDESLOPE BIT6 // bit6 = 1= Glideslope Available
#define VOR_CODE_BACKCOURSE_UNAVAIL BIT5 // bit5 = 1= no localizer backcourse
#define VOR_CODE_DME_AT_GLIDE_SLOPE BIT4 // bit4 = 1= DME transmitter at Glide Slope
Transmitter
#define VOR_CODE_NAV_UNAVAILABLE BIT3 // bit3 = 1= no nav signal available
#define VOR_CODE_VOICE_AVAILABLE BIT2 // bit2 = Voice Available
#define VOR_CODE_TACAN BIT1 // bit1 = TACAN
#define VOR_CODE_DME_AVAILABLE BIT0 // bit0 = DME

As this is a decode function that I am likely to want to use many times I have added it to my dragonflight.h
header file:-
//---------------------------------------------------------------------------

//Get VOR type


//Input required = vor1_code or vor2_code and flag type to check
//See gauges.h for VOR CODE INFO FIELD

BOOL VorType(MODULE_VAR vorfield, FLAGS vorflag)


{
if ((vorfield.var_value.b&vorflag)!= FLAGS0) return TRUE;
else return FALSE;
}

In the master source file for the gauge I add the following lines. The reason for putting them in the master
source file is so that they can be accessed from any sub-gauge.

//VOR1 Identity
MODULE_VAR vor1_code = {VOR1_CODE};
MODULE_VAR vor1_str = {VOR1_SIGNAL_STRENGTH};

//VOR2 Identity
MODULE_VAR vor2_code = {VOR2_CODE};
MODULE_VAR vor2_str = {VOR2_SIGNAL_STRENGTH};

The following code snip is a piece from an EHSI that changes or removes the VOR2 symbol depending on the
type of VOR field and also whether a VOR signal is present (see range table above).

lookup_var(&vor2_code);
lookup_var(&vor2_str);
vor2_sig = vor2_str.var_value.n;

//If the VOR is within range

if (vor2_sig > 0)
{

//If the VOR is a VORLOC then display the VORLOC symbol

if (VorType(vor2_code,VOR_CODE_IS_LOCALIZER))
{
SHOW_LISTELEMENT(pgauge->elements_list[0],24);
HIDE_LISTELEMENT(pgauge->elements_list[0],23);
HIDE_LISTELEMENT(pgauge->elements_list[0],22);
HIDE_LISTELEMENT(pgauge->elements_list[0],21);
}
else

//If the VOR contains a TACAN then display the VORTAC sysmbol

if (VorType(vor2_code,VOR_CODE_TACAN))
{
HIDE_LISTELEMENT(pgauge->elements_list[0],24);
SHOW_LISTELEMENT(pgauge->elements_list[0],23);
HIDE_LISTELEMENT(pgauge->elements_list[0],22);
HIDE_LISTELEMENT(pgauge->elements_list[0],21);
}
else

//VOR must be simply a VOR (if default FS2K navaid then will be VORDME)

{
HIDE_LISTELEMENT(pgauge->elements_list[0],24);
HIDE_LISTELEMENT(pgauge->elements_list[0],23);
HIDE_LISTELEMENT(pgauge->elements_list[0],22);
SHOW_LISTELEMENT(pgauge->elements_list[0],21);
}
}
else

//VOR is out of range so remove all displays

Dragonflight Design Page 107 of 112


{
HIDE_LISTELEMENT(pgauge->elements_list[0],24);
HIDE_LISTELEMENT(pgauge->elements_list[0],23);
HIDE_LISTELEMENT(pgauge->elements_list[0],22);
HIDE_LISTELEMENT(pgauge->elements_list[0],21);
}

Fuel Tanks
Not all fuel tanks are created equal… some behave completely differently from others. If you are trying to
create a custom fuel system then steer clear of using the FUEL_TANK_CENTER variable and also the
FUEL_TANK_<side>_MAIN variables.

The two MAIN tanks will deliver fuel to all engines in total disregard of the KEY_FUEL_SELECTOR commands,
making them effectively useless for anything that has more than one fuel tank. In the case of the CENTER tanks,
CENTER2 and CENTER3 were created specifically for the FS2K/2K2 Concorde and are the only tanks that you can
transfer fuel to and from. In all other respects they seem to behave normally. The CENTER tank will deliver fuel
as normal but if you check FUEL_TANK_CENTER_LEVEL, you find that it permanently reads zero.

Effectively then, you have access to six fuel tanks that will work „correctly‟ (aux, center2/3 and tip). I prefer to
keep the two CENTERx tanks for last use, so if I had a four-engined aircraft that has one tank per engine, I would
assign the tanks as follows:

trigger_key_event (KEY_FUEL_SELECTOR_SET,FUEL_TANK_LEFT_TIP);
trigger_key_event (KEY_FUEL_SELECTOR_2_SET,FUEL_TANK_LEFT_AUX);
trigger_key_event (KEY_FUEL_SELECTOR_3_SET,FUEL_TANK_RIGHT_AUX);
trigger_key_event (KEY_FUEL_SELECTOR_4_SET,FUEL_TANK_RIGHT_TIP);

As with everything in this paper, this is only one way to set up the fuel tanks. You should also be able to see the
potential for setting up crossfeed selections by using the _SET commands.
Creating Reusable Functions
If you intend to develop more than one panel, you are going to find that you are repeating the same (or a
similar) set of code over and again. You may find that you are literally creating from scratch or just using
copy‟n‟paste with all its attendant errors… The obvious candidates for reusable functions are the various radio
navigation aids, but there are many other systems that can be „black boxed‟. In general, this type of function
would be something that works in the background and provides data to drive an onscreen display but does not
directly drive an onscreen display itself.

The first step towards this is to create a single header file that contains all of the MODULE_VAR variables that you
are going to use in your current and future projects. This is always going to be a work in progress unless you sit
down and work your way through the fsxgauges_sp2.h file….I don‟t recommend it! This will create a known
starting point for any future functions and also any future projects – no more looking up the MODULE_VAR and
then creating yet another pointer to it. I have my MODULE_VARs in a header file called – unsurprisingly –
module_vars.h. This is just the first few lines of it dealing with the APU variables:

//APU
MODULE_VAR apu_rpm = {APU_PCT_RPM};
MODULE_VAR apu_starter = {APU_PCT_STARTER};
MODULE_VAR apu_volts = {APU_VOLTS};
MODULE_VAR apu_gen_switch = {APU_GENERATOR_SWITCH};
MODULE_VAR apu_gen = {APU_GENERATOR_ACTIVE};

Nothing new or unusual there. Secondly, I suggest you create another header file that contains any and all
variables that are shared globally between gauges and are used by, or called by, any of your shared functions –
mine is called shared_data.h. Give each of these shared variables an instantly recognisable prefix e.g. fuelsys_
for any variable to do with the fuel system.

I then have a whole series of functions in my dragonflight.h file (the one included in this archive is not my
current one as the current one contains quite a lot of information that is irrelevant to this discussion). I have
created a new header file for this archive called fs_functions.h and included a handful of useful functions to get
you started. Each function has a description and a sample usage; I suggest that you stick to this (or a similar)
format for your own functions, if only to remind yourself how to use them. They are mostly navaid-based; ADF
function ##1 illustrates why I have a shared data file as in that case, the function is updating four variables that
are used to drive the ADF frequency display. Note that you would need to copy these global variables into local
variables immediately after you have called the function, otherwise a pass to update the other ADF will update
both ADFs with the same data. Alternatively, you can modify the function to parse data into specific global
ADF1 and ADF2 variables; thus you retain one function that will still deal with both ADFs.

There are also a couple of functions to get the runway state and one large one that returns the current fuel status
of a named tank in gallons, pounds, kilos or as a user-defined weight. On the face of it, this seems a little stupid
as JetA and avgas had recognised weights per lb/kilo, but providing the ability to supply a weight-per-unit
parameter allows you to display remaining weight compensated by temperature.

Anything that can be used in more than one project is a target for a reusable function.

Dragonflight Design Page 109 of 112


Source Codes
I have also included a Source Codes folder which contains complete source codes and bitmaps. If you want to
compile these gauges then you will need the dragonflight.h header file in the \source codes\inc folder; copy this
to the same folder as your gauges.h file. You may use these source codes as the basis of your own instruments
but I'd be grateful if you didn't simply change the bitmaps, recompile the gauges and call them your own. Please
bear in mind also that these were created with Visual C++. These source codes all show the use of MAKE_ICON
and MAKE_STATIC and your attention is drawn to the specific use of any other macros.

1. Aeromarine ASI. A singlegauge illustrating the use of a second set of bitmaps to get a yellow night-lighting
effect. Works from the default lighting bus and shows the use of a non-linearity table and MAKE_NEEDLE
macro.
2. DC voltmeter. A singlegauge illustrating the use of an external shared variable to activate the gauge and also
the fact that you can use your own calculations to drive a gauge (dc volts) while using the input from a
completely unrelated token variable (engine rpm) to provide a base for those calculations.
3. Garmin VORs. A two-gauge multigauge that works from the default lighting and avionics buses. Shows the
use of MAKE_SLIDER.
4. Panel Switches. A simple two-gauge multigauge illustrating the use of SHOW/HIDE macros to activate a
rocker switch and the re-use of the same bitmaps in two different gauges. Also shows how to show and
hide panel windows. Very heavily commented for complete gauge beginners. Please note that this example
was originally written for FS2K / FS2K2; the fssound module (not included) does not work with FSX and
works erratically with FS9.
5. Tscan. A complex multigauge holding all of the components of a standard t-scan panel. Uses all macros!
The altimeter also uses the accurate mb/in conversion shown above.
Final Comments
Included in this archive is a corrected and updated gauges.h (“FSX Header” file folder). This gauge header file
will no longer compile FS98-style gauges as Arne found that the _610_ compatibility macros were causing
more trouble than they were worth, so he has deleted them and also corrected one or two other suspect
structures. Also, FSX will no longer support FS98-style gauges anyway.

The entire header file has been made ANSI-C compatible so, for example, if a gauge containing user-defined
mouse tooltips is compiled in VC++ it may start throwing “invalid pointer casting” or similar errors; the rule
here is to try the gauge and if it works, ignore the errors. Arne has made the comment that he has tested gauges
built in both C and CPP format and although the compilers may have thrown a few small errors, all of the
gauges done this way have worked correctly. Gauges done under Windows XP should also compile and work
now. Finally, he has built a new version of the Flightmap SDK; the original that was included in the Microsoft
Panel SDK was only Visual C (not C++ or any other compiler) compatible. With the release of FSX Service
Pack 1 I updated the FSX PanelSDK gauges.h file to correct the usual batch of Microsoft errors/omissions and
called it fsxgauges_sp2.h.

If you‟re using the MingW or the Borland C++ compiler you will need the relevant header file from within
“Gnu and Borland C++ Header Files”.

"Speedcon.zip" under the "Examples" folder now contains not only Arne Bartels' original .pdf and Word 6
documents but also a C code source file and a Visual Basic .bas file with worked formulas for use in your own
projects.

Related Documents and Archives


Gaupaint.zip Painting gauges by Dai Griffiths. A tutorial on some ways to create gauge bitmaps.
Idehowto.zip Setting up the Visual C++ IDE Environment by Bryan Kostick. Highly recommended.
Panelcf2.zip Writing the panel.cfg file by Dai Griffiths. A short tutorial on the breakdown of the FS
panel.cfg file. Deals with FS98 but as far as it goes is also relevant to FS2K+.
Sdkbugs.zip Bugs, Bugfixes, Miscellanous to Microsoft FS2000 Panel SDK by Arne Bartels.

Additional grateful thanks to:-


Alex Bashkatov Bill Leaming Bryan Kostick Christian Koegler Claude Troncy
Daniel Steiner Dmitry Prosko Doug Dawson Eric Marciano Gordon Small
Jorge Alsina Mathis Elsaesser Nick Jacobs Marco Ravanello Mike Hirst
Oleg Devjatkin Pete Leadbeater Peter Webster Rabbijah Guder Rolf Dieter Buckmann
Ron Hill Scott Macmillan Simon Hradecky Wade Chafe William Call

Updates:-
Errr… whenever someone prods me or something annoys me enough!!!

Required Legal Bit:-


This file is supplied “as is” for information only. I am not responsible for the use you make of it or the results
you get by using it, nor am I responsible for any misleading information that may be within it. ALL attempts
have been made to ensure that the information is correct and usable.

Smiley Bit:-
If you manage to write and release a gauge using info from this document, an acknowledgement would be nice

Dragonflight Design Page 111 of 112


;-). Please feel free to upload this file to other flightsim sites but please, keep all of its archives and contents
intact if you do so. It won't make much sense without them!

-Dai Griffiths, DragonflightDesign@simpilot.net


Copyright (as far as it goes) Dragonflight Design 2008.

You might also like