@@ -507,13 +507,98 @@ def _build_modsettings_xml_p6(ordered_mods: list[BG3ModInfo]) -> str:
507507 return "" .join (parts )
508508
509509
510+ def _apply_manifest_pak_order (
511+ enabled : list [ModEntry ],
512+ mod_infos : dict [str , BG3ModInfo ],
513+ manifest_load_order : list [dict ],
514+ log_fn ,
515+ ) -> list [BG3ModInfo ]:
516+ """Order paks by the collection manifest's loadOrder array.
517+
518+ A single mod folder can ship multiple paks that the collection author
519+ intends to be interleaved with paks from other mods (e.g. load-order
520+ divider packs whose 30+ entries are spread throughout the LO). Walking
521+ by mod folder and emitting all paks per folder destroys this intent.
522+
523+ Strategy: build pak-filename → BG3ModInfo from on-disk scan, then walk
524+ ``manifest_load_order`` in order. Manifest entries point at pak files via
525+ their ``id`` field. Paks present on disk but missing from the manifest
526+ (user-added patches, mods installed outside the collection) are appended
527+ at the end — that maps to "top of modlist.txt" = highest priority for
528+ overrides, which matches how this manager treats user-added content.
529+
530+ Returns BG3ModInfo entries in lowest-priority-first order (ready for
531+ modsettings.lsx, where later entries override earlier ones).
532+ """
533+ _log = _safe_log (log_fn )
534+
535+ # mod_infos is keyed by uuid. Build a casefold lookup once.
536+ uuid_to_info_cf : dict [str , BG3ModInfo ] = {
537+ u .lower (): info for u , info in mod_infos .items ()
538+ }
539+
540+ # Walk manifest in order, matching by data.uuid. Only emit if the uuid
541+ # corresponds to an actually-installed pak.
542+ ordered : list [BG3ModInfo ] = []
543+ seen_uuids : set [str ] = set ()
544+ for manifest_entry in manifest_load_order :
545+ data = manifest_entry .get ("data" ) or {}
546+ uuid = (data .get ("uuid" ) or "" ).strip ().lower ()
547+ if not uuid :
548+ continue
549+ info = uuid_to_info_cf .get (uuid )
550+ if info is None :
551+ continue
552+ if info .uuid in seen_uuids :
553+ continue
554+ seen_uuids .add (info .uuid )
555+ ordered .append (info )
556+
557+ # Append any installed paks not covered by the manifest.
558+ # Walk in modlist order (already lowest-priority-first) so user-added
559+ # mods land in the same relative order they appear in modlist.txt.
560+ by_source : dict [str , list [BG3ModInfo ]] = {}
561+ for info in mod_infos .values ():
562+ if info .source_mod :
563+ by_source .setdefault (info .source_mod , []).append (info )
564+ for entry in enabled :
565+ for info in by_source .get (entry .name , ()):
566+ if info .uuid in seen_uuids :
567+ continue
568+ seen_uuids .add (info .uuid )
569+ ordered .append (info )
570+
571+ # Dependency sweep: the manifest already orders deps before dependents
572+ # (curators verify their collections boot), but user-added trailing
573+ # paks may reference deps that were emitted later by the manifest.
574+ # Walk the result and pull each pak's missing deps to immediately
575+ # before the pak itself, preserving the manifest's pak-level interleave.
576+ final : list [BG3ModInfo ] = []
577+ placed : set [str ] = set ()
578+
579+ def _emit_with_deps (info : BG3ModInfo ) -> None :
580+ if info .uuid in placed :
581+ return
582+ for dep_uuid in info .dependencies :
583+ dep = mod_infos .get (dep_uuid ) or uuid_to_info_cf .get (dep_uuid .lower ())
584+ if dep is not None and dep .uuid not in placed :
585+ _emit_with_deps (dep )
586+ placed .add (info .uuid )
587+ final .append (info )
588+
589+ for info in ordered :
590+ _emit_with_deps (info )
591+ return final
592+
593+
510594def write_modsettings (
511595 modsettings_path : Path ,
512596 modlist_path : Path ,
513597 staging_root : Path ,
514598 log_fn = None ,
515599 game_data_path : Path | None = None ,
516600 patch_version : int = 8 ,
601+ manifest_load_order : list [dict ] | None = None ,
517602) -> int :
518603 """End-to-end: scan paks, resolve order, write modsettings.lsx.
519604
@@ -527,6 +612,12 @@ def write_modsettings(
527612 - 7: Gustav campaign, Mods node only, Version64 + PublishHandle
528613 - 6: Gustav campaign, ModOrder + Mods nodes, 32-bit Version
529614
615+ *manifest_load_order* — optional list of entries from a collection
616+ manifest's ``loadOrder`` array. When provided, paks are emitted in the
617+ manifest's exact order (curators interleave paks from different mods —
618+ e.g. load-order divider packs — which the default folder-walk order
619+ destroys). Paks installed but not in the manifest are appended.
620+
530621 Returns the number of mod entries written (excluding the campaign entry).
531622 """
532623 _log = _safe_log (log_fn )
@@ -548,8 +639,12 @@ def write_modsettings(
548639 modsettings_path .write_text (xml , encoding = "utf-8" )
549640 return 0
550641
551- _log ("Resolving load order with dependency sorting ..." )
552- ordered = resolve_load_order (enabled , mod_infos )
642+ if manifest_load_order :
643+ _log (f"Resolving load order from collection manifest ({ len (manifest_load_order )} entries) ..." )
644+ ordered = _apply_manifest_pak_order (enabled , mod_infos , manifest_load_order , _log )
645+ else :
646+ _log ("Resolving load order with dependency sorting ..." )
647+ ordered = resolve_load_order (enabled , mod_infos )
553648 _log (f" Load order: { ', ' .join (m .name for m in ordered )} " )
554649
555650 # Build the set of UUIDs that are known to exist (installed mods +
0 commit comments