diff --git a/.github/workflows/release-van.yml b/.github/workflows/release-van.yml
new file mode 100644
index 0000000..cdf6d3d
--- /dev/null
+++ b/.github/workflows/release-van.yml
@@ -0,0 +1,50 @@
+name: Van
+on:
+ push:
+ tags:
+ - van/v*.*.*
+
+jobs:
+ lint:
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ - name: Set up Python 3.10
+ uses: actions/setup-python@v3
+ with:
+ python-version: "3.10"
+ - name: Install ruff
+ run: pip install ruff
+ - name: Lint with ruff
+ run: ruff check .
+
+ release-van:
+ needs: lint
+ name: Create Release
+ runs-on: ubuntu-latest
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ fetch-depth: 0
+ - name: Build and zip folder
+ run: |
+ release_tag="${GITHUB_REF#refs/tags/}"
+ tag_version="${release_tag#van/}"
+ version_tuple=$(echo "$tag_version" | awk -F'[v.]' '{print $2", "$3", "$4}')
+ sed -i "s/\"version\": .*/\"version\": (${version_tuple}),/" bms_blender_plugin/__init__.py
+ zip_file="bms_blender_plugin-van-${tag_version}.zip"
+ zip -r "$zip_file" bms_blender_plugin \
+ -x "*/__pycache__/*" \
+ -x "*.pyc" \
+ -x "*.pyo"
+ echo "Zip file built"
+ echo "RELEASE_TAG=$release_tag" >> $GITHUB_ENV
+ echo "TAG_VERSION=$tag_version" >> $GITHUB_ENV
+ echo "ZIP_FILE=$zip_file" >> $GITHUB_ENV
+ - name: Release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: "${{ env.RELEASE_TAG }}"
+ name: "Van ${{ env.TAG_VERSION }}"
+ body: "Install the uploaded bms_blender_plugin zip asset."
+ files: "${{ env.ZIP_FILE }}"
diff --git a/bms_blender_plugin/__init__.py b/bms_blender_plugin/__init__.py
index c82f414..6fe319d 100644
--- a/bms_blender_plugin/__init__.py
+++ b/bms_blender_plugin/__init__.py
@@ -8,7 +8,7 @@
bl_info = {
"name": "Falcon BMS Plugin",
"author": "Benchmark Sims",
- "version": (0, 0, 20250118),
+ "version": (1, 2, 0),
"blender": (3, 6, 0),
"location": "File > Export",
"description": "Export as Falcon BMS BML",
diff --git a/bms_blender_plugin/common/DOF.xml b/bms_blender_plugin/common/DOF.xml
index d8351ed..aea172c 100644
--- a/bms_blender_plugin/common/DOF.xml
+++ b/bms_blender_plugin/common/DOF.xml
@@ -34,6 +34,7 @@
8
+ ChocksFR/SimpRudder1
9
@@ -57,7 +58,7 @@
14
-
+ AWACSdome/SimpSwingWing4
15
@@ -109,7 +110,7 @@
27
-
+ Canards
28
@@ -200,7 +201,7 @@
49
-
+ RudderToeIn
50
@@ -402,6 +403,7 @@
99
+ PilotVisor
100
@@ -481,15 +483,15 @@
119
- HSI Distance To Beacon Digit 1
+ HSI DME Digit 1
120
- HSI Distance To Beacon Digit 2
+ HSI DME Digit 2
121
- HSI Distance To Beacon Digit 3
+ HSI DME Digit 3
122
@@ -553,23 +555,23 @@
137
- Fuel Digit 1
+ Fuel Qty Total 1
138
- Fuel Digit 2
+ Fuel Qty Total 2
139
- Fuel Digit 3
+ Fuel Qty Total 3
140
- Fuel Digit 4
+ Fuel Qty Left 1
141
- Fuel Digit 5
+ Fuel Qty Left 2
142
@@ -577,7 +579,7 @@
143
- Fuel Forward
+ Fuel Forward/F15 FF right
144
@@ -735,4 +737,284 @@
182
Oil Needle 2
+
+ 183
+ HSI DME Digit 4 (only for EHSI)
+
+
+ 184
+ F15C Fuel Total Needle
+
+
+ 185
+ Fuel Qty Right 1
+
+
+ 186
+ Fuel Qty Right 2
+
+
+ 187
+ F15C Fuel Bingo Bug
+
+
+ 188
+ ALT tens digits (F15C)
+
+
+ 189
+ Temp digit 1 left
+
+
+ 190
+ Temp digit 2 left
+
+
+ 191
+ Temp digit 3 left
+
+
+ 192
+ Temp digit 1 right
+
+
+ 193
+ Temp digit 2 right
+
+
+ 194
+ Temp digit 3 right
+
+
+ 195
+ RPM left digit 1
+
+
+ 196
+ RPM left digit 2
+
+
+ 197
+ RPM left digit 3
+
+
+ 198
+ RPM right digit 1
+
+
+ 199
+ RPM right digit 2
+
+
+ 200
+ JTDS left 0-1
+
+
+ 201
+ JTDS center 0-9
+
+
+ 202
+ JTDS right 0-9
+
+
+ 203
+ RPM right digit 3
+
+
+ 204
+ IFF MCODE left
+
+
+ 205
+ IFF MCODE right
+
+
+ 206
+ IFF MAN FREQ 1
+
+
+ 207
+ IFF MAN FREQ 2
+
+
+ 208
+ IFF MAN FREQ 3
+
+
+ 209
+ IFF MAN FREQ 4
+
+
+ 210
+ AAI MODE DIGIT 1
+
+
+ 211
+ AAI CODE DIGIT 1
+
+
+ 212
+ AAI CODE DIGIT 2
+
+
+ 213
+ AAI CODE DIGIT 3
+
+
+ 214
+ AAI CODE DIGIT 4
+
+
+ 215
+ ILS Freq Digit 5
+
+
+ 216
+ ILS Freq Digit 4
+
+
+ 217
+ ILS Freq Digit 3
+
+
+ 218
+ ILS Freq Digit 2
+
+
+ 219
+ F15 Backup ASI Needle
+
+
+ 220
+ unused
+
+
+ 221
+ unused
+
+
+ 222
+ F15 Light Knob L CONSOLE
+
+
+ 223
+ F15 Light Knob R CONSOLE
+
+
+ 224
+ F15 Light Knob AUX INST
+
+
+ 225
+ F15 Light Knob FLT INST
+
+
+ 226
+ F15 Light Knob ENG INST
+
+
+ 227
+ F15 Light Knob STORM FLOOD
+
+
+ 228
+ F15 TEWS INT knob
+
+
+ 229
+ F15 G-Meter MIN G
+
+
+ 230
+ F15 G-Meter MAX G
+
+
+ 231
+ unused
+
+
+ 232
+ unused
+
+
+ 233
+ unused
+
+
+ 234
+ unused
+
+
+ 235
+ unused
+
+
+ 236
+ unused
+
+
+ 237
+ unused
+
+
+ 238
+ Turn Rate DegS
+
+
+ 239
+ unused
+
+
+ 240
+ unused
+
+
+ 241
+ unused
+
+
+ 242
+ unused
+
+
+ 243
+ unused
+
+
+ 244
+ unused
+
+
+ 245
+ unused
+
+
+ 246
+ unused
+
+
+ 247
+ unused
+
+
+ 248
+ unused
+
+
+ 249
+ unused
+
+
+ 250
+ Radio2channelLeft
+
+
+ 251
+ Radio2channelRight
+
+
+ 255
+ Sideslip Angle Deg
+
\ No newline at end of file
diff --git a/bms_blender_plugin/common/constants.py b/bms_blender_plugin/common/constants.py
new file mode 100644
index 0000000..21add2e
--- /dev/null
+++ b/bms_blender_plugin/common/constants.py
@@ -0,0 +1,22 @@
+"""Acts as a header for constants, etc
+
+Purpose:
+- Avoid literals... :)
+
+Notes:
+
+"""
+
+# Maximum allowable (inclusive) identifier values for DOFs and Switches.
+# Previously 255. Used in __init__.py (object intproperty limits) and export_parent_dat.py (cap highest number)
+BMS_MAX_SWITCH_NUMBER: int = 2048
+BMS_MAX_SWITCH_BRANCH: int = 2048 # Branch uses same bound currently
+BMS_MAX_DOF_NUMBER: int = 2048
+
+# Not recommended but available if someone were to import with wildcard eg. from bms_blender_plugin.common.constants import *
+# If adding above, also add them here if they should be available as if "public"
+__all__ = [
+ "BMS_MAX_SWITCH_NUMBER",
+ "BMS_MAX_SWITCH_BRANCH",
+ "BMS_MAX_DOF_NUMBER",
+]
diff --git a/bms_blender_plugin/common/hydration.py b/bms_blender_plugin/common/hydration.py
new file mode 100644
index 0000000..7a23106
--- /dev/null
+++ b/bms_blender_plugin/common/hydration.py
@@ -0,0 +1,37 @@
+"""Hydration/load_post handler.
+
+Ensures that on .blend load the scene cached switch/DOF lists (if avail) become the
+authoritative source for switch / DOF lists until the user explicitly
+reloads from disk XML via UI.
+ - Allows seamless switching between .blend files with different XML snapshots
+ - Ensures UI lists reflect the loaded .blend's snapshot immediately
+ - Primary use case: user working with a foreign blend file created using different XMLs
+"""
+from __future__ import annotations
+import bpy
+from bpy.app.handlers import persistent
+
+
+@persistent
+def bml_hydrate_after_load(_):
+ try:
+ import bms_blender_plugin.common.util as util
+ util.switches = []
+ util.dofs = []
+ util._switches_hydrated = False
+ util._dofs_hydrated = False
+ # Trigger early hydration so UI immediately reflects snapshot
+ util.get_switches()
+ util.get_dofs()
+ except Exception:
+ pass
+
+
+def register():
+ if bml_hydrate_after_load not in bpy.app.handlers.load_post:
+ bpy.app.handlers.load_post.append(bml_hydrate_after_load)
+
+
+def unregister():
+ if bml_hydrate_after_load in bpy.app.handlers.load_post:
+ bpy.app.handlers.load_post.remove(bml_hydrate_after_load)
\ No newline at end of file
diff --git a/bms_blender_plugin/common/resolve_ids.py b/bms_blender_plugin/common/resolve_ids.py
new file mode 100644
index 0000000..b8d942f
--- /dev/null
+++ b/bms_blender_plugin/common/resolve_ids.py
@@ -0,0 +1,92 @@
+"""Central helpers for resolving DOF / Switch persistent IDs or safe fallbacks.
+
+All systems (validation, nodes, mediator, exporter) should use these helpers
+instead of directly indexing into list indices. This ensures consistent
+resolution order and prevents IndexError crashes when XML/cached lists change.
+
+Resolution order:
+1. Persistent ID properties (authoritative) if set.
+2. Scene cached list (context.scene.dof_list / switch_list) if index in range.
+3. Global cached XML data via get_dofs() / get_switches().
+4. Fallback: return None (caller decides how to proceed gracefully).
+"""
+from __future__ import annotations
+from typing import Optional, Tuple
+import bpy
+
+from bms_blender_plugin.common.blender_types import BlenderNodeType
+from bms_blender_plugin.common.util import get_dofs, get_switches, get_bml_type
+
+
+def resolve_dof_number(obj) -> Optional[int]:
+ """Return persistent DOF number for a DOF object or None if unresolved.
+
+ Safe: never raises IndexError.
+ """
+ if not obj:
+ return None
+ if get_bml_type(obj) != BlenderNodeType.DOF:
+ return None
+
+ # Persistent ID properties first
+ pid = getattr(obj, "bml_dof_number", -1)
+ if isinstance(pid, int) and pid >= 0:
+ return pid
+
+ # Fallback to scene cached list
+ idx = getattr(obj, "dof_list_index", -1)
+ if not isinstance(idx, int) or idx < 0:
+ return None
+
+ # Scene cached list first
+ scene_list = getattr(bpy.context.scene, "dof_list", None)
+ if scene_list and 0 <= idx < len(scene_list):
+ item = scene_list[idx]
+ return getattr(item, "dof_number", None)
+
+ # Global cache fallback
+ print("DOF Resolution: Fallback to global DOF list for index", idx)
+ try:
+ dofs = get_dofs()
+ if 0 <= idx < len(dofs):
+ return dofs[idx].dof_number
+ except Exception:
+ pass
+ return None
+
+
+def resolve_switch_id(obj) -> Tuple[Optional[int], Optional[int]]:
+ """Return (switch_number, branch) for a Switch object or (None, None) if unresolved."""
+ if not obj:
+ return None, None
+ if get_bml_type(obj) != BlenderNodeType.SWITCH:
+ return None, None
+
+ # Persistent ID properties first
+ num = getattr(obj, "bml_switch_number", -1)
+ br = getattr(obj, "bml_switch_branch", -1)
+ if isinstance(num, int) and num >= 0 and isinstance(br, int) and br >= 0:
+ return num, br
+
+ # Fallback to scene cached list
+ idx = getattr(obj, "switch_list_index", -1)
+ if not isinstance(idx, int) or idx < 0:
+ return None, None
+
+ # Scene cached list first
+ scene_list = getattr(bpy.context.scene, "switch_list", None)
+ if scene_list and 0 <= idx < len(scene_list):
+ item = scene_list[idx]
+ return getattr(item, "switch_number", None), getattr(item, "branch_number", None)
+
+ print("Switch Resolution: Fallback to global Switch list for index", idx)
+ try:
+ switches = get_switches()
+ if 0 <= idx < len(switches):
+ sw = switches[idx]
+ return sw.switch_number, sw.branch
+ except Exception:
+ pass
+ return None, None
+
+__all__ = ["resolve_dof_number", "resolve_switch_id"]
diff --git a/bms_blender_plugin/common/switch.xml b/bms_blender_plugin/common/switch.xml
index 3984718..3157cba 100644
--- a/bms_blender_plugin/common/switch.xml
+++ b/bms_blender_plugin/common/switch.xml
@@ -1,4 +1,4 @@
-
+
0
@@ -39,44 +39,12 @@
7
0
- Tail Strobe 0
- Off
-
-
- 7
- 1
- Tail Strobe 1
- Normal
-
-
- 7
- 2
- Tail Strobe 2
- Covert
+ Tail Strobe
8
0
- Position Lights Wing 0
- Off
-
-
- 8
- 1
- Position Lights Wing 1
- Normal
-
-
- 8
- 2
- Position Lights Wing 2
- Dim
-
-
- 8
- 3
- Position Lights Wing 3
- Covert
+ Nav Lights
9
@@ -276,26 +244,7 @@
47
0
- Position Lights Intake Tail 0
- Off
-
-
- 47
- 1
- Position Lights Intake Tail 1
- Normal
-
-
- 47
- 2
- Position Lights Intake Tail 2
- Dim
-
-
- 47
- 3
- Position Lights Intake Tail 3
- Covert
+ Nav Lights Flash
48
@@ -385,8 +334,7 @@
62
0
- Formation lights coverable
- Number of positions define in aircraft dat file
+ Formation lights
63
@@ -403,34 +351,14 @@
66
0
- Tail Flood Light 0
-
-
- 66
- 1
- Tail Flood Light 1
-
-
- 66
- 2
- Tail Flood Light 2
-
-
- 66
- 3
- Tail Flood Light 3
67
0
- AAR Flood Light
- Number of positions define in aircraft dat file
68
0
- Formation lights not coverable
- Number of positions define in aircraft dat file
69
@@ -605,12 +533,27 @@
109
0
- Caution Light ENG FIRE
+ Caution LEFT ENG FIRE
+
+
+ 109
+ 1
+ Caution RIGHT ENG FIRE
+
+
+ 109
+ 2
+ Caution AMAD FIRE
110
0
- Caution Light Engine
+ Caution L BURN THRU
+
+
+ 110
+ 1
+ Caution R BURN THRU
111
@@ -660,7 +603,7 @@
120
0
- HSI Off Flag
+ Canopy Unlocked Light
121
@@ -705,3421 +648,5307 @@
129
0
- Emergency Jettison Button
+ EmergJettNotPush
- 130
- 0
- Alt Gear Handle
+ 129
+ 1
+ EmergJettPushed
- 131
- 0
- Nose Gear Light
+ 129
+ 2
+ Jettison OFF
- 132
- 0
- Main Left Gear Light
+ 129
+ 3
+ Jettison COMBAT
- 133
- 0
- Main Right Gear Light
+ 129
+ 4
+ Jettison A/A
- 134
- 0
- ICP Data Command Switch
+ 129
+ 5
+ Jett not poshed
- 135
- 0
- ICP Drift Cutoff Switch
+ 129
+ 6
+ Jett Pushed
- 136
- 0
- ICP Nextprev Switch
+ 129
+ 7
+ Null
- 137
- 0
- JFS Switch
+ 129
+ 8
+ Null
- 138
- 0
- JFS Light
+ 129
+ 9
+ Null
- 139
- 0
- MPO Switch
+ 129
+ 10
+ Null
- 140
- 0
- EPU Light
+ 129
+ 11
+ Null
- 141
- 0
- EPU Air Light
+ 129
+ 12
+ Null
- 142
- 0
- EPU Hydrazine Light
+ 129
+ 13
+ Null
- 143
- 0
- Elec FLCS pmg Light
+ 129
+ 14
+ Null
- 144
- 0
- Elec Main gen Light
+ 129
+ 15
+ Null
- 145
- 0
- Elec Stby gen Light
+ 129
+ 16
+ Null
- 146
- 0
- Elec EPU gen Light
+ 129
+ 17
+ Null
- 147
- 0
- Elec EPU pmg Light
+ 129
+ 18
+ Null
- 148
- 0
- Elec FLCS rly Light
+ 129
+ 19
+ Null
- 149
- 0
- Elec Toflcs Light
+ 129
+ 20
+ Null
- 150
- 0
- Elec Bat fail Light
+ 129
+ 21
+ Null
- 151
- 0
- Elec Main Power Switch
+ 129
+ 22
+ Null
- 152
- 0
- EPU Switch
+ 129
+ 23
+ Null
- 153
- 0
- Caution Panel FLCS Fault Light
+ 129
+ 24
+ Null
- 154
- 0
- Caution Panel Elec Sys Light
+ 129
+ 25
+ Null
- 155
- 0
- Caution Panel Probe Heat Light
+ 129
+ 26
+ Null
- 156
- 0
- Caution Panel C ADC Light
+ 129
+ 27
+ Null
- 157
- 0
- Caution Panel Stores Config Light
+ 129
+ 28
+ Null
- 158
- 0
- Caution Panel Fwd Fuel Low Light
+ 129
+ 29
+ DIS FREQ down
- 159
- 0
- Caution Panel Aft Fuel Low Light
+ 129
+ 30
+ DIS FREQ middle
- 160
- 0
- Caution Panel Engine Fault Light
+ 129
+ 31
+ DIS FREQ up
- 161
+ 130
0
- Caution Panel Sec Light
+ Alt Gear Handle
- 162
+ 131
0
- Caution Panel Fuel Oil Hot Light
+ Nose Gear Light
- 163
- 0
- Caution Panel Overheat Light
+ 131
+ 1
+ flaps lghtTrans yellow
- 164
- 0
- Caution Panel Buc Light
+ 131
+ 2
+ flaps LghtFull green
- 165
- 0
- Caution Panel Avionics Fault Light
+ 131
+ 3
+ Caution SP BK OUT
- 166
- 0
- Caution Panel Radar Alt Light
+ 131
+ 4
+ Caution AUTO PLT
- 167
- 0
- Caution Panel IFF Light
+ 131
+ 5
+ Caution PITCH RATIO
- 168
- 0
- Caution Panel Seat Not Armed Light
+ 131
+ 6
+ Caution ROLL RATIO
- 169
- 0
- Caution Panel NWS Fail Light
+ 131
+ 7
+ Caution CAS YAW
- 170
- 0
- Caution Panel Anti Skid Light
+ 131
+ 8
+ Caution CAS ROLL
- 171
- 0
- Caution Panel Hook Light
-
+ 131
+ 9
+ Caution CAS PITCH
+
- 172
+ 131
+ 10
+ Null
+
+
+ 131
+ 11
+ Null
+
+
+ 131
+ 12
+ Null
+
+
+ 131
+ 13
+ Null
+
+
+ 131
+ 14
+ Null
+
+
+ 131
+ 15
+ Null
+
+
+ 131
+ 16
+ Null
+
+
+ 131
+ 17
+ Null
+
+
+ 131
+ 18
+ Null
+
+
+ 131
+ 19
+ Null
+
+
+ 131
+ 20
+ Null
+
+
+ 131
+ 21
+ Null
+
+
+ 131
+ 22
+ Null
+
+
+ 131
+ 23
+ Null
+
+
+ 131
+ 24
+ Null
+
+
+ 131
+ 25
+ Null
+
+
+ 131
+ 26
+ Null
+
+
+ 131
+ 27
+ Null
+
+
+ 131
+ 28
+ Null
+
+
+ 131
+ 29
+ Null
+
+
+ 131
+ 30
+ F15 Flap Retracted
+
+
+ 131
+ 31
+ F15 Flap Down
+
+
+ 132
0
- Caution Panel Oxy_Low Light
+ Left Gear Light
- 173
+ 133
0
- Caution Panel Cabin Press Light
+ Right Gear Light
- 174
+ 134
0
- Test For Bogus Unimplemented Lights
+ ICP Data Command Switch
- 175
+ 135
0
- Instruments Light
+ ICP Drift Cutoff Switch
- 176
+ 136
0
- TWS Launch
+ ICP Nextprev Switch
- 177
+ 137
0
- TWS Priority Mode
+ JFS Switch
- 178
+ 138
0
- TWS Open Mode
+ STARTER READY LIGHT
- 179
+ 138
+ 1
+ Caution JFS LOW
+
+
+ 139
0
- TWS Handoff
+ MPO Switch
- 180
+ 140
0
- TWS Target Separation
+ EPU Light
- 181
+ 141
0
- TWS U
+ EPU Air Light
- 182
+ 142
0
- TWS Naval
+ EPU Hydrazine Light
- 183
+ 143
0
- TWS Unknown
+ Elec FLCS pmg Light
- 184
+ 144
0
- TWS System On
+ Elec Main gen Light
- 185
+ 145
0
- TWS Aux Search
+ Elec Stby gen Light
- 186
+ 146
0
- TWS Aux Activity
+ Elec EPU gen Light
- 187
+ 147
0
- TWS Aux Low
+ Elec EPU pmg Light
- 188
+ 148
0
- TWS Aux System
+ Elec FLCS rly Light
- 189
+ 149
0
- RWS Have Pwr
+ Elec Toflcs Light
- 190
+ 150
0
- ECM Power
+ Elec Bat fail Light
- 191
+ 151
0
- ECM Fail
+ Elec Main Power Switch
- 192
+ 152
0
- TFR Stby
+ EPU Switch
- 193
+ 153
0
- TFR Engaged
+ Caution Panel FLCS Fault Light
- 194
+ 154
0
- AVTR
+ Caution L GEN OUT
- 195
+ 154
+ 1
+ Caution R GEN OUT
+
+
+ 155
0
- Outermarker
+ Caution Panel Probe Heat Light
- 196
+ 156
0
- Middlemarker
+ Caution Panel C ADC Light
- 197
+ 157
0
- CMDS Flare Count Digit 1 - 0
+ Caution Panel Stores Config Light
- 197
+ 158
+ 0
+ Caution EMER BST ON
+
+
+ 158
1
- CMDS Flare Count Digit 1 - 1
+ Caution BST SYS MAL
- 197
+ 158
2
- CMDS Flare Count Digit 1 - 2
+ FuelQtySelector BIT
- 197
+ 158
3
- CMDS Flare Count Digit 1 - 3
+ FuelQtySelector FEED
- 197
+ 158
4
- CMDS Flare Count Digit 1 - 4
+ FuelQtySelector INTLWING
- 197
+ 158
5
- CMDS Flare Count Digit 1 - 5
+ FuelQtySelector TANK1
- 197
+ 158
6
- CMDS Flare Count Digit 1 - 6
+ FuelQtySelector EXTWING
- 197
+ 158
7
- CMDS Flare Count Digit 1 - 7
+ FuelQtySelector FXTCTRL
- 197
+ 158
8
- CMDS Flare Count Digit 1 - 8
+ FuelQtySelector CONFTANK
- 197
+ 158
9
- CMDS Flare Count Digit 1 - 9
+ Fuel Flag OFF
- 198
+ 159
0
- CMDS Flare Count Digit 2 - 0
+ Caution Panel Aft Fuel Low Light
- 198
+ 160
+ 0
+ Caution L EEC
+
+
+ 160
1
- CMDS Flare Count Digit 2 - 1
+ Caution R EEC
- 198
+ 160
2
- CMDS Flare Count Digit 2 - 2
+ Caution L INLET
- 198
+ 160
3
- CMDS Flare Count Digit 2 - 3
+ Caution R INLET
- 198
+ 160
4
- CMDS Flare Count Digit 2 - 4
+ Caution R INLET
- 198
+ 160
5
- CMDS Flare Count Digit 2 - 5
+ Caution R INLET
- 198
- 6
- CMDS Flare Count Digit 2 - 6
-
+ 161
+ 0
+ Caution Panel Sec Light
+
- 198
- 7
- CMDS Flare Count Digit 2 - 7
+ 162
+ 0
+ Caution Panel Fuel Oil Hot Light
- 198
- 8
- CMDS Flare Count Digit 2 - 8
+ 163
+ 0
+ Caution Panel Overheat Light
- 198
- 9
- CMDS Flare Count Digit 2 - 9
+ 164
+ 0
+ Caution Panel Buc Light
- 199
+ 165
0
- CMDS Flare Count Digit 3 - 0
+ Caution Panel Avionics Fault Light
- 199
- 1
- CMDS Flare Count Digit 3 - 1
+ 166
+ 0
+ Caution Panel Radar Alt Light
- 199
- 2
- CMDS Flare Count Digit 3 - 2
+ 167
+ 0
+ Caution Panel IFF Light
- 199
- 3
- CMDS Flare Count Digit 3 - 3
+ 168
+ 0
+ Caution Panel Seat Not Armed Light
- 199
- 4
- CMDS Flare Count Digit 3 - 4
+ 169
+ 0
+ Caution Panel NWS Fail Light
- 199
- 5
- CMDS Flare Count Digit 3 - 5
+ 170
+ 0
+ Caution ANTI SKID
- 199
- 6
- CMDS Flare Count Digit 3 - 6
+ 171
+ 0
+ Caution Panel Hook Light
- 199
- 7
- CMDS Flare Count Digit 3 - 7
+ 172
+ 0
+ Caution Panel Oxy_Low Light
- 199
- 8
- CMDS Flare Count Digit 3 - 8
+ 173
+ 0
+ Caution Panel Cabin Press Light
- 199
- 9
- CMDS Flare Count Digit 3 - 9
+ 174
+ 0
+ Test For Bogus Unimplemented Lights
- 200
+ 175
0
- CMDS Chaff Count Digit 1 - 0
+ Standby Instruments Light 0
- 200
+ 175
1
- CMDS Chaff Count Digit 1 - 1
+ Standby Instruments Light 1
- 200
+ 175
2
- CMDS Chaff Count Digit 1 - 2
+ Lights Test 0
- 200
+ 175
3
- CMDS Chaff Count Digit 1 - 3
+ Lights Test 1
- 200
+ 175
4
- CMDS Chaff Count Digit 1 - 4
+ LeftConsoleLight
- 200
+ 175
5
- CMDS Chaff Count Digit 1 - 5
+ Null
- 200
+ 175
6
- CMDS Chaff Count Digit 1 - 6
+ Null
- 200
+ 175
7
- CMDS Chaff Count Digit 1 - 7
+ Null
- 200
+ 175
8
- CMDS Chaff Count Digit 1 - 8
+ Null
- 200
+ 175
9
- CMDS Chaff Count Digit 1 - 9
+ RightConsoleLight
- 201
- 0
- CMDS Chaff Count Digit 2 - 0
+ 175
+ 10
+ F15 NCI RDY Light
- 201
- 1
- CMDS Chaff Count Digit 2 - 1
+ 175
+ 11
+ Null
- 201
- 2
- CMDS Chaff Count Digit 2 - 2
+ 175
+ 12
+ Null
- 201
- 3
- CMDS Chaff Count Digit 2 - 3
+ 175
+ 13
+ Null
- 201
- 4
- CMDS Chaff Count Digit 2 - 4
+ 175
+ 14
+ AuxInstr.Light
- 201
- 5
- CMDS Chaff Count Digit 2 - 5
+ 175
+ 15
+ Null
- 201
- 6
- CMDS Chaff Count Digit 2 - 6
+ 175
+ 16
+ Null
- 201
- 7
- CMDS Chaff Count Digit 2 - 7
+ 175
+ 17
+ Null
- 201
- 8
- CMDS Chaff Count Digit 2 - 8
+ 175
+ 18
+ Null
- 201
- 9
- CMDS Chaff Count Digit 2 - 9
+ 175
+ 19
+ FlightInstr.Light
- 202
- 0
- CMDS Chaff Count Digit 3 - 0
+ 175
+ 20
+ Null
- 202
- 1
- CMDS Chaff Count Digit 3 - 1
+ 175
+ 21
+ Null
- 202
- 2
- CMDS Chaff Count Digit 3 - 2
+ 175
+ 22
+ Null
- 202
- 3
- CMDS Chaff Count Digit 3 - 3
+ 175
+ 23
+ Null
- 202
- 4
- CMDS Chaff Count Digit 3 - 4
+ 175
+ 24
+ EngineInstr.Light
- 202
- 5
- CMDS Chaff Count Digit 3 - 5
+ 175
+ 25
+ Null
- 202
- 6
- CMDS Chaff Count Digit 3 - 6
+ 175
+ 26
+ Null
- 202
- 7
- CMDS Chaff Count Digit 3 - 7
+ 175
+ 27
+ Null
- 202
- 8
- CMDS Chaff Count Digit 3 - 8
+ 175
+ 28
+ Null
- 202
- 9
- CMDS Chaff Count Digit 3 - 9
+ 175
+ 29
+ Stanby Instruments
+
- 203
+ 176
0
- CMDS Go
+ TWS Launch
- 204
+ 177
0
- CMDS Nogo
+ TWS Priority Mode
- 205
+ 178
0
- CMDS Dispense Rdy
+ TWS Open Mode
- 206
+ 179
0
- CMDS Auto Degr
+ TWS Handoff
- 207
+ 180
0
- CMDS Flare Bingo
+ TWS Target Separation
- 208
+ 181
0
- CMDS Chaff Bingo
+ TWS U
- 209
+ 182
0
- DL Power Down
+ TWS Naval
- 209
- 1
- DL Power Up
+ 183
+ 0
+ TWS Unknown
- 209
- 2
- GPS Power Down
+ 184
+ 0
+ TWS System On
- 209
- 3
- GPS Power Up
+ 185
+ 0
+ TWS Aux Search
- 209
- 4
+ 186
+ 0
+ TWS Aux Activity
+
+
+ 187
+ 0
+ TWS Aux Low
+
+
+ 188
+ 0
+ TWS Aux System
+
+
+ 189
+ 0
+ RWS Have Pwr
+
+
+ 190
+ 0
+ ECM Power
+
+
+ 191
+ 0
+ ECM Fail
+
+
+ 192
+ 0
+ TFR Stby
+
+
+ 193
+ 0
+ TFR Engaged
+
+
+ 194
+ 0
+ AVTR
+
+
+ 195
+ 0
+ Outermarker
+
+
+ 196
+ 0
+ Middlemarker
+
+
+ 197
+ 0
+ CMDS Flare Count Digit 1 - 0
+
+
+ 197
+ 1
+ CMDS Flare Count Digit 1 - 1
+
+
+ 197
+ 2
+ CMDS Flare Count Digit 1 - 2
+
+
+ 197
+ 3
+ CMDS Flare Count Digit 1 - 3
+
+
+ 197
+ 4
+ CMDS Flare Count Digit 1 - 4
+
+
+ 197
+ 5
+ CMDS Flare Count Digit 1 - 5
+
+
+ 197
+ 6
+ CMDS Flare Count Digit 1 - 6
+
+
+ 197
+ 7
+ CMDS Flare Count Digit 1 - 7
+
+
+ 197
+ 8
+ CMDS Flare Count Digit 1 - 8
+
+
+ 197
+ 9
+ CMDS Flare Count Digit 1 - 9
+
+
+ 198
+ 0
+ CMDS Flare Count Digit 2 - 0
+
+
+ 198
+ 1
+ CMDS Flare Count Digit 2 - 1
+
+
+ 198
+ 2
+ CMDS Flare Count Digit 2 - 2
+
+
+ 198
+ 3
+ CMDS Flare Count Digit 2 - 3
+
+
+ 198
+ 4
+ CMDS Flare Count Digit 2 - 4
+
+
+ 198
+ 5
+ CMDS Flare Count Digit 2 - 5
+
+
+ 198
+ 6
+ CMDS Flare Count Digit 2 - 6
+
+
+ 198
+ 7
+ CMDS Flare Count Digit 2 - 7
+
+
+ 198
+ 8
+ CMDS Flare Count Digit 2 - 8
+
+
+ 198
+ 9
+ CMDS Flare Count Digit 2 - 9
+
+
+ 199
+ 0
+ CMDS Flare Count Digit 3 - 0
+
+
+ 199
+ 1
+ CMDS Flare Count Digit 3 - 1
+
+
+ 199
+ 2
+ CMDS Flare Count Digit 3 - 2
+
+
+ 199
+ 3
+ CMDS Flare Count Digit 3 - 3
+
+
+ 199
+ 4
+ CMDS Flare Count Digit 3 - 4
+
+
+ 199
+ 5
+ CMDS Flare Count Digit 3 - 5
+
+
+ 199
+ 6
+ CMDS Flare Count Digit 3 - 6
+
+
+ 199
+ 7
+ CMDS Flare Count Digit 3 - 7
+
+
+ 199
+ 8
+ CMDS Flare Count Digit 3 - 8
+
+
+ 199
+ 9
+ CMDS Flare Count Digit 3 - 9
+
+
+ 200
+ 0
+ CMDS Chaff Count Digit 1 - 0
+
+
+ 200
+ 1
+ CMDS Chaff Count Digit 1 - 1
+
+
+ 200
+ 2
+ CMDS Chaff Count Digit 1 - 2
+
+
+ 200
+ 3
+ CMDS Chaff Count Digit 1 - 3
+
+
+ 200
+ 4
+ CMDS Chaff Count Digit 1 - 4
+
+
+ 200
+ 5
+ CMDS Chaff Count Digit 1 - 5
+
+
+ 200
+ 6
+ CMDS Chaff Count Digit 1 - 6
+
+
+ 200
+ 7
+ CMDS Chaff Count Digit 1 - 7
+
+
+ 200
+ 8
+ CMDS Chaff Count Digit 1 - 8
+
+
+ 200
+ 9
+ CMDS Chaff Count Digit 1 - 9
+
+
+ 201
+ 0
+ CMDS Chaff Count Digit 2 - 0
+
+
+ 201
+ 1
+ CMDS Chaff Count Digit 2 - 1
+
+
+ 201
+ 2
+ CMDS Chaff Count Digit 2 - 2
+
+
+ 201
+ 3
+ CMDS Chaff Count Digit 2 - 3
+
+
+ 201
+ 4
+ CMDS Chaff Count Digit 2 - 4
+
+
+ 201
+ 5
+ CMDS Chaff Count Digit 2 - 5
+
+
+ 201
+ 6
+ CMDS Chaff Count Digit 2 - 6
+
+
+ 201
+ 7
+ CMDS Chaff Count Digit 2 - 7
+
+
+ 201
+ 8
+ CMDS Chaff Count Digit 2 - 8
+
+
+ 201
+ 9
+ CMDS Chaff Count Digit 2 - 9
+
+
+ 202
+ 0
+ CMDS Chaff Count Digit 3 - 0
+
+
+ 202
+ 1
+ CMDS Chaff Count Digit 3 - 1
+
+
+ 202
+ 2
+ CMDS Chaff Count Digit 3 - 2
+
+
+ 202
+ 3
+ CMDS Chaff Count Digit 3 - 3
+
+
+ 202
+ 4
+ CMDS Chaff Count Digit 3 - 4
+
+
+ 202
+ 5
+ CMDS Chaff Count Digit 3 - 5
+
+
+ 202
+ 6
+ CMDS Chaff Count Digit 3 - 6
+
+
+ 202
+ 7
+ CMDS Chaff Count Digit 3 - 7
+
+
+ 202
+ 8
+ CMDS Chaff Count Digit 3 - 8
+
+
+ 202
+ 9
+ CMDS Chaff Count Digit 3 - 9
+
+
+ 203
+ 0
+ CMDS Go
+
+
+ 204
+ 0
+ CMDS Nogo
+
+
+ 205
+ 0
+ CMDS Dispense Rdy
+
+
+ 206
+ 0
+ Oxygene green OFF
+
+
+ 206
+ 1
+ Oxygene green ON
+
+
+ 207
+ 0
+ CMDS Flare Bingo
+
+
+ 208
+ 0
+ CMDS Chaff Bingo
+
+
+ 209
+ 0
+ DL Power Down
+
+
+ 209
+ 1
+ DL Power Up
+
+
+ 209
+ 2
+ GPS Power Down
+
+
+ 209
+ 3
+ GPS Power Up
+
+
+ 209
+ 4
UFC Power Down
- 209
+ 209
+ 5
+ UFC Power Up
+
+
+ 209
+ 6
+ MFD Power Down
+
+
+ 209
+ 7
+ MFD Power Up
+
+
+ 209
+ 8
+ SMS Power Down
+
+
+ 209
+ 9
+ SMS Power Up
+
+
+ 209
+ 10
+ FCC Power Down
+
+
+ 209
+ 11
+ FCC Power Up
+
+
+ 209
+ 12
+ Left HP Power Down
+
+
+ 209
+ 13
+ Left HP Power Up
+
+
+ 209
+ 14
+ Right HP Power Down
+
+
+ 209
+ 15
+ Right HP Power Up
+
+
+ 209
+ 16
+ FCR Power Down
+
+
+ 209
+ 17
+ FCR Power Up
+
+
+ 209
+ 18
+ Yaw off
+
+
+ 209
+ 19
+ Yaw reset
+
+
+ 209
+ 20
+ Yaw on
+
+
+ 209
+ 21
+ Roll off
+
+
+ 209
+ 22
+ Roll reset
+
+
+ 209
+ 23
+ Roll on
+
+
+ 209
+ 24
+ Pitch Off
+
+
+ 209
+ 25
+ Pitch reset
+
+
+ 209
+ 26
+ Pitch On
+
+
+ 209
+ 27
+ T/O not pressed
+
+
+ 209
+ 28
+ T/O pressed
+
+
+ 209
+ 29
+ T/O light
+
+
+ 209
+ 30
+ AP Disc Down
+
+
+ 209
+ 31
+ AP Disc Up
+
+
+ 210
+ 0
+ Tank Inserting 0
+
+
+ 210
+ 1
+ Tank Inserting 1
+
+
+ 210
+ 2
+ Refuel Door 0
+
+
+ 210
+ 3
+ Refuel Door 1
+
+
+ 210
+ 4
+ Aux Comm TR AA 0
+
+
+ 210
+ 5
+ Aux Comm TR AA 1
+
+
+ 210
+ 6
+ Anti Collision 0
+
+
+ 210
+ 7
+ Anti Collision 1
+
+
+ 210
+ 8
+ Flash 0
+
+
+ 210
+ 9
+ Flash 1
+
+
+ 210
+ 10
+ Wing Left/Right 0
+
+
+ 210
+ 11
+ Wing Left/Right 1
+
+
+ 210
+ 12
+ Light Master Power 0
+
+
+ 210
+ 13
+ Light Master Power 1
+
+
+ 210
+ 14
+ ECM Power 0
+
+
+ 210
+ 15
+ ECM Power 1
+
+
+ 210
+ 16
+ Max Power 0
+
+
+ 210
+ 17
+ Max Power 1
+
+
+ 210
+ 18
+ MWS Off
+
+
+ 210
+ 19
+ MWS On
+
+
+ 210
+ 20
+ Jammer Off
+
+
+ 210
+ 21
+ Jammer On
+
+
+ 210
+ 22
+ RWR Off
+
+
+ 210
+ 23
+ RWR On
+
+
+ 210
+ 24
+ 01 CMDS 0
+
+
+ 210
+ 25
+ 01 CMDS 1
+
+
+ 210
+ 26
+ 02 CMDS 0
+
+
+ 210
+ 27
+ 02 CMDS 2
+
+
+ 210
+ 28
+ Chaff 0
+
+
+ 210
+ 29
+ Chaff 1
+
+
+ 210
+ 30
+ Flare 0
+
+
+ 210
+ 31
+ Flare 1
+
+
+ 211
+ 0
+ Jettison CMDS Off
+
+
+ 211
+ 1
+ Jettison CMDS On
+
+
+ 211
+ 2
+ Cat Off
+
+
+ 211
+ 3
+ Cat On
+
+
+ 211
+ 4
+ Ground Jettison Off
+
+
+ 211
+ 5
+ Ground Jettison On
+
+
+ 211
+ 6
+ Brake Channel Off
+
+
+ 211
+ 7
+ Brake Channel On
+
+
+ 211
+ 8
+
+
+ 211
+ 9
+
+
+ 211
+ 10
+ Laser Arm Off
+
+
+ 211
+ 11
+ Laser Arm On
+
+
+ 211
+ 12
+ Fuel Wing First Off
+
+
+ 211
+ 13
+ Fuel Wing First On
+
+
+ 211
+ 14
+ MPO Off
+
+
+ 211
+ 15
+ MPO On
+
+
+ 211
+ 16
+
+
+ 211
+ 17
+
+
+ 211
+ 18
+ JSF Off
+
+
+ 211
+ 19
+ JSF On
+
+
+ 211
+ 20
+ IFF Monitor Off
+
+
+ 211
+ 21
+ IFF Monitor On
+
+
+ 212
+ 0
+ Alt Radar Power 0
+
+
+ 212
+ 1
+ Alt Radar Power 1
+
+
+ 212
+ 2
+ Alt Radar Power 2
+
+
+ 212
+ 3
+ Depr Ret 0
+
+
+ 212
+ 4
+ Depr Ret 1
+
+
+ 212
+ 5
+ Depr Ret 2
+
+
+ 212
+ 6
+ DED Data 0
+
+
+ 212
+ 7
+ DED Data 1
+
+
+ 212
+ 8
+ DED Data 2
+
+
+ 212
+ 9
+ HUD Att Fpm 0
+
+
+ 212
+ 10
+ HUD Att Fpm 1
+
+
+ 212
+ 11
+ HUD Att Fpm 2
+
+
+ 212
+ 12
+ HUD Vah Fpm 0
+
+
+ 212
+ 13
+ HUD Vah Fpm 1
+
+
+ 212
+ 14
+ HUD Vah Fpm 2
+
+
+ 212
+ 15
+ Test Step 0
+
+
+ 212
+ 16
+ Test Step 1
+
+
+ 212
+ 17
+ Test Step 2
+
+
+ 212
+ 18
+ HUD Bright 0
+
+
+ 212
+ 19
+ HUD Bright 1
+
+
+ 212
+ 20
+ HUD Bright 2
+
+
+ 212
+ 21
+ HUD Alt Radar 0
+
+
+ 212
+ 22
+ HUD Alt Radar 1
+
+
+ 212
+ 23
+ HUD Alt Radar 2
+
+
+ 212
+ 24
+ HUD Speed 0
+
+
+ 212
+ 25
+ HUD Speed 1
+
+
+ 212
+ 26
+ HUD Speed 2
+
+
+ 212
+ 27
+ Anti Ice 0
+
+
+ 212
+ 28
+ Anti Ice 1
+
+
+ 212
+ 29
+ Anti Ice 2
+
+
+ 213
+ 0
+ IFF Mode4
+ ZERO
+
+
+ 213
+ 1
+ IFF Mode4
+ NORM
+
+
+ 213
+ 2
+ IFF Mode4
+ HOLD
+
+
+ 213
+ 3
+ IFF mode4
+ OUT
+
+
+ 213
+ 4
+ IFF mode4
+ A
+
+
+ 213
+ 5
+ IFF mode4
+ B
+
+
+ 213
+ 6
+ F15 VIDEO RECORD OFF
+ IFF Master EMERG
+
+
+ 213
+ 7
+ F15 VIDEO RECORD AUTO
+ IFF Master NORM
+
+
+ 213
+ 8
+ F15 VIDEO RECORD HUD
+ IFF Master LOW
+
+
+ 213
+ 9
+ IFF MC
+ OUT
+
+
+ 213
+ 10
+ IFF MC
+ ON
+
+
+ 213
+ 11
+ IFF M3/A
+ OUT
+
+
+ 213
+ 12
+ IFF M3/A
+ ON
+
+
+ 213
+ 13
+ IFF M2
+ OUT
+
+
+ 213
+ 14
+ IFF M2
+ ON
+
+
+ 213
+ 15
+ IFF M1
+ OUT
+
+
+ 213
+ 16
+ IFF M1
+ ON
+
+
+ 213
+ 17
+ IFF REPLY
+ LIGHT BRIGHT
+
+
+ 213
+ 18
+ IFF REPLY
+ OFF
+
+
+ 213
+ 19
+ IFF REPLY
+ AUDIO REC
+
+
+ 213
+ 20
+ IFF REPLY
+ LIGHT SWITCH
+
+
+ 213
+ 21
+ ATT Hold Off
+
+
+ 213
+ 22
+ ATT hold On
+
+
+ 213
+ 23
+ Null
+
+
+ 213
+ 24
+ ALT Hold Off
+
+
+ 213
+ 25
+ Null
+
+
+ 213
+ 26
+ ALT Hold On
+
+
+ 213
+ 27
+ Master Arm 0
+
+
+ 213
+ 28
+ Master Arm 1
+
+
+ 213
+ 29
+ Master Arm 2
+
+
+ 214
+ 0
+ RF 0
+
+
+ 214
+ 1
+ RF 1
+
+
+ 214
+ 2
+ RF 2
+
+
+ 214
+ 3
+ C/O Drift 0
+
+
+ 214
+ 4
+ C/O Drift 1
+
+
+ 214
+ 5
+ C/O Drift 2
+
+
+ 214
+ 6
+ FLIR Gain 0
+
+
+ 214
+ 7
+ FLIR Gain 1
+
+
+ 214
+ 8
+ FLIR Gain 2
+
+
+ 214
+ 9
+ Test 0
+
+
+ 214
+ 10
+ Test 1
+
+
+ 214
+ 11
+ Test 2
+
+
+ 214
+ 12
+ EPU 0
+
+
+ 214
+ 13
+ EPU 1
+
+
+ 214
+ 14
+ EPU 2
+
+
+ 214
+ 15
+ Zeroize 0
+
+
+ 214
+ 16
+ Zeroize 1
+
+
+ 214
+ 17
+ Zeroize 2
+
+
+ 214
+ 18
+ Nuclear Consent 0
+
+
+ 214
+ 19
+ Nuclear Consent 1
+
+
+ 214
+ 20
+ Nuclear Consent 2
+
+
+ 214
+ 21
+ UHF Main 0
+
+
+ 214
+ 22
+ UHF Main 1
+
+
+ 214
+ 23
+ UHF Main 2
+
+
+ 214
+ 24
+ Probe Heat 0
+
+
+ 214
+ 25
+ Probe Heat 1
+
+
+ 214
+ 26
+ Probe Heat 2
+
+
+ 214
+ 27
+ IFF Reply Off
+
+
+ 214
+ 28
+ IFF Reply Alpha
+
+
+ 214
+ 29
+ IFF Reply Bravo
+
+
+ 215
+ 0
+ AAI MASTER OFF
+
+
+ 215
+ 1
+ AAI MASTER AUTO
+
+
+ 215
+ 2
+ AAI MASTER NORM
+
+
+ 215
+ 3
+ AAI MASTER CC
+
+
+ 215
+ 4
+ AAI MASTER XCC
+
+
+ 215
+ 5
+ AAI MASTER XNORM
+
+
+ 216
+ 0
+ Caution EECS
+
+
+ 216
+ 1
+ Caution WNDSHLD HOT
+
+
+ 216
+ 2
+ Caution FUEL HOT
+
+
+ 217
+ 0
+ Oxy Quantity 0
+
+
+ 217
+ 1
+ Oxy Quantity 1
+
+
+ 217
+ 2
+ EPU/Gen 0
+
+
+ 217
+ 3
+ EPU/Gen 1
+
+
+ 217
+ 4
+ Master Fuel 0
+
+
+ 217
+ 5
+ Master Fuel 1
+
+
+ 217
+ 6
+ Eng Count 0 (Pri)
+
+
+ 217
+ 7
+ Eng Count 1 (Sec)
+
+
+ 217
+ 8
+ Voice Message Inhibit 0
+
+
+ 217
+ 9
+ Voice Message Inhibit 1
+
+
+ 217
+ 10
+ Plain 0
+
+
+ 217
+ 11
+ Plain 1
+
+
+ 217
+ 12
+ Squelch 0
+
+
+ 217
+ 13
+ Squelch 1
+
+
+ 217
+ 14
+ Mal & Int 0
+
+
+ 217
+ 15
+ Mal & Int 1
+
+
+ 217
+ 16
+ Ext Light Form 0
+
+
+ 217
+ 17
+ Ext Light Form 1
+
+
+ 217
+ 18
+ Ext Light Aerial Refuel 0
+
+
+ 217
+ 19
+ Ext Light Aerial Refuel 1
+
+
+ 217
+ 20
+ CNI Backup 0
+
+
+ 217
+ 21
+ CNI Backup 1
+
+
+ 217
+ 22
+ Audio Intercom 0
+
+
+ 217
+ 23
+ Audio Intercom 1
+
+
+ 217
+ 24
+ Audio Tacan 0
+
+
+ 217
+ 25
+ Audio Tacan 1
+
+
+ 217
+ 26
+ Hook Down
+
+
+ 217
+ 27
+ Hook Up
+
+
+ 217
+ 28
+ UHF Preset 0
+
+
+ 217
+ 29
+ UHF Preset 1
+
+
+ 217
+ 30
+ UHF Preset 2
+
+
+ 218
+ 0
+ Audio ILS 0
+
+
+ 218
+ 1
+ Audio ILS 1
+
+
+ 218
+ 2
+ Comm 1 Sel 0
+
+
+ 218
+ 3
+ Comm 1 Sel 1
+
+
+ 218
+ 4
+ Comm 2 Sel 0
+
+
+ 218
+ 5
+ Comm 2 Sel 1
+
+
+ 218
+ 6
+ Tf 0
+
+
+ 218
+ 7
+ Tf 1
+
+
+ 218
+ 8
+ Voice Secure 0
+
+
+ 218
+ 9
+ Voice Secure 1
+
+
+ 218
+ 10
+ Horn Silencer Up
+
+
+ 218
+ 11
+ Horn Silencer Down
+
+
+ 218
+ 12
+ Fire & Overheat detect Up
+
+
+ 218
+ 13
+ Fire & Overheat detect Down
+
+
+ 218
+ 14
+ Mal & Ind LTS Up
+
+
+ 218
+ 15
+ Mal & Ind LTS Down
+
+
+ 218
+ 16
+
+
+ 218
+ 17
+
+
+ 218
+ 18
+ Launch Bar 0
+
+
+ 218
+ 19
+ Launch Bar 1
+
+
+ 219
+ 0
+ T23A Display Select 0
+
+
+ 219
+ 1
+ T23A Display Select 1
+
+
+ 219
+ 2
+ T23A Display Select 2
+
+
+ 219
+ 3
+ T23A Display Select 3
+
+
+ 219
+ 4
+ Air Source 0
+
+
+ 219
+ 5
+ Air Source 1
+
+
+ 219
+ 6
+ Air Source 2
+
+
+ 219
+ 7
+ Air Source 3
+
+
+ 219
+ 8
+ HSI Instr Mode 0
+
+
+ 219
+ 9
+ HSI Instr Mode 1
+
+
+ 219
+ 10
+ HSI Instr Mode 2
+
+
+ 219
+ 11
+ HSI Instr Mode 3
+
+
+ 219
+ 12
+ Avionics Power Ins 0
+
+
+ 219
+ 13
+ Avionics Power Ins 1
+
+
+ 219
+ 14
+ Avionics Power Ins 2
+
+
+ 219
+ 15
+ Avionics Power Ins 3
+
+
+ 219
+ 16
+ Eng Feed 0
+
+
+ 219
+ 17
+ Eng Feed 1
+
+
+ 219
+ 18
+ Eng Feed 2
+
+
+ 219
+ 19
+ Eng Feed 3
+
+
+ 219
+ 20
+ CMDS Prog 0
+
+
+ 219
+ 21
+ CMDS Prog 1
+
+
+ 219
+ 22
+ CMDS Prog 2
+
+
+ 219
+ 23
+ CMDS Prog 3
+
+
+ 219
+ 24
+ Manual Freq 1st Digit 0
+
+
+ 219
+ 25
+ Manual Freq 1st Digit 1
+
+
+ 219
+ 26
+ Manual Freq 1st Digit 2
+
+
+ 219
+ 27
+ Manual Freq 1st Digit 3
+
+
+ 219
+ 28
+ IFF M1 Digit 2 0
+ SimIFFBackupM1Digit2_0
+
+
+ 219
+ 29
+ IFF M1 Digit 2 1
+ SimIFFBackupM1Digit2_1
+
+
+ 219
+ 30
+ IFF M1 Digit 2 2
+ SimIFFBackupM1Digit2_2
+
+
+ 219
+ 31
+ IFF M1 Digit 2 3
+ SimIFFBackupM1Digit2_3
+
+
+ 220
+ 0
+ IFF Master 0
+ SimIFFMasterOff
+
+
+ 220
+ 1
+ IFF Master 1
+ SimIFFMasterStby
+
+
+ 220
+ 2
+ IFF Master 2
+ SimIFFMasterLow
+
+
+ 220
+ 3
+ IFF Master 3
+ SimIFFMasterNorm
+
+
+ 220
+ 4
+ IFF Master 4
+ SimIFFMasterEmerg
+
+
+ 220
+ 5
+ HMCS Power 0 (Off)
+
+
+ 220
+ 6
+ HMCS Power 1
+
+
+ 220
+ 7
+ HMCS Power 2
+
+
+ 220
+ 8
+ HMCS Power 3
+
+
+ 220
+ 9
+ HMCS Power 4
+
+
+ 221
+ 0
+ CMDS Mode 0
+
+
+ 221
+ 1
+ CMDS Mode 1
+
+
+ 221
+ 2
+ CMDS Mode 2
+
+
+ 221
+ 3
+ CMDS Mode 3
+
+
+ 221
+ 4
+ CMDS Mode 4
+
+
+ 221
5
- UFC Power Up
+ CMDS Mode 5
- 209
+ 221
6
- MFD Power Down
+ Fuel Qty Sel 0
- 209
+ 221
7
- MFD Power Up
+ Fuel Qty Sel 1
- 209
+ 221
8
- SMS Power Down
+ Fuel Qty Sel 2
- 209
+ 221
9
- SMS Power Up
+ Fuel Qty Sel 3
- 209
+ 221
10
- FCC Power Down
+ Fuel Qty Sel 4
- 209
+ 221
11
- FCC Power Up
+ Fuel Qty Sel 5
- 209
+ 222
+ 0
+ UHF Volume 0
+
+
+ 222
+ 1
+ UHF Volume 1
+
+
+ 222
+ 2
+ UHF Volume 2
+
+
+ 222
+ 3
+ UHF Volume 3
+
+
+ 222
+ 4
+ UHF Volume 4
+
+
+ 222
+ 5
+ UHF Volume 5
+
+
+ 222
+ 6
+ UHF Volume 6
+
+
+ 222
+ 7
+
+
+ 222
+ 8
+ IFF M1 Digit 1 0
+ SimIFFBackupM1Digit1_0
+
+
+ 222
+ 9
+ IFF M1 Digit 1 1
+ SimIFFBackupM1Digit1_1
+
+
+ 222
+ 10
+ IFF M1 Digit 1 2
+ SimIFFBackupM1Digit1_2
+
+
+ 222
+ 11
+ IFF M1 Digit 1 3
+ SimIFFBackupM1Digit1_3
+
+
+ 222
12
- Left HP Power Down
+ IFF M1 Digit 1 4
+ SimIFFBackupM1Digit1_4
- 209
+ 222
13
- Left HP Power Up
+ IFF M1 Digit 1 5
+ SimIFFBackupM1Digit1_5
- 209
+ 222
14
- Right HP Power Down
+ IFF M1 Digit 1 6
+ SimIFFBackupM1Digit1_6
- 209
+ 222
15
- Right HP Power Up
+ IFF M1 Digit 1 7
+ SimIFFBackupM1Digit1_7
- 209
+ 222
16
- FCR Power Down
+ IFF M3 Digit 1 0
+ SimIFFBackupM3Digit1_0
- 209
+ 222
17
- FCR Power Up
+ IFF M3 Digit 1 1
+ SimIFFBackupM3Digit1_1
- 209
+ 222
18
- Digital Backup Down
+ IFF M3 Digit 1 2
+ SimIFFBackupM3Digit1_2
- 209
+ 222
19
- Digital Backup Up
+ IFF M3 Digit 1 3
+ SimIFFBackupM3Digit1_3
- 209
+ 222
20
- Alt Flap Down
+ IFF M3 Digit 1 4
+ SimIFFBackupM3Digit1_4
- 209
+ 222
21
- Alt Flap Up
+ IFF M3 Digit 1 5
+ SimIFFBackupM3Digit1_5
- 209
+ 222
22
- FLCS Rest Down
+ IFF M3 Digit 1 6
+ SimIFFBackupM3Digit1_6
- 209
+ 222
23
- FLCS Rest Up
+ IFF M3 Digit 1 7
+ SimIFFBackupM3Digit1_7
- 209
+ 222
24
- LE Flap Lock Down
+ IFF M3 Digit 2 0
+ SimIFFBackupM3Digit2_0
- 209
+ 222
25
- LE Flap Lock Up
+ IFF M3 Digit 2 1
+ SimIFFBackupM3Digit2_1
- 209
+ 222
26
- Manual Tf Down
+ IFF M3 Digit 2 2
+ SimIFFBackupM3Digit2_2
- 209
+ 222
27
- Manual Tf Up
+ IFF M3 Digit 2 3
+ SimIFFBackupM3Digit2_3
- 209
+ 222
28
- Bit Down
+ IFF M3 Digit 2 4
+ SimIFFBackupM3Digit2_4
- 209
+ 222
29
- Bit Up
+ IFF M3 Digit 2 5
+ SimIFFBackupM3Digit2_5
- 209
+ 222
30
- AP Disc Down
+ IFF M3 Digit 2 6
+ SimIFFBackupM3Digit2_6
- 209
+ 222
31
- AP Disc Up
+ IFF M3 Digit 2 7
+ SimIFFBackupM3Digit2_7
- 210
+ 223
0
- Tank Inserting 0
+ Pull 0
- 210
+ 223
1
- Tank Inserting 1
+ Pull 1
- 210
+ 223
2
- Refuel Door 0
+ Pull 2
- 210
+ 223
3
- Refuel Door 1
+ Pull 3
- 210
+ 223
4
- Aux Comm TR AA 0
+ Pull 4
- 210
+ 223
5
- Aux Comm TR AA 1
+ Pull 5
- 210
+ 223
6
- Anti Collision 0
+ Pull 6
- 210
+ 223
7
- Anti Collision 1
+ Pull 7
- 210
+ 223
8
- Flash 0
+ HSI Crs Select 0
+ Shows the knob turning
- 210
+ 223
9
- Flash 1
+ HSI Crs Select 1
- 210
+ 223
10
- Unused
+ HSI Crs Select 2
- 210
+ 223
11
- Unused
+ HSI Crs Select 3
- 210
+ 223
12
- Light Master Power 0
+ HSI Crs Select 4
- 210
+ 223
13
- Light Master Power 1
+ HSI Crs Select 5
- 210
+ 223
14
- ECM Power 0
+ HSI Crs Select 6
- 210
+ 223
15
- ECM Power 1
+ HSI Crs Select 7
- 210
+ 223
16
- Max Power 0
+ HSI Hdg Select 0
+ Shows the knob turning
- 210
+ 223
17
- Max Power 1
+ HSI Hdg Select 1
- 210
+ 223
18
- MWS Off
+ HSI Hdg Select 2
- 210
+ 223
19
- MWS On
+ HSI Hdg Select 3
- 210
+ 223
20
- Jammer Off
+ HSI Hdg Select 4
- 210
+ 223
21
- Jammer On
+ HSI Hdg Select 5
- 210
+ 223
22
- RWR Off
+ HSI Hdg Select 6
- 210
+ 223
23
- RWR On
-
-
- 210
- 24
- 01 CMDS 0
-
-
- 210
- 25
- 01 CMDS 1
-
-
- 210
- 26
- 02 CMDS 0
-
-
- 210
- 27
- 02 CMDS 2
-
-
- 210
- 28
- Chaff 0
-
-
- 210
- 29
- Chaff 1
-
-
- 210
- 30
- Flare 0
-
-
- 210
- 31
- Flare 1
+ HSI Hdg Select 7
- 211
+ 224
0
- Jettison CMDS Off
+ Threat Warn Vol 0
- 211
+ 224
1
- Jettison CMDS On
+ Threat Warn Vol 1
- 211
+ 224
2
- Cat Off
+ Threat Warn Vol 2
- 211
+ 224
3
- Cat On
+ Threat Warn Vol 3
- 211
+ 224
4
- Ground Jettison Off
+ Threat Warn Vol 4
- 211
+ 224
5
- Ground Jettison On
+ Threat Warn Vol 5
- 211
+ 224
6
- Brake Channel Off
+ Threat Warn Vol 6
- 211
+ 224
7
- Brake Channel On
+ Threat Warn Vol 7
- 211
+ 224
8
+ Threat Warn Vol 8
- 211
+ 224
9
+ Missile Warn Vol 0
- 211
+ 224
10
- Laser Arm Off
+ Missile Warn Vol 1
- 211
+ 224
11
- Laser Arm On
+ Missile Warn Vol 2
- 211
+ 224
12
- Fuel Wing First Off
+ Missile Warn Vol 3
- 211
+ 224
13
- Fuel Wing First On
+ Missile Warn Vol 4
- 211
+ 224
14
- MPO Off
+ Missile Warn Vol 5
- 211
+ 224
15
- MPO On
+ Missile Warn Vol 6
- 211
+ 224
16
+ Missile Warn Vol 7
- 211
+ 224
17
+ Missile Warn Vol 8
- 211
+ 224
18
- JSF Off
+ Intercom Vol 0
- 211
+ 224
19
- JSF On
-
+ Intercom Vol 1
+
- 211
+ 224
20
- IFF Monitor Off
-
+ Intercom Vol 2
+
- 211
+ 224
21
- IFF Monitor On
+ Intercom Vol 3
+
+
+ 224
+ 22
+ Intercom Vol 4
+
+
+ 224
+ 23
+ Intercom Vol 5
+
+
+ 224
+ 24
+ Intercom Vol 6
- 212
+ 224
+ 25
+ Intercom Vol 7
+
+
+ 224
+ 26
+ Intercom Vol 8
+
+
+ 225
0
- Alt Radar Power 0
+ Comm1 Volume 0
- 212
+ 225
1
- Alt Radar Power 1
+ Comm1 Volume 1
- 212
+ 225
2
- Alt Radar Power 2
+ Comm1 Volume 2
- 212
+ 225
3
- Depr Ret 0
+ Comm1 Volume 3
- 212
+ 225
4
- Depr Ret 1
+ Comm1 Volume 4
- 212
+ 225
5
- Depr Ret 2
+ Comm1 Volume 5
- 212
+ 225
6
- DED Data 0
+ Comm1 Volume 6
- 212
+ 225
7
- DED Data 1
+ Comm1 Volume 7
- 212
+ 225
8
- DED Data 2
+ Comm1 Volume 8
- 212
+ 225
9
- HUD Att Fpm 0
+ Comm2 Volume 0
- 212
+ 225
10
- HUD Att Fpm 1
+ Comm2 Volume 1
- 212
+ 225
11
- HUD Att Fpm 2
+ Comm2 Volume 2
- 212
+ 225
12
- HUD Vah Fpm 0
+ Comm2 Volume 3
- 212
+ 225
13
- HUD Vah Fpm 1
+ Comm2 Volume 4
- 212
+ 225
14
- HUD Vah Fpm 2
+ Comm2 Volume 5
- 212
+ 225
15
- Test Step 0
+ Comm2 Volume 6
- 212
+ 225
16
- Test Step 1
+ Comm2 Volume 7
- 212
+ 225
17
- Test Step 2
+ Comm2 Volume 8
- 212
+ 225
18
- HUD Bright 0
+ ILS Volume 0
- 212
+ 225
19
- HUD Bright 1
-
+ ILS Volume 1
+
- 212
+ 225
20
- HUD Bright 2
-
+ ILS Volume 2
+
- 212
+ 225
21
- HUD Alt Radar 0
-
+ ILS Volume 3
+
- 212
+ 225
22
- HUD Alt Radar 1
-
+ ILS Volume 4
+
- 212
+ 225
23
- HUD Alt Radar 2
-
+ ILS Volume 5
+
- 212
+ 225
24
- HUD Speed 0
-
+ ILS Volume 6
+
- 212
+ 225
25
- HUD Speed 1
-
+ ILS Volume 7
+
- 212
+ 225
26
- HUD Speed 2
-
-
- 212
- 27
- Anti Ice 0
-
-
- 212
- 28
- Anti Ice 1
-
-
- 212
- 29
- Anti Ice 2
+ ILS Volume 8
- 213
+ 226
0
- IFF Code Zero
- SimIFFMode4ReplyOff
+ Man Freq 2 Digit 0
- 213
+ 226
1
- IFF Code AB
- SimIFFMode4ReplyAlpha
+ Man Freq 2 Digit 1
- 213
+ 226
2
- IFF Code Hold
- SimIFFMode4ReplyBravo
+ Man Freq 2 Digit 2
- 213
+ 226
3
- IFF Enable M1M3
- SimIFFEnableM1M3
+ Man Freq 2 Digit 3
- 213
+ 226
4
- IFF Enable Off
- SimIFFEnableOff
+ Man Freq 2 Digit 4
- 213
+ 226
5
- IFF Enable M3MS
- SimIFFEnableM3MS
+ Man Freq 2 Digit 5
- 213
+ 226
6
- AVTR 0
+ Man Freq 2 Digit 6
- 213
+ 226
7
- AVTR 1
+ Man Freq 2 Digit 7
- 213
+ 226
8
- AVTR 2
+ Man Freq 2 Digit 8
- 213
+ 226
9
- Landing Light 0
- Landing light off
+ Man Freq 2 Digit 9
- 213
+ 226
10
- Landing Light 1
- Taxi light on
+ Man Freq 3 Digit 0
- 213
+ 226
11
- Landing Light 2
- Landing light on
+ Man Freq 3 Digit 1
- 213
+ 226
12
- ECM Xmit 0
+ Man Freq 3 Digit 2
- 213
+ 226
13
- ECM Xmit 1
+ Man Freq 3 Digit 3
- 213
+ 226
14
- ECM Xmit 2
+ Man Freq 3 Digit 4
- 213
+ 226
15
- Eng Data Ab 0
+ Man Freq 3 Digit 5
- 213
+ 226
16
- Eng Data Ab 1
+ Man Freq 3 Digit 6
- 213
+ 226
17
- Eng Data Ab 2
+ Man Freq 3 Digit 7
- 213
+ 226
18
- Hot Mike 0
+ Man Freq 3 Digit 8
- 213
+ 226
19
- Hot Mike 1
+ Man Freq 3 Digit 9
- 213
+ 226
20
- Hot Mike 2
+ Man Freq 4 Digit 0
- 213
+ 226
21
- AP Roll 0 (Left Down)
+ Man Freq 4 Digit 1
- 213
+ 226
22
- AP Roll 1 (Left Mid)
+ Man Freq 4 Digit 2
- 213
+ 226
23
- AP Roll 2 (Left Up)
+ Man Freq 4 Digit 3
- 213
+ 226
24
- AP Pitch 0 (Right Down)
+ Man Freq 4 Digit 4
- 213
+ 226
25
- AP Pitch 1 (Right Mid)
+ Man Freq 4 Digit 5
- 213
+ 226
26
- AP Pitch 2 (Right Up)
+ Man Freq 4 Digit 6
- 213
+ 226
27
- Master Arm 0
+ Man Freq 4 Digit 7
- 213
+ 226
28
- Master Arm 1
+ Man Freq 4 Digit 8
- 213
+ 226
29
- Master Arm 2
+ Man Freq 4 Digit 9
- 214
+ 227
0
- RF 0
+ Man Freq 5 Digit 0
- 214
+ 227
1
- RF 1
+ Man Freq 5 Digit 1
- 214
+ 227
2
- RF 2
+ Man Freq 5 Digit 2
- 214
+ 227
3
- C/O Drift 0
+ Man Freq 5 Digit 3
- 214
+ 227
4
- C/O Drift 1
+ Man Freq 5 Digit 4
- 214
+ 227
5
- C/O Drift 2
-
-
- 214
- 6
- FLIR Gain 0
-
-
- 214
- 7
- FLIR Gain 1
-
-
- 214
- 8
- FLIR Gain 2
-
-
- 214
- 9
- Test 0
-
-
- 214
- 10
- Test 1
-
-
- 214
- 11
- Test 2
-
-
- 214
- 12
- EPU 0
-
-
- 214
- 13
- EPU 1
-
-
- 214
- 14
- EPU 2
-
-
- 214
- 15
- Zeroize 0
-
-
- 214
- 16
- Zeroize 1
-
-
- 214
- 17
- Zeroize 2
+ Man Freq 5 Digit 5
- 214
- 18
- Nuclear Consent 0
+ 227
+ 6
+ Man Freq 5 Digit 6
- 214
- 19
- Nuclear Consent 1
+ 227
+ 7
+ Man Freq 5 Digit 7
- 214
- 20
- Nuclear Consent 2
+ 227
+ 8
+ Man Freq 5 Digit 8
- 214
- 21
- UHF Main 0
+ 227
+ 9
+ Man Freq 5 Digit 9
- 214
- 22
- UHF Main 1
+ 228
+ 0
+ unused
- 214
- 23
- UHF Main 2
+ 228
+ 1
+ FLCS A Test Light ON
- 214
- 24
- Probe Heat 0
+ 228
+ 2
+ unused
- 214
- 25
- Probe Heat 1
+ 228
+ 3
+ FLCS B Test Light ON
- 214
- 26
- Probe Heat 2
+ 228
+ 4
+ unused
- 214
- 27
- IFF Reply Off
+ 228
+ 5
+ FLCS C Test Light
- 214
- 28
- IFF Reply Alpha
+ 228
+ 6
+ unused
- 214
- 29
- IFF Reply Bravo
+ 228
+ 7
+ FLCS D Test Light
- 215
+ 229
0
- Caution Light Oxy Low
+ Flt Control Run Light
- 216
- 0
- Caution Panel Equip Hot Light
+ 229
+ 1
+ Flt Control Fail Light
- 217
+ 230
0
- Oxy Quantity 0
+ Left console lights 0
- 217
+ 230
1
- Oxy Quantity 1
+ Left console lights 1
- 217
+ 230
2
- EPU/Gen 0
+ Left console lights 2
- 217
+ 230
3
- EPU/Gen 1
+ Left console lights 3
- 217
+ 230
4
- Master Fuel 0
+ Left console lights 4
- 217
+ 230
5
- Master Fuel 1
+ Right console lights 0
- 217
+ 230
6
- Eng Count 0 (Pri)
+ Right console lights 1
- 217
+ 230
7
- Eng Count 1 (Sec)
+ Right console lights 2
- 217
+ 230
8
- Voice Message Inhibit 0
+ Right console lights 3
- 217
+ 230
9
- Voice Message Inhibit 1
+ Right console lights 4
- 217
+ 230
10
- Plain 0
+ AUX inst lights 0
- 217
+ 230
11
- Plain 1
+ AUX inst lights 1
- 217
+ 230
12
- Squelch 0
+ AUX inst lights 2
- 217
+ 230
13
- Squelch 1
+ AUX inst lights 3
- 217
+ 230
14
- Mal & Int 0
+ AUX inst lights 4
- 217
+ 230
15
- Mal & Int 1
+ FLIGHT INSTR lights 0
- 217
+ 230
16
- Ext Light Form 0
+ FLIGHT INSTR lights 1
- 217
+ 230
17
- Ext Light Form 1
+ FLIGHT INSTR lights 2
- 217
+ 230
18
- Ext Light Aerial Refuel 0
+ FLIGHT INSTR lights 3
- 217
+ 230
19
- Ext Light Aerial Refuel 1
+ FLIGHT INSTR lights 4
- 217
+ 230
20
- CNI Backup 0
+ ENG INSTR lights 0
- 217
+ 230
21
- CNI Backup 1
+ ENG INSTR lights 1
- 217
+ 230
22
+ ENG INSTR lights 2
- 217
+ 230
23
+ ENG INSTR lights 3
- 217
+ 230
24
- Audio Tacan 0
+ ENG INSTR lights 4
- 217
+ 230
25
- Audio Tacan 1
+ Flood light 0
- 217
+ 230
26
- Hook 0
+ Flood light 1
- 217
+ 230
27
- Hook 1
+ Flood light 2
- 217
+ 230
28
- UHF Preset 0
+ Flood light 3
- 217
+ 230
29
- UHF Preset 1
+ Flood light 4
- 217
+ 230
30
- UHF Preset 2
+ Flood light 5
- 218
+ 230
+ 31
+ Flood light 6
+
+
+ 231
0
+ Formation Lights 0
- 218
+ 231
1
+ Formation Lights 1
- 218
+ 231
2
- Comm 1 Sel 0
+ Formation Lights 2
- 218
+ 231
3
- Comm 1 Sel 1
+ Formation Lights 3
- 218
+ 231
4
- Comm 2 Sel 0
+ Formation Lights 4
- 218
+ 231
5
- Comm 2 Sel 1
+ Formation Lights 5
- 218
+ 231
6
- Tf 0
+ Formation Lights 6
- 218
+ 231
7
- Tf 1
+ Position Lights 0
- 218
+ 231
8
- Voice Secure 0
+ Position Lights 1
- 218
+ 231
9
- Voice Secure 1
+ Position Lights 2
- 218
+ 231
10
- Horn Silencer Up
+ Position Lights 3
- 218
+ 231
11
- Horn Silencer Down
+ Position Lights 4
- 218
+ 231
12
- Fire & Overheat detect Up
+ Position Lights 5
- 218
+ 231
13
- Fire & Overheat detect Down
+ Position Lights BRT
- 218
+ 231
14
- Mal & Ind LTS Up
+ Position Lights FLASH
- 218
+ 231
15
- Mal & Ind LTS Down
+ Anticollision light Off
- 218
+ 231
16
+ Anticollision light On
- 218
+ 231
17
+ Null
- 218
+ 231
18
- Launch Bar 0
+ Null
- 218
+ 231
19
- Launch Bar 1
+ Null
- 219
+ 231
+ 20
+ Null
+
+
+ 231
+ 21
+ Null
+
+
+ 231
+ 22
+ Int Dim 0
+
+
+ 231
+ 23
+ Int Dim 1
+
+
+ 231
+ 24
+ Int Dim 2
+
+
+ 231
+ 25
+ Int Dim 3
+
+
+ 231
+ 26
+ Int Dim 4
+
+
+ 232
0
- T23A Display Select 0
+ Antenna Select 0
- 219
+ 232
1
- T23A Display Select 1
+ Antenna Select 1
- 219
+ 232
2
- T23A Display Select 2
+ Antenna Select 2
- 219
+ 232
3
- T23A Display Select 3
+ Seat Position 0
- 219
+ 232
4
- Air Source 0
+ Seat Position 1>
- 219
+ 232
5
- Air Source 1
+ Seat Position 2
- 219
+ 232
6
- Air Source 2
+ Engine Anti Ice 0
- 219
+ 232
7
- Air Source 3
+ Engine Anti Ice 1
- 219
+ 232
8
- HSI Instr Mode 0
+ Engine Anti Ice 2
- 219
+ 232
9
- HSI Instr Mode 1
+ Parkinig Brake 0
- 219
+ 232
10
- HSI Instr Mode 2
+ Parkinig Brake 1
- 219
+ 232
11
- HSI Instr Mode 3
+ Parkinig Brake 2
- 219
+ 232
12
- Avionics Power Ins 0
+ Landing Light 0
- 219
+ 232
13
- Avionics Power Ins 1
+ TAXI LIGHTS
- 219
+ 232
14
- Avionics Power Ins 2
+ LIGH OFF
- 219
+ 232
15
- Avionics Power Ins 3
+ LANDING LIGHT
- 219
+ 232
16
- Eng Feed 0
+ Canopy 1
- 219
+ 232
17
- Eng Feed 1
+ Canopy 2
- 219
+ 232
18
- Eng Feed 2
+ JFS Start 0
- 219
+ 232
19
- Eng Feed 3
+ JFS Start 1
- 219
+ 232
20
- CMDS Prog 0
+ JFS Start 2
- 219
+ 232
21
- CMDS Prog 1
+ Wing Tail Light 0
- 219
+ 232
22
- CMDS Prog 2
+ Wing Tail Light 1
- 219
+ 232
23
- CMDS Prog 3
+ Wing Tail Light 2
- 219
+ 232
24
- Manual Freq 1st Digit 0
+ Antiskid OFF
- 219
+ 232
25
- Manual Freq 1st Digit 1
+ Antiskid PULSER
- 219
+ 232
26
- Manual Freq 1st Digit 2
-
-
- 219
- 27
- Manual Freq 1st Digit 3
-
-
- 219
- 28
- IFF M1 Digit 2 0
- SimIFFBackupM1Digit2_0
-
-
- 219
- 29
- IFF M1 Digit 2 1
- SimIFFBackupM1Digit2_1
-
-
- 219
- 30
- IFF M1 Digit 2 2
- SimIFFBackupM1Digit2_2
-
-
- 219
- 31
- IFF M1 Digit 2 3
- SimIFFBackupM1Digit2_3
+ Antiskid NORMAL
- 220
+ 233
0
- IFF Master 0
- SimIFFMasterOff
-
+ left knee pad 0
+
- 220
+ 233
1
- IFF Master 1
- SimIFFMasterStby
-
+ left knee pad 1
+
- 220
+ 233
2
- IFF Master 2
- SimIFFMasterLow
-
+ left knee pad 2
+
- 220
+ 233
3
- IFF Master 3
- SimIFFMasterNorm
-
+ left knee pad 3
+
- 220
+ 233
4
- IFF Master 4
- SimIFFMasterEmerg
-
+ left knee pad 4
+
- 220
+ 233
5
- HMCS Power 0 (Off)
-
+ left knee pad 5
+
- 220
+ 233
6
- HMCS Power 1
-
+ left knee pad 6
+
- 220
+ 233
7
- HMCS Power 2
-
+ left knee pad 7
+
- 220
+ 233
8
- HMCS Power 3
-
+ left knee pad 8
+
- 220
+ 233
9
- HMCS Power 4
-
+ left knee pad 9
+
- 220
+ 233
10
- Extl light main power 0
-
+ left knee pad 10
+
- 220
+ 233
11
- Extl light main power 1
-
+ left knee pad 11
+
- 220
+ 233
12
- Extl light main power 2
+ left knee pad 12
- 220
+ 233
13
- Extl light main power 3
-
+ left knee pad 13
+
- 220
+ 233
14
- Extl light main power 4
-
-
- 221
- 0
- CMDS Mode 0
-
-
- 221
- 1
- CMDS Mode 1
-
-
- 221
- 2
- CMDS Mode 2
-
-
- 221
- 3
- CMDS Mode 3
-
+ left knee pad 14
+
- 221
- 4
- CMDS Mode 4
+ 233
+ 15
+ left knee pad 15
- 221
- 5
- CMDS Mode 5
+ 233
+ 16
+ right knee pad 0
- 221
- 6
- Fuel Qty Sel 0
+ 233
+ 17
+ right knee pad 1
- 221
- 7
- Fuel Qty Sel 1
+ 233
+ 18
+ right knee pad 2
- 221
- 8
- Fuel Qty Sel 2
+ 233
+ 19
+ right knee pad 3
- 221
- 9
- Fuel Qty Sel 3
+ 233
+ 20
+ right knee pad 4
- 221
- 10
- Fuel Qty Sel 4
+ 233
+ 21
+ right knee pad 5
- 221
- 11
- Fuel Qty Sel 5
+ 233
+ 22
+ right knee pad 6
- 222
- 0
- UHF Volume 0
+ 233
+ 23
+ right knee pad 7
- 222
- 1
- UHF Volume 1
+ 233
+ 24
+ right knee pad 8
- 222
- 2
- UHF Volume 2
+ 233
+ 25
+ right knee pad 9
- 222
- 3
- UHF Volume 3
+ 233
+ 26
+ right knee pad 10
- 222
- 4
- UHF Volume 4
+ 233
+ 27
+ right knee pad 11
- 222
- 5
- UHF Volume 5
+ 233
+ 28
+ right knee pad 12
- 222
- 6
- UHF Volume 6
+ 233
+ 29
+ right knee pad 13
- 222
- 7
- UHF Volume 7
+ 233
+ 30
+ right knee pad 14
- 222
- 8
- IFF M1 Digit 1 0
- SimIFFBackupM1Digit1_0
+ 233
+ 31
+ right knee pad 15
- 222
- 9
- IFF M1 Digit 1 1
- SimIFFBackupM1Digit1_1
+ 234
+ 0
+ Anti Collision Mode 0
- 222
- 10
- IFF M1 Digit 1 2
- SimIFFBackupM1Digit1_2
+ 234
+ 1
+ Anti Collision Mode 1
- 222
- 11
- IFF M1 Digit 1 3
- SimIFFBackupM1Digit1_3
+ 234
+ 2
+ Anti Collision Mode 2
- 222
- 12
- IFF M1 Digit 1 4
- SimIFFBackupM1Digit1_4
+ 234
+ 3
+ Anti Collision Mode 3
- 222
- 13
- IFF M1 Digit 1 5
- SimIFFBackupM1Digit1_5
+ 234
+ 4
+ Anti Collision Mode 4
- 222
- 14
- IFF M1 Digit 1 6
- SimIFFBackupM1Digit1_6
+ 234
+ 5
+ Anti Collision Mode 5
- 222
- 15
- IFF M1 Digit 1 7
- SimIFFBackupM1Digit1_7
+ 234
+ 6
+ Anti Collision Mode 6
+
- 222
- 16
- IFF M3 Digit 1 0
- SimIFFBackupM3Digit1_0
+ 234
+ 7
+ Anti Collision Mode 7
- 222
- 17
- IFF M3 Digit 1 1
- SimIFFBackupM3Digit1_1
+ 235
+ 0
+ Caution Light Dbu On
- 222
- 18
- IFF M3 Digit 1 2
- SimIFFBackupM3Digit1_2
+ 236
+ 0
+ Backup Radio Digit Display
- 222
- 19
- IFF M3 Digit 1 3
- SimIFFBackupM3Digit1_3
+ 237
+ 0
+ Caution ATF not engaged
- 222
- 20
- IFF M3 Digit 1 4
- SimIFFBackupM3Digit1_4
+ 238
+ 0
+ F15 MACH OFF flag
- 222
- 21
- IFF M3 Digit 1 5
- SimIFFBackupM3Digit1_5
+ 239
+ 0
+ Caution Inlet Icing
-
- 222
- 22
- IFF M3 Digit 1 6
- SimIFFBackupM3Digit1_6
+
+ 240
+ 0
+ AAR light
- 222
- 23
- IFF M3 Digit 1 7
- SimIFFBackupM3Digit1_7
+ 241
+ 0
+ Fuselage Light 1
- 222
- 24
- IFF M3 Digit 2 0
- SimIFFBackupM3Digit2_0
+ 242
+ 0
+ ECM Button 1 Unpressed Not Lit
- 222
- 25
- IFF M3 Digit 2 1
- SimIFFBackupM3Digit2_1
+ 242
+ 1
+ ECM Button 1 UnPressed All Lit
- 222
- 26
- IFF M3 Digit 2 2
- SimIFFBackupM3Digit2_2
+ 242
+ 2
+ ECM Button 1 Pressed No Lit
- 222
- 27
- IFF M3 Digit 2 3
- SimIFFBackupM3Digit2_3
+ 242
+ 3
+ ECM Button 1 Pressed Standby Lit
- 222
- 28
- IFF M3 Digit 2 4
- SimIFFBackupM3Digit2_4
+ 242
+ 4
+ ECM Button 1 Pressed Active Lit
- 222
- 29
- IFF M3 Digit 2 5
- SimIFFBackupM3Digit2_5
+ 242
+ 5
+ ECM Button 1 Pressed Transmit Lit
- 222
- 30
- IFF M3 Digit 2 6
- SimIFFBackupM3Digit2_6
+ 242
+ 6
+ ECM Button 1 Pressed Failed Lit
- 222
- 31
- IFF M3 Digit 2 7
- SimIFFBackupM3Digit2_7
+ 242
+ 7
+ ECM Button 1 Pressed All Lit
- 223
+ 243
0
- Pull 0
+ ECM Button 2 Unpressed Not Lit
- 223
+ 243
1
- Pull 1
+ ECM Button 2 UnPressed All Lit
- 223
+ 243
2
- Pull 2
+ ECM Button 2 Pressed No Lit
- 223
+ 243
3
- Pull 3
+ ECM Button 2 Pressed Standby Lit
- 223
+ 243
4
- Pull 4
+ ECM Button 2 Pressed Active Lit
- 223
+ 243
5
- Pull 5
+ ECM Button 2 Pressed Transmit Lit
- 223
+ 243
6
- Pull 6
+ ECM Button 2 Pressed Failed Lit
- 223
+ 243
7
- Pull 7
+ ECM Button 2 Pressed All Lit
- 223
- 8
- HSI Crs Select 0
- Shows the knob turning
+ 244
+ 0
+ ECM Button 3 Unpressed Not Lit
- 223
- 9
- HSI Crs Select 1
+ 244
+ 1
+ ECM Button 3 UnPressed All Lit
- 223
- 10
- HSI Crs Select 2
+ 244
+ 2
+ ECM Button 3 Pressed No Lit
- 223
- 11
- HSI Crs Select 3
+ 244
+ 3
+ ECM Button 3 Pressed Standby Lit
- 223
- 12
- HSI Crs Select 4
+ 244
+ 4
+ ECM Button 3 Pressed Active Lit
- 223
- 13
- HSI Crs Select 5
+ 244
+ 5
+ ECM Button 3 Pressed Transmit Lit
- 223
- 14
- HSI Crs Select 6
+ 244
+ 6
+ ECM Button 3 Pressed Failed Lit
- 223
- 15
- HSI Crs Select 7
+ 244
+ 7
+ ECM Button 3 Pressed All Lit
- 223
- 16
- HSI Hdg Select 0
- Shows the knob turning
+ 245
+ 0
+ ECM Button 4 Unpressed Not Lit
- 223
- 17
- HSI Hdg Select 1
+ 245
+ 1
+ ECM Button 4 UnPressed All Lit
- 223
- 18
- HSI Hdg Select 2
+ 245
+ 2
+ ECM Button 4 Pressed No Lit
- 223
- 19
- HSI Hdg Select 3
+ 245
+ 3
+ ECM Button 4 Pressed Standby Lit
- 223
- 20
- HSI Hdg Select 4
+ 245
+ 4
+ ECM Button 4 Pressed Active Lit
- 223
- 21
- HSI Hdg Select 5
+ 245
+ 5
+ ECM Button 4 Pressed Transmit Lit
- 223
- 22
- HSI Hdg Select 6
+ 245
+ 6
+ ECM Button 4 Pressed Failed Lit
- 223
- 23
- HSI Hdg Select 7
+ 245
+ 7
+ ECM Button 4 Pressed All Lit
- 224
+ 246
0
- Threat Warn Vol 0
+ Radar Power OFF
- 224
+ 246
1
- Threat Warn Vol 1
+ Radar Power STBY
- 224
+ 246
2
- Threat Warn Vol 2
+ Radar Power OPR
- 224
+ 246
3
- Threat Warn Vol 3
+ Radar Power EMERG
- 224
+ 246
4
- Threat Warn Vol 4
+ Radar Range 10
- 224
+ 246
5
- Threat Warn Vol 5
+ Radar Range 20
- 224
+ 246
6
- Threat Warn Vol 6
+ Radar Range 40
- 224
+ 246
7
- Threat Warn Vol 7
+ Radar Range 80
- 224
+ 246
8
- Threat Warn Vol 8
+ Radar Range 160
- 224
+ 246
9
- Missile Warn Vol 0
+ RADAR ELSCAN 1
- 224
+ 246
10
- Missile Warn Vol 1
+ RADAR ELSCAN 2
- 224
+ 246
11
- Missile Warn Vol 2
+ RADAR ELSCAN 4
- 224
+ 246
12
- Missile Warn Vol 3
+ RADAR ELSCAN 6
- 224
+ 246
13
- Missile Warn Vol 4
+ RADAR ELSCAN 8
- 224
+ 246
14
- Missile Warn Vol 5
+ RADAR AZSCAN 20
- 224
+ 246
15
- Missile Warn Vol 6
+ RADAR AZSCAN 60
- 224
+ 246
16
- Missile Warn Vol 7
+ RADAR AZSCAN 120
- 224
+ 246
17
- Missile Warn Vol 8
+ Radar Frame LEFT0
- 224
+ 246
18
- Intercom Vol 0
+ Radar Frame LEFT1
- 224
+ 246
19
- Intercom Vol 1
+ Radar Frame LEFT2
- 224
+ 246
20
- Intercom Vol 2
+ Radar Frame LEFT3
- 224
+ 246
21
- Intercom Vol 3
+ Radar Frame RIGHT0
- 224
+ 246
22
- Intercom Vol 4
+ Radar Frame RIGHT1
- 224
+ 246
23
- Intercom Vol 5
+ Radar Frame RIGHT2
- 224
+ 246
24
- Intercom Vol 6
+ Radar Frame RIGHT3
- 224
+ 246
25
- Intercom Vol 7
+ Radar BAND NULL
- 224
+ 246
26
- Intercom Vol 8
+ Radar BAND A
- 225
+ 246
+ 27
+ Radar BAND B
+
+
+ 246
+ 28
+ Radar BAND C
+
+
+ 246
+ 29
+ Radar CHAN 0
+
+
+ 246
+ 30
+ Radar CHAN 1
+
+
+ 246
+ 31
+ Radar CHAN 2
+
+
+ 247
0
- Comm1 Volume 0
+ SplMode OFF
- 225
+ 247
1
- Comm1 Volume 1
+ SplMode MANTRK
- 225
+ 247
2
- Comm1 Volume 2
+ SplMode SI
- 225
+ 247
3
- Comm1 Volume 3
+ SplMode FLOOD
- 225
+ 247
4
- Comm1 Volume 4
+ ModeCtrl MAN
- 225
+ 247
5
- Comm1 Volume 5
+ ModeCtrl AUTO
- 225
+ 247
6
- Comm1 Volume 6
+ NctrEnable ON
- 225
+ 247
7
- Comm1 Volume 7
+ NctrEnable OFF
- 225
+ 247
8
- Comm1 Volume 8
+ ModeSel A/A LRS
- 225
+ 247
9
- Comm2 Volume 0
+ ModeSel A/A VS
- 225
+ 247
10
- Comm2 Volume 1
+ ModeSel A/A SRS
- 225
+ 247
11
- Comm2 Volume 2
+ ModeSel A/A PULSE
- 225
+ 247
12
- Comm2 Volume 3
+ ModeSel BCN
- 225
+ 247
13
- Comm2 Volume 4
+ ModeSel A/G DPLR
- 225
+ 247
14
- Comm2 Volume 5
+ ModeSel A/G RNG
- 225
+ 247
15
- Comm2 Volume 6
-
-
- 225
- 16
- Comm2 Volume 7
-
-
- 225
- 17
- Comm2 Volume 8
-
-
- 225
- 18
- ILS Volumn 0
-
-
- 225
- 19
- ILS Volumn 1
-
-
- 225
- 20
- ILS Volumn 2
-
-
- 225
- 21
- ILS Volumn 3
-
-
- 225
- 22
- ILS Volumn 4
+ ModeSel A/G MAP
- 225
- 23
- ILS Volumn 5
-
-
- 225
- 24
- ILS Volumn 6
-
-
- 225
- 25
- ILS Volumn 7
-
-
- 225
- 26
- ILS Volumn 8
-
-
- 226
+ 248
0
- Man Freq 2 Digit 0
+ EWR/ICS TRNG
- 226
+ 248
1
- Man Freq 2 Digit 1
+ EWR/ICS COMBAT
- 226
+ 248
2
- Man Freq 2 Digit 2
+ PODS STBY
- 226
+ 248
3
- Man Freq 2 Digit 3
+ PODS XMIT
+
- 226
+ 248
4
- Man Freq 2 Digit 4
+ ICS STBY
- 226
+ 248
5
- Man Freq 2 Digit 5
+ ICS AUTO
- 226
+ 248
6
- Man Freq 2 Digit 6
+ ICS MAN
- 226
+ 248
7
- Man Freq 2 Digit 7
+ ICS OFF
- 226
+ 248
8
- Man Freq 2 Digit 8
+ ICS ON
- 226
+ 248
9
- Man Freq 2 Digit 9
+ SET-1 AUTO
- 226
+ 248
10
- Man Freq 3 Digit 0
+ SET-1 MAN
- 226
+ 248
11
- Man Freq 3 Digit 1
+ SET-2 AUTO
- 226
+ 248
12
- Man Freq 3 Digit 2
+ SET-2 MAN
- 226
+ 248
13
- Man Freq 3 Digit 3
+ SET-3 AUTO
- 226
+ 248
14
- Man Freq 3 Digit 4
+ SET-3 MAN
- 226
+ 248
15
- Man Freq 3 Digit 5
+ RWR OFF
- 226
+ 248
16
- Man Freq 3 Digit 6
+ RWR ON
- 226
+ 248
17
- Man Freq 3 Digit 7
+ EWWS OFF
- 226
+ 248
18
- Man Freq 3 Digit 8
+ EWWS ON
- 226
+ 248
19
- Man Freq 3 Digit 9
+ EWWS DEFEAT
- 226
+ 248
20
- Man Freq 4 Digit 0
+ EWWS TONE
- 226
+ 248
21
- Man Freq 4 Digit 1
+ light set-1
- 226
+ 248
22
- Man Freq 4 Digit 2
+ light set-2
- 226
+ 248
23
- Man Freq 4 Digit 3
+ light set-3
- 226
+ 248
24
- Man Freq 4 Digit 4
+ EwWrnLgt PROGRAM
- 226
+ 248
25
- Man Freq 4 Digit 5
+ EwWrnLgt MINIMUM
- 226
+ 248
26
- Man Freq 4 Digit 6
+ EwWrnLgt CHAFF
+
+
+ 248
+ 27
+ EwWrnLgt FLARE
+
+
+ 248
+ 28
+ EwWrnLgt SPRCHAFF
+
+
+ 248
+ 29
+ EwWrnLgt SPRFLARE
+
+
+ 248
+ 30
+ EwWrnLgt wtf1
+
+
+ 248
+ 31
+ EwWrnLgt wtf2
+
+
+ 249
+ 0
+ ECM HAF OFF
+
+
+ 249
+ 1
+ ECM HAF STBY
+
+
+ 249
+ 2
+ ECM HAF OPER
+
+
+ 249
+ 3
+ Radio2 Digit 1
+
+
+ 249
+ 4
+ Radio2 Digit 1
+
+
+ 249
+ 5
+ Radio2 Digit 1
+
+
+ 249
+ 6
+ Radio2 Digit 1
+
+
+ 249
+ 7
+ Radio2 Digit 2
+
+
+ 249
+ 8
+ Radio2 Digit 2
+
+
+ 249
+ 9
+ Radio2 Digit 2
+
+
+ 249
+ 10
+ Radio2 Digit 2
+
+
+ 249
+ 11
+ Radio2 Digit 2
+
+
+ 249
+ 12
+ Radio2 Digit 2
+
+
+ 249
+ 13
+ Radio2 Digit 2
+
+
+ 249
+ 14
+ Radio2 Digit 2
- 226
- 27
- Man Freq 4 Digit 7
+ 249
+ 15
+ Radio2 Digit 2
- 226
- 28
- Man Freq 4 Digit 8
+ 249
+ 16
+ Radio2 Digit 2
- 226
- 29
- Man Freq 4 Digit 9
+ 249
+ 17
+ Radio2 Digit 3
- 227
- 0
- Man Freq 5 Digit 0
+ 249
+ 18
+ Radio2 Digit 3
- 227
- 1
- Man Freq 5 Digit 1
+ 249
+ 19
+ Radio2 Digit 3
- 227
- 2
- Man Freq 5 Digit 2
+ 249
+ 20
+ Radio2 Digit 3
- 227
- 3
- Man Freq 5 Digit 3
+ 249
+ 21
+ Radio2 Digit 3
- 227
- 4
- Man Freq 5 Digit 4
+ 249
+ 22
+ Radio2 Digit 3
- 227
- 5
- Man Freq 5 Digit 5
+ 249
+ 23
+ Radio2 Digit 3
- 227
- 6
- Man Freq 5 Digit 6
+ 249
+ 24
+ Radio2 Digit 3
- 227
- 7
- Man Freq 5 Digit 7
+ 249
+ 25
+ Radio2 Digit 3
- 227
- 8
- Man Freq 5 Digit 8
+ 249
+ 26
+ Radio2 Digit 3
- 227
- 9
- Man Freq 5 Digit 9
+ 250
+ 0
+ unused
- 228
+ 251
0
- Unused
+ JTIDS Mode Off
- 228
+ 251
1
- FLCS A Test Light
+ JTIDS Mode Poll
- 228
+ 251
2
- Unused
+ JTIDS Mode Norm
- 228
+ 251
3
- FLCS B Test Light
+ JTIDS Mode Sil
- 228
+ 251
4
- Unused
+ JTIDS Mode Hold
- 228
+ 251
5
- FLCS C Test Light
+ JTIDS Voice A
- 228
+ 251
6
- Unused
+ JTIDS Voice B
- 228
+ 251
7
- FLCS D Test Light
+ JTIDS CipherNorm
- 229
- 6
- Flt Control Run Light
-
-
- 229
- 7
- Flt Control Fail Light
+ 251
+ 8
+ JTIDS CipherZero
- 230
+ 252
0
- Pri Light Console 0
+ Radio2 Digit 4
- 230
+ 252
1
- Pri Light Console 1
+ Radio2 Digit 4
- 230
+ 252
2
- Pri Light Console 2
+ Radio2 Digit 4
- 230
+ 252
3
- Pri Light Instruments 0
+ Radio2 Digit 4
- 230
+ 252
4
- Pri Light Instruments 1
+ Radio2 Digit 4
- 230
+ 252
5
- Pri Light Instruments 2
+ Radio2 Digit 4
- 230
+ 252
6
- DED Display 0
+ Radio2 Digit 4
- 230
+ 252
7
- DED Display 1
+ Radio2 Digit 4
- 230
+ 252
8
- DED Display 2
+ Radio2 Digit 4
- 230
+ 252
9
- DED Display 3
+ Radio2 Digit 4
- 230
+ 252
10
- DED Display 4
+ Radio2 Digit 5
- 230
+ 252
11
- DED Display 5
+ Radio2 Digit 5
- 230
+ 252
12
- DED Display 6
+ Radio2 Digit 5
- 230
+ 252
13
- Flood Light Console 0
+ Radio2 Digit 5
- 230
+ 252
14
- Flood Light Console 1
+ Radio2 Digit 5
- 230
+ 252
15
- Flood Light Console 2
+ Radio2 Digit 5
- 230
+ 252
16
- Flood Light Console 3
+ Radio2 Digit 5
- 230
+ 252
17
- Flood Light Console 4
+ Radio2 Digit 5
- 230
+ 252
18
- Flood Light Console 5
+ Radio2 Digit 5
- 230
+ 252
19
- Flood Light Console 6
+ Radio2 Digit 5
- 230
+ 252
20
- Flood Light Console 7
+ Radio2 mode Guard/Off
- 230
+ 252
21
- Flood Light Inst 0
+ Radio2 mode Man
- 230
+ 252
22
- Flood Light Inst 1
+ Radio2 mode Chan
- 230
+ 252
23
- Flood Light Inst 2
+ Mirrors Off
- 231
- 0
- Formation Lights
+ 252
+ 24
+ Mirrors On
- 232
+ 253
0
- Antenna Select 0
+ Master Arm 0
- 232
+ 253
1
- Antenna Select 1
+ Master Arm 1
- 232
+ 253
2
- Antenna Select 2
+ AG mode
- 232
+ 253
3
- Seat Position 0
+ ADI mode
- 232
+ 253
4
- Seat Position 1
+ VI mode
+
+
+
- 232
+ 253
5
- Seat Position 2
+ Flare Switch
- 232
+ 253
6
- Engine Anti Ice 0
+ Both Switch
- 232
+ 253
7
- Engine Anti Ice 1
+ Chaff Switch
- 232
+ 253
8
- Engine Anti Ice 2
+ F15 CMDS Mode Off
- 232
+ 253
9
- Parkinig Brake 0
+ F15 CMDS Mode Stby
- 232
+ 253
10
- Parkinig Brake 1
+ F15 CMDS Mode Man Only
- 232
+ 253
11
- Parkinig Brake 2
+ F15 CMDS Mode Semi Auto
- 232
+ 253
12
- Landing Light 0
+ F15 CMDS Mode Auto
- 232
+ 253
13
- Landing Light 1
+ F15 Flare Jett Cover closed
- 232
+ 253
14
- Landing Light 2
+ F15 Flare Jett Cover open
- 232
+ 253
15
- Canopy 0
+ F15 Flare Jettison
- 232
+ 253
16
- Canopy 1
+ MCC AI Light
- 232
+ 253
17
- Canopy 2
+ MCC SAM Light
- 232
+ 253
18
- JFS Start 0
- Down
-
-
- 232
- 19
- JFS Start 1
- Middle
-
-
- 232
- 20
- JFS Start 2
- Up
-
-
- 232
- 21
- Wing Tail Light 0
- Down
-
-
- 232
- 22
- Wing Tail Light 1
- Middle
-
-
- 232
- 23
- Wing Tail Light 2
- Up
+ F15 Shoot Lights
- 233
+ 254
0
- Cockpit knee pad 0
+ Gen toggle left off
- 233
+ 254
1
- Cockpit knee pad 1
+ Gen toggle left on
- 233
+ 254
2
- Cockpit knee pad 2
+ Gen toggle right off
- 233
+ 254
3
- Cockpit knee pad 3
+ Gen toggle right on
- 233
+ 254
4
- Cockpit knee pad 4
+ Eec toggle left off
- 233
+ 254
5
- Cockpit knee pad 5
+ Eec toggle left on
- 233
+ 254
6
- Cockpit knee pad 6
+ Eec toggle right off
- 233
+ 254
7
- Cockpit knee pad 7
+ Eec toggle right on
- 233
+ 254
8
- Cockpit knee pad 8
+ LeftMasterEng OffOpen
- 233
+ 254
9
- Cockpit knee pad 9
+ LeftMasterEng OnOpen
- 233
+ 254
10
- Cockpit knee pad 10
+ LeftMasterEng OnClosed
- 233
+ 254
11
- Cockpit knee pad 11
+ RightMasterEng OffOpen
- 233
+ 254
12
- Cockpit knee pad 12
+ RightMasterEng OnOpen
- 233
+ 254
13
- Cockpit knee pad 13
+ RightMasterEng OnClosed
- 233
+ 254
14
- Cockpit knee pad 14
+ Starter off
- 233
+ 254
15
- Cockpit knee pad 15
+ Starter on
- 233
+ 254
16
- Cockpit knee pad 0
+ MFD bright OFF
- 233
+ 254
17
- Cockpit knee pad 1
+ MPCD bright NIGHT
- 233
+ 254
18
- Cockpit knee pad 2
+ MPCD bright DAY
- 233
+ 254
19
- Cockpit knee pad 3
+ VSD bright OFF
- 233
+ 254
20
- Cockpit knee pad 4
+ VSD bright CONT
+
- 233
+ 254
21
- Cockpit knee pad 5
+ NCI DATA CCC
- 233
+ 254
22
- Cockpit knee pad 6
+ NCI DATA WIND
- 233
+ 254
23
- Cockpit knee pad 7
+ NCI DATA VIS
- 233
+ 254
24
- Cockpit knee pad 8
+ NCI DATA PP
- 233
+ 254
25
- Cockpit knee pad 9
+ NCI DATA DEST
- 233
+ 254
26
- Cockpit knee pad 10
+ NCI DATA O/S
- 233
+ 254
27
- Cockpit knee pad 11
+ SELECT DATA CCC
- 233
+ 254
28
- Cockpit knee pad 12
+ SELECT DATA OFF
- 233
+ 254
29
- Cockpit knee pad 13
+ SELECT DATA GC
- 233
+ 254
30
- Cockpit knee pad 14
+ SELECT DATA INS
- 233
+ 254
31
- Cockpit knee pad 15
-
-
- 234
- 0
- Anti Collision Mode 0
-
-
- 234
- 1
- Anti Collision Mode 1
-
-
- 234
- 2
- Anti Collision Mode 2
-
-
- 234
- 3
- Anti Collision Mode 3
-
-
- 234
- 4
- Anti Collision Mode 4
-
-
- 234
- 5
- Anti Collision Mode 5
-
-
- 234
- 6
- Anti Collision Mode 6
-
-
- 235
- 0
- Caution Light Dbu On
-
-
- 236
- 0
- Backup Radio Digit Display
-
-
- 237
- 0
- Caution ATF not engaged
-
-
- 238
- 0
- Cockpit Legs
-
-
- 239
- 0
- Caution Inlet Icing
-
-
- 240
- 0
- Caution Inlet Icing
+ SELECT DATA TCN
\ No newline at end of file
diff --git a/bms_blender_plugin/common/util.py b/bms_blender_plugin/common/util.py
index b2201ea..f61e3d8 100644
--- a/bms_blender_plugin/common/util.py
+++ b/bms_blender_plugin/common/util.py
@@ -1,9 +1,8 @@
import bpy
-
import bpy.utils.previews
-
import os
import struct
+from contextlib import nullcontext
import lzma
@@ -82,37 +81,74 @@ def get_bml_type(obj, purge_orphaned_object=True):
switches = []
+_switches_hydrated = False # sentinel controlling hydration of global switch list
-def get_switches():
- """Returns a list of BMS Switches which are loaded from the switch.xml"""
- global switches
- if switches is None or len(switches) == 0:
- import os
-
- switches_tree = ElementTree.parse(
- os.path.join(os.path.dirname(__file__), "switch.xml")
- )
- root = switches_tree.getroot()
- switches = []
-
- for switch in root:
- switch_number = int(switch.find("SwitchNum").text)
- branch = int(switch.find("BranchNum").text)
- if switch.find("Name") is not None:
- name = switch.find("Name").text
- else:
- name = ""
-
- if switch.find("Comment") is not None:
- comment = switch.find("Comment").text
- else:
- comment = ""
+def _parse_switch_xml():
+ tree = ElementTree.parse(os.path.join(os.path.dirname(__file__), "switch.xml"))
+ root = tree.getroot()
+ parsed = []
+ for switch in root:
+ switch_number = int(switch.find("SwitchNum").text)
+ branch = int(switch.find("BranchNum").text)
+ name = switch.find("Name").text if switch.find("Name") is not None else ""
+ comment = switch.find("Comment").text if switch.find("Comment") is not None else ""
+ parsed.append(SwitchEnum(switch_number, branch, name, comment))
+ return parsed
- switches.append(SwitchEnum(switch_number, branch, name, comment))
- print(f"Imported {len(switches)} switches from file")
+def get_switches(force_disk: bool = False):
+ """Return switch definitions using hybrid hydration strategy
+ Order of precedence (unless force_disk):
+ 1. Already hydrated global list
+ 2. Scene cached (scene.switch_list) if present & user prefers cached
+ 3. Disk XML parse (and bootstrap scene snapshot if empty)
+ """
+ global switches, _switches_hydrated
+ if _switches_hydrated and not force_disk:
+ return switches
+
+ scene = getattr(bpy.context, 'scene', None)
+ prefs = None
+ try:
+ prefs = bpy.context.preferences.addons[__package__.split('.')[0]].preferences
+ except Exception:
+ pass
+ prefer_scene = getattr(prefs, 'prefer_scene_snapshot', True) if prefs else True
+ warn_mismatch = getattr(prefs, 'warn_xml_mismatch', True) if prefs else True
+ scene_list = getattr(scene, 'switch_list', None) if scene else None
+
+ # Scene snapshot path
+ if not force_disk and prefer_scene and scene_list and len(scene_list) > 0:
+ switches = [
+ SwitchEnum(int(it.switch_number), int(it.branch_number), it.name, getattr(it, 'comment', ""))
+ for it in scene_list
+ ]
+ _switches_hydrated = True
+ if warn_mismatch:
+ try:
+ disk_list = _parse_switch_xml()
+ if len(disk_list) != len(switches) or any(
+ (a.switch_number, a.branch) != (b.switch_number, b.branch)
+ for a, b in zip(switches, disk_list[:len(switches)])
+ ):
+ print("[BMS get_switches] switch.xml differs from scene snapshot – using scene snapshot (Reload switch.xml to adopt disk changes).")
+ except Exception:
+ pass
+ return switches
+
+ # Disk parse
+ disk_switches = _parse_switch_xml()
+ switches = disk_switches
+ _switches_hydrated = True
+ if scene_list is not None and len(scene_list) == 0:
+ for sw in switches:
+ item = scene_list.add()
+ item.name = sw.name
+ item.switch_number = sw.switch_number
+ item.branch_number = sw.branch
+ print(f"[BMS get_switches] Imported {len(switches)} switches from file")
return switches
@@ -145,30 +181,57 @@ def get_scripts():
dofs = []
-
-
-def get_dofs():
- """Returns a list of BMS DOFs which are loaded from the DOF.xml"""
- global dofs
-
- if dofs is None or len(dofs) == 0:
- dofs_tree = ElementTree.parse(
- os.path.join(os.path.dirname(__file__), "DOF.xml")
- )
- root = dofs_tree.getroot()
- dofs = []
-
- for dof in root:
- dof_number = int(dof.find("DOFNum").text)
- if dof.find("Name") is not None and dof.find("Name").text is not None:
- name = dof.find("Name").text
- else:
- name = ""
-
- dofs.append(DofEnum(dof_number, name))
-
- print(f"Imported {len(dofs)} dofs from file")
-
+_dofs_hydrated = False
+
+
+def _parse_dof_xml():
+ tree = ElementTree.parse(os.path.join(os.path.dirname(__file__), "DOF.xml"))
+ root = tree.getroot()
+ parsed = []
+ for dof in root:
+ dof_number = int(dof.find("DOFNum").text)
+ name = dof.find("Name").text if (dof.find("Name") is not None and dof.find("Name").text) else ""
+ parsed.append(DofEnum(dof_number, name))
+ return parsed
+
+
+def get_dofs(force_disk: bool = False):
+ """Return DOF definitions (hybrid hydration like switches)."""
+ global dofs, _dofs_hydrated
+ if _dofs_hydrated and not force_disk:
+ return dofs
+
+ scene = getattr(bpy.context, 'scene', None)
+ prefs = None
+ try:
+ prefs = bpy.context.preferences.addons[__package__.split('.')[0]].preferences
+ except Exception:
+ pass
+ prefer_scene = getattr(prefs, 'prefer_scene_snapshot', True) if prefs else True
+ warn_mismatch = getattr(prefs, 'warn_xml_mismatch', True) if prefs else True
+ scene_list = getattr(scene, 'dof_list', None) if scene else None
+
+ if not force_disk and prefer_scene and scene_list and len(scene_list) > 0:
+ dofs = [DofEnum(int(it.dof_number), it.name) for it in scene_list]
+ _dofs_hydrated = True
+ if warn_mismatch:
+ try:
+ disk_list = _parse_dof_xml()
+ if len(disk_list) != len(dofs) or any(a.dof_number != b.dof_number for a, b in zip(dofs, disk_list[:len(dofs)])):
+ print("[BMS get_dofs] DOF.xml differs from scene snapshot – using scene snapshot (Reload DOF.xml to adopt disk changes).")
+ except Exception:
+ pass
+ return dofs
+
+ disk_dofs = _parse_dof_xml()
+ dofs = disk_dofs
+ _dofs_hydrated = True
+ if scene_list is not None and len(scene_list) == 0:
+ for de in dofs:
+ item = scene_list.add()
+ item.name = de.name
+ item.dof_number = de.dof_number
+ print(f"[BMS get_dofs] Imported {len(dofs)} dofs from file")
return dofs
@@ -208,6 +271,66 @@ def get_callbacks():
return callbacks
+# -----------------------------------------------------------------------------
+# Get switch label for a given persistent switch ID/branch. Uses cached scene switch list first, then xml.
+# -----------------------------------------------------------------------------
+def lookup_switch_label(switch_number: int, branch_number: int) -> str | None:
+ """Return switch label from scene switch_list first, then global XML cache.
+
+ Args:
+ switch_number: persistent switch number
+ branch_number: persistent branch number
+ Returns:
+ Matching label (may be empty string) or None if not found.
+ """
+ try:
+ scene_list = getattr(bpy.context.scene, "switch_list", None)
+ if scene_list:
+ for item in scene_list:
+ if getattr(item, "switch_number", None) == switch_number and getattr(item, "branch_number", None) == branch_number:
+ return getattr(item, "name", None)
+ except Exception:
+ pass
+ # Fallback to global cache
+ try:
+ for sw in get_switches():
+ if sw.switch_number == switch_number and sw.branch == branch_number:
+ return sw.name
+ except Exception:
+ pass
+ return None
+
+
+def lookup_dof_label(dof_number: int) -> str | None:
+ """Return DOF label from scene dof_list first, then global cache."""
+ try:
+ scene_list = getattr(bpy.context.scene, "dof_list", None)
+ if scene_list:
+ for item in scene_list:
+ if getattr(item, "dof_number", None) == dof_number:
+ return getattr(item, "name", None)
+ except Exception:
+ pass
+ try:
+ for de in get_dofs():
+ if de.dof_number == dof_number:
+ return de.name
+ except Exception:
+ pass
+ return None
+
+__all__ = [
+ # existing public functions intentionally not exhaustively re-listed here
+ "get_switches",
+ "get_dofs",
+ "get_callbacks",
+ "get_bml_type",
+ "get_parent_dof_or_switch",
+ "lookup_switch_label",
+ "lookup_dof_label",
+]
+
+
def flatten_collection(collection, parent_collection):
"""Removes all non-switch collections from the tree, moving their objects up
and deletes empty collections with no children"""
@@ -249,7 +372,7 @@ def get_non_translate_dof_parent(obj):
def copy_collection_flat(
- from_collection, to_collection, excluded_collections, scale_factor
+ from_collection, to_collection, excluded_collections, scale_factor, export_profiler=None
):
"""Copies a collection and all of its objects but not its child-collections.
Also applies a scale factor to its objects"""
@@ -260,12 +383,12 @@ def copy_collection_flat(
if collection_object.parent is None:
# root object - copy that
copied_object = copy_object(
- collection_object, None, to_collection, scale_factor
+ collection_object, None, to_collection, scale_factor, export_profiler
)
for collection_child in from_collection.children:
copy_collection_flat(
- collection_child, to_collection, excluded_collections, scale_factor
+ collection_child, to_collection, excluded_collections, scale_factor, export_profiler
)
# toggle object mode to make sure that the scaling has been applied (Blender quirk)
@@ -273,6 +396,9 @@ def copy_collection_flat(
if copied_object:
bpy.context.view_layer.objects.active = copied_object
bpy.ops.object.mode_set(mode="OBJECT")
+
+ # Single scene update at the end to refresh all transform matrices - attempt to fix nested DOF transforms failing due to Blender quirk
+ bpy.context.view_layer.update()
def reset_dof(obj):
@@ -292,95 +418,195 @@ def reset_dof(obj):
obj.delta_scale.z = 1
-def copy_object(obj, parent, collection, scale_factor=1):
+def copy_object(obj, parent, collection, scale_factor=1, export_profiler=None):
"""Recursively copies an object and all of its children and moves their copies to a given collection.
Also applies a scale factor"""
if not obj.hide_render and len(obj.users_collection) != 0:
- copied_object = obj.copy()
- copied_object.parent = parent
- copied_object.matrix_parent_inverse = obj.matrix_parent_inverse.copy()
-
- if obj.data:
- copied_object.data = copied_object.data.copy()
- for k, e in obj.items():
- copied_object[k] = e
-
- # copy and apply all modifiers
- for obj_modifier in obj.modifiers:
- copied_object_modifiers = obj.modifiers.get(obj_modifier.name, None)
- if not copied_object_modifiers:
- copied_object_modifiers = obj.modifiers.new(
- obj_modifier.name, obj_modifier.type
- )
-
- # collect names of writable properties
- properties = [
- p.identifier
- for p in obj_modifier.bl_rna.properties
- if not p.is_readonly
- ]
-
- # copy those properties
- for prop in properties:
- setattr(copied_object_modifiers, prop, getattr(obj_modifier, prop))
-
- # set all DOFs to 0
- if get_bml_type(obj, False) == BlenderNodeType.DOF:
- reset_dof(copied_object)
-
- # scale only the root objects
- if scale_factor != 1 and obj.parent is None:
- copied_object.scale *= scale_factor
- copied_object.location *= scale_factor
-
- collection.objects.link(copied_object)
-
- # override any selection restriction
- copied_object.hide_select = False
- copied_object.hide_viewport = False
- copied_object.hide_set(False)
+ with export_profiler.stage("collection copy: duplicate objects") if export_profiler else nullcontext():
+ copied_object = obj.copy()
+ copied_object.parent = parent
+ copied_object.matrix_parent_inverse = obj.matrix_parent_inverse.copy()
+
+ if obj.data:
+ copied_object.data = copied_object.data.copy()
+ for k, e in obj.items():
+ copied_object[k] = e
+
+ # copy and apply all modifiers
+ for obj_modifier in obj.modifiers:
+ copied_object_modifiers = obj.modifiers.get(obj_modifier.name, None)
+ if not copied_object_modifiers:
+ copied_object_modifiers = obj.modifiers.new(
+ obj_modifier.name, obj_modifier.type
+ )
+
+ # collect names of writable properties
+ properties = [
+ p.identifier
+ for p in obj_modifier.bl_rna.properties
+ if not p.is_readonly
+ ]
+
+ # copy those properties
+ for prop in properties:
+ setattr(copied_object_modifiers, prop, getattr(obj_modifier, prop))
+
+ # set all DOFs to 0
+ if get_bml_type(obj, False) == BlenderNodeType.DOF:
+ reset_dof(copied_object)
+
+ # scale only the root objects
+ if scale_factor != 1 and obj.parent is None:
+ copied_object.scale *= scale_factor
+ copied_object.location *= scale_factor
+
+ collection.objects.link(copied_object)
+
+ # override any selection restriction
+ copied_object.hide_select = False
+ copied_object.hide_viewport = False
+ copied_object.hide_set(False)
for obj_child in obj.children:
- copy_object(obj_child, copied_object, collection, scale_factor)
+ copy_object(obj_child, copied_object, collection, scale_factor, export_profiler)
return copied_object
-def apply_all_modifiers(collection):
- """Applies all modifiers to objects which are rooted in the given collection"""
- for obj in collection.objects:
- if obj.parent is None:
- apply_all_modifiers_on_obj(obj)
-
+def apply_all_modifiers(collection, export_profiler=None):
+ """Applies all modifiers and transforms to every object in the collection.
-def apply_all_modifiers_on_obj(obj):
- """Applies all modifiers to a single object.
- Empties (DOFs, Slots and Switches) are excepted, since applying their modifiers would reset their positions.
+ After copy_collection_flat all objects (including children) reside in a flat
+ collection, but parent-child transform order still matters. We batch modifier
+ conversion globally, then batch transform application by hierarchy depth so a
+ selected batch never contains both a parent and one of its descendants.
"""
- if obj:
- bpy.ops.object.select_all(action="DESELECT")
- # apply the modifiers
- obj.select_set(True)
- bpy.context.view_layer.objects.active = obj
-
- if obj.type == "MESH":
- bpy.ops.object.mode_set(mode="OBJECT")
- bpy.ops.object.convert(target="MESH", keep_original=False)
-
- if get_bml_type(obj) not in [
- BlenderNodeType.DOF,
- BlenderNodeType.SLOT,
- BlenderNodeType.HOTSPOT,
- ]:
- bpy.ops.object.transform_apply()
- else:
- # only apply scaling operations to those objects
- # all other operations would reset them since they are empties
- bpy.ops.object.transform_apply(
- location=False, rotation=False, scale=True, properties=False
- )
+ all_objs = list(collection.objects)
+ special_types = (BlenderNodeType.DOF, BlenderNodeType.SLOT, BlenderNodeType.HOTSPOT)
+ transform_epsilon = 1e-7
+
+ def _stage(stage_name):
+ return export_profiler.stage(stage_name) if export_profiler else nullcontext()
+
+ def _vector_nearly_equal(vector, expected):
+ return all(abs(vector[index] - expected[index]) <= transform_epsilon for index in range(3))
+
+ def _matrix_nearly_identity(matrix):
+ for row_index in range(4):
+ for column_index in range(4):
+ expected = 1.0 if row_index == column_index else 0.0
+ if abs(matrix[row_index][column_index] - expected) > transform_epsilon:
+ return False
+ return True
+
+ def _needs_full_transform_apply(obj):
+ return not _matrix_nearly_identity(obj.matrix_basis)
+
+ def _needs_scale_transform_apply(obj):
+ return not (
+ _vector_nearly_equal(obj.scale, (1.0, 1.0, 1.0))
+ and _vector_nearly_equal(obj.delta_scale, (1.0, 1.0, 1.0))
+ )
- for child in obj.children:
- apply_all_modifiers_on_obj(child)
+ def _hierarchy_levels(objects):
+ object_names = {obj.name for obj in objects}
+ levels = []
+ visited = set()
+
+ def add_obj(obj, depth):
+ if obj.name in visited or obj.name not in object_names:
+ return
+ visited.add(obj.name)
+ while len(levels) <= depth:
+ levels.append([])
+ levels[depth].append(obj)
+ for child in obj.children:
+ add_obj(child, depth + 1)
+
+ for obj in objects:
+ if obj.parent is None or obj.parent.name not in object_names:
+ add_obj(obj, 0)
+
+ for obj in objects:
+ add_obj(obj, 0)
+
+ return levels
+
+ def _batch_transform_apply(objects, **kwargs):
+ if not objects:
+ return
+ bpy.ops.object.select_all(action="DESELECT")
+ for obj in objects:
+ obj.select_set(True)
+ bpy.context.view_layer.objects.active = objects[0]
+ bpy.ops.object.transform_apply(**kwargs)
+
+ with export_profiler.stage("modifier application: apply modifiers") if export_profiler else nullcontext():
+ with _stage("modifier application: batch mesh convert"):
+ mesh_objs = [obj for obj in all_objs if obj.type == "MESH"]
+ bpy.ops.object.select_all(action="DESELECT")
+ for obj in mesh_objs:
+ obj.select_set(True)
+ if mesh_objs:
+ bpy.context.view_layer.objects.active = mesh_objs[0]
+ bpy.ops.object.mode_set(mode="OBJECT")
+ bpy.ops.object.convert(target="MESH", keep_original=False)
+
+ # Store reference points after convert (modifiers resolved) but before
+ # transform_apply zeroes the location.
+ with _stage("modifier application: store reference points"):
+ for obj in all_objs:
+ if obj.type == "MESH" and get_bml_type(obj) not in special_types:
+ obj["bms_reference_point"] = tuple(obj.location)
+
+ regular_applied = 0
+ regular_skipped = 0
+ regular_batches = 0
+ special_applied = 0
+ special_skipped = 0
+ special_batches = 0
+
+ for level_objs in _hierarchy_levels(all_objs):
+ regular_objs = []
+ special_objs = []
+
+ for obj in level_objs:
+ if get_bml_type(obj) in special_types:
+ if _needs_scale_transform_apply(obj):
+ special_objs.append(obj)
+ else:
+ special_skipped += 1
+ elif _needs_full_transform_apply(obj):
+ regular_objs.append(obj)
+ else:
+ regular_skipped += 1
+
+ if regular_objs:
+ with _stage("modifier application: batch regular transforms"):
+ _batch_transform_apply(regular_objs)
+ regular_applied += len(regular_objs)
+ regular_batches += 1
+
+ if special_objs:
+ with _stage("modifier application: batch special scale transforms"):
+ _batch_transform_apply(
+ special_objs,
+ location=False,
+ rotation=False,
+ scale=True,
+ properties=False,
+ )
+ special_applied += len(special_objs)
+ special_batches += 1
+
+ if regular_objs or special_objs:
+ bpy.context.view_layer.update()
+
+ print(
+ "[BML Export] Transform apply batches: "
+ f"{regular_batches} regular / {special_batches} special; "
+ f"objects applied: {regular_applied} regular / {special_applied} special; "
+ f"skipped: {regular_skipped} regular / {special_skipped} special"
+ )
def uncompress_file(src, dest):
diff --git a/bms_blender_plugin/exporter/__init__.py b/bms_blender_plugin/exporter/__init__.py
index e69de29..5d2f9a4 100644
--- a/bms_blender_plugin/exporter/__init__.py
+++ b/bms_blender_plugin/exporter/__init__.py
@@ -0,0 +1,9 @@
+from . import validation_dialogs
+
+
+def register():
+ validation_dialogs.register()
+
+
+def unregister():
+ validation_dialogs.unregister()
\ No newline at end of file
diff --git a/bms_blender_plugin/exporter/bml_mesh.py b/bms_blender_plugin/exporter/bml_mesh.py
index 7bcd93a..08bad88 100644
--- a/bms_blender_plugin/exporter/bml_mesh.py
+++ b/bms_blender_plugin/exporter/bml_mesh.py
@@ -1,6 +1,7 @@
import bpy
import bmesh
import math
+from contextlib import nullcontext
from mathutils import Vector
from bms_blender_plugin.common.bml_structs import (
@@ -18,21 +19,22 @@
from bms_blender_plugin.common.coordinates import to_bms_coords
-def get_bml_mesh_data(obj, max_vertex_index):
+def get_bml_mesh_data(obj, max_vertex_index, export_profiler=None):
"""Returns the raw mesh data in the BML format as a tuple of vertices and vertex indices"""
mesh = obj.data
- bm = bmesh.new()
- bm.from_mesh(mesh)
+ with export_profiler.stage("mesh extraction: triangulate") if export_profiler else nullcontext():
+ bm = bmesh.new()
+ bm.from_mesh(mesh)
- bmesh.ops.triangulate(bm, faces=bm.faces[:])
- bm.to_mesh(mesh)
- bm.free()
+ bmesh.ops.triangulate(bm, faces=bm.faces[:])
+ bm.to_mesh(mesh)
+ bm.free()
- uv_names = [uvlayer.name for uvlayer in mesh.uv_layers]
if len(mesh.loops) > 0:
- mesh.calc_normals()
- for name in uv_names:
- mesh.calc_tangents(uvmap=name)
+ with export_profiler.stage("mesh extraction: normals/tangents") if export_profiler else nullcontext():
+ mesh.calc_normals()
+ if mesh.uv_layers.active:
+ mesh.calc_tangents(uvmap=mesh.uv_layers.active.name)
pb_vertices = []
pb_vertices_per_face = []
@@ -64,56 +66,59 @@ def get_bml_mesh_data(obj, max_vertex_index):
world_normal = world_coord.inverted_safe().transposed().to_3x3()
- for face in mesh.polygons:
- # loop over face loop
- for vert in [mesh.loops[i] for i in face.loop_indices]:
- vertex_pbr = VertexPBR()
- vertex_index = vert.vertex_index
- vertex_indices.append(vert.index + max_vertex_index)
- # position
- object_global_coord = to_bms_coords(
- world_coord @ mesh.vertices[vertex_index].co
- )
- vertex_pbr.position = Vector3(
- object_global_coord.x, object_global_coord.y, object_global_coord.z
- )
- # normal
- object_global_normal = to_bms_coords(world_normal @ vert.normal)
- # normalize the vector to remove any rounding errors
- object_global_normal = object_global_normal.normalized()
- vertex_pbr.normal = Vector3(
- object_global_normal.x, object_global_normal.y, object_global_normal.z
- )
+ active_uv_layer = mesh.uv_layers.active.data if mesh.uv_layers.active else None
- # tangent & uv
- if mesh.uv_layers.active:
- object_global_tangent = to_bms_coords(vert.tangent)
- vertex_pbr.tangent = Vector3(
- object_global_tangent.x,
- object_global_tangent.y,
- object_global_tangent.z,
+ with export_profiler.stage("mesh extraction: build vertices") if export_profiler else nullcontext():
+ for face in mesh.polygons:
+ # loop over face loop
+ for vert in [mesh.loops[i] for i in face.loop_indices]:
+ vertex_pbr = VertexPBR()
+ vertex_index = vert.vertex_index
+ vertex_indices.append(vert.index + max_vertex_index)
+
+ # position
+ object_global_coord = to_bms_coords(
+ world_coord @ mesh.vertices[vertex_index].co
+ )
+ vertex_pbr.position = Vector3(
+ object_global_coord.x, object_global_coord.y, object_global_coord.z
)
- vertex_pbr.handedness = vert.bitangent_sign
- uv = tuple(
- to_bms_coords(tuple(mesh.uv_layers.active.data[vert.index].uv))
+ # normal
+ object_global_normal = to_bms_coords(world_normal @ vert.normal)
+ # normalize the vector to remove any rounding errors
+ object_global_normal = object_global_normal.normalized()
+ vertex_pbr.normal = Vector3(
+ object_global_normal.x, object_global_normal.y, object_global_normal.z
)
- vertex_pbr.uv = Vector2(uv[0], uv[1])
- pb_vertices_per_face.append(vertex_pbr)
+ # tangent & uv
+ if active_uv_layer:
+ object_global_tangent = to_bms_coords(vert.tangent)
+ vertex_pbr.tangent = Vector3(
+ object_global_tangent.x,
+ object_global_tangent.y,
+ object_global_tangent.z,
+ )
+ vertex_pbr.handedness = vert.bitangent_sign
+
+ uv = tuple(to_bms_coords(tuple(active_uv_layer[vert.index].uv)))
+ vertex_pbr.uv = Vector2(uv[0], uv[1])
- # switch the handedness by swapping the vertices
- pb_vertices.append(pb_vertices_per_face[0])
- pb_vertices.append(pb_vertices_per_face[2])
- pb_vertices.append(pb_vertices_per_face[1])
- pb_vertices_per_face = []
+ pb_vertices_per_face.append(vertex_pbr)
+
+ # switch the handedness by swapping the vertices
+ pb_vertices.append(pb_vertices_per_face[0])
+ pb_vertices.append(pb_vertices_per_face[2])
+ pb_vertices.append(pb_vertices_per_face[1])
+ pb_vertices_per_face = []
return {"vertices": pb_vertices, "vertex_indices": vertex_indices}
-def get_pbr_light_data(obj, max_vertex_index):
+def get_pbr_light_data(obj, max_vertex_index, export_profiler=None):
"""Returns the BML specific data for a PBR billboard light (BBL)"""
# lookup table for the signs of the uv2 coords in a rectangle
uv2_sign_lookup = [(1, 1), (-1, 1), (-1, -1), (1, -1)]
@@ -130,87 +135,88 @@ def get_pbr_light_data(obj, max_vertex_index):
vertex_index = max_vertex_index
- for face in mesh.polygons:
- # loop over face loop
-
- if len(face.vertices) != 4:
- raise Exception("BBLights can only consist of rectangular planes")
-
- # calculate width and height
- face_width = (
- mesh.vertices[face.vertices[0]].co - mesh.vertices[face.vertices[1]].co
- ).length
- face_height = (
- mesh.vertices[face.vertices[1]].co - mesh.vertices[face.vertices[2]].co
- ).length
-
- # load the stored colors and normals from the polygon layers
- color = (
- mesh.polygon_layers_float["bml_color_r"].data[face.index].value,
- mesh.polygon_layers_float["bml_color_g"].data[face.index].value,
- mesh.polygon_layers_float["bml_color_b"].data[face.index].value,
- mesh.polygon_layers_float["bml_color_a"].data[face.index].value,
- )
-
- normal = Vector(
- (
- mesh.polygon_layers_float["bml_normal_x"].data[face.index].value,
- mesh.polygon_layers_float["bml_normal_y"].data[face.index].value,
- mesh.polygon_layers_float["bml_normal_z"].data[face.index].value,
+ with export_profiler.stage("mesh extraction: build light vertices") if export_profiler else nullcontext():
+ for face in mesh.polygons:
+ # loop over face loop
+
+ if len(face.vertices) != 4:
+ raise Exception("BBLights can only consist of rectangular planes")
+
+ # calculate width and height
+ face_width = (
+ mesh.vertices[face.vertices[0]].co - mesh.vertices[face.vertices[1]].co
+ ).length
+ face_height = (
+ mesh.vertices[face.vertices[1]].co - mesh.vertices[face.vertices[2]].co
+ ).length
+
+ # load the stored colors and normals from the polygon layers
+ color = (
+ mesh.polygon_layers_float["bml_color_r"].data[face.index].value,
+ mesh.polygon_layers_float["bml_color_g"].data[face.index].value,
+ mesh.polygon_layers_float["bml_color_b"].data[face.index].value,
+ mesh.polygon_layers_float["bml_color_a"].data[face.index].value,
)
- )
-
- current_light_position = world_coord @ face.center
-
- # the normal will already be set to [0, 0, 0] for omnidirectional lights by join_objects_with_same_materials()
- current_light_normal = to_bms_coords(world_normal @ normal)
- # normalize the vector to remove any rounding errors
- current_light_normal = current_light_normal.normalized()
- # for each poly, add two triangles (== 6 vertices)
- # blender iterates counter-clockwise, so the following vertex indices will form 2 adjacent triangles of a
- # rectangular poly
+ normal = Vector(
+ (
+ mesh.polygon_layers_float["bml_normal_x"].data[face.index].value,
+ mesh.polygon_layers_float["bml_normal_y"].data[face.index].value,
+ mesh.polygon_layers_float["bml_normal_z"].data[face.index].value,
+ )
+ )
- for i in [1, 0, 3, 1, 3, 2]:
- vs_input_light = VSInputLight()
- vertex_indices.append(vertex_index)
+ current_light_position = world_coord @ face.center
- # the position is identical for all vertices of a BBL
- current_light_position_bms_coords = to_bms_coords(current_light_position)
- vs_input_light.position = Vector3(
- current_light_position_bms_coords.x,
- current_light_position_bms_coords.y,
- current_light_position_bms_coords.z,
- )
+ # the normal will already be set to [0, 0, 0] for omnidirectional lights by join_objects_with_same_materials()
+ current_light_normal = to_bms_coords(world_normal @ normal)
+ # normalize the vector to remove any rounding errors
+ current_light_normal = current_light_normal.normalized()
+
+ # for each poly, add two triangles (== 6 vertices)
+ # blender iterates counter-clockwise, so the following vertex indices will form 2 adjacent triangles of a
+ # rectangular poly
+
+ for i in [1, 0, 3, 1, 3, 2]:
+ vs_input_light = VSInputLight()
+ vertex_indices.append(vertex_index)
+
+ # the position is identical for all vertices of a BBL
+ current_light_position_bms_coords = to_bms_coords(current_light_position)
+ vs_input_light.position = Vector3(
+ current_light_position_bms_coords.x,
+ current_light_position_bms_coords.y,
+ current_light_position_bms_coords.z,
+ )
- # ... same as the normal
- vs_input_light.normal = Vector3(
- current_light_normal.x, current_light_normal.y, current_light_normal.z
- )
+ # ... same as the normal
+ vs_input_light.normal = Vector3(
+ current_light_normal.x, current_light_normal.y, current_light_normal.z
+ )
- # ... and its color
- color_bytes = (
- (from_blender_color(color[3]) << 24)
- + (from_blender_color(color[2]) << 16)
- + (from_blender_color(color[1]) << 8)
- + (from_blender_color(color[0]) << 0)
- )
- vs_input_light.color = color_bytes
+ # ... and its color
+ color_bytes = (
+ (from_blender_color(color[3]) << 24)
+ + (from_blender_color(color[2]) << 16)
+ + (from_blender_color(color[1]) << 8)
+ + (from_blender_color(color[0]) << 0)
+ )
+ vs_input_light.color = color_bytes
- # uv1 - just a regular texture uv
- if mesh.uv_layers.active:
- uv = tuple(to_bms_coords(tuple(mesh.uv_layers.active.data[i].uv)))
- vs_input_light.uv1 = Vector2(uv[0], uv[1])
+ # uv1 - just a regular texture uv
+ if mesh.uv_layers.active:
+ uv = tuple(to_bms_coords(tuple(mesh.uv_layers.active.data[i].uv)))
+ vs_input_light.uv1 = Vector2(uv[0], uv[1])
- # uv2 - extrude the 4 corners of the vertex from the center point as origin
- uv2_signs = uv2_sign_lookup[i]
- uv2 = Vector(
- (uv2_signs[0] * face_width / 2, uv2_signs[1] * face_height / 2)
- )
- vs_input_light.uv2 = Vector2(uv2[0], uv2[1])
+ # uv2 - extrude the 4 corners of the vertex from the center point as origin
+ uv2_signs = uv2_sign_lookup[i]
+ uv2 = Vector(
+ (uv2_signs[0] * face_width / 2, uv2_signs[1] * face_height / 2)
+ )
+ vs_input_light.uv2 = Vector2(uv2[0], uv2[1])
- bbl_vertices.append(vs_input_light)
- vertex_index += 1
+ bbl_vertices.append(vs_input_light)
+ vertex_index += 1
return {"vertices": bbl_vertices, "vertex_indices": vertex_indices}
diff --git a/bms_blender_plugin/exporter/bml_output.py b/bms_blender_plugin/exporter/bml_output.py
index 473d106..cf153f6 100644
--- a/bms_blender_plugin/exporter/bml_output.py
+++ b/bms_blender_plugin/exporter/bml_output.py
@@ -1,5 +1,6 @@
import datetime
import math
+from time import perf_counter
import bpy
@@ -17,6 +18,10 @@
)
from bms_blender_plugin.exporter.export_parent_dat import get_slots, export_parent_dat
from bms_blender_plugin.exporter.export_bounding_boxes import export_bounding_boxes
+from bms_blender_plugin.exporter.export_profiler import ExportProfiler
+from bms_blender_plugin.exporter.export_validation import (
+ show_validation_dialog_export,
+)
from mathutils import Vector
@@ -30,7 +35,17 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo
* A single 3dButtons.dat
"""
+ # PRE-FLIGHT VALIDATION: Check only the export scope (derived from LODs or active collection)
+ print("Validating scene...\n")
+
+ if show_validation_dialog_export(context, lods=lods):
+ # A dialog was invoked; cancel export and let the user resolve, then retry
+ print("Export cancelled")
+ return "Export cancelled by user", []
+
start_time = datetime.datetime.now()
+ start_perf_counter = perf_counter()
+ export_profiler = ExportProfiler()
print(f"Starting BML export at {start_time}\n")
# blender uses meters as base unit, BMS works in feet
@@ -56,7 +71,10 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo
"""
# Gather all bounding boxes into an array of bounding_box objects.
- BBox_Array = [BoundingBox(obj) for obj in context.scene.objects if get_bml_type(obj) == BlenderNodeType.BBOX]
+ with export_profiler.stage("scene: gather bounding boxes"):
+ BBox_Array = [
+ BoundingBox(obj) for obj in context.scene.objects if get_bml_type(obj) == BlenderNodeType.BBOX
+ ]
"""
Get the first bounding box min and max coordinates. A bit arbitrary at this point,
but in instances of a single bbox, no harm and I suspect that these will be in named order.
@@ -81,16 +99,18 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo
elif len(lods) == 0:
raise Exception("No active collection and no LODs - can not export")
- all_exported_bmls, all_material_names, all_hotspots = export_lods(
- context, file_directory, file_prefix, lods, scale_factor, export_settings
- )
+ with export_profiler.stage("scene: export lods"):
+ all_exported_bmls, all_material_names, all_hotspots = export_lods(
+ context, file_directory, file_prefix, lods, scale_factor, export_settings, export_profiler
+ )
if export_settings.export_materials_file or export_settings.export_textures:
- export_materials(
- all_material_names,
- file_directory,
- export_settings,
- )
+ with export_profiler.stage("scene: export materials"):
+ export_materials(
+ all_material_names,
+ file_directory,
+ export_settings,
+ )
if export_settings.export_materials_sets and len(context.scene.bml_material_sets) > 1:
number_of_texture_sets = len(context.scene.bml_material_sets)
@@ -98,27 +118,30 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo
number_of_texture_sets = 1
if export_settings.export_parent_dat:
- export_parent_dat(
- context,
- file_directory,
- file_prefix,
- bounding_box_1_min_coords,
- bounding_box_1_max_coords,
- scale_factor,
- number_of_texture_sets,
- get_slots(context.scene),
- lods
- )
+ with export_profiler.stage("scene: export parent.dat"):
+ export_parent_dat(
+ context,
+ file_directory,
+ file_prefix,
+ bounding_box_1_min_coords,
+ bounding_box_1_max_coords,
+ scale_factor,
+ number_of_texture_sets,
+ get_slots(context.scene),
+ lods
+ )
if export_settings.export_hotspots:
- export_hotspots(all_hotspots, file_directory)
+ with export_profiler.stage("scene: export hotspots"):
+ export_hotspots(all_hotspots, file_directory)
# If there is more than one bounding box defined, output all to a file.
if len(BBox_Array) > 1:
- export_bounding_boxes(BBox_Array, file_directory)
+ with export_profiler.stage("scene: export bounding boxes"):
+ export_bounding_boxes(BBox_Array, file_directory)
- elapsed = datetime.datetime.now() - start_time
+ elapsed = datetime.timedelta(seconds=perf_counter() - start_perf_counter)
elapsed_minutes = divmod(elapsed.total_seconds(), 60)
success_message = (
@@ -128,4 +151,6 @@ def export_bml(context, lods, file_directory, file_prefix, export_settings: Expo
print(success_message)
+ if export_profiler.has_records():
+ print(export_profiler.format_summary())
return success_message, all_exported_bmls
diff --git a/bms_blender_plugin/exporter/export_lods.py b/bms_blender_plugin/exporter/export_lods.py
index 781ed96..36d05e7 100644
--- a/bms_blender_plugin/exporter/export_lods.py
+++ b/bms_blender_plugin/exporter/export_lods.py
@@ -1,5 +1,16 @@
+"""
+Performance Notes:
+- Modifier application was the dominant cost (~92% of export time): per-object
+ bpy.ops calls each trigger a full depsgraph evaluation. apply_all_modifiers()
+ now batches convert + transform_apply into O(1) operator calls regardless of
+ scene size.
+- Material batching gives ~10-12% improvement in DOF/switch-heavy scenes.
+- Further perf improvements: batch DOF processing, reduce object selection calls.
+"""
+
import os
import struct
+from contextlib import nullcontext
import bpy
@@ -35,7 +46,7 @@
def export_lods(
- context, file_directory, file_prefix, lod_list, scale_factor, export_settings: ExportSettings
+ context, file_directory, file_prefix, lod_list, scale_factor, export_settings: ExportSettings, export_profiler=None
):
"""Exports multiple LODs to single *.bml files and their material sets to *.mti files.
Returns a list of exported files, a list of all material names and
@@ -49,7 +60,7 @@ def export_lods(
bml_file_path = os.path.join(file_directory, file_prefix + lod.file_suffix + ".bml")
material_names, hotspots = export_single_collection(
- context, lod.collection, scale_factor, export_settings, bml_file_path
+ context, lod.collection, scale_factor, export_settings, bml_file_path, export_profiler
)
material_set_filepath = bml_file_path.replace(".bml", ".mti")
@@ -72,59 +83,68 @@ def export_lods(
def export_single_collection(
- context, collection, scale_factor, export_settings: ExportSettings, file_path
+ context, collection, scale_factor, export_settings: ExportSettings, file_path, export_profiler=None
):
"""Exports a single Blender collection to a BML file."""
# create a temporary collection and copy the current collection's visible objects into it
collection_copy_root = bpy.data.collections.new(collection.name + "_export")
bpy.context.scene.collection.children.link(collection_copy_root)
- copy_collection_flat(
- collection,
- collection_copy_root,
- [collection_copy_root],
- scale_factor,
- )
+ with export_profiler.stage("lod: copy collection") if export_profiler else nullcontext():
+ copy_collection_flat(
+ collection,
+ collection_copy_root,
+ [collection_copy_root],
+ scale_factor,
+ export_profiler,
+ )
- apply_all_modifiers(collection_copy_root)
+ with export_profiler.stage("lod: apply modifiers") if export_profiler else nullcontext():
+ apply_all_modifiers(collection_copy_root, export_profiler)
# make sure we are on the base texture set
- revert_to_base_material_set(context, collection_copy_root)
+ with export_profiler.stage("lod: revert material set") if export_profiler else nullcontext():
+ revert_to_base_material_set(context, collection_copy_root)
# get the data of the root collection
- nodes_output = get_nodes(
- context,
- collection_copy_root,
- export_settings.script,
- export_settings.auto_smooth_value,
- )
+ with export_profiler.stage("lod: build payload") if export_profiler else nullcontext():
+ nodes_output = get_nodes(
+ context,
+ collection_copy_root,
+ export_settings.script,
+ export_settings.auto_smooth_value,
+ export_profiler,
+ )
payload = nodes_output["data"]
material_names = nodes_output["material_names"]
hotspots = nodes_output["hotspots"]
payload_size = len(payload)
- if export_settings.compression == Compression.NONE:
- payload_compressed_size = payload_size
- elif export_settings.compression == Compression.LZ_4:
- payload = compress_lz_4(payload)
- payload_compressed_size = len(payload)
- elif export_settings.compression == Compression.LZMA:
- payload = compress_lzma(payload)
- payload_compressed_size = len(payload)
- else:
- raise Exception("Unknown compression exception")
+ with export_profiler.stage("lod: final compression") if export_profiler else nullcontext():
+ if export_settings.compression == Compression.NONE:
+ payload_compressed_size = payload_size
+ elif export_settings.compression == Compression.LZ_4:
+ payload = compress_lz_4(payload)
+ payload_compressed_size = len(payload)
+ elif export_settings.compression == Compression.LZMA:
+ payload = compress_lzma(payload)
+ payload_compressed_size = len(payload)
+ else:
+ raise Exception("Unknown compression exception")
- header = Header(
- 2, payload_size, payload_compressed_size, export_settings.compression
- )
- data = header.to_data() + payload
+ with export_profiler.stage("lod: assemble file") if export_profiler else nullcontext():
+ header = Header(
+ 2, payload_size, payload_compressed_size, export_settings.compression
+ )
+ data = header.to_data() + payload
if export_settings.export_models:
- with open(file_path, "wb") as bml_file:
- bml_file.write(data)
- print(
- f"Finished exporting LOD with {nodes_output['nodes_amount']} nodes to {file_path}...\n"
- )
+ with export_profiler.stage("lod: write file") if export_profiler else nullcontext():
+ with open(file_path, "wb") as bml_file:
+ bml_file.write(data)
+ print(
+ f"Finished exporting LOD with {nodes_output['nodes_amount']} nodes to {file_path}...\n"
+ )
# delete the copied collection and its children
if (
@@ -133,19 +153,21 @@ def export_single_collection(
"bms_blender_plugin"
].preferences.do_not_delete_export_collection
):
- for obj in collection_copy_root.objects:
- bpy.data.objects.remove(obj, do_unlink=True)
- bpy.data.collections.remove(collection_copy_root)
+ with export_profiler.stage("lod: cleanup temp collection") if export_profiler else nullcontext():
+ for obj in collection_copy_root.objects:
+ bpy.data.objects.remove(obj, do_unlink=True)
+ bpy.data.collections.remove(collection_copy_root)
return material_names, hotspots
-def get_nodes(context, root_collection, script, auto_smooth_value):
+def get_nodes(context, root_collection, script, auto_smooth_value, export_profiler=None):
"""Recursively builds the BML node list for a given collection with all of its elements
(refer to the BMLv2 format definition).
Returns a triple of the nodes in binary format, the material list and the amount of nodes
"""
material_names = []
+ material_lookup = {}
nodes = []
current_vertices_index = 0
current_vertices_size = 0
@@ -170,9 +192,10 @@ def _recursively_parse_nodes(objects):
):
prepared_objects = objects
else:
- prepared_objects = join_objects_with_same_materials(
- objects, dict(), auto_smooth_value
- )
+ with export_profiler.stage("nodes: join by material") if export_profiler else nullcontext():
+ prepared_objects = join_objects_with_same_materials(
+ objects, dict(), auto_smooth_value
+ )
# parse all objects of the current collection
for obj in prepared_objects:
@@ -183,8 +206,10 @@ def _recursively_parse_nodes(objects):
nodes,
vertex_indices,
material_names,
+ material_lookup,
current_vertices_index,
current_vertices_size,
+ export_profiler,
)
elif get_bml_type(obj) == BlenderNodeType.PBR_LIGHT:
@@ -193,8 +218,10 @@ def _recursively_parse_nodes(objects):
nodes,
vertex_indices,
material_names,
+ material_lookup,
current_vertices_index,
current_vertices_size,
+ export_profiler,
)
elif get_bml_type(obj) == BlenderNodeType.SLOT: # Slots can be empty
@@ -211,7 +238,7 @@ def _recursively_parse_nodes(objects):
# end of parsing, append parsed data to the nodes list
if parsed_nodes:
- vertices_data.extend(parsed_nodes.vertex_data)
+ vertices_data.append(parsed_nodes.vertex_data)
current_vertices_index += parsed_nodes.vertices_length
current_vertices_size += parsed_nodes.vertices_size
@@ -250,44 +277,51 @@ def _recursively_parse_nodes(objects):
script_no = int(script)
material_count = len(material_names)
- data = struct.pack(" 0:
if get_bml_type(obj) == BlenderNodeType.SWITCH:
try:
- switch = get_switches()[obj.switch_list_index]
- """parent.dat requires max(switch)+1 to function correctly due to a = vs <= issue in the BMS code.
- This Should be resolved for 4.38."""
- required_switch_index = switch.switch_number+1
- if required_switch_index > highest_switch_number:
- highest_switch_number = required_switch_index
- except IndexError:
- raise IndexError(f"Switch index {obj.switch_list_index} not found in switch.xml. Object: {obj.name}. Please update XML files and reload switch list.")
+ switch_number, _branch = resolve_switch_id(obj)
+ except Exception:
+ switch_number = None
+ if switch_number is None:
+ # legacy fallback
+ try:
+ sw = get_switches()[obj.switch_list_index]
+ switch_number = sw.switch_number
+ except Exception:
+ switch_number = 0
+ required_switch_index = switch_number + 1 # parent.dat off-by-one requirement
+ if required_switch_index > highest_switch_number:
+ highest_switch_number = required_switch_index
elif get_bml_type(obj) == BlenderNodeType.DOF:
try:
- dof = get_dofs()[obj.dof_list_index]
- """parent.dat requires max(dof)+1 to function correctly due to a = vs <= issue in the BMS code.
- This Should be resolved for 4.38."""
- required_dof_index = dof.dof_number+1
- if required_dof_index > highest_dof_number:
- highest_dof_number = required_dof_index
- except IndexError:
- raise IndexError(f"DOF index {obj.dof_list_index} not found in dof.xml. Object: {obj.name}. Please update XML files and reload DOF list.")
+ dof_number = resolve_dof_number(obj)
+ except Exception:
+ dof_number = None
+ if dof_number is None:
+ try:
+ dof_enum = get_dofs()[obj.dof_list_index]
+ dof_number = dof_enum.dof_number
+ except Exception:
+ dof_number = 0
+ required_dof_index = dof_number + 1
+ if required_dof_index > highest_dof_number:
+ highest_dof_number = required_dof_index
# Cap values at BMS maximum
- highest_switch_number = min(highest_switch_number, BMS_MAX_VALUE)
- highest_dof_number = min(highest_dof_number, BMS_MAX_VALUE)
+ highest_switch_number = min(highest_switch_number, BMS_MAX_SWITCH_NUMBER)
+ highest_dof_number = min(highest_dof_number, BMS_MAX_DOF_NUMBER)
return highest_switch_number, highest_dof_number
diff --git a/bms_blender_plugin/exporter/export_profiler.py b/bms_blender_plugin/exporter/export_profiler.py
new file mode 100644
index 0000000..2ae61e4
--- /dev/null
+++ b/bms_blender_plugin/exporter/export_profiler.py
@@ -0,0 +1,34 @@
+from collections import OrderedDict
+from contextlib import contextmanager
+from time import perf_counter
+
+
+class ExportProfiler:
+ """Collects stage-level export timings."""
+
+ def __init__(self):
+ self._durations = OrderedDict()
+
+ @contextmanager
+ def stage(self, stage_name):
+ start_time = perf_counter()
+ try:
+ yield
+ finally:
+ self.add_duration(stage_name, perf_counter() - start_time)
+
+ def add_duration(self, stage_name, duration_seconds):
+ current_duration = self._durations.get(stage_name, 0.0)
+ self._durations[stage_name] = current_duration + duration_seconds
+
+ def has_records(self):
+ return len(self._durations) > 0
+
+ def format_summary(self):
+ total_duration = sum(self._durations.values())
+ lines = ["Export timing summary:"]
+ for stage_name, duration_seconds in self._durations.items():
+ percent = (duration_seconds / total_duration * 100) if total_duration else 0.0
+ lines.append(f" {stage_name}: {duration_seconds:.3f}s ({percent:.1f}%)")
+ lines.append(f" total tracked: {total_duration:.3f}s")
+ return "\n".join(lines)
diff --git a/bms_blender_plugin/exporter/export_render_controls.py b/bms_blender_plugin/exporter/export_render_controls.py
index 554f54b..31dd543 100644
--- a/bms_blender_plugin/exporter/export_render_controls.py
+++ b/bms_blender_plugin/exporter/export_render_controls.py
@@ -11,6 +11,7 @@
)
from bms_blender_plugin.common.blender_types import BlenderEditorNodeType, BlenderNodeTreeType
from bms_blender_plugin.common.util import get_dofs
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number
from bms_blender_plugin.nodes_editor.dof_editor import (
update_node_links,
)
@@ -69,9 +70,16 @@ def get_render_control_nodes(node_start_index=0):
(ArgType.DOF_ID, render_control_node.arguments[0].type.argument_id)
)
result_type = ArgType.DOF_ID
- result_id = get_dofs()[
- render_control_node.parent_dof.dof_list_index
- ].dof_number
+ try:
+ result_id = resolve_dof_number(render_control_node.parent_dof)
+ except Exception:
+ result_id = None
+ if result_id is None:
+ # legacy fallback to list index
+ try:
+ result_id = get_dofs()[render_control_node.parent_dof.dof_list_index].dof_number
+ except Exception:
+ result_id = 0
elif render_control_node.arguments[0].type.argument_type == ArgType.SCRATCH_VARIABLE_ID:
# the DOF node receives its data from a scratch variable - create a "SET" RC for it
math_op = MathOp.SET
@@ -79,9 +87,15 @@ def get_render_control_nodes(node_start_index=0):
(ArgType.SCRATCH_VARIABLE_ID, render_control_node.arguments[0].type.argument_id)
)
result_type = ArgType.DOF_ID
- result_id = get_dofs()[
- render_control_node.parent_dof.dof_list_index
- ].dof_number
+ try:
+ result_id = resolve_dof_number(render_control_node.parent_dof)
+ except Exception:
+ result_id = None
+ if result_id is None:
+ try:
+ result_id = get_dofs()[render_control_node.parent_dof.dof_list_index].dof_number
+ except Exception:
+ result_id = 0
elif render_control_node.arguments[0].type.argument_type == ArgType.DOF_ID:
# the DOF node receives its data directly from an RC with a target DOF - nothing to do
diff --git a/bms_blender_plugin/exporter/export_validation.py b/bms_blender_plugin/exporter/export_validation.py
new file mode 100644
index 0000000..299c754
--- /dev/null
+++ b/bms_blender_plugin/exporter/export_validation.py
@@ -0,0 +1,360 @@
+"""
+Export validation module for pre-flight checks before BML export.
+
+Validates scene state to catch common issues that could cause export failures
+or silent data corruption, providing clear feedback and resolution options.
+Import LodItem type.
+
+To add a new validation check:
+1. Add an issue type to ValidationIssueType enum if required
+2. Add a grouping property to ValidationIssue dataclass
+3. Add a new _check_*_issues() method to ExportValidator class
+4. Call your new method from validate_scene()
+
+...for issues that need user input for resolution (not just collecting stats):
+5. Add filter function(s) like get_*_issues() if resolution of the issue needs special dialog handling
+6. Create dialog operator in validation_dialogs.py if needed - eg. user choice required for resolution
+7. Update run_validation_with_dialogs() to handle your new issue type with dialogs
+
+Example: Adding a "missing material" validation check:
+# Step 1: Add to ValidationIssueType enum
+MATERIAL_MISSING = "material_missing"
+
+# Step 2: Add grouping property to ValidationIssue
+@property
+def is_material_issue(self) -> bool:
+ return self.issue_type == ValidationIssueType.MATERIAL_MISSING
+
+# Step 3: Add validation method to ExportValidator
+def _check_material_issues(self, context) -> List[ValidationIssue]:
+ issues = []
+ for obj in context.scene.objects:
+ if not obj.material_slots:
+ issues.append(ValidationIssue(
+ ValidationIssueType.MATERIAL_MISSING,
+ [obj],
+ f"Object '{obj.name}' has no materials assigned"
+ ))
+ return issues
+
+# Step 4: Call from validate_scene()
+issues.extend(self._check_material_issues(context))
+
+# Steps 5-7: Add dialog handling if needed
+"""
+
+import bpy
+from dataclasses import dataclass
+from typing import List, Optional, Iterable
+from enum import Enum
+
+from bms_blender_plugin.common.blender_types import BlenderNodeType, LodItem
+from bms_blender_plugin.common.util import get_bml_type, get_dofs, get_switches
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number, resolve_switch_id
+
+
+class ValidationIssueType(Enum):
+ """Types of validation issues that can be detected."""
+ DOF_OUT_OF_RANGE = "dof_out_of_range"
+ SWITCH_OUT_OF_RANGE = "switch_out_of_range"
+ DOF_MISSING_PERSISTENT_ID = "dof_missing_persistent_id"
+ SWITCH_MISSING_PERSISTENT_ID = "switch_missing_persistent_id"
+
+
+@dataclass
+class ValidationIssue:
+ """Represents a single validation issue found in the scene."""
+ """Contains properties to identify groups of issue types for batch handling."""
+ issue_type: ValidationIssueType
+ objects: List[bpy.types.Object]
+ description: str
+
+ @property
+ def is_out_of_range_issue(self) -> bool:
+ """True if this is an out-of-range XML reference issue."""
+ return self.issue_type in (
+ ValidationIssueType.DOF_OUT_OF_RANGE,
+ ValidationIssueType.SWITCH_OUT_OF_RANGE
+ )
+
+ @property
+ def is_missing_persistent_id_issue(self) -> bool:
+ """True if this is a missing persistent ID issue."""
+ return self.issue_type in (
+ ValidationIssueType.DOF_MISSING_PERSISTENT_ID,
+ ValidationIssueType.SWITCH_MISSING_PERSISTENT_ID
+ )
+
+
+class ExportValidator:
+ """Validates scene state before export to catch common issues."""
+
+ def validate_scene(self, context, objects: Optional[Iterable[bpy.types.Object]] = None) -> List[ValidationIssue]:
+ """
+ Returns list of validation issues found in scene.
+
+ Add new validation method calls here:
+ issues.extend(self._check_your_new_validation(context))
+ """
+ issues = []
+ issues.extend(self._check_dof_issues(context, objects))
+ issues.extend(self._check_switch_issues(context, objects))
+ # Add new validation checks here
+ return issues
+
+ def _get_dof_max_index(self, context) -> int:
+ """Prefer cached Scene DOF list; fallback to util cache."""
+ try:
+ if hasattr(context.scene, 'dof_list') and len(context.scene.dof_list) > 0:
+ return len(context.scene.dof_list) - 1
+ except Exception:
+ pass
+ try:
+ available_dofs = get_dofs()
+ return len(available_dofs) - 1
+ except Exception:
+ return -1
+
+ def _get_switch_max_index(self, context) -> int:
+ """Prefer cached Scene Switch list; fallback to util cache."""
+ try:
+ if hasattr(context.scene, 'switch_list') and len(context.scene.switch_list) > 0:
+ return len(context.scene.switch_list) - 1
+ except Exception:
+ pass
+ try:
+ available_switches = get_switches()
+ return len(available_switches) - 1
+ except Exception:
+ return -1
+
+ def _iter_target_objects(self, context, objects: Optional[Iterable[bpy.types.Object]]):
+ if objects is not None:
+ # Ensure we iterate once over a stable list
+ return list(objects)
+ return list(context.scene.objects)
+
+ def _check_dof_issues(self, context, objects: Optional[Iterable[bpy.types.Object]]) -> List[ValidationIssue]:
+ """Check for DOF-related validation issues."""
+ issues = []
+ max_dof_index = self._get_dof_max_index(context)
+ if max_dof_index < 0:
+ # No list available; skip checks safely
+ return issues
+
+ out_of_range_objects = []
+ missing_persistent_id_objects = []
+
+ for obj in self._iter_target_objects(context, objects):
+ if get_bml_type(obj) != BlenderNodeType.DOF:
+ continue
+
+ persistent_id = getattr(obj, "bml_dof_number", -1)
+
+ # Use resolver-aligned classification: consistent with export/runtime behavior
+ try:
+ resolved_dof_number = resolve_dof_number(obj)
+ except Exception:
+ resolved_dof_number = None
+
+ if persistent_id < 0:
+ # No persistent ID assigned
+ if resolved_dof_number is None:
+ # Cannot be resolved by any means - truly unresolvable
+ out_of_range_objects.append(obj)
+ else:
+ # Resolvable via scene cache or XML but no persistent ID - migration needed
+ missing_persistent_id_objects.append(obj)
+
+ # Create issues for out-of-range objects
+ if out_of_range_objects:
+ description = (
+ f"Found {len(out_of_range_objects)} DOF(s) that cannot be resolved to valid DOF numbers. "
+ "These objects have no persistent ID and their list indices don't match any XML entries. "
+ "Export will use fallback DOF number 0."
+ )
+ issues.append(ValidationIssue(
+ ValidationIssueType.DOF_OUT_OF_RANGE,
+ out_of_range_objects,
+ description
+ ))
+
+ # Create issues for missing persistent IDs
+ if missing_persistent_id_objects:
+ description = (
+ f"Found {len(missing_persistent_id_objects)} DOF(s) using legacy list indices "
+ "without persistent IDs. These will work for export but may break "
+ "if DOF.xml files are reordered."
+ )
+ issues.append(ValidationIssue(
+ ValidationIssueType.DOF_MISSING_PERSISTENT_ID,
+ missing_persistent_id_objects,
+ description
+ ))
+
+ return issues
+
+ def _check_switch_issues(self, context, objects: Optional[Iterable[bpy.types.Object]]) -> List[ValidationIssue]:
+ """Check for Switch-related validation issues."""
+ issues = []
+ max_switch_index = self._get_switch_max_index(context)
+ if max_switch_index < 0:
+ return issues
+
+ out_of_range_objects = []
+ missing_persistent_id_objects = []
+
+ for obj in self._iter_target_objects(context, objects):
+ if get_bml_type(obj) != BlenderNodeType.SWITCH:
+ continue
+
+ persistent_number = getattr(obj, "bml_switch_number", -1)
+ persistent_branch = getattr(obj, "bml_switch_branch", -1)
+
+ # Use resolver-aligned classification: consistent with export/runtime behavior
+ try:
+ resolved_switch_number, resolved_branch = resolve_switch_id(obj)
+ except Exception:
+ resolved_switch_number, resolved_branch = None, None
+
+ if persistent_number < 0 or persistent_branch < 0:
+ # No persistent ID assigned
+ if resolved_switch_number is None or resolved_branch is None:
+ # Cannot be resolved by any means - truly unresolvable
+ out_of_range_objects.append(obj)
+ else:
+ # Resolvable via scene cache or XML but no persistent ID - migration needed
+ missing_persistent_id_objects.append(obj)
+
+ # Create issues for out-of-range objects
+ if out_of_range_objects:
+ description = (
+ f"Found {len(out_of_range_objects)} Switch(es) that cannot be resolved to valid switch numbers. "
+ "These objects have no persistent IDs and their list indices don't match any XML entries. "
+ "Export will use fallback switch number 0:0."
+ )
+ issues.append(ValidationIssue(
+ ValidationIssueType.SWITCH_OUT_OF_RANGE,
+ out_of_range_objects,
+ description
+ ))
+
+ # Create issues for missing persistent IDs
+ if missing_persistent_id_objects:
+ description = (
+ f"Found {len(missing_persistent_id_objects)} Switch(es) using legacy list indices "
+ "without persistent IDs. These will work for export but may break "
+ "if Switch.xml files are reordered."
+ )
+ issues.append(ValidationIssue(
+ ValidationIssueType.SWITCH_MISSING_PERSISTENT_ID,
+ missing_persistent_id_objects,
+ description
+ ))
+
+ return issues
+
+
+def validate_export_readiness(context, objects: Optional[Iterable[bpy.types.Object]] = None) -> List[ValidationIssue]:
+ """
+ Main entry point for export validation.
+
+ Returns list of validation issues that should be addressed before export.
+ Empty list means scene is ready for export.
+ """
+ validator = ExportValidator()
+ return validator.validate_scene(context, objects)
+
+
+def get_out_of_range_issues(issues: List[ValidationIssue]) -> List[ValidationIssue]:
+ """Filter issues to only out-of-range XML reference problems."""
+ return [issue for issue in issues if issue.is_out_of_range_issue]
+
+
+def get_missing_persistent_id_issues(issues: List[ValidationIssue]) -> List[ValidationIssue]:
+ """Filter issues to only missing persistent ID problems."""
+ return [issue for issue in issues if issue.is_missing_persistent_id_issue]
+
+
+def select_objects_from_issues(issues: List[ValidationIssue]):
+ """Select all objects referenced in the given validation issues."""
+ bpy.ops.object.select_all(action='DESELECT')
+
+ for issue in issues:
+ for obj in issue.objects:
+ obj.select_set(True)
+
+ # Set first object as active if any were selected
+ selected_objects = [obj for issue in issues for obj in issue.objects]
+ if selected_objects:
+ bpy.context.view_layer.objects.active = selected_objects[0]
+
+
+def run_validation_with_dialogs(context) -> bool:
+ """
+ DEPRECATED: Asynchronous dialogs cannot be orchestrated reliably here.
+ Kept for backward compatibility; returns True as a no-op.
+ Use validate_export_readiness() + show_validation_dialog_if_needed() instead.
+ """
+ return True
+
+
+def _collect_objects_from_collection(coll: bpy.types.Collection) -> List[bpy.types.Object]:
+ result = set()
+ def _rec(c):
+ for o in c.objects:
+ result.add(o)
+ for ch in c.children:
+ _rec(ch)
+ _rec(coll)
+ return list(result)
+
+
+def _collect_objects_from_active_collection(context) -> List[bpy.types.Object]:
+ alc = context.view_layer.active_layer_collection
+ coll = alc.collection if alc else None
+ if not coll:
+ return []
+ return _collect_objects_from_collection(coll)
+
+
+def _collect_objects_from_lods(lods: Iterable[LodItem]) -> List[bpy.types.Object]:
+ objs = set()
+ for li in lods:
+ coll = getattr(li, 'collection', None)
+ if coll:
+ for o in _collect_objects_from_collection(coll):
+ objs.add(o)
+ return list(objs)
+
+
+def show_validation_dialog_export(
+ context,
+ objects: Optional[Iterable[bpy.types.Object]] = None,
+ lods: Optional[Iterable[LodItem]] = None,
+) -> bool:
+ """
+ Stateless helper to show the appropriate validation dialog if issues exist.
+ Returns True if a dialog was invoked (export should be cancelled by caller),
+ False if no issues were found.
+ """
+ # Determine the validation scope if not explicitly provided
+ if objects is None:
+ if lods:
+ objects = _collect_objects_from_lods(lods)
+ else:
+ objects = _collect_objects_from_active_collection(context)
+
+ issues = validate_export_readiness(context, objects)
+ if not issues:
+ return False
+ # Prioritize out-of-range (schema mismatches) over missing IDs
+ use_lods_flag = bool(lods)
+ if get_out_of_range_issues(issues):
+ bpy.ops.bml.validation_out_of_range_dialog('INVOKE_DEFAULT', use_lods=use_lods_flag)
+ return True
+ if get_missing_persistent_id_issues(issues):
+ bpy.ops.bml.validation_missing_id_dialog('INVOKE_DEFAULT', use_lods=use_lods_flag)
+ return True
+ return False
+
\ No newline at end of file
diff --git a/bms_blender_plugin/exporter/parser.py b/bms_blender_plugin/exporter/parser.py
index 4ae27bc..b98edb9 100644
--- a/bms_blender_plugin/exporter/parser.py
+++ b/bms_blender_plugin/exporter/parser.py
@@ -1,3 +1,4 @@
+from contextlib import nullcontext
import math
from mathutils import Matrix, Vector
@@ -6,8 +7,8 @@
from bms_blender_plugin.common.bml_structs import Primitive, PrimitiveTopology, Vector3, Slot, D3DMatrix, Switch, \
DofType, Dof
from bms_blender_plugin.common.hotspot import Hotspot, MouseButton, ButtonType
-from bms_blender_plugin.common.util import get_bml_type, get_objcenter, get_switches, get_dofs, \
- get_non_translate_dof_parent
+from bms_blender_plugin.common.util import get_bml_type, get_non_translate_dof_parent
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number, resolve_switch_id
from bms_blender_plugin.exporter.bml_mesh import get_bml_mesh_data, get_pbr_light_data
from bms_blender_plugin.common.coordinates import to_bms_coords
@@ -25,14 +26,23 @@ def __init__(self, vertex_data, vertices_length, vertices_size):
self.vertices_size = vertices_size
+def _get_material_index(material_name, material_names, material_lookup):
+ material_index = material_lookup.get(material_name)
+ if material_index is None:
+ material_index = len(material_names)
+ material_lookup[material_name] = material_index
+ material_names.append(material_name)
+ return material_index
+
+
def parse_mesh(
- obj, nodes, vertex_indices, material_names, vertex_index_offset, vertex_start_offset
+ obj, nodes, vertex_indices, material_names, material_lookup, vertex_index_offset, vertex_start_offset, export_profiler=None
):
"""Adds a mesh to the BML node list"""
print(f"parsing mesh {obj.name}")
# Prepare the mesh
- obj_data = get_bml_mesh_data(obj, vertex_index_offset)
+ obj_data = get_bml_mesh_data(obj, vertex_index_offset, export_profiler)
obj_vertices = obj_data["vertices"]
obj_indices = obj_data["vertex_indices"]
@@ -42,23 +52,25 @@ def parse_mesh(
else:
material_name = "BML-Default"
- try:
- material_index = material_names.index(material_name)
- except ValueError:
- material_index = len(material_names)
- material_names.append(material_name)
+ material_index = _get_material_index(material_name, material_names, material_lookup)
vertex_size = 48 # since we only support v2 Primitives
- obj_vertices_data = []
- for obj_vertex in obj_vertices:
- obj_vertices_data += obj_vertex.to_data()
-
- # DOF children use coordinates local to their DOF
- if get_bml_type(obj.parent) == BlenderNodeType.DOF and obj.parent.dof_type != DofType.TRANSLATE.name:
- reference_point = to_bms_coords((0, 0, 0))
+ with export_profiler.stage("mesh: pack vertex/index data") if export_profiler else nullcontext():
+ obj_vertices_data = b"".join(
+ chunk for obj_vertex in obj_vertices for chunk in obj_vertex.to_data()
+ )
+ vertex_indices.extend(obj_indices)
+
+ # Use stored reference point if available, otherwise fall back to current location.
+ # Property assigned in util.py - preserves Blender origin to use as reference point for alpha sorting
+ # All objects now use their origins for reference points, including DOF children
+ if "bms_reference_point" in obj:
+ stored_position = Vector(obj["bms_reference_point"])
+ reference_point = to_bms_coords(stored_position)
else:
- reference_point = get_objcenter(obj)
+ # Fallback for objects without stored reference point
+ reference_point = to_bms_coords(obj.location)
node = Primitive(
index=len(nodes),
@@ -79,7 +91,6 @@ def parse_mesh(
)
nodes.append(node)
- vertex_indices += obj_indices
return ParsedNodes(
vertex_data=obj_vertices_data,
@@ -93,14 +104,16 @@ def parse_bbl_light(
nodes,
vertex_indices,
material_names,
+ material_lookup,
vertex_index_offset,
vertex_start_offset,
+ export_profiler=None,
):
"""Adds a PBR billboard light to the BML node list"""
print(f"parsing PBR BB light {obj.name}")
# Prepare the mesh
- obj_data = get_pbr_light_data(obj, vertex_index_offset)
+ obj_data = get_pbr_light_data(obj, vertex_index_offset, export_profiler)
obj_vertices = obj_data["vertices"]
obj_indices = obj_data["vertex_indices"]
@@ -110,19 +123,25 @@ def parse_bbl_light(
else:
material_name = "BML-BillboardGlowLight"
- try:
- material_index = material_names.index(material_name)
- except ValueError:
- material_index = len(material_names)
- material_names.append(material_name)
+ material_index = _get_material_index(material_name, material_names, material_lookup)
vertex_size = 44 # size for PBR BB light
- obj_vertices_data = []
- for obj_vertex in obj_vertices:
- obj_vertices_data += obj_vertex.to_data()
+ with export_profiler.stage("mesh: pack vertex/index data") if export_profiler else nullcontext():
+ obj_vertices_data = b"".join(
+ chunk for obj_vertex in obj_vertices for chunk in obj_vertex.to_data()
+ )
+ vertex_indices.extend(obj_indices)
- reference_point = get_objcenter(obj)
+ # Use stored reference point if available, otherwise fall back to world translation
+ # All objects now use their origins for reference points, including DOF children
+ if "bms_reference_point" in obj:
+ stored_position = Vector(obj["bms_reference_point"])
+ reference_point = to_bms_coords(stored_position)
+ else:
+ # Fallback for objects without stored reference point
+ reference_point = to_bms_coords(obj.matrix_world.translation)
+
node = Primitive(
index=len(nodes),
topology=PrimitiveTopology.TRIANGLE_LIST,
@@ -142,7 +161,6 @@ def parse_bbl_light(
)
nodes.append(node)
- vertex_indices += obj_indices
return ParsedNodes(
vertex_data=obj_vertices_data,
@@ -171,21 +189,40 @@ def parse_slot(obj, nodes):
def parse_switch(obj, nodes):
- """Adds a BML Switch to the BML node list"""
+ """Adds a BML Switch to the BML node list.
+
+ Resolution order delegated to resolve_switch_id(). If unresolved defaults to (0,0).
+ """
print(f"{obj.name} is a SWITCH")
- switch = get_switches()[obj.switch_list_index]
- nodes.append(
- Switch(len(nodes), switch.switch_number, switch.branch, obj.switch_default_on)
- )
+ try:
+ switch_number, branch = resolve_switch_id(obj)
+ except Exception:
+ switch_number, branch = None, None
+ if switch_number is None or branch is None:
+ switch_number, branch = 0, 0
+ nodes.append(Switch(len(nodes), switch_number, branch, obj.switch_default_on))
return ParsedNodes(vertex_data=[], vertices_length=0, vertices_size=0)
def parse_dof(obj, nodes):
- """Adds a BML DOF to the BML node list"""
- print(f"{obj.name} is a DOF")
- # add the DOF start node
+ """Adds a BML DOF to the BML node list.
- dof = get_dofs()[obj.dof_list_index]
+ Resolution order delegated to resolve_dof_number(); default 0 if unresolved.
+ """
+ print(f"{obj.name} is a DOF")
+ try:
+ resolved_number = resolve_dof_number(obj)
+ except Exception:
+ resolved_number = None
+ if resolved_number is None:
+ resolved_number = 0
+
+ class _TmpDof:
+ def __init__(self, dof_number):
+ self.dof_number = dof_number
+ self.name = f"DOF {dof_number}"
+
+ dof = _TmpDof(resolved_number)
obj_orig_rotation_mode = obj.rotation_mode
obj.rotation_mode = "QUATERNION"
diff --git a/bms_blender_plugin/exporter/validation_dialogs.py b/bms_blender_plugin/exporter/validation_dialogs.py
new file mode 100644
index 0000000..68d8f21
--- /dev/null
+++ b/bms_blender_plugin/exporter/validation_dialogs.py
@@ -0,0 +1,248 @@
+"""
+Export validation dialog operators.
+
+Provides user-friendly dialogs for resolving validation issues before export.
+"""
+
+import bpy
+from bpy.props import EnumProperty, StringProperty, BoolProperty
+from bpy.types import Operator
+
+from bms_blender_plugin.exporter.export_validation import (
+ select_objects_from_issues,
+ validate_export_readiness,
+ get_out_of_range_issues,
+ get_missing_persistent_id_issues,
+)
+from bms_blender_plugin.ui_tools.operators.assign_from_index import assign_persistent_ids_to_objects
+
+
+def _collect_objects_from_collection(coll):
+ result = set()
+ def _recurse(c):
+ for o in c.objects:
+ result.add(o)
+ for ch in c.children:
+ _recurse(ch)
+ _recurse(coll)
+ return list(result)
+
+
+def _export_scope_objects(context, use_lods=False):
+ """Return objects in the active export collection hierarchy."""
+ if use_lods and hasattr(context.scene, 'lod_list') and len(context.scene.lod_list) > 0:
+ objs = set()
+ for li in context.scene.lod_list:
+ coll = getattr(li, 'collection', None)
+ if coll:
+ for o in _collect_objects_from_collection(coll):
+ objs.add(o)
+ return list(objs)
+ # Fallback to active collection
+ alc = context.view_layer.active_layer_collection
+ coll = alc.collection if alc else None
+ if not coll:
+ return []
+ return _collect_objects_from_collection(coll)
+
+
+class BML_OT_ValidationOutOfRangeDialog(Operator):
+ """Dialog for handling out-of-range XML reference issues."""
+
+ bl_idname = "bml.validation_out_of_range_dialog"
+ bl_label = "Export Validation Warning"
+ bl_description = "Resolve out-of-range XML reference issues"
+
+ # Store the validation issues and context for the dialog
+ issues_data: StringProperty(default="") # type: ignore[misc]
+ use_lods: BoolProperty(default=False) # type: ignore[misc] # whether to scope validation to all LOD collections
+
+ action: EnumProperty( # type: ignore[misc]
+ name="Action",
+ description="Choose how to handle the out-of-range issues",
+ items=[
+ ('SELECT', 'Select Objects & Cancel', 'Select problematic objects and cancel export for manual fixing'),
+ ('RELOAD', 'Reload XML & Retry', 'Reload XML files and retry validation'),
+ ('CONTINUE', 'Continue Export', 'Proceed with export using fallback values (may cause incorrect behavior)')
+ ],
+ default='SELECT'
+ )
+
+ def draw(self, context):
+ layout = self.layout
+
+ layout.label(text="⚠️ Export Validation Warning", icon='ERROR')
+ layout.separator()
+
+ # Recompute issues; avoid relying on temporary scene properties
+ issues = validate_export_readiness(context, _export_scope_objects(context, self.use_lods))
+ out_of_range_issues = get_out_of_range_issues(issues)
+
+ if out_of_range_issues:
+ total_objects = sum(len(issue.objects) for issue in out_of_range_issues)
+ layout.label(text=f"Found {total_objects} objects with out-of-range XML references:")
+
+ box = layout.box()
+ for issue in out_of_range_issues:
+ box.label(text=f"• {issue.issue_type.value.replace('_', ' ').title()}")
+ object_names = [obj.name for obj in issue.objects[:3]] # Show first 3
+ if len(issue.objects) > 3:
+ object_names.append(f"... and {len(issue.objects) - 3} more")
+ box.label(text=f" Objects: {', '.join(object_names)}")
+
+ layout.separator()
+ layout.label(text="This usually means your XML files are outdated.")
+ layout.separator()
+
+ layout.prop(self, "action", expand=True)
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self, width=500)
+
+ def execute(self, context):
+ # Recompute on execute to reflect current state
+ issues = validate_export_readiness(context, _export_scope_objects(context, self.use_lods))
+ out_of_range_issues = get_out_of_range_issues(issues)
+
+ if self.action == 'SELECT':
+ select_objects_from_issues(out_of_range_issues)
+ self.report({'INFO'}, f"Selected {sum(len(issue.objects) for issue in out_of_range_issues)} problematic objects")
+
+ elif self.action == 'RELOAD':
+ try:
+ # Use existing reload operators from preferences.py
+ # These handle proper cache clearing and repopulation
+ bpy.ops.bml.reload_switch_list()
+ bpy.ops.bml.reload_dof_list()
+ self.report({'INFO'}, "XML files reloaded. Re-run export.")
+
+ except Exception as e:
+ self.report({'ERROR'}, f"Failed to reload XML files: {str(e)}")
+
+ elif self.action == 'CONTINUE':
+ self.report({'WARNING'}, "Continuing export with fallback behavior")
+ # Don't set any flags - let export continue
+
+ return {'FINISHED'}
+
+ def _cleanup_scene_properties(self, context):
+ """Clean up temporary scene properties used by validation system."""
+ if hasattr(context.scene, '_bml_validation_issues'):
+ delattr(context.scene, '_bml_validation_issues')
+ if hasattr(context.scene, '_bml_export_cancelled'):
+ delattr(context.scene, '_bml_export_cancelled')
+ if hasattr(context.scene, '_bml_export_retry'):
+ delattr(context.scene, '_bml_export_retry')
+
+
+class BML_OT_ValidationMissingIDDialog(Operator):
+ """Dialog for handling missing persistent ID issues.
+
+ Updated: Inline confirmation (no secondary pop-up) and clearer, action-focused labels.
+ """
+
+ bl_idname = "bml.validation_missing_id_dialog"
+ bl_label = "DOF/Switch IDs Missing"
+ bl_description = "Assign persistent DOF / Switch IDs before continuing export"
+ use_lods: BoolProperty(default=False) # type: ignore[misc]
+
+ action: EnumProperty( # type: ignore[misc]
+ name="Action",
+ description="Choose how to handle missing persistent IDs",
+ items=[
+ ('SELECT', 'Select & Cancel', 'Select objects and cancel export so you can assign IDs manually'),
+ ('AUTO_ASSIGN', 'Assign IDs & Continue', 'Automatically assign persistent IDs (recommended) and continue export'),
+ ('IGNORE', 'Ignore & Continue', 'Continue export without assigning (falls back to legacy index resolution; risky)')
+ ],
+ default='SELECT'
+ )
+
+ def draw(self, context):
+ layout = self.layout
+ layout.label(text="Persistent IDs Required", icon='INFO')
+ layout.separator()
+
+ # Recompute issues to reflect current state
+ issues = validate_export_readiness(context, _export_scope_objects(context, self.use_lods))
+ missing_id_issues = get_missing_persistent_id_issues(issues)
+
+ if missing_id_issues:
+ total_objects = sum(len(issue.objects) for issue in missing_id_issues)
+ layout.label(text=f"Found {total_objects} objects using legacy indices without persistent IDs:")
+
+ box = layout.box()
+ for issue in missing_id_issues:
+ issue_type = issue.issue_type.value.replace('_', ' ').title()
+ box.label(text=f"• {issue_type}: {len(issue.objects)} objects")
+
+ layout.separator()
+ col = layout.column(align=True)
+ col.label(text="Objects are still using legacy list indices.", icon='ERROR')
+ col.label(text="Assigning persistent IDs prevents future XML changes from breaking exports.")
+ col.label(text="Recommended: Assign IDs & Continue.")
+ layout.separator()
+
+ layout.prop(self, "action", expand=True)
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_props_dialog(self, width=500)
+
+ def execute(self, context):
+ # Recompute on execute to reflect current state
+ issues = validate_export_readiness(context, _export_scope_objects(context, self.use_lods))
+ missing_id_issues = get_missing_persistent_id_issues(issues)
+
+ if self.action == 'SELECT':
+ select_objects_from_issues(missing_id_issues)
+ self.report({'INFO'}, f"Selected {sum(len(issue.objects) for issue in missing_id_issues)} objects needing persistent IDs")
+
+ elif self.action == 'AUTO_ASSIGN':
+ total_objects = sum(len(issue.objects) for issue in missing_id_issues)
+ switch_count, dof_count = self._auto_assign_persistent_ids(missing_id_issues)
+ total_assigned = switch_count + dof_count
+
+ # Console summary (acts as log)
+ print(f"[BML][AUTO_ASSIGN] Target Objects: {total_objects} | Switches Assigned: {switch_count} | DOFs Assigned: {dof_count}")
+
+ if total_assigned == total_objects:
+ self.report({'INFO'}, f"Successfully assigned persistent IDs to all {total_assigned} objects")
+ elif total_assigned > 0:
+ self.report({'WARNING'}, f"Assigned persistent IDs to {total_assigned} of {total_objects} objects (some may have out-of-range indices)")
+ else:
+ self.report({'ERROR'}, "Failed to assign any persistent IDs - check console for details")
+ # Continue with export
+
+ elif self.action == 'IGNORE':
+ self.report({'WARNING'}, "Continuing without assigning persistent IDs (legacy index fallback)")
+ # Continue with export
+
+ return {'FINISHED'}
+
+ def _auto_assign_persistent_ids(self, issues):
+ """Auto-assign persistent IDs to specific objects from validation issues."""
+ # Collect all objects from the issues that need persistent ID assignment
+ target_objects = []
+ for issue in issues:
+ target_objects.extend(issue.objects)
+
+ if not target_objects:
+ return 0, 0
+
+ try:
+ # Use targeted assignment that only processes the specific objects, returns (switches_assigned, dofs_assigned)
+ return assign_persistent_ids_to_objects(bpy.context, target_objects)
+ except Exception as e:
+ # If assignment fails, fallback gracefully
+ print(f"Persistent ID auto-assignment failed: {e}")
+ return 0, 0
+
+
+# Registration
+def register():
+ bpy.utils.register_class(BML_OT_ValidationOutOfRangeDialog)
+ bpy.utils.register_class(BML_OT_ValidationMissingIDDialog)
+
+
+def unregister():
+ bpy.utils.unregister_class(BML_OT_ValidationMissingIDDialog)
+ bpy.utils.unregister_class(BML_OT_ValidationOutOfRangeDialog)
diff --git a/bms_blender_plugin/nodes_editor/dof_editor.py b/bms_blender_plugin/nodes_editor/dof_editor.py
index 23f2c33..764f878 100644
--- a/bms_blender_plugin/nodes_editor/dof_editor.py
+++ b/bms_blender_plugin/nodes_editor/dof_editor.py
@@ -8,6 +8,7 @@
BlenderNodeTreeType,
)
from bms_blender_plugin.common.util import get_dofs
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number
from bms_blender_plugin.nodes_editor import dof_node_categories
from bms_blender_plugin.nodes_editor.dof_base_node import DofBaseNode
from bms_blender_plugin.nodes_editor.dof_nodes.dof_input_node import NodeDofModelInput
@@ -144,22 +145,28 @@ def _check_nodes_recursively(recursive_node):
# make sure that nodes are not connected with themselves
if outgoing_node == recursive_node:
- continue
+ continue # protect against accidental self-links
if (
get_bml_node_type(outgoing_node) == BlenderEditorNodeType.DOF_MODEL
and outgoing_node.parent_dof
and get_bml_node_type(recursive_node) != BlenderEditorNodeType.DOF_MODEL
):
- dof_number = list_dof_numbers[
- outgoing_node.parent_dof.dof_list_index
- ].dof_number
+ # We are at a non-DOF node feeding a DOF node, we want to auto-link
+ dof_number = resolve_dof_number(outgoing_node.parent_dof)
+ if dof_number is None:
+ # fallback: derive from list index if persistent ID missing
+ idx = getattr(outgoing_node.parent_dof, 'dof_list_index', -1)
+ if 0 <= idx < len(list_dof_numbers):
+ dof_number = list_dof_numbers[idx].dof_number
+ if dof_number is None:
+ continue # give up silently if still unresolved
nodes_with_same_dof_number = dofs_dict[dof_number]
for node_with_same_dof_number in nodes_with_same_dof_number:
node_tree.links.new(
recursive_node.outputs[0],
node_with_same_dof_number.inputs[0],
)
- dofs_dict[dof_number] = []
+ dofs_dict[dof_number] = [] # block so we don't do this again
if (
get_bml_node_type(recursive_node)
diff --git a/bms_blender_plugin/nodes_editor/dof_nodes/dof_input_node.py b/bms_blender_plugin/nodes_editor/dof_nodes/dof_input_node.py
index d823d83..743da91 100644
--- a/bms_blender_plugin/nodes_editor/dof_nodes/dof_input_node.py
+++ b/bms_blender_plugin/nodes_editor/dof_nodes/dof_input_node.py
@@ -3,7 +3,7 @@
from bms_blender_plugin.common.blender_types import BlenderEditorNodeType
from bms_blender_plugin.common.bml_structs import DofType, ArgType
-from bms_blender_plugin.common.util import get_dofs
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number
from bms_blender_plugin.nodes_editor.dof_base_node import (
DofBaseNode,
subscribe_node,
@@ -180,11 +180,17 @@ def check_connections(node_tree, node):
BaseRenderControl.set_result_type(node, ArgType.SCRATCH_VARIABLE_ID)
elif node.parent_dof:
# linked to a render control or another DOF - set our result type to the DOF of the current node
- BaseRenderControl.set_result_type(
- node,
- ArgType.DOF_ID,
- get_dofs()[node.parent_dof.dof_list_index].dof_number,
- )
+ dof_num = resolve_dof_number(node.parent_dof)
+ if dof_num is not None:
+ BaseRenderControl.set_result_type(
+ node,
+ ArgType.DOF_ID,
+ dof_num,
+ )
+ else:
+ # Fallback to scratch if unresolved to avoid crashes
+ # TODO: warn user
+ BaseRenderControl.set_result_type(node, ArgType.SCRATCH_VARIABLE_ID)
def register():
diff --git a/bms_blender_plugin/nodes_editor/util.py b/bms_blender_plugin/nodes_editor/util.py
index 70dee8a..7c4e03a 100644
--- a/bms_blender_plugin/nodes_editor/util.py
+++ b/bms_blender_plugin/nodes_editor/util.py
@@ -1,5 +1,6 @@
from bms_blender_plugin.common.blender_types import BlenderEditorNodeType, BlenderNodeTreeType
from bms_blender_plugin.common.util import get_dofs
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number
def get_incoming_nodes(node):
@@ -57,8 +58,15 @@ def get_valid_dof_nodes(tree):
for node in tree.nodes:
if get_bml_node_type(node) == BlenderEditorNodeType.DOF_MODEL and node.parent_dof:
- dof_number = list_dof_numbers[node.parent_dof.dof_list_index].dof_number
- if dof_number not in dofs.keys():
+ # Resolve via persistent ID first; fall back to list index if valid
+ dof_number = resolve_dof_number(node.parent_dof)
+ if dof_number is None:
+ idx = getattr(node.parent_dof, 'dof_list_index', -1)
+ if 0 <= idx < len(list_dof_numbers):
+ dof_number = list_dof_numbers[idx].dof_number
+ if dof_number is None:
+ continue # skip invalid/unresolved
+ if dof_number not in dofs:
dofs[dof_number] = [node]
else:
dofs[dof_number].append(node)
@@ -129,7 +137,14 @@ def get_socket_distinct_outgoing_dof_numbers(output_socket):
receiving_node = link.to_socket.node
if get_bml_node_type(receiving_node) == BlenderEditorNodeType.DOF_MODEL:
if receiving_node.parent_dof:
- dof_number = get_dofs()[receiving_node.parent_dof.dof_list_index].dof_number
+ dof_number = resolve_dof_number(receiving_node.parent_dof)
+ if dof_number is None:
+ idx = getattr(receiving_node.parent_dof, 'dof_list_index', -1)
+ dofs_enum = get_dofs()
+ if 0 <= idx < len(dofs_enum):
+ dof_number = dofs_enum[idx].dof_number
+ if dof_number is None:
+ continue
if dof_number not in dof_numbers:
dof_numbers.append(dof_number)
else:
@@ -174,12 +189,24 @@ def get_bml_node_tree_type(obj):
def dof_nodes_have_equal_dof_numbers(node_1, node_2):
- """Returns if 2 nodes have equal DOF numbers. Returns false if either of the nodes or their parent DOFs are None"""
+ """Returns if 2 nodes have equal RESOLVED DOF numbers. Returns false if either node, parent DOF, or DOF number cannot be resolved."""
if (get_bml_node_type(node_1) != BlenderEditorNodeType.DOF_MODEL or not node_1.parent_dof
or get_bml_node_type(node_2) != BlenderEditorNodeType.DOF_MODEL or not node_2.parent_dof):
return False
+ # Fast path (same node or same DOF object)
if node_1 == node_2 or node_1.parent_dof == node_2.parent_dof:
return True
- return node_1.parent_dof.dof_list_index == node_2.parent_dof.dof_list_index
+ # Compare resolved DOF numbers instead of list indices
+ try:
+ dof_number_1 = resolve_dof_number(node_1.parent_dof)
+ dof_number_2 = resolve_dof_number(node_2.parent_dof)
+
+ # Both must resolve to valid numbers to be considered equal
+ if dof_number_1 is not None and dof_number_2 is not None:
+ return dof_number_1 == dof_number_2
+ else:
+ return False
+ except Exception:
+ return False
diff --git a/bms_blender_plugin/preferences.py b/bms_blender_plugin/preferences.py
index a0f3982..215ebf3 100644
--- a/bms_blender_plugin/preferences.py
+++ b/bms_blender_plugin/preferences.py
@@ -4,7 +4,79 @@
from bms_blender_plugin.common.blender_types import BlenderNodeType
from bms_blender_plugin.common.bml_structs import DofType
-from bms_blender_plugin.common.util import get_bml_type
+from bms_blender_plugin.common.util import get_bml_type, get_dofs, get_switches, get_callbacks
+
+
+class ReloadDofList(Operator):
+ """Reload DOF list from DOF.xml file"""
+ bl_idname = "bml.reload_dof_list"
+ bl_label = "Reload DOF.xml"
+ bl_description = "Reload the DOF list from the DOF.xml file. Use this after modifying the XML file"
+ bl_options = {"REGISTER"}
+
+ def execute(self, context):
+ # Clear the global cache first
+ import bms_blender_plugin.common.util as util_module
+ util_module.dofs = []
+
+ # Clear the cache
+ util_module._dofs_hydrated = False
+ context.scene.dof_list.clear()
+ for dof in get_dofs(force_disk=True): # force_disk to bypass scene cache
+ item = context.scene.dof_list.add()
+ item.name = dof.name
+ item.dof_number = int(dof.dof_number)
+ self.report({'INFO'}, f"Reloaded {len(context.scene.dof_list)} DOFs from DOF.xml")
+ return {'FINISHED'}
+
+
+class ReloadSwitchList(Operator):
+ """Reload Switch list from switch.xml file"""
+ bl_idname = "bml.reload_switch_list"
+ bl_label = "Reload switch.xml"
+ bl_description = "Reload the Switch list from the switch.xml file. Use this after modifying the XML file"
+ bl_options = {"REGISTER"}
+
+ def execute(self, context):
+ # Clear the global cache first
+ import bms_blender_plugin.common.util as util_module
+ util_module.switches = []
+
+ # Clear the cache
+ util_module._switches_hydrated = False
+ context.scene.switch_list.clear()
+ for switch in get_switches(force_disk=True): # force_disk to bypass scene cache
+ item = context.scene.switch_list.add()
+ item.name = switch.name
+ item.switch_number = int(switch.switch_number)
+ item.branch_number = int(switch.branch)
+ self.report({'INFO'}, f"Reloaded {len(context.scene.switch_list)} Switches from switch.xml")
+ return {'FINISHED'}
+
+
+class ReloadCallbackList(Operator):
+ """Reload Callback list from callbacks.xml file"""
+ bl_idname = "bml.reload_callback_list"
+ bl_label = "Reload callbacks.xml"
+ bl_description = "Reload the Callback list from the callbacks.xml file. Use this after modifying the XML file"
+ bl_options = {"REGISTER"}
+
+ def execute(self, context):
+ # Clear the global cache first
+ import bms_blender_plugin.common.util as util_module
+ util_module.callbacks = []
+
+ # Clear the scene cache
+ context.scene.bml_all_callbacks.clear()
+
+ # Repopulate the scene cache immediately
+ for callback in get_callbacks():
+ new_callback = context.scene.bml_all_callbacks.add()
+ new_callback.name = callback.name
+ new_callback.group = callback.group
+
+ self.report({'INFO'}, f"Reloaded {len(context.scene.bml_all_callbacks)} Callbacks from callbacks.xml")
+ return {'FINISHED'}
class ExporterPreferences(bpy.types.AddonPreferences):
@@ -30,6 +102,17 @@ class ExporterPreferences(bpy.types.AddonPreferences):
default=True,
)
+ prefer_scene_snapshot: BoolProperty(
+ name="Prefer Scene Snapshot",
+ description="Use scene-cached switch/DOF lists if present. (Recommended)",
+ default=True,
+ )
+ warn_xml_mismatch: BoolProperty(
+ name="Warn on XML Mismatch",
+ description="Print a console warning if disk XML differs from scene cache.",
+ default=True,
+ )
+
copy_to_clipboard_command: StringProperty(
name="Alternative 'Copy to Clipboard' command",
description="Override command to copy text to the clipboard (especially useful on Linux)",
@@ -89,6 +172,20 @@ class ExporterPreferences(bpy.types.AddonPreferences):
description="The size of the Empty to display a Scale DOF as"
)
+ switch_empty_type: EnumProperty(
+ name="Switch",
+ description="The Empty to display a Switch as",
+ items=empty_enum_items,
+ default="PLAIN_AXES",
+ )
+
+ switch_empty_size: FloatProperty(
+ default=1.0,
+ min=0.01,
+ name="Size",
+ description="The size of the Empty to display a Switch as"
+ )
+
def draw(self, context):
layout = self.layout
@@ -114,6 +211,25 @@ def draw(self, context):
box.operator(ApplyEmptyDisplaysToDofs.bl_idname, icon="CHECKMARK")
+ layout.separator()
+ layout.label(text="Switch Display")
+ box = layout.box()
+ row = box.row()
+ row.prop(self, "switch_empty_type")
+ row.prop(self, "switch_empty_size")
+
+ box.operator(ApplyEmptyDisplaysToSwitches.bl_idname, icon="CHECKMARK")
+
+ layout.separator()
+ layout.label(text="Data Management")
+ box = layout.box()
+ box.operator(ReloadDofList.bl_idname, icon="FILE_REFRESH")
+ box.operator(ReloadSwitchList.bl_idname, icon="FILE_REFRESH")
+ box.operator(ReloadCallbackList.bl_idname, icon="FILE_REFRESH")
+ box.separator()
+ box.prop(self, "prefer_scene_snapshot")
+ box.prop(self, "warn_xml_mismatch")
+
layout.separator()
layout.row().label(text="Debug options")
layout.row().label(text="Use at your own risk. All options should be OFF by default.", icon="ERROR")
@@ -163,11 +279,42 @@ def execute(self, context):
return {"FINISHED"}
+class ApplyEmptyDisplaysToSwitches(Operator):
+ """Applies the preferences for the Switch empties to all objects in the scene"""
+ bl_idname = "bml.apply_empty_displays_to_switches"
+ bl_label = "Apply to all Switches"
+ bl_description = "Applies the display preferences to all Switches in the scene"
+
+ # noinspection PyMethodMayBeStatic
+ def execute(self, context):
+ switch_empty = context.preferences.addons[
+ "bms_blender_plugin"
+ ].preferences.switch_empty_type
+ switch_empty_size = context.preferences.addons[
+ "bms_blender_plugin"
+ ].preferences.switch_empty_size
+
+ for obj in bpy.data.objects:
+ if get_bml_type(obj) == BlenderNodeType.SWITCH:
+ obj.empty_display_type = switch_empty
+ obj.empty_display_size = switch_empty_size
+
+ return {"FINISHED"}
+
+
def register():
+ bpy.utils.register_class(ReloadDofList)
+ bpy.utils.register_class(ReloadSwitchList)
+ bpy.utils.register_class(ReloadCallbackList)
bpy.utils.register_class(ApplyEmptyDisplaysToDofs)
+ bpy.utils.register_class(ApplyEmptyDisplaysToSwitches)
bpy.utils.register_class(ExporterPreferences)
def unregister():
bpy.utils.unregister_class(ExporterPreferences)
+ bpy.utils.unregister_class(ApplyEmptyDisplaysToSwitches)
bpy.utils.unregister_class(ApplyEmptyDisplaysToDofs)
+ bpy.utils.unregister_class(ReloadCallbackList)
+ bpy.utils.unregister_class(ReloadSwitchList)
+ bpy.utils.unregister_class(ReloadDofList)
diff --git a/bms_blender_plugin/ui_tools/dof_behaviour.py b/bms_blender_plugin/ui_tools/dof_behaviour.py
index 9ec19b8..5e7f3e0 100644
--- a/bms_blender_plugin/ui_tools/dof_behaviour.py
+++ b/bms_blender_plugin/ui_tools/dof_behaviour.py
@@ -19,7 +19,10 @@
get_dofs,
reset_dof,
get_parent_dof_or_switch,
+ lookup_switch_label,
+ lookup_dof_label,
)
+from bms_blender_plugin.common.resolve_ids import resolve_dof_number, resolve_switch_id
from bms_blender_plugin.nodes_editor.util import get_bml_node_type, get_bml_node_tree_type
@@ -47,10 +50,20 @@ def rebuild_cache(cls):
@classmethod
def subscribe(cls, dof):
- """Subscribes a DOF to dof_input updates for his DOF number"""
+ """Subscribes a DOF to dof_input updates for using resolved DOF number.
+
+ Uses resolve_dof_number() to tolerate legacy index-only DOFs. If the DOF
+ can't be resolved (returns None) we skip subscription silently rather than
+ raising an exception that could spam the depsgraph handler while the user
+ is mid-migration."""
if get_bml_type(dof) != BlenderNodeType.DOF:
return
- dof_number = get_dofs()[dof.dof_list_index].dof_number
+ try:
+ dof_number = resolve_dof_number(dof)
+ except Exception:
+ dof_number = None
+ if dof_number is None:
+ return
# first time subscription
if dof not in cls.dof_dof_number.keys():
@@ -81,9 +94,21 @@ def subscribe(cls, dof):
@classmethod
def unsubscribe(cls, dof):
"""Unsubscribes a DOF from all subscriptions"""
- dof_number = get_dofs()[dof.dof_list_index].dof_number
- cls.dof_number_dofs[dof_number].remove(dof)
- cls.dof_dof_number.pop(dof)
+ try:
+ dof_number = resolve_dof_number(dof)
+ except Exception:
+ dof_number = None
+ if dof_number is None:
+ # Fallback: attempt legacy index if still valid
+ try:
+ dof_number = get_dofs()[dof.dof_list_index].dof_number
+ except Exception:
+ dof_number = None
+ if dof_number is None:
+ return
+ if dof_number in cls.dof_number_dofs and dof in cls.dof_number_dofs[dof_number]:
+ cls.dof_number_dofs[dof_number].remove(dof)
+ cls.dof_dof_number.pop(dof, None)
@classmethod
def post_new_dof_value(cls, dof):
@@ -94,12 +119,25 @@ def post_new_dof_value(cls, dof):
if dof not in cls.dof_dof_number:
cls.rebuild_cache()
- dof_number = get_dofs()[dof.dof_list_index].dof_number
+ try:
+ dof_number = resolve_dof_number(dof)
+ except Exception:
+ dof_number = None
+ if dof_number is None:
+ # Legacy fallback
+ try:
+ dof_number = get_dofs()[dof.dof_list_index].dof_number
+ except Exception:
+ return
+
+ if dof_number not in cls.dof_number_dofs:
+ # Nothing to propagate to
+ return
dofs_to_cleanup = []
new_dof_input = dof.dof_input
- for other_dof in cls.dof_number_dofs[dof_number]:
+ for other_dof in list(cls.dof_number_dofs[dof_number]):
if (
len(other_dof.users_collection) > 0
and other_dof.dof_input != new_dof_input
@@ -109,23 +147,60 @@ def post_new_dof_value(cls, dof):
dofs_to_cleanup.append(other_dof)
# clean up orphaned DOFs which might have accumulated
- for dof in dofs_to_cleanup:
- cls.dof_number_dofs[dof_number].remove(dof)
- cls.dof_dof_number.pop(dof)
- bpy.data.objects.remove(dof)
+ for cleanup_dof in dofs_to_cleanup:
+ cls.dof_number_dofs[dof_number].remove(cleanup_dof)
+ cls.dof_dof_number.pop(cleanup_dof, None)
+ try:
+ bpy.data.objects.remove(cleanup_dof)
+ except Exception:
+ pass
def update_switch_or_dof_name(obj, context):
- """Updates the name of a DOF or Switch when their respective DOF/Switch values are changed. Overwrites any previous
- name updates by the user."""
- if get_bml_type(obj) == BlenderNodeType.SWITCH:
- active_switch = get_switches()[obj.switch_list_index]
- obj.name = f"Switch - {active_switch.name} ({active_switch.switch_number})"
- elif get_bml_type(obj) == BlenderNodeType.DOF:
- active_dof = get_dofs()[obj.dof_list_index]
-
- name = f"DOF - {active_dof.name} ({active_dof.dof_number})"
- obj.name = name
+ """Update object.name for Switch/DOF using persistent IDs, preferring scene-cached list first.
+ Fallback order per type:
+ Persistent IDs -> resolver -> direct index -> Unset
+ """
+ node_type = get_bml_type(obj)
+ if node_type == BlenderNodeType.SWITCH:
+ sw_num = getattr(obj, "bml_switch_number", -1)
+ sw_branch = getattr(obj, "bml_switch_branch", -1)
+ if sw_num >= 0 and sw_branch >= 0:
+ label = lookup_switch_label(sw_num, sw_branch) or "Custom"
+ obj.name = f"Switch - {label} ({sw_num}:{sw_branch})"
+ else:
+ try:
+ resolved_num, resolved_branch = resolve_switch_id(obj)
+ except Exception:
+ resolved_num, resolved_branch = None, None
+ if resolved_num is not None and resolved_branch is not None:
+ label = lookup_switch_label(resolved_num, resolved_branch) or "Custom"
+ obj.name = f"Switch - {label} ({resolved_num}:{resolved_branch})"
+ else:
+ try:
+ sw = get_switches()[getattr(obj, 'switch_list_index', -1)]
+ obj.name = f"Switch - {sw.name} ({sw.switch_number}:{sw.branch})"
+ except Exception:
+ obj.name = "Switch - Unset"
+ elif node_type == BlenderNodeType.DOF:
+ dof_num = getattr(obj, "bml_dof_number", -1)
+ if dof_num >= 0:
+ label = lookup_dof_label(dof_num) or "Custom"
+ obj.name = f"DOF - {label} ({dof_num})"
+ else:
+ try:
+ resolved = resolve_dof_number(obj)
+ except Exception:
+ resolved = None
+ if resolved is not None:
+ label = lookup_dof_label(resolved) or "Custom"
+ obj.name = f"DOF - {label} ({resolved})"
+ else:
+ try:
+ de = get_dofs()[getattr(obj, 'dof_list_index', -1)]
+ obj.name = f"DOF - {de.name} ({de.dof_number})"
+ except Exception:
+ obj.name = "DOF - Unset"
for tree in bpy.data.node_groups.values():
if isinstance(tree, nodes_editor.dof_editor.DofNodeTree):
diff --git a/bms_blender_plugin/ui_tools/operators/__init__.py b/bms_blender_plugin/ui_tools/operators/__init__.py
index a5b1d15..1f93868 100644
--- a/bms_blender_plugin/ui_tools/operators/__init__.py
+++ b/bms_blender_plugin/ui_tools/operators/__init__.py
@@ -9,6 +9,123 @@
update_switch_or_dof_name, dof_set_input, dof_get_input,
)
from bms_blender_plugin.ui_tools.slot_behaviour import update_slot_number
+from bms_blender_plugin.common.util import get_switches, get_dofs, get_bml_type
+from bms_blender_plugin.common.constants import (
+ BMS_MAX_SWITCH_NUMBER,
+ BMS_MAX_SWITCH_BRANCH,
+ BMS_MAX_DOF_NUMBER,
+)
+
+
+def _update_switch_list_index(obj, context):
+ """Whenever the list index changes, force persistent switch number/branch to match the selected XML entry."""
+ try:
+ switches = get_switches()
+ if 0 <= obj.switch_list_index < len(switches):
+ sw = switches[obj.switch_list_index]
+ obj.bml_switch_number = sw.switch_number
+ obj.bml_switch_branch = sw.branch
+ try:
+ print(f"[DEBUG] _update_switch_list_index: obj={getattr(obj,'name',None)} index={obj.switch_list_index} -> {sw.switch_number}:{sw.branch}")
+ except Exception:
+ pass
+ else:
+ try:
+ print(f"[DEBUG] _update_switch_list_index: obj={getattr(obj,'name',None)} index={getattr(obj,'switch_list_index',None)} out_of_range (len={len(switches)})")
+ except Exception:
+ pass
+ except Exception:
+ pass
+ update_switch_or_dof_name(obj, context)
+
+
+def _update_dof_list_index(obj, context):
+ """Whenever the list index changes, force persistent DOF number to match the selected XML entry."""
+ try:
+ dofs = get_dofs()
+ if 0 <= obj.dof_list_index < len(dofs):
+ de = dofs[obj.dof_list_index]
+ obj.bml_dof_number = de.dof_number
+ except Exception:
+ pass
+ update_switch_or_dof_name(obj, context)
+
+
+
+# Keep legacy list index in sync when persistent switch IDs are edited manually
+def _update_persistent_switch_ids(obj, context):
+ """When user edits persistent switch number/branch, update switch_list_index to matching XML entry if found.
+ If both IDs are -1 (not set), leave index unchanged for backward compatibility.
+ """
+ update_switch_or_dof_name(obj, context)
+ def _tag_redraw(ctx):
+ try:
+ if ctx and ctx.screen:
+ for area in ctx.screen.areas:
+ area.tag_redraw()
+ except Exception:
+ pass
+
+ try:
+ sw_num = getattr(obj, "bml_switch_number", -1)
+ sw_branch = getattr(obj, "bml_switch_branch", -1)
+ if sw_num >= 0 and sw_branch >= 0:
+ scene_list = getattr(bpy.context.scene, 'switch_list', None)
+ found_index = None
+ if scene_list:
+ for i, item in enumerate(scene_list):
+ if item.switch_number == sw_num and item.branch_number == sw_branch:
+ found_index = i
+ break
+ if found_index is None:
+ switches = get_switches()
+ for i, sw in enumerate(switches):
+ if sw.switch_number == sw_num and sw.branch == sw_branch:
+ found_index = i
+ break
+ if found_index is not None and getattr(obj, 'switch_list_index', -1) != found_index:
+ obj.switch_list_index = found_index
+ _tag_redraw(context)
+ # If unset leave legacy index
+ except Exception:
+ pass
+
+# Keep legacy list index in sync when persistent DOF ID is edited manually
+def _update_persistent_dof_number(obj, context):
+ """When user edits persistent DOF number, update dof_list_index to matching XML entry if found.
+ If ID is -1 (not set), leave index unchanged.
+ """
+ update_switch_or_dof_name(obj, context)
+ def _tag_redraw(ctx):
+ try:
+ if ctx and ctx.screen:
+ for area in ctx.screen.areas:
+ area.tag_redraw()
+ except Exception:
+ pass
+
+ try:
+ dof_num = getattr(obj, "bml_dof_number", -1)
+ if dof_num >= 0:
+ scene_list = getattr(bpy.context.scene, 'dof_list', None)
+ found_index = None
+ if scene_list:
+ for i, item in enumerate(scene_list):
+ if item.dof_number == dof_num:
+ found_index = i
+ break
+ if found_index is None:
+ dofs = get_dofs()
+ for i, de in enumerate(dofs):
+ if de.dof_number == dof_num:
+ found_index = i
+ break
+ if found_index is not None and getattr(obj, 'dof_list_index', -1) != found_index:
+ obj.dof_list_index = found_index
+ _tag_redraw(context)
+ # Unset -> leave legacy index
+ except Exception:
+ pass
def register_blender_properties():
@@ -69,15 +186,42 @@ def register_blender_properties():
# Switches
bpy.types.Object.switch_list_index = bpy.props.IntProperty(
- name="Index for switch_list", default=0, update=update_switch_or_dof_name
+ name="Index for switch_list", default=0, update=_update_switch_list_index
)
bpy.types.Object.switch_default_on = bpy.props.BoolProperty(
- name="Default ON", description="The switch is ON by default", default=False
+ name="ON by default", description="This switch is ON by default", default=False
+ )
+ # Persistent switch number & branch (new). -1 => unset (legacy scenes)
+ bpy.types.Object.bml_switch_number = bpy.props.IntProperty(
+ name="Switch #",
+ description="Persistent switch number used for export (independent of switch.xml ordering)",
+ default=-1,
+ min=-1,
+ max=BMS_MAX_SWITCH_NUMBER,
+ update=_update_persistent_switch_ids,
+ )
+ bpy.types.Object.bml_switch_branch = bpy.props.IntProperty(
+ name="Branch #",
+ description="Persistent branch number used for export (independent of switch.xml ordering)",
+ default=-1,
+ min=-1,
+ max=BMS_MAX_SWITCH_BRANCH,
+ update=_update_persistent_switch_ids,
)
# DOFs
bpy.types.Object.dof_list_index = bpy.props.IntProperty(
- name="Index for dof_list", default=0, update=update_switch_or_dof_name
+ name="Index for dof_list", default=0, update=_update_dof_list_index
+ )
+
+ # Persistent DOF number (new)
+ bpy.types.Object.bml_dof_number = bpy.props.IntProperty(
+ name="DOF #",
+ description="Persistent DOF number used for export (independent of DOF.xml ordering)",
+ default=-1,
+ min=-1,
+ max=BMS_MAX_DOF_NUMBER,
+ update=_update_persistent_dof_number,
)
bpy.types.Object.dof_type = bpy.props.EnumProperty(
@@ -194,5 +338,38 @@ def register_blender_properties():
update=dof_update_input,
)
+ # Silent legacy migration disabled: manual validation-driven assignment required.
+ try:
+ for obj in bpy.data.objects:
+ update_switch_or_dof_name(obj, None)
+ except Exception:
+ pass
+
+
+class BML_OT_reconcile_dof_switch_indices(bpy.types.Operator):
+ bl_idname = "bml.reconcile_dof_switch_indices"
+ bl_label = "Reconcile DOF/Switch Indices"
+ bl_description = "Synchronize xml list indices with current persistent IDs. Persistent ID -> List Index. (Prioritize scene cached list.)"
+ bl_options = {"UNDO"}
+
+ def execute(self, context):
+ count = 0
+ for obj in bpy.data.objects:
+ t = get_bml_type(obj)
+ if t == BlenderNodeType.SWITCH:
+ _update_persistent_switch_ids(obj, context)
+ count += 1
+ elif t == BlenderNodeType.DOF:
+ _update_persistent_dof_number(obj, context)
+ count += 1
+ self.report({'INFO'}, f"Reconciled indices for {count} DOF/Switch objects")
+ return {'FINISHED'}
+
register_blender_properties()
+
+# Explicit registration for reconciliation operator (others auto-executed above)
+try:
+ bpy.utils.register_class(BML_OT_reconcile_dof_switch_indices)
+except Exception:
+ pass
diff --git a/bms_blender_plugin/ui_tools/operators/assign_from_index.py b/bms_blender_plugin/ui_tools/operators/assign_from_index.py
new file mode 100644
index 0000000..f3c702c
--- /dev/null
+++ b/bms_blender_plugin/ui_tools/operators/assign_from_index.py
@@ -0,0 +1,377 @@
+import bpy
+
+from bms_blender_plugin.common.util import get_switches, get_dofs, get_bml_type, get_parent_dof_or_switch
+from bms_blender_plugin.common.blender_types import BlenderNodeType
+from bms_blender_plugin.ui_tools.dof_behaviour import update_switch_or_dof_name
+
+
+class BML_OT_assign_switch_from_index(bpy.types.Operator):
+ # Populate persistent Switch number and branch from the currently selected list entry - useful if object was created before persistent ID properties added
+ # If the index out of range, nothing changed and a warning report issued
+
+ bl_idname = "bml.assign_switch_from_index"
+ bl_label = "Assign from Index"
+ bl_description = (
+ "Assign persistent Switch Number and Branch from the current switch list selection. "
+ "Uses switch_list_index; overwrites existing persistent IDs."
+ )
+ bl_options = {"UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ if not obj:
+ return False
+ target = get_parent_dof_or_switch(obj)
+ return target is not None and get_bml_type(target) == BlenderNodeType.SWITCH
+
+ def execute(self, context):
+ obj = get_parent_dof_or_switch(context.active_object)
+ switches = get_switches()
+ idx = getattr(obj, "switch_list_index", -1)
+ try:
+ print(f"[DEBUG] assign_switch_from_index.pre: obj={getattr(obj,'name',None)} index={idx} switches_len={len(switches)}")
+ except Exception:
+ pass
+ if 0 <= idx < len(switches):
+ sw = switches[idx]
+ obj.bml_switch_number = sw.switch_number
+ obj.bml_switch_branch = sw.branch
+ update_switch_or_dof_name(obj, context)
+ try:
+ print(f"[DEBUG] assign_switch_from_index.post: obj={getattr(obj,'name',None)} assigned={sw.switch_number}:{sw.branch} from_index={idx}")
+ except Exception:
+ pass
+ self.report({'INFO'}, f"Assigned Switch #{sw.switch_number} Branch {sw.branch} from index {idx}")
+ return {'FINISHED'}
+ try:
+ print(f"[DEBUG] assign_switch_from_index.out_of_range: obj={getattr(obj,'name',None)} index={idx} len={len(switches)}")
+ except Exception:
+ pass
+ self.report({'WARNING'}, (
+ f"Switch list index {idx} out of range; no assignment performed. "
+ f"List may be stale or truncated – reload switch.xml (disable/enable addon) or refresh definitions."
+ ))
+ return {'CANCELLED'}
+
+
+class BML_OT_assign_dof_from_index(bpy.types.Operator):
+ # Populate persistent DOF number from the currently selected list entry - useful if object was created before persistent ID properties added
+
+ bl_idname = "bml.assign_dof_from_index"
+ bl_label = "Assign from Index"
+ bl_description = (
+ "Assign persistent DOF Number from the current DOF list selection. "
+ "Uses dof_list_index; overwrites existing persistent ID."
+ )
+ bl_options = {"UNDO"}
+
+ @classmethod
+ def poll(cls, context):
+ obj = context.active_object
+ if not obj:
+ return False
+ target = get_parent_dof_or_switch(obj)
+ return target is not None and get_bml_type(target) == BlenderNodeType.DOF
+
+ def execute(self, context):
+ obj = get_parent_dof_or_switch(context.active_object)
+ dofs = get_dofs()
+ idx = getattr(obj, "dof_list_index", -1)
+ if 0 <= idx < len(dofs):
+ de = dofs[idx]
+ obj.bml_dof_number = de.dof_number
+ update_switch_or_dof_name(obj, context)
+ self.report({'INFO'}, f"Assigned DOF #{de.dof_number} from index {idx}")
+ return {'FINISHED'}
+ self.report({'WARNING'}, (
+ f"DOF list index {idx} out of range; no assignment performed. "
+ f"List may be stale or truncated – reload DOF.xml (disable/enable addon) or refresh definitions."
+ ))
+ return {'CANCELLED'}
+
+
+class BML_OT_reassign_all_ids(bpy.types.Operator):
+ # Batch assign persistent switch/dof ID/branch from current list indices (convert legacy index-based method to persistent property method)
+ # Scope: entire scene or only active collection hierarchy
+ # Reassign each for consistency
+
+
+ bl_idname = "bml.reassign_all_ids"
+ bl_label = "Re-Assign All IDs"
+ bl_description = (
+ "Batch assign persistent Switch / DOF IDs from current list indices across the chosen scope. "
+ "Overwrites existing persistent IDs. Use wrapper operators in UI for specific targets."
+ )
+ bl_options = {"UNDO"}
+
+ scope = bpy.props.EnumProperty(
+ name="Scope",
+ items=(
+ ("SCENE", "Whole Scene", "Process every object in the scene"),
+ ("ACTIVE_COLLECTION", "Active Collection", "Process only objects in the active collection (recursive)"),
+ ),
+ default="SCENE",
+ )
+
+ target_types = bpy.props.EnumProperty(
+ name="Target",
+ items=(
+ ("BOTH", "Switches & DOFs", "Assign both"),
+ ("SWITCH", "Switches Only", "Assign only switches"),
+ ("DOF", "DOFs Only", "Assign only dofs"),
+ ),
+ default="BOTH",
+ )
+
+ confirm = bpy.props.BoolProperty(default=True)
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def _collect_objects(self, context):
+ if self.scope == "SCENE":
+ return list(context.scene.objects)
+ # ACTIVE_COLLECTION path
+ coll = context.view_layer.active_layer_collection.collection if context.view_layer.active_layer_collection else None
+ if not coll:
+ return []
+ result = set()
+
+ def _recurse(c):
+ for obj in c.objects:
+ result.add(obj)
+ for child in c.children:
+ _recurse(child)
+
+ _recurse(coll)
+ return list(result)
+
+ def execute(self, context):
+ switches_enum = get_switches()
+ dofs_enum = get_dofs()
+ processed_switches = 0
+ processed_dofs = 0
+ objs = self._collect_objects(context)
+ for obj in objs:
+ bml_type = get_bml_type(obj)
+ if self.target_types in {"BOTH", "SWITCH"} and bml_type == BlenderNodeType.SWITCH:
+ idx = getattr(obj, "switch_list_index", -1)
+ if 0 <= idx < len(switches_enum):
+ sw = switches_enum[idx]
+ obj.bml_switch_number = sw.switch_number
+ obj.bml_switch_branch = sw.branch
+ update_switch_or_dof_name(obj, context)
+ processed_switches += 1
+ if self.target_types in {"BOTH", "DOF"} and bml_type == BlenderNodeType.DOF:
+ idx = getattr(obj, "dof_list_index", -1)
+ if 0 <= idx < len(dofs_enum):
+ de = dofs_enum[idx]
+ obj.bml_dof_number = de.dof_number
+ update_switch_or_dof_name(obj, context)
+ processed_dofs += 1
+ self.report({'INFO'}, f"Re-assigned IDs - Switches: {processed_switches}, DOFs: {processed_dofs}")
+ return {'FINISHED'}
+
+
+# ---------------------------------------------------------------------------
+# Internal shared helper for wrapper batch operators (simpler popup usage)
+# ---------------------------------------------------------------------------
+def _batch_reassign(context, scope: str, target: str, target_objects=None):
+ """
+ Batch assign persistent IDs from list indices.
+
+ Args:
+ target_objects: Optional list of specific objects to process.
+ If None, processes all objects in scope.
+ """
+ switches_enum = get_switches()
+ dofs_enum = get_dofs()
+ processed_switches = 0
+ processed_dofs = 0
+
+ def collect(scope_mode):
+ if scope_mode == "SCENE":
+ return list(context.scene.objects)
+ coll = context.view_layer.active_layer_collection.collection if context.view_layer.active_layer_collection else None
+ if not coll:
+ return []
+ result = set()
+ def _rec(c):
+ for o in c.objects:
+ result.add(o)
+ for ch in c.children:
+ _rec(ch)
+ _rec(coll)
+ return list(result)
+
+ # Use target_objects if provided, otherwise collect from scope
+ if target_objects is not None:
+ objs = target_objects
+ else:
+ objs = collect(scope)
+
+ for obj in objs:
+ bml_type = get_bml_type(obj)
+ if target in {"SWITCH", "BOTH"} and bml_type == BlenderNodeType.SWITCH:
+ idx = getattr(obj, "switch_list_index", -1)
+ if 0 <= idx < len(switches_enum):
+ sw = switches_enum[idx]
+ obj.bml_switch_number = sw.switch_number
+ obj.bml_switch_branch = sw.branch
+ update_switch_or_dof_name(obj, context)
+ processed_switches += 1
+ if target in {"DOF", "BOTH"} and bml_type == BlenderNodeType.DOF:
+ idx = getattr(obj, "dof_list_index", -1)
+ if 0 <= idx < len(dofs_enum):
+ de = dofs_enum[idx]
+ obj.bml_dof_number = de.dof_number
+ update_switch_or_dof_name(obj, context)
+ processed_dofs += 1
+ return processed_switches, processed_dofs
+
+
+def assign_persistent_ids_to_objects(context, objects):
+ """
+ Assign persistent IDs to specific objects only.
+
+ Returns (switches_assigned, dofs_assigned) counts.
+ Used by validation dialogs for targeted assignment.
+ """
+ return _batch_reassign(context, "SCENE", "BOTH", target_objects=objects)
+
+
+class BML_OT_reassign_switches_scene(bpy.types.Operator):
+ bl_idname = "bml.reassign_switches_scene"
+ bl_label = "Re-Assign All Switches (Scene)"
+ bl_description = (
+ "Batch assign persistent IDs for every SWITCH in the entire scene "
+ "from its switch_list_index."
+ )
+ bl_options = {"UNDO"}
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def execute(self, context):
+ ps, _ = _batch_reassign(context, "SCENE", "SWITCH")
+ self.report({'INFO'}, f"Re-assigned {ps} switches (scene)")
+ return {'FINISHED'}
+
+
+class BML_OT_reassign_switches_collection(bpy.types.Operator):
+ bl_idname = "bml.reassign_switches_collection"
+ bl_label = "Re-Assign All Switches (Active Collection)"
+ bl_description = (
+ "Batch assign persistent IDs for every SWITCH in active collection (recursive) "
+ "from their switch_list_index."
+ )
+ bl_options = {"UNDO"}
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def execute(self, context):
+ ps, _ = _batch_reassign(context, "ACTIVE_COLLECTION", "SWITCH")
+ self.report({'INFO'}, f"Re-assigned {ps} switches (active collection)")
+ return {'FINISHED'}
+
+
+class BML_OT_reassign_dofs_scene(bpy.types.Operator):
+ bl_idname = "bml.reassign_dofs_scene"
+ bl_label = "Re-Assign All DOFs (Scene)"
+ bl_description = (
+ "Batch assign persistent ID for every DOF in the entire scene from its dof_list_index."
+ )
+ bl_options = {"UNDO"}
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def execute(self, context):
+ _, pd = _batch_reassign(context, "SCENE", "DOF")
+ self.report({'INFO'}, f"Re-assigned {pd} DOFs (scene)")
+ return {'FINISHED'}
+
+
+class BML_OT_reassign_dofs_collection(bpy.types.Operator):
+ bl_idname = "bml.reassign_dofs_collection"
+ bl_label = "Re-Assign All DOFs (Active Collection)"
+ bl_description = (
+ "Batch assign persistent ID for DOFs under the active collection (recursive) from dof_list_index."
+ )
+ bl_options = {"UNDO"}
+
+ def invoke(self, context, event):
+ return context.window_manager.invoke_confirm(self, event)
+
+ def execute(self, context):
+ _, pd = _batch_reassign(context, "ACTIVE_COLLECTION", "DOF")
+ self.report({'INFO'}, f"Re-assigned {pd} DOFs (active collection)")
+ return {'FINISHED'}
+
+
+class BML_OT_assign_switch_popup(bpy.types.Operator):
+ # Popup to choose scope for assigning switch persistent IDs.
+ bl_idname = "bml.assign_switch_popup"
+ bl_label = "Assign Switch IDs"
+ bl_description = (
+ "Assign persistent IDs for switch(es)."
+ )
+
+ def invoke(self, context, event):
+ def draw_fn(self_, ctx):
+ self_.layout.label(text="Assign persistent Switch IDs:")
+ col = self_.layout.column(align=True)
+ col.operator("bml.assign_switch_from_index", text="This Switch Only", icon='OBJECT_DATA')
+ col.operator("bml.reassign_switches_scene", icon='SEQUENCE_COLOR_04')
+ col.operator("bml.reassign_switches_collection", icon='SEQUENCE_COLOR_02')
+ self_.layout.separator()
+ self_.layout.label(text="Esc or click outside to cancel")
+ context.window_manager.popup_menu(draw_fn, title="Switch ID Assignment", icon='OUTLINER_OB_EMPTY')
+ return {'FINISHED'}
+
+
+class BML_OT_assign_dof_popup(bpy.types.Operator):
+ # Popup to choose scope for assigning DOF persistent IDs.
+ bl_idname = "bml.assign_dof_popup"
+ bl_label = "Assign DOF IDs"
+ bl_description = (
+ "Assign persistent IDs for DOF(s)."
+ )
+
+ def invoke(self, context, event):
+ def draw_fn(self_, ctx):
+ self_.layout.label(text="Assign persistent DOF IDs:")
+ col = self_.layout.column(align=True)
+ col.operator("bml.assign_dof_from_index", text="This DOF Only", icon='EMPTY_ARROWS')
+ col.operator("bml.reassign_dofs_scene", icon='SEQUENCE_COLOR_04')
+ col.operator("bml.reassign_dofs_collection", icon='SEQUENCE_COLOR_02')
+ self_.layout.separator()
+ self_.layout.label(text="Esc or click outside to cancel")
+ context.window_manager.popup_menu(draw_fn, title="DOF ID Assignment", icon='EMPTY_ARROWS')
+ return {'FINISHED'}
+
+
+def register():
+ bpy.utils.register_class(BML_OT_assign_switch_from_index)
+ bpy.utils.register_class(BML_OT_assign_dof_from_index)
+ bpy.utils.register_class(BML_OT_reassign_all_ids)
+ bpy.utils.register_class(BML_OT_reassign_switches_scene)
+ bpy.utils.register_class(BML_OT_reassign_switches_collection)
+ bpy.utils.register_class(BML_OT_reassign_dofs_scene)
+ bpy.utils.register_class(BML_OT_reassign_dofs_collection)
+ bpy.utils.register_class(BML_OT_assign_switch_popup)
+ bpy.utils.register_class(BML_OT_assign_dof_popup)
+
+
+def unregister():
+ bpy.utils.unregister_class(BML_OT_assign_dof_popup)
+ bpy.utils.unregister_class(BML_OT_assign_switch_popup)
+ bpy.utils.unregister_class(BML_OT_reassign_dofs_collection)
+ bpy.utils.unregister_class(BML_OT_reassign_dofs_scene)
+ bpy.utils.unregister_class(BML_OT_reassign_switches_collection)
+ bpy.utils.unregister_class(BML_OT_reassign_switches_scene)
+ bpy.utils.unregister_class(BML_OT_reassign_all_ids)
+ bpy.utils.unregister_class(BML_OT_assign_dof_from_index)
+ bpy.utils.unregister_class(BML_OT_assign_switch_from_index)
diff --git a/bms_blender_plugin/ui_tools/operators/create_switch.py b/bms_blender_plugin/ui_tools/operators/create_switch.py
index c77d4f6..614194c 100644
--- a/bms_blender_plugin/ui_tools/operators/create_switch.py
+++ b/bms_blender_plugin/ui_tools/operators/create_switch.py
@@ -28,6 +28,12 @@ def execute(self, context):
f"Switch - {switch.name} ({switch.switch_number})", None
)
switch_object.bml_type = str(BlenderNodeType.SWITCH)
+ switch_object.empty_display_type = context.preferences.addons[
+ "bms_blender_plugin"
+ ].preferences.switch_empty_type
+ switch_object.empty_display_size = context.preferences.addons[
+ "bms_blender_plugin"
+ ].preferences.switch_empty_size
if context.active_object:
# assumes that every object is linked to at least one collection
diff --git a/bms_blender_plugin/ui_tools/panels/dof_panel.py b/bms_blender_plugin/ui_tools/panels/dof_panel.py
index 6e24c19..17619b4 100644
--- a/bms_blender_plugin/ui_tools/panels/dof_panel.py
+++ b/bms_blender_plugin/ui_tools/panels/dof_panel.py
@@ -28,6 +28,37 @@ class DofList(UIList):
def __init__(self):
self.use_filter_show = True
+ def filter_items(self, context, data, propname):
+ """Custom filter that matches both name and DOF numbers"""
+ dofs = getattr(data, propname)
+
+ flt_flags = []
+ flt_neworder = []
+
+ # Check if there's a search filter active
+ if self.filter_name:
+ # Start with name-based filtering
+ flt_flags = bpy.types.UI_UL_list.filter_items_by_name(
+ self.filter_name, self.bitflag_filter_item, dofs, "name"
+ )
+
+ # Also check if the filter text matches DOF numbers
+ filter_text = self.filter_name.lower().strip()
+ if filter_text.isdigit():
+ for i, dof in enumerate(dofs):
+ # If name filter already matched, keep it
+ if flt_flags[i] & self.bitflag_filter_item:
+ continue
+
+ # Check if filter matches DOF number (supports partial matching)
+ if str(dof.dof_number).startswith(filter_text):
+ flt_flags[i] |= self.bitflag_filter_item
+ else:
+ # No filter, sort by name
+ flt_neworder = bpy.types.UI_UL_list.sort_items_by_name(dofs, "name")
+
+ return flt_flags, flt_neworder
+
def draw_item(
self, context, layout, data, item, icon, active_data, active_propname, index
):
@@ -68,7 +99,32 @@ def draw(self, context):
"dof_list_index",
)
row = layout.row()
- row.prop(dof, "dof_type")
+ # Persistent ID box shown first
+ box_ids = layout.box()
+ box_ids.label(text="Persistent DOF Properties:")
+ box_ids.prop(dof, "bml_dof_number")
+ dof_num = getattr(dof, "bml_dof_number", -1)
+ if dof_num < 0:
+ row_unset = box_ids.row(align=True)
+ row_unset.label(text="Not Assigned", icon="ERROR")
+ # Use popup to provide single + scene/collection batch assignment options
+ row_unset.operator("bml.assign_dof_popup", text="Assign...", icon="IMPORT")
+ else:
+ found = False
+ try:
+ from bms_blender_plugin.common.util import get_dofs
+ for de in get_dofs():
+ if de.dof_number == dof_num:
+ found = True
+ break
+ except Exception:
+ pass
+ if not found:
+ box_ids.label(text="Warning: DOF number not found in DOF.xml (still exported)", icon="INFO")
+
+ # DOF Type selector moved below persistent ID box for clarity
+ type_row = layout.row()
+ type_row.prop(dof, "dof_type")
layout.separator()
row = layout.row()
@@ -122,12 +178,13 @@ def draw(self, context):
layout.label(text=f"Unknown DOF type: {active_object.dof_type}")
layout.separator()
- layout.label(text="DOF Options")
- layout.prop(dof, "dof_check_limits")
- layout.prop(dof, "dof_reverse")
- layout.prop(dof, "dof_normalise")
- layout.prop(dof, "dof_multiplier")
- layout.prop(dof, "dof_multiply_min_max")
+ options_box = layout.box()
+ options_box.label(text="DOF Options")
+ options_box.prop(dof, "dof_check_limits")
+ options_box.prop(dof, "dof_reverse")
+ options_box.prop(dof, "dof_normalise")
+ options_box.prop(dof, "dof_multiplier")
+ options_box.prop(dof, "dof_multiply_min_max")
def register():
diff --git a/bms_blender_plugin/ui_tools/panels/switch_panel.py b/bms_blender_plugin/ui_tools/panels/switch_panel.py
index 6ebfbd5..ea0a830 100644
--- a/bms_blender_plugin/ui_tools/panels/switch_panel.py
+++ b/bms_blender_plugin/ui_tools/panels/switch_panel.py
@@ -25,17 +25,57 @@ class SwitchList(UIList):
def __init__(self):
self.use_filter_show = True
+ def filter_items(self, context, data, propname):
+ """Custom filter that matches both name and switch/branch numbers"""
+ switches = getattr(data, propname)
+
+ flt_flags = []
+ flt_neworder = []
+
+ # Check if there's a search filter active
+ if self.filter_name:
+ # Start with name-based filtering
+ flt_flags = bpy.types.UI_UL_list.filter_items_by_name(
+ self.filter_name, self.bitflag_filter_item, switches, "name"
+ )
+
+ # Also check if the filter text matches switch or branch numbers
+ filter_text = self.filter_name.lower().strip()
+ if filter_text.isdigit() or ':' in filter_text:
+ for i, switch in enumerate(switches):
+ # If name filter already matched, keep it
+ if flt_flags[i] & self.bitflag_filter_item:
+ continue
+
+ # Check if filter matches switch number
+ if filter_text.isdigit() and str(switch.switch_number).startswith(filter_text):
+ flt_flags[i] |= self.bitflag_filter_item
+ # Check if filter matches switch:branch format
+ elif ':' in filter_text:
+ switch_branch_text = f"{switch.switch_number}:{switch.branch_number}"
+ if switch_branch_text.startswith(filter_text):
+ flt_flags[i] |= self.bitflag_filter_item
+ else:
+ # No filter: preserve original insertion (XML) order which is already numeric (switch_number, branch_number)
+ if switches:
+ # Flag all items visible; no reordering
+ flt_flags = [self.bitflag_filter_item] * len(switches)
+ flt_neworder = [] # empty => keep original order
+
+ return flt_flags, flt_neworder
+
def draw_item(
self, context, layout, data, item, icon, active_data, active_propname, index
):
custom_icon = "OUTLINER_OB_EMPTY"
if self.layout_type in {"DEFAULT", "COMPACT"}:
- layout.label(text=f"{item.name} ({item.switch_number})", icon=custom_icon)
+ # Display switch number and branch together e.g. 213:24
+ layout.label(text=f"{item.name} ({item.switch_number}:{item.branch_number})", icon=custom_icon)
elif self.layout_type in {"GRID"}:
layout.alignment = "CENTER"
- layout.label(text=item.switch_number, icon=custom_icon)
+ layout.label(text=f"{item.switch_number}:{item.branch_number}", icon=custom_icon)
class SwitchPanel(BasePanel, bpy.types.Panel):
@@ -65,14 +105,41 @@ def draw(self, context):
switch,
"switch_list_index",
)
-
- comment = get_switches()[switch.switch_list_index].comment
-
- if comment and comment != "":
- layout.row()
+ # Comment (legacy list based)
+ try:
+ comment = get_switches()[switch.switch_list_index].comment
+ except Exception:
+ comment = ""
+ if comment:
layout.label(text=comment)
- layout.prop(switch, "switch_default_on")
+ box = layout.box()
+ box.label(text="Persistent Switch Properties:")
+ row_ids = box.row(align=True)
+ row_ids.prop(switch, "bml_switch_number")
+ row_ids.prop(switch, "bml_switch_branch")
+
+ # Show mismatch / status info
+ sw_num = getattr(switch, "bml_switch_number", -1)
+ sw_branch = getattr(switch, "bml_switch_branch", -1)
+ if sw_num < 0 or sw_branch < 0:
+ row_unset = box.row(align=True)
+ row_unset.label(text="Not Assigned", icon="ERROR")
+ row_unset.operator("bml.assign_switch_popup", text="Assign...", icon="IMPORT")
+ else:
+ # Check if present in current list
+ found = False
+ try:
+ for sw in get_switches():
+ if sw.switch_number == sw_num and sw.branch == sw_branch:
+ found = True
+ break
+ except Exception:
+ pass
+ if not found:
+ box.label(text="Warning: IDs not found in switch.xml (still exported)", icon="INFO")
+
+ box.prop(switch, "switch_default_on")
def register():
diff --git a/bms_blender_plugin/ui_tools/panels/tools_panel.py b/bms_blender_plugin/ui_tools/panels/tools_panel.py
index 2e9d6c2..f1a1949 100644
--- a/bms_blender_plugin/ui_tools/panels/tools_panel.py
+++ b/bms_blender_plugin/ui_tools/panels/tools_panel.py
@@ -26,9 +26,9 @@ def draw(self, context):
obj_bms_coords = to_bms_coords(
context.active_object.matrix_world.translation
)
- x_coord_text = f"{round(obj_bms_coords.x * scale_factor, 2): .2f}"
- y_coord_text = f"{round(obj_bms_coords.y * scale_factor, 2): .2f}"
- z_coord_text = f"{round(obj_bms_coords.z * scale_factor, 2): .2f}"
+ x_coord_text = f"{obj_bms_coords.x * scale_factor: .6f}"
+ y_coord_text = f"{obj_bms_coords.y * scale_factor: .6f}"
+ z_coord_text = f"{obj_bms_coords.z * scale_factor: .6f}"
layout.label(text="BMS Coordinates")
box = layout.box()