@@ -3164,6 +3164,310 @@ def walk_calls(body_node, caller_nid: str) -> None:
31643164 return {"nodes" : nodes , "edges" : edges , "raw_calls" : raw_calls }
31653165
31663166
3167+ # ── DMI (BYOND icon files) ────────────────────────────────────────────────────
3168+ # .dmi is a PNG with a `zTXt`/`tEXt` "Description" chunk containing BYOND state
3169+ # metadata. We don't care about pixels — we want the icon state names, because
3170+ # `icon_state = "X"` in DM code references them.
3171+
3172+ def _read_dmi_description (data : bytes ) -> str :
3173+ """Pull the BYOND metadata text out of a .dmi PNG, or empty string on failure."""
3174+ import struct
3175+ import zlib
3176+ if not data .startswith (b"\x89 PNG\r \n \x1a \n " ):
3177+ return ""
3178+ i = 8
3179+ while i + 8 <= len (data ):
3180+ length = struct .unpack (">I" , data [i :i + 4 ])[0 ]
3181+ chunk_type = data [i + 4 :i + 8 ]
3182+ payload = data [i + 8 :i + 8 + length ]
3183+ if chunk_type in (b"tEXt" , b"zTXt" ):
3184+ try :
3185+ null = payload .index (b"\x00 " )
3186+ except ValueError :
3187+ return ""
3188+ keyword = payload [:null ]
3189+ if keyword == b"Description" :
3190+ if chunk_type == b"zTXt" :
3191+ # zTXt: keyword \0 method(1 byte) compressed-data
3192+ return zlib .decompress (payload [null + 2 :]).decode ("utf-8" , errors = "replace" )
3193+ return payload [null + 1 :].decode ("utf-8" , errors = "replace" )
3194+ i += 8 + length + 4 # length + type + payload + crc
3195+ return ""
3196+
3197+
3198+ def extract_dmi (path : Path ) -> dict :
3199+ """Extract icon state names from a .dmi (BYOND PNG icon sheet)."""
3200+ try :
3201+ data = path .read_bytes ()
3202+ except Exception as e :
3203+ return {"nodes" : [], "edges" : [], "error" : str (e )}
3204+
3205+ str_path = str (path )
3206+ stem = _file_stem (path )
3207+ file_nid = _make_id (str (path ))
3208+ nodes : list [dict ] = [{
3209+ "id" : file_nid , "label" : path .name , "file_type" : "code" ,
3210+ "source_file" : str_path , "source_location" : "L1" ,
3211+ }]
3212+ edges : list [dict ] = []
3213+
3214+ description = _read_dmi_description (data )
3215+ if not description :
3216+ return {"nodes" : nodes , "edges" : edges }
3217+
3218+ seen : set [str ] = {file_nid }
3219+ line_no = 0
3220+ for raw_line in description .splitlines ():
3221+ line_no += 1
3222+ stripped = raw_line .strip ()
3223+ if not stripped .startswith ("state =" ):
3224+ continue
3225+ # state = "name"
3226+ value = stripped .split ("=" , 1 )[1 ].strip ()
3227+ if value .startswith ('"' ) and value .endswith ('"' ) and len (value ) >= 2 :
3228+ state_name = value [1 :- 1 ]
3229+ else :
3230+ state_name = value
3231+ if not state_name :
3232+ continue
3233+ nid = _make_id (stem , "state" , state_name )
3234+ if nid in seen :
3235+ continue
3236+ seen .add (nid )
3237+ label = f'"{ state_name } "'
3238+ nodes .append ({
3239+ "id" : nid , "label" : label , "file_type" : "code" ,
3240+ "source_file" : str_path , "source_location" : f"L{ line_no } " ,
3241+ })
3242+ edges .append ({
3243+ "source" : file_nid , "target" : nid , "relation" : "contains" ,
3244+ "confidence" : "EXTRACTED" , "source_file" : str_path ,
3245+ "source_location" : f"L{ line_no } " , "weight" : 1.0 ,
3246+ })
3247+
3248+ return {"nodes" : nodes , "edges" : edges }
3249+
3250+
3251+ # ── DMM (BYOND map files) ─────────────────────────────────────────────────────
3252+ # A .dmm starts with a tile dictionary — each `"key" = (type, type{var=val}, ...)`
3253+ # names one or more types that compose a tile — then a grid laying tile keys in
3254+ # a 3D coordinate space. We only need the dictionary section: every type path
3255+ # referenced from a map is a `uses` edge from the map file to that type, which
3256+ # lets queries answer "where is /obj/foo placed?". Format reference:
3257+ # https://github.com/SpaceManiac/SpacemanDMM/blob/master/crates/dmm-tools/src/dmm/read.rs
3258+
3259+ # Grid-section header: "(x,y,z) = {" or TGM variant. The dictionary is everything
3260+ # before the first match.
3261+ _DMM_GRID_RE = re .compile (r"^\(\s*\d+\s*,\s*\d+\s*,\s*\d+\s*\)\s*=" , re .MULTILINE )
3262+
3263+
3264+ def _split_dmm_tile (body : str ) -> list [str ]:
3265+ """Split a tile's `(type1, type2{...}, type3)` body into raw entries at top-level commas."""
3266+ out : list [str ] = []
3267+ buf : list [str ] = []
3268+ depth = 0
3269+ in_string = False
3270+ escape = False
3271+ for ch in body :
3272+ if escape :
3273+ buf .append (ch )
3274+ escape = False
3275+ continue
3276+ if in_string :
3277+ buf .append (ch )
3278+ if ch == "\\ " :
3279+ escape = True
3280+ elif ch == '"' :
3281+ in_string = False
3282+ continue
3283+ if ch == '"' :
3284+ in_string = True
3285+ buf .append (ch )
3286+ elif ch in "({[" :
3287+ depth += 1
3288+ buf .append (ch )
3289+ elif ch in ")}]" :
3290+ depth -= 1
3291+ buf .append (ch )
3292+ elif ch == "," and depth == 0 :
3293+ out .append ("" .join (buf ).strip ())
3294+ buf = []
3295+ else :
3296+ buf .append (ch )
3297+ tail = "" .join (buf ).strip ()
3298+ if tail :
3299+ out .append (tail )
3300+ return out
3301+
3302+
3303+ def _dmm_type_path (entry : str ) -> str :
3304+ """Strip var overrides and trailing whitespace, leaving just `/foo/bar/baz`."""
3305+ brace = entry .find ("{" )
3306+ if brace != - 1 :
3307+ entry = entry [:brace ]
3308+ return entry .strip ()
3309+
3310+
3311+ def extract_dmm (path : Path ) -> dict :
3312+ """Extract type-path references from a .dmm map file's tile dictionary."""
3313+ try :
3314+ text = path .read_text (encoding = "utf-8" , errors = "replace" )
3315+ except Exception as e :
3316+ return {"nodes" : [], "edges" : [], "error" : str (e )}
3317+
3318+ str_path = str (path )
3319+ stem = _file_stem (path )
3320+ file_nid = _make_id (str (path ))
3321+ nodes : list [dict ] = [{
3322+ "id" : file_nid , "label" : path .name , "file_type" : "code" ,
3323+ "source_file" : str_path , "source_location" : "L1" ,
3324+ }]
3325+ edges : list [dict ] = []
3326+
3327+ # Trim to the dictionary section (everything before the first grid header).
3328+ grid_match = _DMM_GRID_RE .search (text )
3329+ dict_text = text [:grid_match .start ()] if grid_match else text
3330+
3331+ # Parse line-by-line, accumulating until we close the parens of each tile.
3332+ seen_targets : set [str ] = set ()
3333+ buf : list [str ] = []
3334+ open_line = 0
3335+ depth = 0
3336+ in_string = False
3337+ escape = False
3338+ for line_idx , line in enumerate (dict_text .splitlines (), start = 1 ):
3339+ for ch in line :
3340+ if escape :
3341+ escape = False
3342+ elif in_string :
3343+ if ch == "\\ " :
3344+ escape = True
3345+ elif ch == '"' :
3346+ in_string = False
3347+ elif ch == '"' :
3348+ in_string = True
3349+ elif ch == "(" :
3350+ if depth == 0 :
3351+ open_line = line_idx
3352+ depth += 1
3353+ elif ch == ")" :
3354+ depth -= 1
3355+ buf .append (ch )
3356+ buf .append ("\n " )
3357+ if depth == 0 and buf :
3358+ chunk = "" .join (buf )
3359+ buf = []
3360+ # Find the outermost (...) span in this completed chunk.
3361+ lp = chunk .find ("(" )
3362+ rp = chunk .rfind (")" )
3363+ if lp == - 1 or rp == - 1 or rp <= lp :
3364+ continue
3365+ inner = chunk [lp + 1 :rp ]
3366+ for entry in _split_dmm_tile (inner ):
3367+ tpath = _dmm_type_path (entry )
3368+ if not tpath .startswith ("/" ):
3369+ continue
3370+ tgt = _make_id (tpath )
3371+ if tgt in seen_targets :
3372+ continue
3373+ seen_targets .add (tgt )
3374+ edges .append ({
3375+ "source" : file_nid , "target" : tgt , "relation" : "uses" ,
3376+ "context" : "map" , "confidence" : "EXTRACTED" ,
3377+ "source_file" : str_path ,
3378+ "source_location" : f"L{ open_line } " , "weight" : 1.0 ,
3379+ })
3380+
3381+ return {"nodes" : nodes , "edges" : edges }
3382+
3383+
3384+ # ── DMF (BYOND interface forms) ───────────────────────────────────────────────
3385+ # Hierarchical key-value text:
3386+ # window "main"
3387+ # elem "map"
3388+ # type = MAP
3389+ # Indented blocks; control names live under `elem "X"`; the most useful thing
3390+ # to surface is the window/elem hierarchy because `winset()`/`winget()` calls
3391+ # in DM code reference these by name.
3392+
3393+ _DMF_WINDOW_RE = re .compile (r'^\s*window\s+"([^"]+)"\s*$' )
3394+ _DMF_ELEM_RE = re .compile (r'^\s*elem\s+"([^"]+)"\s*$' )
3395+ _DMF_TYPE_RE = re .compile (r'^\s*type\s*=\s*(\S+)\s*$' )
3396+
3397+
3398+ def extract_dmf (path : Path ) -> dict :
3399+ """Extract windows and controls from a .dmf interface file."""
3400+ try :
3401+ text = path .read_text (encoding = "utf-8" , errors = "replace" )
3402+ except Exception as e :
3403+ return {"nodes" : [], "edges" : [], "error" : str (e )}
3404+
3405+ str_path = str (path )
3406+ stem = _file_stem (path )
3407+ file_nid = _make_id (str (path ))
3408+ nodes : list [dict ] = [{
3409+ "id" : file_nid , "label" : path .name , "file_type" : "code" ,
3410+ "source_file" : str_path , "source_location" : "L1" ,
3411+ }]
3412+ edges : list [dict ] = []
3413+ seen : set [str ] = {file_nid }
3414+
3415+ current_window_nid : str | None = None
3416+ current_elem_nid : str | None = None
3417+ current_elem_name : str | None = None
3418+
3419+ for line_idx , line in enumerate (text .splitlines (), start = 1 ):
3420+ m = _DMF_WINDOW_RE .match (line )
3421+ if m :
3422+ name = m .group (1 )
3423+ nid = _make_id (stem , "window" , name )
3424+ if nid not in seen :
3425+ seen .add (nid )
3426+ nodes .append ({
3427+ "id" : nid , "label" : f'window "{ name } "' , "file_type" : "code" ,
3428+ "source_file" : str_path , "source_location" : f"L{ line_idx } " ,
3429+ })
3430+ edges .append ({
3431+ "source" : file_nid , "target" : nid , "relation" : "contains" ,
3432+ "confidence" : "EXTRACTED" , "source_file" : str_path ,
3433+ "source_location" : f"L{ line_idx } " , "weight" : 1.0 ,
3434+ })
3435+ current_window_nid = nid
3436+ current_elem_nid = None
3437+ current_elem_name = None
3438+ continue
3439+ m = _DMF_ELEM_RE .match (line )
3440+ if m and current_window_nid is not None :
3441+ name = m .group (1 )
3442+ nid = _make_id (stem , "elem" , current_window_nid , name )
3443+ if nid not in seen :
3444+ seen .add (nid )
3445+ nodes .append ({
3446+ "id" : nid , "label" : f'elem "{ name } "' , "file_type" : "code" ,
3447+ "source_file" : str_path , "source_location" : f"L{ line_idx } " ,
3448+ })
3449+ edges .append ({
3450+ "source" : current_window_nid , "target" : nid ,
3451+ "relation" : "contains" , "confidence" : "EXTRACTED" ,
3452+ "source_file" : str_path , "source_location" : f"L{ line_idx } " ,
3453+ "weight" : 1.0 ,
3454+ })
3455+ current_elem_nid = nid
3456+ current_elem_name = name
3457+ continue
3458+ m = _DMF_TYPE_RE .match (line )
3459+ if m and current_elem_nid is not None and current_elem_name is not None :
3460+ # Encode the BYOND control type into the elem's label so it shows
3461+ # up in the graph (MAP, BUTTON, INPUT, …).
3462+ ctype = m .group (1 )
3463+ for n in nodes :
3464+ if n ["id" ] == current_elem_nid and " [" not in n ["label" ]:
3465+ n ["label" ] = f'elem "{ current_elem_name } " [{ ctype } ]'
3466+ break
3467+
3468+ return {"nodes" : nodes , "edges" : edges }
3469+
3470+
31673471# ── Julia extractor (custom walk) ────────────────────────────────────────────
31683472
31693473def extract_julia (path : Path ) -> dict :
@@ -6335,6 +6639,9 @@ def walk_object(obj_node, parent_nid: str, parent_key: str | None,
63356639 ".json" : extract_json ,
63366640 ".dm" : extract_dm ,
63376641 ".dme" : extract_dm ,
6642+ ".dmi" : extract_dmi ,
6643+ ".dmm" : extract_dmm ,
6644+ ".dmf" : extract_dmf ,
63386645}
63396646
63406647
0 commit comments