Discussion MFDs supporting multiple vessels - standardization proposal

Enjo

Mostly harmless
Addon Developer
Tutorial Publisher
Donator
Joined
Nov 25, 2007
Messages
1,665
Reaction score
13
Points
38
Location
Germany
Website
www.enderspace.de
Preferred Pronouns
Can't you smell my T levels?
I've seen many ways of dealing with the limitation of Orbiter allowing to store only one instance of MFD in one time. Most of these ways included using a quite number of global variables. I'd like to come with a proposal of standardizing this additional feature because I think that it would be best for an MFD dev to focus on the MFD creation itself and because I haven't seen it actually done right (this includes my own code up to some point). For example, we have Prune function here:
http://www.orbiterwiki.org/wiki/Storing_MFD_Instances#Keeping_data_valid
but firstly, CTD sometimes occur when vessels' data are being deleted, and secondly, has anybody noticed, that there's no deletion of valid MFDData instances upon simulation exit?

Below is my proposal. I'd like to leave it here for review first, and then perhaps create a VS MFD project template, or a library. The documentation for this project can be found here, and the code is in attachment.

All that the MFD dev should need to do is the following:
1) Derive from MFDData to add any additional members and functionalities
2) Derive from PluginMultipleVessels, where the method ConstructNewMFDData should return a new MFDData derived object pointer
3) Declare a global pointer to the derived Plugin class, initialize it in Orbiter's InitModule() and delete it in ExitModule(), along with the usual MFD inits and cleanups

Code:
MyPluginMultipleVessels * pMyPluginMultipleVessels; 
 
// Called on module init 
DLLCLBK void InitModule (HINSTANCE hDLL) 
{ 
// init spec
    g_MFDmode = oapiRegisterMFDMode (spec); 
    pMyPluginMultipleVessels = new MyPluginMultipleVessels(hDLL); 
} 
 
// Called on module exit 
DLLCLBK void ExitModule (HINSTANCE hDLL) 
{ 
    // Unregister the custom MFD mode when the module is unloaded 
    oapiUnregisterMFDMode (g_MFDmode); 
    delete pMyPluginMultipleVessels; 
}
4) Call certain Plugin methods on certain events:

Code:
// Called every timestep 
DLLCLBK void opcPreStep (double SimT, double SimDT, double mjd) 
{ 
    pMyPluginMultipleVessels->Init(); // The essence of the code inside is executed only once 
    pMyPluginMultipleVessels->Update(); // This sould be called on each step 
} 
 
// Called on vessel deletion 
DLLCLBK void opcDeleteVessel( OBJHANDLE  hVessel ) 
{ 
    // Signal the PluginMultipleVessels that this vessel should be removed from it's structure 
    pMyPluginMultipleVessels->DeleteVessel( hVessel ); 
}
although note, that since Orbiter 2009 these callbacks are depreciated, so this step should be avoided and be replaced with a more elegant method. We'll get to it later. It's probably a question to Martins

5) Declare the derived MFDData in the MFD class

6) Pass the Plugin to MFD's constructor and initialize the MFDData and perform a simple check

Code:
// Constructor 
MyMFDMultipleVessels::MyMFDMultipleVessels (DWORD w, DWORD h, VESSEL *vessel, MyPluginMultipleVessels * pluginMultipleVessels ) : 
    MFD2 (w, h, vessel) 
// Initialisation list 
// init m_data with PluginMultipleVessels's return value, depending on the vessel pointer 
// If dynamic cast fails, the m_data member becomes NULL 
    , m_data(dynamic_cast<MyMFDData *>(pluginMultipleVessels->AssociateMFDData(vessel))) 
    , m_sound(pluginMultipleVessels->GetSound()) 
{ 
 
    if ( m_data == NULL ) // Programming error 
        sprintf_s(oapiDebugString(), 512, "m_data pointer type is not compatible with the pointer that was being assigned to it in Ctor"); 
}
7) Call Update on the MFDData in MFD::Update()

