@@ -81,6 +81,19 @@ type ModDepInfo = ModInfoProbablyMissing & {
8181 optional : boolean ;
8282} ;
8383
84+ // Mods that can stand in for one another: if any one in the list is enabled,
85+ // a dependency on any *other* member is treated as satisfied. Special-case for
86+ // the multiplayer clients — CelesteNet.Client and its CN fork MiaoNet.
87+ // (MiaoNet's installed name is literally "MiaoNet"; see main.rs
88+ // get_installed_miaonet.)
89+ const CELESTENET_ALT_LIST = [ 'CelesteNet.Client' , 'MiaoNet' , 'Miao.CelesteNet.Client' ] ;
90+ // Returns the (raw) names of all enabled alternatives covering `name` (excludes
91+ // `name` itself). Empty if `name` isn't in the list or no alternative is on.
92+ const altCovering = ( name : string , modMap : Map < string , ModInfo > ) : string [ ] => {
93+ if ( ! CELESTENET_ALT_LIST . includes ( name ) ) return [ ] ;
94+ return CELESTENET_ALT_LIST . filter ( ( alt ) => alt !== name && modMap . get ( alt ) ?. enabled ) ;
95+ } ;
96+
8497const modListContext = createContext < {
8598 switchMod : ( id : string , enabled : boolean , recursive ?: boolean ) => void ;
8699 switchProfile : ( name : string ) => void ;
@@ -95,6 +108,7 @@ const modListContext = createContext<{
95108 showDetailed : boolean ;
96109 alwaysOnMods : string [ ] ;
97110 switchAlwaysOn : ( name : string , enabled : boolean ) => void ;
111+ isCoveredByAlt : ( name : string ) => string [ ] ;
98112 autoDisableNewMods : boolean ;
99113 hasUpdateMods : {
100114 name : string ;
@@ -252,6 +266,7 @@ const ModLocal = ({
252266 } , [ name , ctx . hasUpdateMods ] ) ;
253267
254268 const isAlwaysOn = ctx ?. alwaysOnMods . includes ( name ) ;
269+ const coveredByAlt = ctx ?. isCoveredByAlt ?.( name ) ?? [ ] ;
255270
256271 const [ editingComment , setEditingComment ] = useState ( false ) ;
257272 const refCommentInput = useRef < HTMLInputElement > ( null ) ;
@@ -381,6 +396,20 @@ const ModLocal = ({
381396 </ ModBadge >
382397 ) }
383398
399+ { coveredByAlt . length > 0 && (
400+ < ModBadge
401+ bg = "#0d47a1"
402+ color = "white"
403+ title = { _i18n . t ( '{alt} 已开启,可替代本 Mod' , {
404+ alt : coveredByAlt . join ( ', ' ) ,
405+ } ) }
406+ >
407+ { _i18n . t ( '{alt} 已开启' , {
408+ alt : coveredByAlt . join ( ', ' ) ,
409+ } ) }
410+ </ ModBadge >
411+ ) }
412+
384413 < span
385414 className = "modName"
386415 onClick = { ( ) => setEditingComment ( true ) }
@@ -656,7 +685,9 @@ export const Manage = () => {
656685 }
657686
658687 if ( ! modMap . has ( dep . name ) ) {
659- mergeSM ( { status : 'missing' , message : '' } , dep . name ) ;
688+ if ( altCovering ( dep . name , modMap ) . length === 0 ) {
689+ mergeSM ( { status : 'missing' , message : '' } , dep . name ) ;
690+ }
660691 continue ;
661692 }
662693
@@ -671,7 +702,7 @@ export const Manage = () => {
671702 ) ;
672703 }
673704
674- if ( ! installedDep . enabled ) {
705+ if ( ! installedDep . enabled && altCovering ( dep . name , modMap ) . length === 0 ) {
675706 mergeSM (
676707 {
677708 status : 'not-enabled' ,
@@ -898,6 +929,7 @@ export const Manage = () => {
898929 if ( enabled ) setAlwaysOnMods ( [ ...alwaysOnMods , name ] ) ;
899930 else setAlwaysOnMods ( alwaysOnMods . filter ( ( v ) => v !== name ) ) ;
900931 } ,
932+ isCoveredByAlt : ( name : string ) => altCovering ( name , installedModMap ) ,
901933 alwaysOnMods,
902934 autoDisableNewMods,
903935 modComments, setModComment ( name : string , comment : string ) {
@@ -972,6 +1004,7 @@ export const Manage = () => {
9721004 const switchList : string [ ] = [ ] ;
9731005 const excludeFromAutoEnableList = [
9741006 'CelesteNet.Client' ,
1007+ 'MiaoNet' ,
9751008 'Miao.CelesteNet.Client' ,
9761009 ] ;
9771010
@@ -1165,6 +1198,7 @@ export const Manage = () => {
11651198 [
11661199 currentProfile ,
11671200 installedMods ,
1201+ installedModMap ,
11681202 gamePath ,
11691203 modPath ,
11701204 fullTree ,
0 commit comments