1
0
mirror of https://github.com/SquidDev-CC/CC-Tweaked synced 2025-08-28 16:22:18 +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 2021-12-17 10:06:33 -08:00 committed by Jonathan Coates
parent 26f0ad9fa7
commit 8737d4f6a8
No known key found for this signature in database
GPG Key ID: B9E431FF07C98D06
2 changed files with 127 additions and 26 deletions

View File

@ -3,73 +3,138 @@
* Copyright Daniel Ratcliffe, 2011-2021. Do not distribute without permission.
* Send enquiries to dratcliffe@gmail.com
*/
package dan200.computercraft.fabric.mixin;
import dan200.computercraft.fabric.util.ServerTranslationEntry;
import net.minecraft.util.StringDecomposer;
import net.minecraft.util.FormattedCharSequence;
import net.minecraft.network.chat.FormattedText;
import net.minecraft.network.chat.Style;
import net.minecraft.locale.Language;
import net.fabricmc.loader.api.FabricLoader;
import net.fabricmc.loader.api.ModContainer;
import com.google.common.collect.ImmutableMap;
import com.google.gson.JsonParseException;
import dan200.computercraft.shared.peripheral.generic.data.ItemData;
import net.fabricmc.loader.api.FabricLoader;
import net.minecraft.locale.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.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
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 loadFromJson( 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;
loadFromJson( inputStream, biConsumer );
}
catch( JsonParseException | IOException e )
catch ( JsonParseException | IOException e )
{
LOGGER.error( "Couldn't read strings from " + path, e );
}
}
@Inject( method = "loadDefault", 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 loadDefault( CallbackInfoReturnable<Language> cir, ImmutableMap.Builder<String, String> builder )
@Inject( method = "loadDefault", cancellable = true, at = @At( "HEAD" ) )
private static void loadDefault( 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 getOrDefault( String key )
{
return map.getOrDefault( key, key );
}
@Override
public boolean has( String key )
{
return map.containsKey( key );
}
@Override
public boolean isDefaultRightToLeft()
{
return false;
}
@Override
public FormattedCharSequence getVisualOrder( @NotNull FormattedText text )
{
return visitor -> text.visit( ( style, string ) -> StringDecomposer.iterateFormatted( string, style, visitor ) ? Optional.empty() : FormattedText.STOP_ITERATION, Style.EMPTY ).isPresent();
}
} );
}
}

View File

@ -0,0 +1,36 @@
/*
* 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 record ServerTranslationEntry(ModMetadata providingModMetadata, String key, String value )
{
public String getModId()
{
return providingModMetadata.getId();
}
public Set<String> getDependencyIds()
{
Set<String> deps = providingModMetadata.getDependencies().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();
}
}