Code:
// Repaint the MFD 
bool MyMFDMultipleVessels::Update ( oapi::Sketchpad * skp ) 
{ 
    if ( m_data == NULL ) 
        return false; 

    // Update all ship's variables 
    m_data->Update(); 
 
    // Draws the MFD title 
    Title (skp, "My Multiple Vessels MFD"); 
 
    PrintResults(skp); 
 
    return true; 
}
8) If it's necessary, you can update each of the vessels' MFDData in your derived plugin's UpdateClient() that runs even if the MFD itself is disabled.

Code:
void MyPluginMultipleVessels::UpdateClient( MFDData * data ) 
{ 
    // Normally you'd use dynamic_cast and check if the returned value is NULL, 
    // but this method is supposed to work fast enough. 
    MyMFDData * myMFDData = static_cast<MyMFDData *> (data); 
    AutopilotBase * apBase = m_apMan.GetAP(myMFDData->GetAutopilotType()); 
    if (apBase != NULL) 
    { 
        // Only now it's reasonable to call data->Update(). The Guide method calls it 
        // Of course, if there were more methods here needing the updated ship data, 
        // you'd move the Update() to this scope, and call it only once. 
        apBase->Guide(myMFDData); 
    } 
}
And now, have a look at the PluginMultipleVessels. The thing that the MFD dev normally shouldn't care about, unless he/she's interested.

Code:
#define STRICT 
#define ORBITER_MODULE 
#include <OrbiterSDK.h> 
 
#include "PluginMultipleVessels.hpp" 
 
PluginMultipleVessels::PluginMultipleVessels( HINSTANCE hDLL ) 
    : 
    oapi::Module( hDLL ) 
{ 
    m_inited = false; 
} 
 
PluginMultipleVessels::~PluginMultipleVessels() 
{ 
    // Perform an automatic clean up of all MFDData instances 
    for (VMFDData::const_iterator it = m_vMFDData.begin(); it != m_vMFDData.end(); ++it) 
        delete *it; 
} 
 
void PluginMultipleVessels::clbkPreStep (double simt, double simdt, double mjd) 
{ 
    Update(); 
} 
 
void PluginMultipleVessels::clbkDeleteVessel (OBJHANDLE hVessel) 
{ 
 
    DeleteVessel( hVessel ); 
} 
 
void PluginMultipleVessels::Init() 
{ 
    if ( ! m_inited ) 
    { 
        m_inited = true; // Enter here only once 
        InitClient(); 
    } 
} 
 
void PluginMultipleVessels::Update() 
{ 
    // Do necessary PluginMultipleVessels code first 
 
    // and now call the overriden class' UpdateClient method for each MFDData * 
    for (VMFDData::const_iterator it = m_vMFDData.begin(); it != m_vMFDData.end(); ++it) 
    { 
        if ( (*it)->IsValid() ) // Update only valid MFDDatas 
        { 
            UpdateClient( *it ); // Call the derived update on this MFDData 
        } 
    } 
} 
 
MFDData * PluginMultipleVessels::AssociateMFDData(VESSEL * vessel) 
{ 
    // Scan through the MFDData vector 
    for (VMFDData::const_iterator it = m_vMFDData.begin(); it != m_vMFDData.end(); ++it) 
    { 
        // if the handle of the vessel this MFD is linked to is the same as the handle of the vessel 
        // inside the MFDData object that is currently being scanned 
        if (vessel == (*it)->GetVessel()) 
        { 
            // Return a pointer to this MFDData (dereferenced iterator) 
            return *it; 
        } 
    } 
    // If no match is found, let's create one from scratch. 
    // Create a new MFDData object for the given vessel, effectively linking them together 
    MFDData * dat = ConstructNewMFDData(vessel); 
    m_vMFDData.push_back(dat); // store this new MFDData object inside the MFDData structure. 
    return dat; // Return a pointer to the new MFDData object. 
} 
 
void PluginMultipleVessels::DeleteVessel( OBJHANDLE  hVessel ) 
{ 
    for (VMFDData::iterator it = m_vMFDData.begin(); it != m_vMFDData.end(); ++it) 
    { 
        // Searching for the argument vessel in our structure 
        if ( (*it)->GetVessel()->GetHandle() == hVessel ) 
        { 
            // found it 
            (*it)->Invalidate();  // Simply invalidate this MFDData object. A CTD-secure method 
            break; 
        } 
    } 
}
And while we're here, there's a catch. Neither void clbkPreStep(), nor clbkDeleteVessel() are called by the core, despite I derived from oapi::Module, and passed the hDLL to the superclass. This is why I need to use the deprecated opcPreStep() and opcDeleteVessel()


