Updated for CVE-lite version 0.9
Creating a single-stage CVE-lite vessel
Here we shall use the Fregat
source code as an example.
I shall assume you have installed cvel.h
into ~/orbitersdk/include,
and cvel.lib into ~/orbitersdk/lib and make sure
the vital inter-vessel communications dll, cvelmessage.dll, is in ~/Modules.
The CVE-lite programmer's guide and function reference.
Included in the CVE-lite
library, as of version 0.9 is a full PDF copy of the programmer's
guide and function reference. This provides a more detailed breakdown
of CVE-lite's functionality.
CVE-Lite's requirements
The main requirement of CVE-lite vessels is that they are
able to respond to GPDS
signals at any time. These signals are transmitted via the cvelmessage.dll and can be sent
by any vessel to any other using SendCentralMessage().
They consequently need to be polled for by all CVE-lite vessels. The polling
function is bool
CheckCentralMessage(). This returns true when a command to this
vessel has been processed (and consequently the vessel needs to perform
a refresh) and false, when
there is nothing pending.
It is therefore imperitive that there exists a function you can call on
demand and as many times as you like that can clear out all the meshes
and (optionally) thrusters, and re-draw without disrupting normal ship
operation (for example, it shouldn't re-fill the fuel tank!).
Below we find an walked-through example (using the Fregat code), that
shows how this can be done, and how to make a complete single-stage CVE-lite vessel.
Basics
First thing is to include cvel.h into the project, either
in the header file (as here) or in the main .cpp file, and make sure cvel.lib is linked into your
project, normally by right-clicking and selecting "add file to project" in the file
view on the left of the MSVC++ IDE.
Second, create your own
vessel class, subclassing from COMMON_VESSEL_EX
and containing all the Orbiter usual
suspects.
class FREGAT : public COMMON_VESSEL_EX { public: FREGAT (OBJHANDLE hObj, int fmodel); ~FREGAT(); void TimeStep( double simt ); // need a timestep loop to conduct the GPDS polling void VisualCreated( VESSEL *vessel, VISHANDLE vis, int refcount ); // This a good place to do initial drawing. // WARNING since v0.9 you need to call this yourself, the ovc overide is no longer in place! void SetClassCaps( FILEHANDLE cfg ); void LoadState( FILEHANDLE scn, void *vs ); void SaveState( FILEHANDLE scn ); void SetMainStage(); // IMPORTANT -- this is the "refresh" function (see later) void SetAttControlsFregat(); // this is just to set up the thrusters. I like to keep them separate. int ConsumeKey( const char *keystate ); int ConsumeBufferedKey(DWORD dwKey, bool fPressed, const char *keystate); protected: PROPELLANT_HANDLE ph_first; THRUSTER_HANDLE th_first[1]; THGROUP_HANDLE thg_first,thg_pitchup, thg_pitchdown; MESHHANDLE hFregat; SURFHANDLE tex;
};
|
Thirdly, for all the important housekeeping functions, cast the vessel
pointer as one of your class members, and re-direct Orbiter's callbacks. Eg.
DLLCLBK void ovcTimestep (VESSEL *vessel, double simt) { FREGAT *fregat = (FREGAT*)vessel; fregat->TimeStep( simt ); }
|
Repeat this for the other functions that need to be triggered from the
ovc* callbacks INCLUDING ovcVisualCreated()
which USED TO be called by CVE-lite itself! Since v0.9 you need
to replace the ovcVisualCreated() callback, else your function will not
be accessed!
DLLCLBK void ovcVisualCreated ( VESSEL *vessel, VISHANDLE vis, int refcount) { FREGAT *fregat = (FREGAT*)vessel; fregat->VisualCreated( vessel, vis, refcount); }
|
Nuts and bolts
Here we move on to the main
Fregat.cpp
file.
One of the first things to do at vessel creation is to make sure we
have collected all the GPDS
lines due to us (including the InitFuelLevel
variable), so that we can draw them from the off. This is best
accomplished by putting it in the vessel constructor, before the
propellent resources are allocated and meshes drawn. We use the
aforementioned CheckCentralMessage()
-- in this case we don't need to test the output, since we know a
redraw will take place soon after the constructor has run, anyway.
FREGAT::FREGAT (OBJHANDLE hObj, int fmodel) : COMMON_VESSEL_EX (hObj) { hFregat = oapiLoadMeshGlobal ("fregat"); tex = oapiRegisterExhaustTexture("Exhaust_atsme");
/* set the propellant handle to NULL for good measure */ ph_first=NULL;
}
|
A critical piece of
programming for CVE-lite
functionality is to ensure
that whether your vessel is created from startup ( ovcLoadState is called) or
dynamically, the vessel initialises correctly. The easiest way to do
this is to create a " SetStageX()"
function, that cleans out the whole set of thrusters, meshes et al, checks propellents have been
created and re-initialises
everything. This can be then called as often as necesasry. For example
at LoadState, and also in
VisualCreated().
This multiply-executable setup stage is called the Refresh function.
void FREGAT::VisualCreated ( VESSEL *vessel, VISHANDLE vis, int refcount) { SetMainStage(); // this is a good place to ask for the initial setup. }
|
We need to create our propellants in SetClassCaps() if we should
ever be used as an upper stage in a user's custom vessel. Remember it's
better to create all your propellant resources here, and delete them in
the Refresh function, if need
be. Otherwise initial fuel levels will not be passed through.
void FREGAT::SetClassCaps( FILEHANDLE cfg ) { /* verify that the propellent hasn't been created before making it */ if (ph_first==NULL) ph_first = CreatePropellantResource(FIRST_PROP_MASS); }
|
There are two functions that deal with GPDS
lines in scenario files. ParsePayloadString()
should be called for every LoadState line, and SaveDefaultStateEx() should be
called during the SaveState function.
You'll notice we call the Refresh
function ( SetMainStage())
after the loadstate, since we'll have loaded GPDS lines from the scenario.
void FREGAT::SaveState( FILEHANDLE scn ) { SaveDefaultState (scn); SaveDefaultStateEx(scn); /* save payload lines */ }
void FREGAT::LoadState( FILEHANDLE scn, void *vs ) { char *line; while (oapiReadScenario_nextline (scn, line)) { ParsePayloadString(line); /* load payload lines */
ParseScenarioLineEx (line, vs); } SetMainStage(); /* always set the stage whenever something may have changed */ }
|
When dealing with user-defined payloads, it's important to tell your
vessel how much they all weigh! The CVE-lite
function CalculateMass()
calculates the additional mass added by your payloads. Here we call it
in the Refresh function SetMainStage(). Equally we want
to erase all the meshes, so we can re-draw them. The enhanced ClearMeshesEx() takes care of
this.
/* always add payload mass [CalculateMass()] to the vessel's empty mass */ SetEmptyMass ( FREGAT_DRYMASS + CalculateMass() ); /* Clear old meshes before we redraw them */ ClearMeshesEx();
|
So, we've cleaned out our meshes, we've re-set our mass and, if the
tank hasn't yet been created, we've created the fuel tank and filled it
to the required level. How do we draw the payload meshes? Simple. Draw().
/* call draw to render the payloads */ Draw();
|
Right, now we need to think about polling the GPDS message stack to see if we've
been sent anything new. The simplest way to do this is to run a little
test in Timestep(). We've
already seen CheckCentralMessage()
in action in the constructor, but this time it's different -- we do need to test the output to see if
we have to redraw. But fortunately if we do, we can simply call the Refresh function!
void FREGAT::TimeStep( double simt ) { /* This function loops through the pending GPDS requests and returns a bool (true/false) /* if any GPDS requests have been implemented. /* If any have (eg adding a new payload), you will need to re-draw your ship, hence the /* call to the (repeatable) SetMainStage(); */
if( CheckCentralMessage() ) { // at least one message pending, we need to refresh while( CheckCentralMessage() ) ;// loop through any remaining SetMainStage(); // refresh } }
|
OK. We're flying, we're able to update on demand, and we've been able
to inherit payloads from previous stages, we can set our fuel level
after creation, and even accept payloads from other vessels at any
time. We can load and save our payload configuration and our empty
mass. What else do we need? How about being able to jettison the
payloads and extra stages?
This is actually one of the hardest parts of multipayload programming,
but CVE-lite takes care of it
all for you! Simply call DeployPayload().
The function will calculate which ship is the last on the stack and
should be jettisoned first, work out any upper stages that need to go
with it and handle all the outbound inter-vessel communications. It
returns a VESSEL pointer,
should you wish to then use any normal Orbiter calls to interact with it.
In Fregat, we bind this to the "J" key -- it doesn't hurt to call it if
you have no payloads to jettison, so there are no checks in place.
Remember to call the Refresh
function -- as possibly you've lost a payload!
case OAPI_KEY_J: result = 1;
/* call DeployPayload() to jettison the GPDS payloads in sequence */
DeployPayload();
/* Always re-set the stage after any kind of change */ SetMainStage();
|
Caveats
There are a some things
that are worth bearing in mind throughout coding with COMMON_VESSEL_EX:
- DLLCLBK void ovcVisualCreated()
This used to be stolen by
the
library to initialise the VISHANDLE for internal mesh rendering
purposes. This is no longer the case.
It's vital that you re-include
an ovcVisualCreated() function
if you're going to call an internal one.
It's important to note,
when designing payloads for GPDS
vessels, that the position at which the vessel loads its mesh is
presumed to be the origin (0,0,0).
Therefore it's best to model your meshes with the centre of mass at the
origin of the mesh. Equally, your payload will always be loaded with
its axes aligned with the launch vehicle (this is a limitation of the
current Orbiter API. Sorry).
CVE-lite compatible?
In order to be able to
describe your vessel as CVE-lite
compatible it must meet the following criteria:
- be able to read standard GPDS lines from scenario files, and
write its payloads back out using the same system (implement the ParseScenarioLineEx() and
SaveDefaultStateEx() functions as above);
- be able to be created by another vessel during the
game -- this means no reliance on ovcLoadState()
to start up;
- be able to refresh meshes correctly after a
separation (implement the "Refresh"
function as above);
- be able to accept GDPS
lines from the cvelmessage.dll
and correcly refresh (implement the
startup and TimeStep() calls to CheckCentralMessage(), plus the
"Refresh" function as above);
- be able to carry payloads taking into account their
mass, jettison them and transmit the neccessary GPDS commands to them using cvelmessage.dll (implement CalculateMass(), Draw(),
DeployPayload() as above);
- be able to act as an upperstage, even if it's not
realistic (for example, in a
multistaged rocket, default to the final stage in the event no
LoadState() is available and implement the CheckCentralMessage()s and
"Refresh" function as above);
If you would like your CVE-lite compatible vessel listed on
the links page, please contact the webmaster
on orbiter@aibs.org.uk
Instructions
You may wish to instruct your users in the art of the GPDS language. Never fear! Aside
from directing them to the GPDS page, you can also
download this HOWTO to include with your
package's distribution.
Good luck!
|