@@ -38,6 +38,25 @@ private static int GetBatchExportMaxConcurrency()
3838 return Math . Max ( 1 , Environment . ProcessorCount / 2 ) ;
3939 }
4040
41+ /// <summary>
42+ /// Given an AssetBundle jacket path (e.g. ".../AssetBundleImages/jacket/ui_jacket_000123.ab"),
43+ /// compute the companion small jacket path in the sibling "jacket_s" directory
44+ /// (e.g. ".../AssetBundleImages/jacket_s/ui_jacket_000123_s.ab").
45+ /// Returns null if the path shape is unexpected.
46+ /// </summary>
47+ private static string ? GetAssetBundleJacketSmallPath ( string assetBundleJacketPath )
48+ {
49+ var dir = Path . GetDirectoryName ( assetBundleJacketPath ) ;
50+ if ( string . IsNullOrWhiteSpace ( dir ) ) return null ;
51+ var parentDir = Path . GetDirectoryName ( dir ) ;
52+ if ( string . IsNullOrWhiteSpace ( parentDir ) ) return null ;
53+
54+ var jacketSDir = Path . Combine ( parentDir , "jacket_s" ) ;
55+ var nameWithoutExt = Path . GetFileNameWithoutExtension ( assetBundleJacketPath ) ;
56+ var ext = Path . GetExtension ( assetBundleJacketPath ) ;
57+ return Path . Combine ( jacketSDir , nameWithoutExt + "_s" + ext ) ;
58+ }
59+
4160 private static readonly ConcurrentDictionary < string , string > FileHashCache = new ( StringComparer . OrdinalIgnoreCase ) ;
4261
4362 private static string GetFileHash ( FileInfo fileInfo )
@@ -204,6 +223,19 @@ private void CopyMusicToDirectory(
204223 {
205224 CopySharedFileIfNeeded ( music . AssetBundleJacket + ".manifest" , Path . Combine ( jacketRootDir , jacketFileName + ".manifest" ) , copiedSharedDestinations ) ;
206225 }
226+
227+ // Issue #42: jacket_s lives in a sibling directory, must be exported into AssetBundleImages/jacket_s/
228+ var jacketSPath = GetAssetBundleJacketSmallPath ( music . AssetBundleJacket ) ;
229+ if ( jacketSPath is not null && System . IO . File . Exists ( jacketSPath ) )
230+ {
231+ var jacketSRootDir = Path . Combine ( Path . GetDirectoryName ( jacketRootDir ) ! , "jacket_s" ) ;
232+ var jacketSFileName = Path . GetFileName ( jacketSPath ) ;
233+ CopySharedFileIfNeeded ( jacketSPath , Path . Combine ( jacketSRootDir , jacketSFileName ) , copiedSharedDestinations ) ;
234+ if ( System . IO . File . Exists ( jacketSPath + ".manifest" ) )
235+ {
236+ CopySharedFileIfNeeded ( jacketSPath + ".manifest" , Path . Combine ( jacketSRootDir , jacketSFileName + ".manifest" ) , copiedSharedDestinations ) ;
237+ }
238+ }
207239 }
208240 else if ( music . PseudoAssetBundleJacket is not null )
209241 {
@@ -399,6 +431,17 @@ public void ExportOpt(int id, string assetDir, bool removeEvents = false, bool l
399431 {
400432 zipArchive . CreateEntryFromFile ( music . AssetBundleJacket + ".manifest" , $ "AssetBundleImages/jacket/{ Path . GetFileName ( music . AssetBundleJacket ) } .manifest") ;
401433 }
434+
435+ // Issue #42: jacket_s lives in a sibling directory, must be exported into AssetBundleImages/jacket_s/
436+ var jacketSPath = GetAssetBundleJacketSmallPath ( music . AssetBundleJacket ) ;
437+ if ( jacketSPath is not null && System . IO . File . Exists ( jacketSPath ) )
438+ {
439+ zipArchive . CreateEntryFromFile ( jacketSPath , $ "AssetBundleImages/jacket_s/{ Path . GetFileName ( jacketSPath ) } ") ;
440+ if ( System . IO . File . Exists ( jacketSPath + ".manifest" ) )
441+ {
442+ zipArchive . CreateEntryFromFile ( jacketSPath + ".manifest" , $ "AssetBundleImages/jacket_s/{ Path . GetFileName ( jacketSPath ) } .manifest") ;
443+ }
444+ }
402445 }
403446 else if ( music . PseudoAssetBundleJacket is not null )
404447 {
@@ -456,10 +499,11 @@ public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
456499 var newNonDxId = newId % 10000 ;
457500
458501 var abJacketTarget = Path . Combine ( StaticSettings . StreamingAssets , assetDir , "AssetBundleImages" , "jacket" , $ "ui_jacket_{ newNonDxId : 000000} .ab") ;
502+ var abJacketSTarget = Path . Combine ( StaticSettings . StreamingAssets , assetDir , "AssetBundleImages" , "jacket_s" , $ "ui_jacket_{ newNonDxId : 000000} _s.ab") ;
459503 var acbawbTarget = Path . Combine ( StaticSettings . StreamingAssets , assetDir , "SoundData" , $ "music{ newNonDxId : 000000} ") ;
460504 var movieTarget = Path . Combine ( StaticSettings . StreamingAssets , assetDir , "MovieData" , $ "{ newNonDxId : 000000} ") ;
461505 var newMusicDir = Path . Combine ( StaticSettings . StreamingAssets , assetDir , "music" , $ "music{ newId : 000000} ") ;
462- DeleteIfExists ( abJacketTarget , abJacketTarget + ".manifest" , acbawbTarget + ".acb" , acbawbTarget + ".awb" , movieTarget + ".dat" , movieTarget + ".mp4" , newMusicDir ) ;
506+ DeleteIfExists ( abJacketTarget , abJacketTarget + ".manifest" , abJacketSTarget , abJacketSTarget + ".manifest" , acbawbTarget + ".acb" , acbawbTarget + ".awb" , movieTarget + ".dat" , movieTarget + ".mp4" , newMusicDir ) ;
463507 var abiDir = Path . Combine ( StaticSettings . StreamingAssets , assetDir , @"AssetBundleImages\jacket" ) ;
464508 Directory . CreateDirectory ( abiDir ) ;
465509
@@ -480,9 +524,21 @@ public async Task ModifyId(int id, [FromBody] int newId, string assetDir)
480524 logger . LogInformation ( "Convert jacket: {music.AssetBundleJacket} -> {abJacketTarget}" , music . AssetBundleJacket , abJacketTarget ) ;
481525 System . IO . File . WriteAllBytes ( localJacketTarget , music . GetMusicJacketPngData ( ) ! ) ;
482526 FileSystem . DeleteFile ( music . AssetBundleJacket , UIOption . OnlyErrorDialogs , RecycleOption . SendToRecycleBin ) ;
527+ // AB→PNG: the old .ab.manifest no longer has a matching .ab at the new ID, so just delete it instead of moving
483528 if ( System . IO . File . Exists ( music . AssetBundleJacket + ".manifest" ) )
484529 {
485- FileSystem . MoveFile ( music . AssetBundleJacket + ".manifest" , abJacketTarget + ".manifest" , UIOption . OnlyErrorDialogs ) ;
530+ FileSystem . DeleteFile ( music . AssetBundleJacket + ".manifest" , UIOption . OnlyErrorDialogs , RecycleOption . SendToRecycleBin ) ;
531+ }
532+
533+ // Issue #42: also clean up the companion jacket_s AB so it doesn't stay orphaned under the old ID
534+ var oldJacketSPath = GetAssetBundleJacketSmallPath ( music . AssetBundleJacket ) ;
535+ if ( oldJacketSPath is not null && System . IO . File . Exists ( oldJacketSPath ) )
536+ {
537+ FileSystem . DeleteFile ( oldJacketSPath , UIOption . OnlyErrorDialogs , RecycleOption . SendToRecycleBin ) ;
538+ if ( System . IO . File . Exists ( oldJacketSPath + ".manifest" ) )
539+ {
540+ FileSystem . DeleteFile ( oldJacketSPath + ".manifest" , UIOption . OnlyErrorDialogs , RecycleOption . SendToRecycleBin ) ;
541+ }
486542 }
487543 }
488544
@@ -583,7 +639,7 @@ public async Task ExportAsMaidata(int id, string assetDir, bool ignoreVideo = fa
583639 }
584640
585641 var ma2Contents = new List < ( int , string [ ] ) > ( ) ;
586-
642+
587643 // 关于clock_count功能,我决定不走MaiLib了,而是我们自己解析。因为ma2.Compose返回的是裸谱面inote中的内容,没有办法合理的把clock信息插进去。因此,我们自己解析吧。
588644 // 选用最难的一张有效谱面的MET值作为全曲的&clock_count
589645 int clockCount = 0 ;
@@ -614,7 +670,7 @@ public async Task ExportAsMaidata(int id, string assetDir, bool ignoreVideo = fa
614670 simaiFile . AppendLine ( $ "&chartconverter=MaiChartManager v{ Application . ProductVersion } ") ;
615671 simaiFile . AppendLine ( "&ChartConvertTool=MaiChartManager" ) ;
616672 simaiFile . AppendLine ( $ "&ChartConvertToolVersion={ Application . ProductVersion } ") ;
617-
673+
618674 // 根据前面读取的结果,向simaiFile中最终写入谱面信息相关字段
619675 foreach ( var ( i , ma2Content ) in ma2Contents )
620676 {
@@ -656,7 +712,7 @@ public async Task ExportAsMaidata(int id, string assetDir, bool ignoreVideo = fa
656712 Comment = version ? . GenreName ,
657713 AlbumArt = img ,
658714 } ;
659-
715+
660716 if ( ! AudioConvert . TryResolveAcbAwb ( GetAudioCandidateIds ( music ) , out _ , out var acbPath , out var awbPath ) || acbPath is null || awbPath is null )
661717 {
662718 var message = BuildAudioResolveErrorMessage ( music ) ;
0 commit comments