-
Notifications
You must be signed in to change notification settings - Fork 0
Model Matching
JoinFS has to turn "the aircraft/livery the other pilot (or a recording) is using" into "a model I can actually ask the simulator to create". This page documents the matching algorithm in Substitution.cs (Match()), what data feeds it, its fundamental limitations, and a known pitfall around duplicate/redundant model scan folders that can make a perfectly-installed livery fail to render.
For how your own simulator specifically reports your aircraft, and how your JoinFS build resolves what your friends report, see the per-simulator pages linked at the bottom of this page.
This is the single most important thing to understand before reporting a "wrong model" as a bug: algorithmic model matching only ever knows what your simulator reports, both about your own configuration (e.g. your callsign/registration) and about the aircraft/livery itself. JoinFS has no independent way to verify any of it - it isn't looking at the 3D model, the manufacturer's real-world spec sheet, or anything outside of what the simulator (or the aircraft/livery file itself) hands it.
Concretely, matching only ever sees:
- From your own configuration: your nickname/callsign, and a registration/tail number - either whatever's set in the simulator itself (the livery's baked-in default, or something you've customized), or imported from SimBrief.
-
From the aircraft/livery, self-reported: its title and livery/variation name, and - only if the aircraft's author filled them in, and filled them in correctly - its ICAO type designator (e.g.
A20N), wake turbulence category, and ICAO airline operator code.
If that self-reported data is wrong, missing, or non-standard, matching has no way to recover the "true" answer. The classic example: an add-on's title/description might say "Airbus 320 Neo", but if its icao_type_designator field is left blank, or contains something other than the real ICAO code A20N (a free-text field - some authors type the model name into it, some leave it in a different language, some just leave it blank), JoinFS cannot know it's really an A320neo from that field. It falls back to guessing from the title text instead - a much weaker signal, prone to both misses (titles that don't resemble any known aircraft name) and occasional false positives (a short/generic word in the title coincidentally matching an unrelated aircraft type). This isn't a JoinFS bug to fix - it's a hard limit on what's knowable from self-reported, unvalidated data. See the per-simulator pages for exactly which of these fields your simulator can and can't give reliably, and how confident that data is once JoinFS has it.
The scorer (see below) weighs all of the following together - a candidate model doesn't need to match on all of them, or even on the "obvious" one, to win:
| Attribute | Source | Notes |
|---|---|---|
ICAO type designator (e.g. A20N) |
Aircraft/livery config, or a live-confirmed read, or (last resort) guessed from the title | Strongest signal when present and correct |
Doc8643 class code (e.g. L2P, H1T) |
Derived from the ICAO type designator via a bundled reference table, or confirmed directly from the aircraft/livery config or live simulator data | Encodes platform + engine count + engine type in one code |
| Wake turbulence category (WTC) | Same sources as class code | e.g. L/M/H/J
|
| Engine count / engine type | Encoded in the class code above | Not a separate reported field |
ICAO airline operator code (e.g. DLH) |
Aircraft/livery config, or live ATC data (validated against a known-airline list) | Only meaningful for airline liveries |
| Registration / tail number | Your own configuration (sim-configured or SimBrief), matched against the candidate model's own baked-in registration | Exact match strongly preferred over a substring match |
| Typerole (Airliner, Rotorcraft, SingleProp, ...) | Derived from class code where possible, otherwise a coarse title-text classification | Weakest standalone signal, mainly useful as a tie-breaker |
| Livery/variation name, title | Self-reported | Weakest signals - word-overlap and prefix-matching only, used when nothing stronger is available |
A candidate installed model is scored on every one of these independently and the highest-scoring one wins (above a minimum threshold - otherwise JoinFS falls back to a configured default, or the first installed model, or nothing). This means, for example, a Diamond DA62 can win over a Douglas DC-3 for a Beechcraft Baron request purely because it shares the Baron's class code and wake turbulence category, even though none of them share an ICAO type designator.
Every rendered aircraft records exactly how it was matched. To see it: right-click an aircraft in the Aircraft list and choose "Explain Match..." (only enabled once a match has actually been computed for that aircraft). It shows:
- A side-by-side comparison of every attribute above - what was requested vs. what the matched model has - with the score each contributed and whether it was decisive.
- The other top-scoring candidates that were considered and why they didn't win.
- A step-by-step trace of what was tried (explicit user override, exact install, scoring, configured default, ...).
- A warning if the matched model's ICAO type was only ever guessed from its title, never confirmed.
- A "Copy to clipboard" / "Export Debug Bundle..." option to share the full report (plus the underlying models/matching data files) when asking for help.
Substitution.Match() is called for every remote/AI/recorded aircraft JoinFS needs to render. It receives the aircraft's reported title (and, on MSFS2024, livery/variation), ICAO type designator, class code, wake turbulence category, ICAO airline code, registration, and typerole. It returns a Model (a locally scanned/installed aircraft) plus a Substitution.Type describing how it was matched.
The Substitution.Type is shown as a suffix on the Sub Model column in the Aircraft list:
| Type | Suffix shown | Meaning |
|---|---|---|
Substitute |
(S) |
Explicit user override (Settings ▸ Model Matching) |
Original |
(none) | Exact title (+ livery, MSFS2024) found among currently scanned models |
Icao |
(none) | The winning candidate shares the requested ICAO type designator |
Category |
(none) | The winning candidate shares the requested Doc8643 class code (or a looser platform+engine-type category), without necessarily sharing the exact ICAO type |
Auto |
(A) |
The winning candidate scored highest on weaker signals (typerole, livery-name overlap, title prefix) |
Default |
(D) |
A configured default model - either the fine-grained (typerole + class code + WTC) default, or the coarse per-typerole default, whichever matched |
AI |
(AI) |
Reserved for an (currently disabled) embedding-based matcher |
If nothing above matches at all, JoinFS falls back to the first installed model in its list, or renders nothing if no models are installed.
-
Substitute — an explicit user-defined title→model override (
matches[title]). Always wins outright - this is a deliberate user instruction, never overridden by scoring. -
Original —
GetModel(title, livery): an exact string match against the currently scannedmodelslist. This is the common case when the exact livery is installed. - Scoring — a single unified pass across every installed model sharing the requested ICAO type, class code, loose platform+engine category, or typerole with the remote aircraft. Each candidate is scored on every attribute in the table above (weighted - ICAO type and class code/WTC contribute the most, livery/title text the least), and the highest-scoring candidate above a minimum threshold wins. A model whose class code was only ever guessed (not confirmed) contributes less to its own score than a confirmed one, so a confident real match isn't beaten by a coincidental guess.
- Default — a configured default model for the requested typerole (optionally narrowed to a specific class code + WTC combination, if you've configured one and it's a better fit) is used when nothing scored highly enough above.
- Last resort — the first model in the installed list, or nothing.
This replaces an older, stricter tiered design (exact ICAO type, then exact class code, then a loose category, then a title-prefix search, each an all-or-nothing gate before falling to the next) with one scoring pass across all of them at once - so, for instance, a same-class/same-WTC candidate can beat a same-ICAO-type candidate that happens to be a worse fit on every other signal, and matches no longer depend on hitting an exact tier boundary.
The scoring tier's strongest signals (ICAO type, class code, WTC) require the remote aircraft to actually have reported an ICAO type designator. For live network traffic this comes from the aircraft's flight plan (see the per-simulator pages for how reliably that's populated). For recorded flight playback, older recordings made before ICAO data capture was added have none of this, and will only ever hit the weaker typerole/livery/title-text signals or the configured default - re-record to get full matching.
Installed models are scanned from the simulator's own folders plus any additional folders you've configured, and cached to disk at:
%LOCALAPPDATA%\JoinFS-<Simulator>\models - <SimulatorName>.txt
-
Every connection unconditionally loads this cache file into memory (
LoadModels()), regardless of whether the files on disk still exist. - Only an actual scan rebuilds it from what's really on disk:
models.Clear()followed by a live folder walk (or, for MSFS2024, a live SimConnect enumeration). This runs automatically on connect if "Scan at launch" is checked in the Scan For Models dialog (Settings.Default.ModelScanOnConnection), or manually via Scan For Models… → Scan. - Scanning parses/enumerates what it finds and trusts it - it does not verify that the package actually loads in the simulator (e.g. it never checks whether a livery's
base_containerresolves to an installed base aircraft, though it does try to resolve that reference to read the base aircraft's own data - see the MSFS2024 per-simulator page). - Models matching an entry in the bundled/community-maintained ban list (non-flyable scenery props, static display liveries, and a couple of known-broken entries) are excluded from the scan entirely - they never appear in the Substitute dialog and can never become a default fallback. If you believe a genuinely flyable aircraft is being wrongly excluded, check the log for
"Model banned, excluded from scan"lines.
Consequence: if you delete a livery folder but never re-run a scan (or "Scan at launch" is off), JoinFS keeps matching to it from the stale cache — SimConnect will then refuse to create it (SIMCONNECT_EXCEPTION_CREATE_OBJECT_FAILED, logged as ERROR - Failed to inject aircraft ...), and the aircraft list shows a permanently red, never-created ghost. Re-running Scan For Models purges genuinely-deleted entries.
Tools like MSFS Addon Linker relocate large add-on packages (liveries, aircraft) out of the Community folder onto another drive, then create a symlink/junction back into Community so the simulator still sees them as installed there.
The Scan For Models dialog lets you add additional folders on top of whatever the simulator itself reports — these are simply appended to the scan list, with no deduplication or validation against what the simulator's own content system actually has loaded.
If you point one of these additional folders at the Addon Linker's raw storage/source location — instead of only scanning through the actual Community folder (which already reaches that content via the junction) — JoinFS ends up registering the same aircraft from two different roots. This was exactly the scenario that produced a red, unrendered aircraft even though the livery was genuinely installed and JoinFS's own model list contained a seemingly perfect, exact-title match (tier Original, no fallback needed) — SimConnect still refused to create it.
Symptoms:
- Aircraft list "Distance" cell is stuck red; nothing renders.
- The Sub Model column shows the exact title/variation you expected — this is an
Originalmatch, not a fallback, so it's not a "matching couldn't find anything" problem. - The log shows
ERROR - Failed to inject aircraft '<callsign>' ... - Sub '<title>'(SimConnectCREATE_OBJECT_FAILED).
Fix: only configure JoinFS's Folders/additional-folders to point at the actual Community folder(s) the simulator uses — not an add-on manager's separate source/staging folder that already reaches the sim through a symlink/junction elsewhere. Duplicating a source doesn't add any real content; it just gives JoinFS a second, potentially inconsistent view of the same aircraft to pick from. If you're not sure whether a configured folder is redundant, remove it and re-run Scan For Models — if the aircraft you care about still resolves and renders correctly, you didn't need it.
When an aircraft shows up red / never renders:
- Check the JoinFS log for
ERROR - Failed to inject aircraft/ERROR - Failed to inject objectand note theSubmodel it tried to use. - If
Subis exactly the aircraft you thought you removed: it's probably still on disk somewhere JoinFS scans (or in the persisted models cache). Confirm on disk, then re-run Scan For Models. - If it's genuinely gone from disk and the log shows a different, substituted
Submodel: matching worked as intended (a scored substitution or configured default) — if it still fails to inject, that substituted model itself may be broken/uninstallable. - Check Scan For Models for any additional folders that duplicate content already reachable through the simulator's own
Communityfolder (see pitfall above), especially if you use an add-on manager that relocates packages via symlinks/junctions. - If replaying a recording, confirm it was made after the ICAO-fallback fix described above — older recordings won't have
icaoType/icaoAirlineand can only use the weaker typerole/title/default tiers. - Open Explain Match on the aircraft in question - the attribute comparison and "other candidates considered" panel usually make it obvious whether the requested aircraft's own self-reported data was the problem (see "What matching can and can't know" above), or whether a better installed candidate simply wasn't found/scanned.
What your own simulator reports about your aircraft to your friends, and how your build resolves what they report back, differs enough between simulators to warrant their own pages: