@@ -418,6 +418,8 @@ PsbList BuildChildrenFromMotion(PsbDictionary dic, IPsbCollection parent)
418418 objectChildrenItem [ "parameterize" ] = motionItem . ContainsKey ( "parameterize" ) && motionItem [ "parameterize" ] is not PsbNull ?
419419 parameter [ ( ( PsbNumber ) motionItem [ "parameterize" ] ) . IntValue ]
420420 : FillDefaultParameterize ( dic ) ;
421+ // priorityFrameList is a draw-order list of flattened layer indices, not a layer id list.
422+ // After mc=1 helper layers are inserted, these indices must be remapped.
421423 objectChildrenItem [ "priorityFrameList" ] = BuildPriorityFrameList ( ( PsbList ) motionItem [ "priority" ] ) ;
422424 objectChildrenItem [ "referenceModelFileList" ] = motionItem [ "referenceModelFileList" ] ;
423425 objectChildrenItem [ "referenceProjectFileList" ] = motionItem [ "referenceProjectFileList" ] ;
@@ -429,7 +431,22 @@ PsbList BuildChildrenFromMotion(PsbDictionary dic, IPsbCollection parent)
429431 int lastTime = lastTimeVal ? . IntValue ?? 61 ;
430432 var layer = ( PsbList ) motionItem [ "layer" ] ;
431433 layer . Parent = objectChildrenItem ;
434+
435+ // Capture flat layer order before topology changes.
436+ // Note: mc=1 means meshCombine-enabled helper layer.
437+ // Once meshCombinator is materialized into physical helper layers, flatten indices shift.
438+ var oldFlatList = FlattenLayerTree ( layer ) ;
439+
432440 BuildLayerChildren ( layer , parameter , lastTime ) ;
441+
442+ // priorityFrameList stores indices into flattened layer order.
443+ // If we inserted helpers (count changed), remap old indices to new positions.
444+ var newFlatList = FlattenLayerTree ( layer ) ;
445+ if ( oldFlatList . Count != newFlatList . Count )
446+ {
447+ RemapPriorityFrameList ( objectChildrenItem [ "priorityFrameList" ] as PsbList , oldFlatList , newFlatList ) ;
448+ }
449+
433450 objectChildrenItem [ "layerChildren" ] = motionItem [ "layer" ] ;
434451
435452 objectChildren_children . Add ( objectChildrenItem ) ;
@@ -463,7 +480,8 @@ void BuildLayerChildren(IPsbCollection child, PsbList parameter, int lastTime)
463480 dic [ "className" ] = classType . ToString ( ) . ToPsbString ( ) ;
464481 dic [ "comment" ] = PsbString . Empty ;
465482
466- //Restore meshCombinator BEFORE BuildFrameList so new frames get processed
483+ // Restore meshCombinator before BuildFrameList so packed mesh data is expanded into real helper layers.
484+ // meshCombinator is the packed container that stores ci=0 / ci>0 meshCombine tracks.
467485 if ( dic . ContainsKey ( "meshCombinator" ) && dic [ "meshCombinator" ] is PsbDictionary meshCombinator )
468486 {
469487 RestoreMeshCombinator ( dic , meshCombinator , parameter , lastTime ) ;
@@ -527,7 +545,8 @@ void BuildLayerChildren(IPsbCollection child, PsbList parameter, int lastTime)
527545 } ;
528546 }
529547
530- //Expand meshSyncChildMask
548+ // meshSyncChildMask controls which child meshCombine layers inherit or sync to this layer.
549+ // Expand the packed bitmask into explicit MMO fields.
531550 if ( dic [ "meshSyncChildMask" ] is PsbNumber number )
532551 {
533552 dic [ "meshSyncChildShape" ] = ( number . IntValue & 8 ) == 8 ? 1 . ToPsbNumber ( ) : PsbNumber . Zero ;
@@ -740,6 +759,8 @@ PsbDictionary BuildCharaProfileItem(string id, string label, string path)
740759 /// Restore meshCombinator to individual layers with mesh frame data.
741760 /// <para>During MMO→PSB compilation, stripMeshCombineLayer extracts mesh data from layers
742761 /// and packs it into meshCombinator on a parent layer. This method reverses that process:</para>
762+ /// <para>Terminology: in combinatorList, ci means combinator index.
763+ /// Typically ci=0 is the parent/self mesh track, ci>0 are helper-layer tracks.</para>
743764 /// <para>If the parent already has its own explicit parameter, keep it and materialize
744765 /// combinator entries as nested meshCombine child layers.</para>
745766 /// <para>Otherwise ci=0 restores into the parent layer's frameList and the remaining
@@ -777,6 +798,7 @@ private void RestoreMeshCombinator(PsbDictionary parent, PsbDictionary meshCombi
777798
778799 // If the parent does not already expose a different parameter, restore the first
779800 // combinator directly onto the parent and materialize the remaining ones as child layers.
801+ // In most assets this first entry corresponds to ci=0.
780802 if ( restoreFirstToParent && combinatorList [ 0 ] is PsbDictionary firstCombinator )
781803 {
782804 var variable = firstCombinator [ "variable" ] as PsbDictionary ;
@@ -839,14 +861,8 @@ private void RestoreMeshCombinator(PsbDictionary parent, PsbDictionary meshCombi
839861 }
840862 }
841863
842- // Reparenting the original children under restored helper nodes changes the
843- // canonical layer paths for MAkar120 and regresses visibility back to a
844- // lower-body-only state. Keep the remaining combinators packed until we can
845- // restore them without rewriting the visible child topology.
846- if ( startIndex >= combinatorList . Count )
847- {
848- parent . Remove ( "meshCombinator" ) ;
849- }
864+ MaterializeMeshCombinatorChildren ( parent , combinatorList , startIndex , parameter , lastTime , valuesPerMesh , templateContent ) ;
865+ parent . Remove ( "meshCombinator" ) ;
850866 }
851867
852868 private void MaterializeMeshCombinatorChildren ( PsbDictionary parent , PsbList combinatorList , int startIndex ,
@@ -862,6 +878,8 @@ private void MaterializeMeshCombinatorChildren(PsbDictionary parent, PsbList com
862878 PsbList currentChildren = topChildren ;
863879 PsbDictionary deepestHelper = null ;
864880
881+ // Build helper layers as nested chain, not flat siblings:
882+ // helper(startIndex) -> helper(startIndex+1) -> ... -> originalChildren
865883 for ( var index = startIndex ; index < combinatorList . Count ; index ++ )
866884 {
867885 if ( ! ( combinatorList [ index ] is PsbDictionary combinator ) ||
@@ -882,7 +900,7 @@ private void MaterializeMeshCombinatorChildren(PsbDictionary parent, PsbList com
882900 continue ;
883901 }
884902
885- var allValues = DecodeMeshValues ( rawMeshList , meshCount , valuesPerMesh , isDelta : false ) ;
903+ var allValues = DecodeMeshValues ( rawMeshList , meshCount , valuesPerMesh , isDelta : true ) ;
886904 if ( allValues == null )
887905 {
888906 continue ;
@@ -894,6 +912,7 @@ private void MaterializeMeshCombinatorChildren(PsbDictionary parent, PsbList com
894912 helper [ "label" ] = GetMeshCombinatorLayerLabel ( key ) . ToPsbString ( ) ;
895913 helper [ "frameList" ] = BuildMeshFrameList ( allValues , meshCount , neutralIndex , valuesPerMesh , lastTime , templateContent ) ;
896914 helper [ "parameterize" ] = paramIndex . ToPsbNumber ( ) ;
915+ // mc means meshCombine. 1 means this node is a meshCombine helper layer.
897916 helper [ "meshCombine" ] = 1 . ToPsbNumber ( ) ;
898917 helper . Remove ( "meshCombinator" ) ;
899918 helper . Parent = currentChildren ;
@@ -956,6 +975,7 @@ private PsbList BuildMeshFrameList(double[] allValues, int meshCount, int neutra
956975 content [ "src" ] = "blank" . ToPsbString ( ) ;
957976 }
958977
978+ // mesh is the PSB-side mesh payload; it contains bp (BezierPatch) and cc (ColorControl).
959979 content [ "mesh" ] = BuildMeshDict ( allValues , meshIndex , neutralIndex , valuesPerMesh ) ;
960980 frameList . Add ( new PsbDictionary
961981 {
@@ -1136,6 +1156,7 @@ private static PsbDictionary BuildMeshDict(double[] allValues, int meshIndex, in
11361156 {
11371157 if ( meshIndex == neutralIndex )
11381158 {
1159+ // bp = BezierPatch, cc = ColorControl.
11391160 return new PsbDictionary { { "bp" , PsbNull . Null } , { "cc" , PsbNull . Null } } ;
11401161 }
11411162
@@ -1144,6 +1165,7 @@ private static PsbDictionary BuildMeshDict(double[] allValues, int meshIndex, in
11441165 {
11451166 bp . Add ( new PsbNumber ( allValues [ meshIndex * valuesPerMesh + vi ] ) ) ;
11461167 }
1168+ // BuildFrameList later maps them to MMO keys mbp/mcc.
11471169 return new PsbDictionary { { "bp" , bp } , { "cc" , PsbNull . Null } } ;
11481170 }
11491171
@@ -1442,6 +1464,7 @@ private void BuildFrameList(PsbList frameList, MmoItemClass classType, out MmoFr
14421464
14431465 if ( content . ContainsKey ( "mesh" ) ) //25
14441466 {
1467+ // mbp/mcc are MMO-side mesh keys corresponding to PSB mesh.bp / mesh.cc.
14451468 content [ "mbp" ] = content [ "mesh" ] . Children ( "bp" ) ;
14461469 content [ "mcc" ] = content [ "mesh" ] . Children ( "cc" ) ;
14471470 content . Remove ( "mesh" ) ; //PSB v4 field, not needed in MMO
@@ -2203,7 +2226,7 @@ private static void FillDefaultsIntoChildren(PsbDictionary dic, MmoItemClass cla
22032226
22042227 /// <summary>
22052228 /// Default identity 4×4 bezier patch control points (32 doubles).
2206- /// Layout: (x,y) pairs, row by row. x ∈ {-0.1, 7/30, 17/30, 0.9 }, y ∈ {0, 1/3, 2/3, 1}
2229+ /// Layout: (x,y) pairs, row by row. x ∈ {0, 1/3, 2/3, 1 }, y ∈ {0, 1/3, 2/3, 1}
22072230 /// </summary>
22082231 private static readonly double [ ] BezierPatchDefault = GenerateBezierPatchDefault ( ) ;
22092232
@@ -2214,7 +2237,7 @@ private static double[] GenerateBezierPatchDefault()
22142237 {
22152238 for ( int col = 0 ; col < 4 ; col ++ )
22162239 {
2217- values [ ( row * 4 + col ) * 2 ] = - 0.1 + col / 3.0 ;
2240+ values [ ( row * 4 + col ) * 2 ] = col / 3.0 ;
22182241 values [ ( row * 4 + col ) * 2 + 1 ] = row / 3.0 ;
22192242 }
22202243 }
@@ -2256,6 +2279,103 @@ private static IPsbValue FillDefaultTargetOwn()
22562279 } ;
22572280 }
22582281
2282+ /// <summary>
2283+ /// Flatten a layer tree into a pre-order list (same order as EMT editor's flattenLayerList).
2284+ /// </summary>
2285+ private static List < PsbDictionary > FlattenLayerTree ( PsbList layers )
2286+ {
2287+ var result = new List < PsbDictionary > ( ) ;
2288+ foreach ( var item in layers )
2289+ {
2290+ if ( item is PsbDictionary dic )
2291+ {
2292+ result . Add ( dic ) ;
2293+ if ( dic [ "children" ] is PsbList children )
2294+ {
2295+ result . AddRange ( FlattenLayerTree ( children ) ) ;
2296+ }
2297+ }
2298+ }
2299+ return result ;
2300+ }
2301+
2302+ /// <summary>
2303+ /// After mc=1 layers are inserted by RestoreMeshCombinator, the flat layer order changes
2304+ /// but priorityFrameList still contains indices from the old (pre-insertion) order.
2305+ /// This method remaps those indices and adds new layers to the priority list.
2306+ /// mc = meshCombine helper flag; ci = combinator index during restoration.
2307+ /// </summary>
2308+ private static void RemapPriorityFrameList ( PsbList priorityFrameList , List < PsbDictionary > oldFlatList , List < PsbDictionary > newFlatList )
2309+ {
2310+ if ( priorityFrameList == null || priorityFrameList . Count == 0 )
2311+ return ;
2312+
2313+ // Build lookup: layer reference → new index
2314+ var newIndexMap = new Dictionary < PsbDictionary , int > ( newFlatList . Count ) ;
2315+ for ( int i = 0 ; i < newFlatList . Count ; i ++ )
2316+ {
2317+ newIndexMap [ newFlatList [ i ] ] = i ;
2318+ }
2319+
2320+ foreach ( var frame in priorityFrameList )
2321+ {
2322+ if ( ! ( frame is PsbDictionary fd ) || ! ( fd [ "content" ] is PsbList content ) )
2323+ continue ;
2324+
2325+ var remappedIndices = new List < int > ( newFlatList . Count ) ;
2326+ var usedNewIndices = new HashSet < int > ( ) ;
2327+
2328+ // Remap existing entries: old index → same layer object → new index
2329+ foreach ( var idx in content )
2330+ {
2331+ if ( idx is PsbNumber num && num . IntValue >= 0 && num . IntValue < oldFlatList . Count )
2332+ {
2333+ var layer = oldFlatList [ num . IntValue ] ;
2334+ if ( newIndexMap . TryGetValue ( layer , out int newIdx ) )
2335+ {
2336+ remappedIndices . Add ( newIdx ) ;
2337+ usedNewIndices . Add ( newIdx ) ;
2338+ }
2339+ }
2340+ }
2341+
2342+ // Insert new layers (mc=1 etc.) that weren't in old list.
2343+ // Place each at its natural position relative to its first mapped neighbour.
2344+ for ( int ni = 0 ; ni < newFlatList . Count ; ni ++ )
2345+ {
2346+ if ( usedNewIndices . Contains ( ni ) )
2347+ continue ;
2348+
2349+ // Find the first already-mapped index that comes after this new layer
2350+ // in the flat list, and insert the new layer just before it in the priority.
2351+ int insertPos = - 1 ;
2352+ for ( int search = ni + 1 ; search < newFlatList . Count ; search ++ )
2353+ {
2354+ if ( usedNewIndices . Contains ( search ) )
2355+ {
2356+ insertPos = remappedIndices . IndexOf ( search ) ;
2357+ break ;
2358+ }
2359+ }
2360+
2361+ if ( insertPos >= 0 )
2362+ remappedIndices . Insert ( insertPos , ni ) ;
2363+ else
2364+ remappedIndices . Add ( ni ) ;
2365+
2366+ usedNewIndices . Add ( ni ) ;
2367+ }
2368+
2369+ // Replace content with remapped indices
2370+ var newContent = new PsbList ( remappedIndices . Count ) ;
2371+ foreach ( var ri in remappedIndices )
2372+ {
2373+ newContent . Add ( ri . ToPsbNumber ( ) ) ;
2374+ }
2375+ fd [ "content" ] = newContent ;
2376+ }
2377+ }
2378+
22592379 #endregion
22602380 }
22612381}
0 commit comments