Skip to content

Commit

Permalink
feat: refactor to use ThreadLocal instead of class smuggling
Browse files Browse the repository at this point in the history
  • Loading branch information
CallMeEchoCodes committed Oct 4, 2024
1 parent b968225 commit 37149a9
Show file tree
Hide file tree
Showing 10 changed files with 110 additions and 229 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,8 @@
import com.mojang.serialization.Codec;
import com.mojang.serialization.MapCodec;
import com.mojang.serialization.codecs.RecordCodecBuilder;
import dev.spiritstudios.specter.impl.serialization.text.StyledTranslatableVisitor;
import dev.spiritstudios.specter.impl.serialization.text.TranslatableVisitor;
import net.minecraft.text.StringVisitable;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.text.TextContent;
import dev.spiritstudios.specter.impl.serialization.SpecterSerializationClient;
import net.minecraft.text.*;

import java.util.Optional;

Expand All @@ -18,37 +14,39 @@
* @param index The index of the argument to resolve this content to.
*/
public record DynamicTextContent(int index) implements TextContent {
public static MapCodec<DynamicTextContent> CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(Codec.INT.fieldOf("index").forGetter(DynamicTextContent::index)).apply(instance, DynamicTextContent::new)
);

public static final TextContent.Type<DynamicTextContent> TYPE = new Type<>(
CODEC,
"dynamic"
);

@Override
public Type<?> getType() {
return TYPE;
}

@Override
public <T> Optional<T> visit(StringVisitable.Visitor<T> visitor) {
if (!(visitor instanceof TranslatableVisitor<T> translatableVisitor) || translatableVisitor.getArgs().length <= index)
return visitor.accept("{" + index + "}");

Object arg = translatableVisitor.getArgs()[index];
if (arg instanceof Text text) return text.visit(visitor);
return visitor.accept(arg.toString());
}

@Override
public <T> Optional<T> visit(StringVisitable.StyledVisitor<T> visitor, Style style) {
if (!(visitor instanceof StyledTranslatableVisitor<T> translatableVisitor) || translatableVisitor.getArgs().length <= index)
return visitor.accept(style, "{" + index + "}");

Object arg = translatableVisitor.getArgs()[index];
if (arg instanceof Text text) return text.visit(visitor, style);
return visitor.accept(style, arg.toString());
}
public static MapCodec<DynamicTextContent> CODEC = RecordCodecBuilder.mapCodec(
instance -> instance.group(Codec.INT.fieldOf("index").forGetter(DynamicTextContent::index)).apply(instance, DynamicTextContent::new)
);

public static final TextContent.Type<DynamicTextContent> TYPE = new Type<>(
CODEC,
"dynamic"
);

@Override
public Type<?> getType() {
return TYPE;
}

@Override
public <T> Optional<T> visit(StringVisitable.Visitor<T> visitor) {
TranslatableTextContent parent = SpecterSerializationClient.CURRENT_TRANSLATABLE.get().peek();
if (parent == null || parent.getArgs().length <= index)
return visitor.accept("{" + index + "}");

Object arg = parent.getArgs()[index];
if (arg instanceof Text text) return text.visit(visitor);
return visitor.accept(arg.toString());
}

