Professional Documents
Culture Documents
Table of Contents
14th April 2004 Removes any references to XML code but otherwise is identical to the sd2gau17 tutorial.
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?
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.
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.
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
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.
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"
#include "subgauges\port_oil.c"
#include "subgauges\star_oil.c"
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.
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:
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.
#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
//
As you can see, the .rc file is where all your copyright and legal info resides. You may add as many
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
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
and
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
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:-
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.
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:
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:"
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.
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.
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 )
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!
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
};
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
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 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 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 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 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:-
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 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.
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 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.
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 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 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!
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 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:-
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_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.
BIT15: Use with the MAKE_STRING macro only. If line 13 is set to RGB 0,0,0 (transparent black) then the
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.
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:-
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.
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.
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 )
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.
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,
... )
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
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:-
#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:-
//---------------------------------------------------------------------------
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);
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.
//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)
{
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!).
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},
NEEDLE_UPDATE_CALLBACK programmer_oil_cb;
// NEEDLE_UPDATE_CALLBACK token_variable_oil_cb;
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 )
//---------------------------------------------------------------------------
FLOAT64 FSAPI programmer_oil_cb (PELEMENT_NEEDLE pelement )
{
//Trigger the callback lookup for airspeed
lookup_var(&fop1_oil);
val = fop1_oil.var_value.n;
return val;
}
//---------------------------------------------------------------------------
/* FLOAT64 FSAPI token_variable_oil_cb (PELEMENT_NEEDLE pelement )
{
//This token-variable driven callback works directly without any problems
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!
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
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 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)
//-------------------------------------------------
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
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.
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
};
//---------------------------------------------------------------------------
return FALSE;
}
//---------------------------------------------------------------------------
if (power_on == 1)
{
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 power is applied and DME is turned off or the test is finished then blank the 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)
{
if (dme_knob_set == 1)
{
dme_distance = dme_distance * 60;
dme_knots_mins = (dme_distance/dme_speed) % 60;
}
else
if (dme_knob_set == 1 || dme_knob_set == 2)
{
if (dme_miles == -1)
{
dme_no_data = 0;
}
else
{
dme_no_data = 1;
}
}
}
else
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!!!
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.
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)
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).
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 )
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
//----------------------------------------------------------------------------
{
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 )
UINT32 light_state = 0;
//---------------------------------------------------------------------------
{
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.
case PANEL_SERVICE_PRE_INITIALIZE:
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!
#include "..\inc\gauges.h"
#include "..\inc\eventid.h"
#include "360.tiller.h"
#include "tiller.c"
//---------------------------------------------------------------------------
// Gauge table entries
GAUGE_TABLE_BEGIN()
GAUGE_TABLE_ENTRY(&gaugehdr_tiller)
GAUGE_TABLE_END()
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}
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 )
//------------------------------------------------------------------------
//Get current mouse position relative to the size of the mousebox vs. screen resolution
/*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;
if (old_pos < 0)
{
old_pos = 0;
}
else
{
trigger_key_event(KEY_RUDDER_SET, rudder_pos);
rudder = old_pos;
return FALSE;
}
//---------------------------------------------------------------------------
case PANEL_SERVICE_PRE_INITIALIZE:
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.
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.
return FALSE;
}
Under certain, rather common circumstances, the mouse callback function does not deliver clean results.
Suppose we have this declaration for the mouse function:
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.
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.
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.
UINT32 startup = 0;
UINT32 time_check = 0;
UINT32 pos = 0;
MOUSE_FUNCTION mag_plus;
MOUSE_FUNCTION mag_minus;
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?
//--------------------------------------------------------------
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.
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.
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.
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;
return Result;
}
//---------------------------------------------------------------------------
FLOAT64 FSAPI GlideY(PELEMENT_MOVING_IMAGE pelement)
{
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
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
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
}
//---------------------------------------------------------------------------
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.
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;
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()" */
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
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.
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.
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
So the HELP_CESS_ALTIMETER HelpID covers the whole gauge except for the small area defined for the Pressure
Setting knob
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.
Unfortunately, you cannot add more HelpIDs to FS2K+; you will need to use Tooltips instead.
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
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.
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)
MOUSE_END
Don't forget to #include the tooltip header file in the master gauge.
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 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.
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.
FLOAT64 fAlt;
FLOAT64 fBaroUS;
FLOAT64 fBaroMet;
//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;
}
MOUSE_BEGIN( palt_mouse_rect,0, 0, 0 )
//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
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.
//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!"
MOUSE_BEGIN( palt_mouse_rect,0, 0, 0 )
//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 )
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
//--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;
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.
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
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.
//---------------------------------------------------------------------------
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
break;
etc.
However you chose to do the update on the client variable(s), the line(s)
must be the first line in that section as shown above, otherwise the compiler will throw an error.
//---------------------------------------------------------------------------
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()" */
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.
//---------------------------------------------------------------------------
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:-
//---------------------------------------------------------------------------
// Altitude preselection
//---------------------------------------------------------------------------
void FSAPI fcp65_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()"
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
UINT32 switch_on = 0;
//---------------------------------------------------------------------------
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()"
//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.
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()
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!
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
//-------------------------------------------------------------------------------
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(fgets(freq_string,2,old_freq)==NULL)
break;
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;
}
}
}
fclose(old_freq);
fclose(new_freq);
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
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 )
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:
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.
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[] =
{
MAKE_STATIC(asi_background,
ASI_BACKGROUND,
&asi_face_plist,
NULL,
IMAGE_USE_TRANSPARENCY | IMAGE_USE_ERASE,
0,
0, 0 )
//---------------------------------------------------------------------------
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:
}
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;
}
}
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.
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:
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.
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….
PELEMENT_HEADER padi_ladi_face[] =
{
&ladi_face.header,
NULL
};
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.
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.
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
DInputKeyboard.cpp
DInputKeyboard.h
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"
#define NO_KEY 0 //not absolutely necessary as a simple 0 will do the job also
int keypressed=NO_KEY;
/* "initialize_routine()" */
case PANEL_SERVICE_PRE_INITIALIZE:
break;
case PANEL_SERVICE_CONNECT_TO_WINDOW:
hFSMainWnd= GetFSWindowHandle();
CreatDirectInputDevice( hFSMainWnd);
break;
/* "draw_routine()" */
case PANEL_SERVICE_PRE_DRAW:
break;
/* "kill_routine()" */
case PANEL_SERVICE_PRE_KILL:
break;
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.
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
/* "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.
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.
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:-
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.
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"
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, )
MAKE_STATIC( port_flow_background,
FFG_BMP_BACKGROUND,
&port_flow_plist4,
NULL,
IMAGE_USE_TRANSPARENCY,
0,
{0, 0} )
//----------------------------------------------------
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.
[Window07]
file=copilot.bmp
size_mm=640
position=7
visible=0
ident=COPILOT_PANEL
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:-
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.
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.
PCHAR GaugeParameter=(PCHAR)pelement->gauge_header->parameters;
rpm.id=SwitchRpmToken(atoi(GaugeParameter));
lookup_var(&rpm);
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.
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.
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];
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>)
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:-
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;-
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+ 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
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
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:-
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).
1. The test for checking maximum power is done from a simple text file. When the power level is achieved,
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!
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!):-
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.
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.
//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)
//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"
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.
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)
REDRAW_IMAGE( 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
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)
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)
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)
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)
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:
//---------------------------------------------------------------------------
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.
<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:-
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.
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.
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;
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;
lookup_var(&avionics);
avionics_on = avionics.var_value.n;
lookup_var(&adf_curr);
adf_degrees = adf_curr.var_value.n;
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:
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."
//----------------------------------------------------------------------
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)
//---------------------------------------------------------------------------
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>
Then for each sub-gauge that requires a BCD conversion use the following syntax:-
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)
//--------------------------------------------------------------------------
//-------------------------------------------------
case PANEL_SERVICE_PRE_UPDATE:
high_units_left = 0; //Thou
high_units_right = 0; //Hun
low_units_left = 0; //Ten
low_units_right = 0; //Unit
lookup_var(&trans);
xpdr = trans.var_value.d;
//Run a continous loop to break down the return into decimal figures
//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.
//--------------------------------------------------------------
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;
break;
etc;
For some reason Microsoft have called the 100Hz tuneable frequency ADF_500_HZ_TUNABLE!!! Use the
following additional code:-
//----------------------------------------------------
lookup_var(&adf1_ext_ok);
adf1_ext = adf1_ext_ok.var_value.n;
//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.
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….
//---snip
lookup_var(&mach);
mspeed = mach.var_value.n;
mspeed = mspeed / 204;
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:-
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:-
//---------------------------------------------------------------------------
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 (vor2_sig > 0)
{
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
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.
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.
Updates:-
Errr… whenever someone prods me or something annoys me enough!!!
Smiley Bit:-
If you manage to write and release a gauge using info from this document, an acknowledgement would be nice