Skip to content
22 changes: 22 additions & 0 deletions bms_blender_plugin/common/constants.py
Original file line number Diff line number Diff line change
@@ -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",
]
37 changes: 37 additions & 0 deletions bms_blender_plugin/common/hydration.py
Original file line number Diff line number Diff line change
@@ -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)
92 changes: 92 additions & 0 deletions bms_blender_plugin/common/resolve_ids.py
Original file line number Diff line number Diff line change
@@ -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"]
Loading