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:
parent
cbff505297
commit
24ec601e74
@ -3,52 +3,62 @@
|
||||
* 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 )
|
||||
{
|
||||
@ -56,20 +66,68 @@ public class MixinLanguage
|
||||
}
|
||||
}
|
||||
|
||||
@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();
|
||||
}
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user