SOUND

In the exemplary implementation in the attachment, you'll also find a handy C++ sound wrapper over Dan's library, with an interface as simple as this. One thing is that you need to create your own enum of sound IDs and map it in a SoundMap-derived class, like this:

Code:
#include "MySoundMap.hpp" 
 
MySoundMap::MySoundMap() 
{ 
    // Here, you have to map each of your enum values onto a sound sample 
    this->operator[]( HALF_ORBITAL_VEL ) = "Sound/LaunchMFDEnjo/HalfOV.wav"; 
    this->operator[]( TARGET_MANUAL ) = "Sound/LaunchMFDEnjo/ManualTarget.wav"; 
    this->operator[]( TARGET_SELECTED ) = "Sound/LaunchMFDEnjo/TargetSelected.wav"; 
    this->operator[]( CUT_ENGINES ) = "Sound/LaunchMFDEnjo/CutEngines.wav"; 
    this->operator[]( ALTITUDE_SET ) = "Sound/LaunchMFDEnjo/alt_set.wav"; 
    this->operator[]( ALTITUDE_AUTO ) = "Sound/LaunchMFDEnjo/AltitudeAutomatic.wav"; 
    this->operator[]( HUD_ENABLED ) = "Sound/LaunchMFDEnjo/hud_en.wav"; 
    this->operator[]( HUD_DISABLED ) = "Sound/LaunchMFDEnjo/hud_dis.wav"; 
    this->operator[]( BEEP_ENABLED ) = "Sound/LaunchMFDEnjo/beep_en.wav"; 
    this->operator[]( VOICE_ENABLED ) = "Sound/LaunchMFDEnjo/voice_en.wav"; 
    this->operator[]( VOICE_DISABLED ) = "Sound/LaunchMFDEnjo/voice_dis.wav"; 
    this->operator[]( BEEP ) = "Sound/LaunchMFDEnjo/beep_am_light22.wav"; 
}
Now the map must be passed to the Sound class in Plugin-derived class:

Code:
MyPluginMultipleVessels::MyPluginMultipleVessels( HINSTANCE hDLL )
    :    PluginMultipleVessels( hDLL )
	, m_sound(m_soundMap)
{
}
That's it. Now you need to call Sound::Connect() from MFD::Update(), like this:

Code:
// Repaint the MFD 
bool MyMFDMultipleVessels::Update ( oapi::Sketchpad * skp ) 
{ 

    // Sound can be connected only within Update 
    m_sound.Connect( "MyMFDMultipleVessels_Enjo, lol"); 
    return true; 
}
and you can just use m_sound.PlaySound( TARGET_SELECTED )

Of course, the m_sound member should be initialized. I do it by passing it to MFD from derived Plugin class, inside MFD's constructor:

Code:
// Constructor 
MyMFDMultipleVessels::MyMFDMultipleVessels (DWORD w, DWORD h, VESSEL *vessel, MyPluginMultipleVessels * pluginMultipleVessels ) : 
    MFD2 (w, h, vessel) 
// Initialisation list 
    : m_sound(pluginMultipleVessels->GetSound()) 
{ 
 
}
I think it's all simple enough. Go ahead, if anybody would like to join this effort, or simply share your comments.
 

Attachments

  • multipleVesselsMFDwSound.zip
    426.5 KB · Views: 1
Last edited:

agentgonzo

Grounded since '09
Addon Developer
Joined
Feb 8, 2008
Messages
1,649
Reaction score
4
Points
38
Location
Hampshire, UK
Website
orbiter.quorg.org
I end up hand-coding classes each time I start a new MFD but the end result is normally always the same technique. It does away with global variables and is fairly simple.

