mirror of
				https://github.com/SquidDev-CC/CC-Tweaked
				synced 2025-10-31 05:33:00 +00:00 
			
		
		
		
	Add key collision resolution strategy to MixinLanguage
Clawing this code back from an ill-thought-out Fabric PR. Now our mixin will load all mod's en_us lang files into the default language instance and not crash if mods provide different values for the same key. I don't know if this resolution strategy is good, but it is *something*.
This commit is contained in:
		 Toad-Dev
					Toad-Dev
				
			
				
					committed by
					
						 Jonathan Coates
						Jonathan Coates
					
				
			
			
				
	
			
			
			 Jonathan Coates
						Jonathan Coates
					
				
			
						parent
						
							cbff505297
						
					
				
				
					commit
					24ec601e74
				
			| @@ -3,73 +3,131 @@ | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
|  | ||||
| package dan200.computercraft.fabric.mixin; | ||||
|  | ||||
| import com.google.common.collect.ImmutableMap; | ||||
| import com.google.gson.JsonParseException; | ||||
| import dan200.computercraft.shared.peripheral.generic.data.ItemData; | ||||
| import dan200.computercraft.fabric.util.ServerTranslationEntry; | ||||
| import net.fabricmc.loader.api.FabricLoader; | ||||
| import net.fabricmc.loader.api.ModContainer; | ||||
| import net.minecraft.client.font.TextVisitFactory; | ||||
| import net.minecraft.text.OrderedText; | ||||
| import net.minecraft.text.StringVisitable; | ||||
| import net.minecraft.text.Style; | ||||
| import net.minecraft.util.Language; | ||||
| import org.apache.logging.log4j.Logger; | ||||
| import org.jetbrains.annotations.NotNull; | ||||
| import org.spongepowered.asm.mixin.Final; | ||||
| import org.spongepowered.asm.mixin.Mixin; | ||||
| import org.spongepowered.asm.mixin.Shadow; | ||||
| import org.spongepowered.asm.mixin.injection.At; | ||||
| import org.spongepowered.asm.mixin.injection.Inject; | ||||
| import org.spongepowered.asm.mixin.injection.callback.CallbackInfoReturnable; | ||||
| import org.spongepowered.asm.mixin.injection.callback.LocalCapture; | ||||
|  | ||||
| import java.io.IOException; | ||||
| import java.io.InputStream; | ||||
| import java.util.HashMap; | ||||
| import java.nio.file.Files; | ||||
| import java.nio.file.Path; | ||||
| import java.util.*; | ||||
| import java.util.function.BiConsumer; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| /** | ||||
|  * Loads all mods en_us lang file into the default Language instance on the dedicated server. | ||||
|  * Needed so that lua code running on the server can access the display name of items. | ||||
|  * | ||||
|  * @see ItemData#fill | ||||
|  * Loads all mods' en_us lang file into the Language instance on the dedicated server with a basic strategy for | ||||
|  * resolving collisions. | ||||
|  */ | ||||
| @Mixin( Language.class ) | ||||
| public class MixinLanguage | ||||
| { | ||||
|     @Shadow | ||||
|     @Final | ||||
|     private static Logger LOGGER; | ||||
|     @Shadow | ||||
|     @Final | ||||
|     private static String DEFAULT; | ||||
|  | ||||
|     @Shadow | ||||
|     public static void load( InputStream inputStream, BiConsumer<String, String> entryConsumer ) | ||||
|     private static void loadFromJson( InputStream inputStream, BiConsumer<String, String> entryConsumer ) | ||||
|     { | ||||
|     } | ||||
|  | ||||
|     private static void loadModLangFile( String modId, BiConsumer<String, String> biConsumer ) | ||||
|     private static void loadModLangFile( ModContainer modContainer, BiConsumer<String, String> biConsumer ) | ||||
|     { | ||||
|         String path = "/assets/" + modId + "/lang/en_us.json"; | ||||
|         Path path = modContainer.getPath( "assets/" + modContainer.getMetadata().getId() + "/lang/" + DEFAULT + ".json" ); | ||||
|         if( !Files.exists( path ) ) return; | ||||
|  | ||||
|         try ( InputStream inputStream = Language.class.getResourceAsStream( path ) ) | ||||
|         try( InputStream inputStream = Files.newInputStream( path ) ) | ||||
|         { | ||||
|             if ( inputStream == null ) return; | ||||
|             load( inputStream, biConsumer ); | ||||
|             loadFromJson( inputStream, biConsumer ); | ||||
|         } | ||||
|         catch ( JsonParseException | IOException e ) | ||||
|         catch( JsonParseException | IOException e ) | ||||
|         { | ||||
|             LOGGER.error( "Couldn't read strings from " + path, e ); | ||||
|         } | ||||
|     } | ||||
|  | ||||
|     @Inject( method = "create", locals = LocalCapture.CAPTURE_FAILSOFT, at = @At( value = "INVOKE", remap = false, target = "Lcom/google/common/collect/ImmutableMap$Builder;build()Lcom/google/common/collect/ImmutableMap;" ) ) | ||||
|     private static void create( CallbackInfoReturnable<Language> cir, ImmutableMap.Builder<String, String> builder ) | ||||
|     @Inject( method = "create", cancellable = true, at = @At( "HEAD" ) ) | ||||
|     private static void create( CallbackInfoReturnable<Language> cir ) | ||||
|     { | ||||
|         /*  We must ensure that the keys are de-duplicated because we can't catch the error that might otherwise | ||||
|          *  occur when the injected function calls build() on the ImmutableMap builder. So we use our own hash map and | ||||
|          *  exclude "minecraft", as the injected function has already loaded those keys at this point. | ||||
|          */ | ||||
|         HashMap<String, String> translations = new HashMap<>(); | ||||
|         Map<String, List<ServerTranslationEntry>> translations = new HashMap<>(); | ||||
|  | ||||
|         FabricLoader.getInstance().getAllMods().stream().map( modContainer -> modContainer.getMetadata().getId() ) | ||||
|             .filter( id -> !id.equals( "minecraft" ) ).forEach( id -> { | ||||
|                 loadModLangFile( id, translations::put ); | ||||
|         for( ModContainer mod : FabricLoader.getInstance().getAllMods() ) | ||||
|         { | ||||
|             loadModLangFile( mod, ( k, v ) -> { | ||||
|                 if( !translations.containsKey( k ) ) translations.put( k, new ArrayList<>() ); | ||||
|                 translations.get( k ).add( new ServerTranslationEntry( mod.getMetadata(), k, v ) ); | ||||
|             } ); | ||||
|         } | ||||
|  | ||||
|         builder.putAll( translations ); | ||||
|         ImmutableMap.Builder<String, String> builder = ImmutableMap.builder(); | ||||
|  | ||||
|         for( Map.Entry<String, List<ServerTranslationEntry>> keyEntry : translations.entrySet() ) | ||||
|         { | ||||
|             if( keyEntry.getValue().size() == 1 ) | ||||
|             { | ||||
|                 // Only one value provided for this key | ||||
|                 builder.put( keyEntry.getKey(), keyEntry.getValue().get( 0 ).value() ); | ||||
|             } | ||||
|             else | ||||
|             { | ||||
|                 // Collision occurred for this key. | ||||
|                 // Strategy: Resolve collision by choosing value provided by the mod that depends on the greatest number | ||||
|                 // of other mods in this collision cluster, according to mod metadata. | ||||
|                 // Rationale: The mod that intends to overwrite another mod's keys is more likely to declare the | ||||
|                 // overwritee as a dependency. | ||||
|                 Set<String> clusterIds = keyEntry.getValue().stream().map( ServerTranslationEntry::getModId ).collect( Collectors.toSet() ); | ||||
|                 ServerTranslationEntry pickedEntry = Collections.max( keyEntry.getValue(), | ||||
|                     Comparator.comparingInt( entry -> entry.getDependencyIntersectionSize( clusterIds ) ) ); | ||||
|                 builder.put( keyEntry.getKey(), pickedEntry.value() ); | ||||
|             } | ||||
|         } | ||||
|  | ||||
|         final Map<String, String> map = builder.build(); | ||||
|         cir.setReturnValue( new Language() | ||||
|         { | ||||
|             @Override | ||||
|             public String get( String key ) | ||||
|             { | ||||
|                 return map.getOrDefault( key, key ); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean hasTranslation( String key ) | ||||
|             { | ||||
|                 return map.containsKey( key ); | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public boolean isRightToLeft() | ||||
|             { | ||||
|                 return false; | ||||
|             } | ||||
|  | ||||
|             @Override | ||||
|             public OrderedText reorder( @NotNull StringVisitable text ) | ||||
|             { | ||||
|                 return visitor -> text.visit( ( style, string ) -> TextVisitFactory.visitFormatted( string, style, visitor ) ? Optional.empty() : StringVisitable.TERMINATE_VISIT, Style.EMPTY ).isPresent(); | ||||
|             } | ||||
|         } ); | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,57 @@ | ||||
| /* | ||||
|  * This file is part of ComputerCraft - http://www.computercraft.info | ||||
|  * Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission. | ||||
|  * Send enquiries to dratcliffe@gmail.com | ||||
|  */ | ||||
| package dan200.computercraft.fabric.util; | ||||
|  | ||||
| import net.fabricmc.loader.api.metadata.ModDependency; | ||||
| import net.fabricmc.loader.api.metadata.ModMetadata; | ||||
|  | ||||
| import java.util.Set; | ||||
| import java.util.stream.Collectors; | ||||
|  | ||||
| // A utility class for holding translation mappings prior collision resolution. | ||||
| public class ServerTranslationEntry | ||||
| { | ||||
|     private final ModMetadata providingModMetadata; | ||||
|     private final String key; | ||||
|     private final String value; | ||||
|  | ||||
|     public ServerTranslationEntry( ModMetadata providingModMetadata, String key, String value ) | ||||
|     { | ||||
|         this.providingModMetadata = providingModMetadata; | ||||
|         this.key = key; | ||||
|         this.value = value; | ||||
|     } | ||||
|  | ||||
|     public String key() | ||||
|     { | ||||
|         return key; | ||||
|     } | ||||
|  | ||||
|     public String value() | ||||
|     { | ||||
|         return value; | ||||
|     } | ||||
|  | ||||
|     public String getModId() | ||||
|     { | ||||
|         return providingModMetadata.getId(); | ||||
|     } | ||||
|  | ||||
|     public Set<String> getDependencyIds() | ||||
|     { | ||||
|         Set<String> deps = providingModMetadata.getDepends().stream().map( ModDependency::getModId ).collect( Collectors.toSet() ); | ||||
|         // For the purposes of handling key collisions, all mods should depend on minecraft | ||||
|         if( !getModId().equals( "minecraft" ) ) deps.add( "minecraft" ); | ||||
|         return deps; | ||||
|     } | ||||
|  | ||||
|     public int getDependencyIntersectionSize( Set<String> idSet ) | ||||
|     { | ||||
|         Set<String> intersection = getDependencyIds(); | ||||
|         intersection.retainAll( idSet ); | ||||
|         return intersection.size(); | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user