@Override
public <T> Optional<T> visit(StringVisitable.StyledVisitor<T> visitor, Style style) {
TranslatableTextContent parent = SpecterSerializationClient.CURRENT_TRANSLATABLE.get().peek();
if (parent == null || parent.getArgs().length <= index)
return visitor.accept(style, "{" + index + "}");

Object arg = parent.getArgs()[index];
if (arg instanceof Text text) return text.visit(visitor, style);
return visitor.accept(style, arg.toString());
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,20 @@
package dev.spiritstudios.specter.impl.serialization;

import com.google.common.collect.ImmutableMap;
import dev.spiritstudios.specter.api.serialization.text.DynamicTextContent;
import dev.spiritstudios.specter.api.serialization.text.TextContentRegistry;
import net.fabricmc.api.ClientModInitializer;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableTextContent;

import java.util.ArrayDeque;
import java.util.Deque;

public class SpecterSerializationClient implements ClientModInitializer {
public static final ThreadLocal<ImmutableMap.Builder<String, Text>> TEXT_TRANSLATIONS_BUILDER = ThreadLocal.withInitial(ImmutableMap.Builder::new);

public static final ThreadLocal<Deque<TranslatableTextContent>> CURRENT_TRANSLATABLE = ThreadLocal.withInitial(ArrayDeque::new);

@Override
public void onInitializeClient() {
TextContentRegistry.register("index", DynamicTextContent.TYPE);
Expand Down

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalBooleanRef;
import com.mojang.serialization.JsonOps;
import dev.spiritstudios.specter.impl.serialization.text.smuggler.TranslationEntryConsumer;
import dev.spiritstudios.specter.impl.serialization.SpecterSerializationClient;
import net.minecraft.text.Text;
import net.minecraft.text.TextCodecs;
import net.minecraft.util.Language;
Expand All @@ -19,26 +19,23 @@

@Mixin(Language.class)
public class LanguageMixin {
@WrapOperation(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/JsonHelper;asString(Lcom/google/gson/JsonElement;Ljava/lang/String;)Ljava/lang/String;"))
private static String load(JsonElement element, String name, Operation<String> original, @Local(argsOnly = true) BiConsumer<String, String> entryConsumer, @Share("skip") LocalBooleanRef skip) {
if (element.isJsonPrimitive()) {
skip.set(false);
return original.call(element, name);
}
@WrapOperation(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/JsonHelper;asString(Lcom/google/gson/JsonElement;Ljava/lang/String;)Ljava/lang/String;"))
private static String load(JsonElement element, String name, Operation<String> original, @Local(argsOnly = true) BiConsumer<String, String> entryConsumer, @Share("skip") LocalBooleanRef skip) {
if (element.isJsonPrimitive()) {
skip.set(false);
return original.call(element, name);
}

if (!(entryConsumer instanceof TranslationEntryConsumer consumer))
throw new IllegalStateException("Text entry created without a text consumer");
Text text = TextCodecs.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow(JsonParseException::new);
SpecterSerializationClient.TEXT_TRANSLATIONS_BUILDER.get().put(name, text);

Text text = TextCodecs.CODEC.parse(JsonOps.INSTANCE, element).getOrThrow(JsonParseException::new);
consumer.accept(name, text);
skip.set(true);
return "";
}

skip.set(true);
return "";
}

@WrapOperation(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Ljava/util/function/BiConsumer;accept(Ljava/lang/Object;Ljava/lang/Object;)V"))
private static <T, U> void skip(BiConsumer<T, U> instance, T t, U u, Operation<Void> original, @Share("skip") LocalBooleanRef skip) {
if (skip.get()) return;
instance.accept(t, u);
}
@WrapOperation(method = "load(Ljava/io/InputStream;Ljava/util/function/BiConsumer;)V", at = @At(value = "INVOKE", target = "Ljava/util/function/BiConsumer;accept(Ljava/lang/Object;Ljava/lang/Object;)V"))
private static <T, U> void skip(BiConsumer<T, U> instance, T t, U u, Operation<Void> original, @Share("skip") LocalBooleanRef skip) {
if (skip.get()) return;
instance.accept(t, u);
}
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,9 @@
package dev.spiritstudios.specter.mixin.serialization.client;

import com.google.common.collect.ImmutableList;
import com.llamalad7.mixinextras.injector.wrapoperation.Operation;
import com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation;
import com.llamalad7.mixinextras.sugar.Share;
import com.llamalad7.mixinextras.sugar.ref.LocalRef;
import dev.spiritstudios.specter.impl.serialization.text.StyledTranslatableVisitor;
import dev.spiritstudios.specter.impl.serialization.SpecterSerializationClient;
import dev.spiritstudios.specter.impl.serialization.text.TextTranslationSupplier;
import dev.spiritstudios.specter.impl.serialization.text.TranslatableVisitor;
import net.minecraft.text.StringVisitable;
import net.minecraft.text.Style;
import net.minecraft.text.Text;
import net.minecraft.text.TranslatableTextContent;
import net.minecraft.util.Language;
Expand All @@ -26,46 +20,53 @@

@Mixin(TranslatableTextContent.class)
public abstract class TranslatableTextContentMixin {
@Shadow
@Final
private String key;
@Shadow
@Final
private String key;

@Shadow
private List<StringVisitable> translations;
@Shadow
private List<StringVisitable> translations;

@Shadow
public abstract Object[] getArgs();
@Inject(
method = {
"visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;",
"visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;"
},
at = @At("HEAD")
)
private <T> void push(CallbackInfoReturnable<Optional<T>> cir) {
if (SpecterSerializationClient.CURRENT_TRANSLATABLE.get().contains((TranslatableTextContent) (Object) this))
throw new IllegalStateException("Detected recursive translation: " + key);

@Inject(method = "updateTranslations", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;)Ljava/lang/String;"), cancellable = true)
private void updateTranslations(CallbackInfo ci) {
Language language = Language.getInstance();
if (!(language instanceof TextTranslationSupplier supplier))
return;
SpecterSerializationClient.CURRENT_TRANSLATABLE.get().push((TranslatableTextContent) (Object) this);
}

Text text = supplier.specter_serialization$getText(key);
if (text == null) return;

translations = ImmutableList.of(text);
ci.cancel();
}
@Inject(
method = {
"visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;",
"visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;"
},
at = @At("RETURN")
)
private <T> void pop(CallbackInfoReturnable<Optional<T>> cir) {
SpecterSerializationClient.CURRENT_TRANSLATABLE.get().pop();

@Inject(method = "visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/TranslatableTextContent;updateTranslations()V", shift = At.Shift.AFTER))
private <T> void visit(StringVisitable.Visitor<T> visitor, CallbackInfoReturnable<Optional<T>> cir, @Share("translatableVisitor") LocalRef<TranslatableVisitor<T>> translatableVisitorRef) {
translatableVisitorRef.set(new TranslatableVisitor<>(visitor, getArgs()));
}
if (SpecterSerializationClient.CURRENT_TRANSLATABLE.get().isEmpty())
SpecterSerializationClient.CURRENT_TRANSLATABLE.remove();
}

@Inject(method = "visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/TranslatableTextContent;updateTranslations()V", shift = At.Shift.AFTER))
private <T> void visit(StringVisitable.StyledVisitor<T> visitor, Style style, CallbackInfoReturnable<Optional<T>> cir, @Share("translatableVisitor") LocalRef<StyledTranslatableVisitor<T>> translatableVisitorRef) {
translatableVisitorRef.set(new StyledTranslatableVisitor<>(visitor, getArgs()));
}

@WrapOperation(method = "visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/StringVisitable;visit(Lnet/minecraft/text/StringVisitable$Visitor;)Ljava/util/Optional;"))
private <T> Optional<T> visitWrapped(StringVisitable instance, StringVisitable.Visitor<T> tVisitor, Operation<Optional<T>> original, @Share("translatableVisitor") LocalRef<TranslatableVisitor<T>> translatableVisitorRef) {
return original.call(instance, translatableVisitorRef.get());
}
@Inject(method = "updateTranslations", at = @At(value = "INVOKE", target = "Lnet/minecraft/util/Language;get(Ljava/lang/String;)Ljava/lang/String;"), cancellable = true)
private void updateTranslations(CallbackInfo ci) {
Language language = Language.getInstance();
if (!(language instanceof TextTranslationSupplier supplier))
return;

@WrapOperation(method = "visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;", at = @At(value = "INVOKE", target = "Lnet/minecraft/text/StringVisitable;visit(Lnet/minecraft/text/StringVisitable$StyledVisitor;Lnet/minecraft/text/Style;)Ljava/util/Optional;"))
private <T> Optional<T> visitWrapped(StringVisitable instance, StringVisitable.StyledVisitor<T> tStyledVisitor, Style style, Operation<Optional<T>> original, @Share("translatableVisitor") LocalRef<StyledTranslatableVisitor<T>> translatableVisitorRef) {
return original.call(instance, translatableVisitorRef.get(), style);
}
Text text = supplier.specter_serialization$getText(key);
if (text == null) return;

translations = ImmutableList.of(text);
ci.cancel();
}
}
Loading

0 comments on commit 37149a9

Please sign in to comment.