Your main MyMFD class (that inherits from MFD, or MFD2) has a static member that is a hashtable of pointers to new MyVesselSpecificMFD objects. The key for the hashtable is the HANDLE for the vessel. MyVesselSpecificMFD inherits from MFD (or MFD2) so acts exactly the same as you would expect (to keep things simple).

The MyMFD::Update function gets the current vessel pointer and uses this to get the MyVesselSpecificMFD object (or instantiates a new one) and then calls Update() in that object.

If you need things to do stuff in the background (as you normally would on opcPre/PostStep) then define a new interface that has an opcPre/PostStep and get your MyVesselSpecificMFD to implement this (or an inherited class from MFD with virtual functions for opcPre/Post step if you want the use of these functions to be optional). Then pass this through the same way as update, but for all pointers in MyMFD hashtable.

That's about all there is to it. I do it slightly differently each time, but it's quite simple.
 

agentgonzo

Grounded since '09
Addon Developer
Joined
Feb 8, 2008
Messages
1,649
Reaction score
4
Points
38
Location
Hampshire, UK
Website
orbiter.quorg.org
I couldn't sleep last night so I got to thinking about this. This is what I came up with.

Two basic classes:
- Abstract MFDPersister (to which the user derives a simple class)
- MyMFD (your MFD class you are developing)

Create a new class called MFDPersister which extends MFD†.
The basic method of function would be that it acts as a new layer between orbiter and your MFD (which I'll call MyMFD throughout here). On creation, it creates one instance of MyMFD (if not already created) and stores this pointer in a hashtable with the vessel handle as the key. Every MFD function that orbiter calls is then calling directly into the MFDPersister class, which does a lookup on the current vessel to get the correct MFD and then calls that MFD's method.

In detail:

MFDPersister is an abstract class contains a static hashtable of HANDLEs to MFD* pointers. It contains one pure virtual method
Code:
virtual MFD* MFDPersister::createNewMFDInstance(DWORD w, DWORD h, VESSEL *vessel) = 0;
This will be used by a derived class to create an instance of MyMFD and return the pointer to it.

It contains a private method:
Code:
MFD* MFDPersister::getMFDForVessel(HANDLE vesselHandle);
that returns the vessel pointer from the static hashtable.

It's constructor is of the form:
Code:
MFDPersister(DWORD w, DWORD h, VESSEL *vessel)
{
   if(<vessel-does-not-appear-in-hashtable>)
   {
      MFD* childMFD = createNewMFDInstance(w, n, vessel);
      // Add childMFD to the hashtable. 
   }
}

All other MFD functions then take the form:
Code:
bool MFDPersister::Update (Sketchpad *sketchpad) 
{
   return getMFDForVessel(oapiGetVesselHandle())->Update(sketchpad);
}

That's it. All that comes as one class and so one .cpp and one .h file. It obviously leaks memory with the above at one MFD instance per vessel but you can use the Orbiter API calls to listen for vessel destruction and delete the pointers. Persisting across orbiter sessions should be easy by modifying the 'save' and 'read' functions that you get with the MFD class. Some sanity checking inside getMFDForVessel would obviously be preferable and maybe lazy creation rather than creating the MFD via the constructor (but this makes no difference conceptually).

All the user has to do is the following:
Create a new derived class:
Code:
class MyMFDPersister : public MFDPersister
{
   virtual MFD* createNewMFDInstance(DWORD w, DWORD h, VESSEL *vessel);
}

MFD* MyMFDPersister::createNewMFDInstance(DWORD w, DWORD n, VESSEL *vessel)
{
   return new MyMFD(w, n, vessel);
}


int YourMessageHandler(UINT msg, UINT mfd, WPARAM wparam, LPARAM lparam)
Standard message parser for messages passed from Orbiter
{
   switch (msg) 
   {
   case OAPI_MSG_MFD_OPENED:
      return (int) new MyMFDPersister(LOWORD(wparam), HIWORD(wparam), (VESSEL*)lparam, mfd);
   }
   return 0;
}

MyMFD is derived from the normal MFD class so nothing special has to be done there.

†It'd probably be better to extend MFD2 (or the latest version of MFD to force people to move to newer implementations and stop using obsolete methods and classes, but for the purpose of this I'll just keep calling it MFD as it's simpler).
 
Top