Okay, a few words on APIs. May be useful as an overview of TFXplorer’s extension architecture.
TFXplorer is based heavily on extensions now (or plugins, or mods, whatever you call it). There is no built-in terrain or plane. The TAW/EF2000/TFX terrains are provided from an extension, and the F-22 is provided from an extension as well. I want to pursue the extensions mechanism because it is central to modern flight simulators like Microsoft’s or DCS.
So TFXplorer must offer extensions an API. Extensions must use that API to declare assets, to render their planes, etc.
The classic (and most simple) way to provide an API is via exported C functions. That’s what the C runtime does (fopen), or C++ (std::chrono::system_clock), or Linux (open), or Windows (CreateFile).
TFXplorer/3View did this when the extension system was originally introduced, and some outdated parts of its API are still provided in that manner.
This approach has a drawback, however, and that’s versioning/compatibility. Once a function is exported, we cannot change its interface without breaking dependent applications. That’s why Win32 provides weird names like CreateFile2 or GetVersionEx – in order to not break applications which use the old CreateFile and GetVersion.
Not breaking old extensions can be solved by supporting the old functions forever, but this leads to another problem: Users mixing versions of the API. You have no means of forcing users to use your new API over your old one, and they will likely mix them up (e.g. use the old ReadFile on a file opened with the new CreateFile2). To account for that, your implementation complexity no longer grows linear with the number of supported versions, but exponentially.
This can be solved by stepping away from global functions. Instead of offering the extension all supported functions, the extension has to ask for a specific version and will receive this specific API. This approach is known from Direct3D 9, where the only function that’s initially available is Direct3DCreate9 (similar for newer versions and for DXGI). You call it and pass the version number of the SDK you’re compiling with, and you receive an IDirect3D9 interface with the actual API matching that SDK version. The Vulkan API goes a similar way, only the version number is encoded in the structure type you pass to vkCreateInstance.
Internally, you can maintain the APIs isolated from one another. Extensions cannot mix up APIs except by bad intention (they’d have to initialize the API twice with different versions).
Problem solved. TFXplorer introduced this in 2020, though the transition is, again, incomplete. There’s another problem lurking, though.
TFXplorer extensions are based on callbacks. Extensions initially provide a table à “I provide plane ABC and if the user wants to fly one, call function XYZ to create it.” This makes sense as the GUI/simulation dictate what has to be done, and extensions are only asked how it should be done.
But TFXplorer provides a huge set of functions, and entropy dictates that extensions will eventually call them in any possible combination. There are, however, combinations that don’t make any sense – for example, an extension may start placing airplanes in the level when all we asked it for was loading its sound effects. Or it may start loading terrain data when all we asked it to do was drawing an airplane.
(The last example is real: When I added the moving map to the F-22 cockpit, I just threw the code for loading it right into the drawing function. This is bad for a number of reasons. But it seems that having rotten smelly code in your codebase is useful to identify problems in your API early )
The API needs separation of concerns. When an extension draws a plane, it should only use the draw functions. When an extension starts a scenario, it should only use the functions to place airplanes/tanks/buildings. So we shouldn’t let an extension access the entire API in the first place!
Here’s how it works now:
Extensions provide a table à “I provide plane ABC and if the user wants to fly one, call function XYZ to create it.” The table starts with the version number of the SDK the extension was built with. (An inline function in the SDK header ensures this.)
TFXplorer internally remembers the version number for the extension so it can return the proper API for the SDK it was compiled with. (Of course I currently maintain only one version internally and that won’t change until the first actual release in 2025 or so.)
When TFXplorer actually calls into an extension for things like “I need plane ABC, please create it”, then it passes a minimal interface which only contains the functions an extension is allowed to call during that task. (This interface is versioned according to 1. and 2..)
The interfaces for individual tasks are mutually exclusive. Consistency is checked whenever an API function is called.
This has a nice side-effect for multi-threading: The interfaces being passed to extensions need not all use the same implementation. Instead of letting extensions tinker with simulation state directly, we can now easily create job objects, call extensions in parallel, pass them individual job objects to do their stuff, and synchronize the results afterwards instead of doing it all over the place. It also has nice implications on testing.
This transition has just started and is not yet in the new repository, but it does work well for me so far. It uncovered a load of code smell from 2012–2014 that’s being fixed.
Apart from that, the repo contains a slightly extended Settings page.