#1424: Trial changing a small number of inner enums to classes/interfaces to better support custom values

This PR is a subset of the enum PR #931 and is designed as a low impact
trial run of the design and backwards compatibility to inform
subsequent development.

Additional plugin compatibility features may be available by setting
`settings.compatibility.enum-compatibility-mode` to `true` in
`bukkit.yml`.

By: DerFrZocker <derrieple@gmail.com>
This commit is contained in:
CraftBukkit/Spigot
2024-07-06 17:14:22 +10:00
parent f59f0d1c9b
commit 41b8d833db
27 changed files with 1424 additions and 117 deletions

View File

@@ -51,11 +51,6 @@ public class FieldRename {
};
}
@RerouteStatic("java/lang/Enum")
public static <T extends Enum<T>> T valueOf(Class<T> enumClass, String name, @InjectPluginVersion ApiVersion apiVersion) {
return Enum.valueOf(enumClass, rename(apiVersion, enumClass.getName().replace('.', '/'), name));
}
@RequireCompatibility("allow-old-keys-in-registry")
public static <T extends Keyed> T get(Registry<T> registry, NamespacedKey namespacedKey) {
// We don't have version-specific changes, so just use current, and don't inject a version

View File

@@ -0,0 +1,7 @@
package org.bukkit.craftbukkit.legacy.enums;
/**
* A crash dummy to use, instead of the old enums which matured to Abstracthood or Interfacehood and the baby enums which are still growing.
*/
public enum DummyEnum {
}

View File

@@ -0,0 +1,275 @@
package org.bukkit.craftbukkit.legacy.enums;
import com.google.common.base.Converter;
import com.google.common.base.Enums;
import com.google.common.collect.ImmutableSet;
import com.google.common.collect.Lists;
import java.io.Serializable;
import java.lang.reflect.Array;
import java.lang.reflect.Field;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
import java.util.Optional;
import java.util.function.Function;
import java.util.stream.Collector;
import java.util.stream.Collectors;
import org.bukkit.NamespacedKey;
import org.bukkit.Registry;
import org.bukkit.craftbukkit.legacy.FieldRename;
import org.bukkit.craftbukkit.legacy.reroute.DoNotReroute;
import org.bukkit.craftbukkit.legacy.reroute.InjectPluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.NotInBukkit;
import org.bukkit.craftbukkit.legacy.reroute.RequireCompatibility;
import org.bukkit.craftbukkit.legacy.reroute.RequirePluginVersion;
import org.bukkit.craftbukkit.legacy.reroute.RerouteArgumentType;
import org.bukkit.craftbukkit.legacy.reroute.RerouteReturnType;
import org.bukkit.craftbukkit.legacy.reroute.RerouteStatic;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.craftbukkit.util.ClassTraverser;
import org.bukkit.entity.Cat;
import org.bukkit.entity.Frog;
import org.bukkit.entity.Villager;
import org.bukkit.map.MapCursor;
import org.bukkit.util.OldEnum;
@NotInBukkit
@RequireCompatibility("enum-compatibility-mode")
@RequirePluginVersion(maxInclusive = "1.20.6")
public class EnumEvil {
private static final Map<Class<?>, LegacyRegistryData> REGISTRIES = new HashMap<>();
static {
// Add Classes which got changed here
REGISTRIES.put(Villager.Type.class, new LegacyRegistryData(Registry.VILLAGER_TYPE, Villager.Type::valueOf));
REGISTRIES.put(Villager.Profession.class, new LegacyRegistryData(Registry.VILLAGER_PROFESSION, Villager.Profession::valueOf));
REGISTRIES.put(Frog.Variant.class, new LegacyRegistryData(Registry.FROG_VARIANT, Frog.Variant::valueOf));
REGISTRIES.put(Cat.Type.class, new LegacyRegistryData(Registry.CAT_VARIANT, Cat.Type::valueOf));
REGISTRIES.put(MapCursor.Type.class, new LegacyRegistryData(Registry.MAP_DECORATION_TYPE, MapCursor.Type::valueOf));
}
public static LegacyRegistryData getRegistryData(Class<?> clazz) {
ClassTraverser it = new ClassTraverser(clazz);
LegacyRegistryData registryData;
while (it.hasNext()) {
registryData = REGISTRIES.get(it.next());
if (registryData != null) {
return registryData;
}
}
return null;
}
@DoNotReroute
public static Registry<?> getRegistry(Class<?> clazz) {
LegacyRegistryData registryData = getRegistryData(clazz);
if (registryData != null) {
return registryData.registry();
}
return null;
}
@RerouteStatic("com/google/common/collect/Maps")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumMap newEnumMap(Class<?> objectClass) {
return new ImposterEnumMap(objectClass);
}
@RerouteStatic("com/google/common/collect/Maps")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumMap newEnumMap(Map map) {
return new ImposterEnumMap(map);
}
@RerouteStatic("com/google/common/collect/Sets")
public static Collector<?, ?, ?> toImmutableEnumSet() {
return Collectors.toUnmodifiableSet();
}
@RerouteStatic("com/google/common/collect/Sets")
@RerouteReturnType("java/util/EnumSet")
public static ImposterEnumSet newEnumSet(Iterable<?> iterable, Class<?> clazz) {
ImposterEnumSet set = ImposterEnumSet.noneOf(clazz);
for (Object some : iterable) {
set.add(some);
}
return set;
}
@RerouteStatic("com/google/common/collect/Sets")
public static ImmutableSet<?> immutableEnumSet(Iterable<?> iterable) {
return ImmutableSet.of(iterable);
}
@RerouteStatic("com/google/common/collect/Sets")
public static ImmutableSet<?> immutableEnumSet(@RerouteArgumentType("java/lang/Enum") Object first, @RerouteArgumentType("[java/lang/Enum") Object... rest) {
return ImmutableSet.of(first, rest);
}
@RerouteStatic("com/google/common/base/Enums")
public static Field getField(@RerouteArgumentType("java/lang/Enum") Object value) {
if (value instanceof Enum eValue) {
return Enums.getField(eValue);
}
try {
return value.getClass().getField(((OldEnum) value).name());
} catch (NoSuchFieldException impossible) {
throw new AssertionError(impossible);
}
}
@RerouteStatic("com/google/common/base/Enums")
public static com.google.common.base.Optional getIfPresent(Class clazz, String name, @InjectPluginVersion ApiVersion apiVersion) {
if (clazz.isEnum()) {
return Enums.getIfPresent(clazz, name);
}
Registry registry = getRegistry(clazz);
if (registry == null) {
return com.google.common.base.Optional.absent();
}
name = FieldRename.rename(apiVersion, clazz.getName().replace('.', '/'), name);
return com.google.common.base.Optional.fromNullable(registry.get(NamespacedKey.fromString(name.toLowerCase(Locale.ROOT))));
}
@RerouteStatic("com/google/common/base/Enums")
public static Converter stringConverter(Class clazz, @InjectPluginVersion ApiVersion apiVersion) {
if (clazz.isEnum()) {
return Enums.stringConverter(clazz);
}
return new StringConverter(apiVersion, clazz);
}
public static Object[] getEnumConstants(Class<?> clazz) {
if (clazz.isEnum()) {
return clazz.getEnumConstants();
}
Registry<?> registry = getRegistry(clazz);
if (registry == null) {
return clazz.getEnumConstants();
}
// Need to do this in such away to avoid ClassCastException
List<?> values = Lists.newArrayList(registry);
Object array = Array.newInstance(clazz, values.size());
for (int i = 0; i < values.size(); i++) {
Array.set(array, i, values.get(i));
}
return (Object[]) array;
}
public static String name(@RerouteArgumentType("java/lang/Enum") Object object) {
if (object instanceof OldEnum<?>) {
return ((OldEnum<?>) object).name();
}
return ((Enum<?>) object).name();
}
public static int compareTo(@RerouteArgumentType("java/lang/Enum") Object object, @RerouteArgumentType("java/lang/Enum") Object other) {
if (object instanceof OldEnum<?>) {
return ((OldEnum) object).compareTo((OldEnum) other);
}
return ((Enum) object).compareTo((Enum) other);
}
public static Class<?> getDeclaringClass(@RerouteArgumentType("java/lang/Enum") Object object) {
Class<?> clazz = object.getClass();
Class<?> zuper = clazz.getSuperclass();
return (zuper == Enum.class) ? clazz : zuper;
}
public static Optional<Enum.EnumDesc> describeConstable(@RerouteArgumentType("java/lang/Enum") Object object) {
return getDeclaringClass(object)
.describeConstable()
.map(c -> Enum.EnumDesc.of(c, name(object)));
}
@RerouteStatic("java/lang/Enum")
@RerouteReturnType("java/lang/Enum")
public static Object valueOf(Class enumClass, String name, @InjectPluginVersion ApiVersion apiVersion) {
name = FieldRename.rename(apiVersion, enumClass.getName().replace('.', '/'), name);
LegacyRegistryData registryData = getRegistryData(enumClass);
if (registryData != null) {
return registryData.function().apply(name);
}
return Enum.valueOf(enumClass, name);
}
public static String toString(@RerouteArgumentType("java/lang/Enum") Object object) {
return object.toString();
}
public static int ordinal(@RerouteArgumentType("java/lang/Enum") Object object) {
if (object instanceof OldEnum<?>) {
return ((OldEnum<?>) object).ordinal();
}
return ((Enum<?>) object).ordinal();
}
public record LegacyRegistryData(Registry<?> registry, Function<String, ?> function) {
}
private static final class StringConverter<T extends OldEnum<T>> extends Converter<String, T> implements Serializable {
private final ApiVersion apiVersion;
private final Class<T> clazz;
private transient LegacyRegistryData registryData;
StringConverter(ApiVersion apiVersion, Class<T> clazz) {
this.apiVersion = apiVersion;
this.clazz = clazz;
}
@Override
protected T doForward(String value) {
if (registryData == null) {
registryData = getRegistryData(clazz);
}
value = FieldRename.rename(apiVersion, clazz.getName().replace('.', '/'), value);
return (T) registryData.function().apply(value);
}
@Override
protected String doBackward(T enumValue) {
return enumValue.name();
}
@Override
public boolean equals(Object object) {
if (object instanceof StringConverter<?> that) {
return this.clazz.equals(that.clazz);
}
return false;
}
@Override
public int hashCode() {
return clazz.hashCode();
}
@Override
public String toString() {
return "Enums.stringConverter(" + clazz.getName() + ".class)";
}
private static final long serialVersionUID = 0L;
}
}

View File

@@ -0,0 +1,137 @@
package org.bukkit.craftbukkit.legacy.enums;
import java.util.AbstractMap;
import java.util.Collection;
import java.util.EnumMap;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import java.util.TreeMap;
/**
* The "I can't believe it works" map.
* It replaces every EnumMap with the ImposterEnumMap and uses a HashMap instead of an object array.
* Used so that plugins which use an EnumMap still work.
*/
public class ImposterEnumMap extends AbstractMap<Object, Object> {
private final Class<?> objectClass;
private final Map map;
public ImposterEnumMap(Class<?> objectClass) {
this.objectClass = objectClass;
this.map = getMap(objectClass);
}
public ImposterEnumMap(EnumMap enumMap) {
this.objectClass = DummyEnum.class;
this.map = enumMap.clone();
}
public ImposterEnumMap(Map map) {
if (map instanceof ImposterEnumMap) {
this.objectClass = ((ImposterEnumMap) map).objectClass;
this.map = getMap(objectClass);
} else {
this.objectClass = DummyEnum.class;
this.map = new TreeMap();
}
this.map.putAll(map);
}
private static Map getMap(Class<?> objectClass) {
// Since we replace every enum map we might also replace some maps which are for real enums.
// If this is the case use a EnumMap instead of a HashMap
if (objectClass.isEnum()) {
return new EnumMap(objectClass);
} else {
return new HashMap();
}
}
@Override
public int size() {
return map.size();
}
@Override
public boolean containsValue(Object value) {
return map.containsValue(value);
}
@Override
public boolean containsKey(Object key) {
return map.containsKey(key);
}
@Override
public Object get(Object key) {
return map.get(key);
}
@Override
public Object put(Object key, Object value) {
typeCheck(key);
return map.put(key, value);
}
@Override
public Object remove(Object key) {
return map.remove(key);
}
@Override
public void putAll(Map<? extends Object, ?> m) {
if (map instanceof EnumMap<?, ?>) {
map.putAll(m);
}
super.putAll(m);
}
@Override
public void clear() {
map.clear();
}
@Override
public Set<Object> keySet() {
return map.keySet();
}
@Override
public Collection<Object> values() {
return map.values();
}
@Override
public Set<Entry<Object, Object>> entrySet() {
return map.entrySet();
}
@Override
public boolean equals(Object o) {
return map.equals(o);
}
@Override
public int hashCode() {
return map.hashCode();
}
@Override
public ImposterEnumMap clone() {
ImposterEnumMap enumMap = new ImposterEnumMap(objectClass);
enumMap.putAll(map);
return enumMap;
}
private void typeCheck(Object object) {
if (objectClass != DummyEnum.class) {
if (!objectClass.isAssignableFrom(object.getClass())) {
throw new ClassCastException(object.getClass() + " != " + objectClass);
}
}
}
}

View File

@@ -0,0 +1,346 @@
package org.bukkit.craftbukkit.legacy.enums;
import java.util.AbstractSet;
import java.util.Collection;
import java.util.Collections;
import java.util.EnumSet;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.TreeSet;
import org.bukkit.Registry;
import org.bukkit.util.OldEnum;
import org.jetbrains.annotations.NotNull;
public class ImposterEnumSet extends AbstractSet<Object> {
private final Class<?> objectClass;
private final Set set;
private static Set createSet(Class<?> clazz) {
if (clazz.isEnum()) {
return EnumSet.noneOf((Class<Enum>) clazz);
} else {
return new TreeSet();
}
}
public static ImposterEnumSet noneOf(Class<?> clazz) {
Set set = createSet(clazz);
return new ImposterEnumSet(set, clazz);
}
public static ImposterEnumSet allOf(Class<?> clazz) {
Set set;
if (clazz.isEnum()) {
set = EnumSet.allOf((Class<Enum>) clazz);
} else {
set = new HashSet();
Registry registry = EnumEvil.getRegistry(clazz);
if (registry == null) {
throw new IllegalArgumentException("Class " + clazz + " is not an Enum nor an OldEnum");
}
for (Object object : registry) {
set.add(object);
}
}
return new ImposterEnumSet(set, clazz);
}
public static ImposterEnumSet copyOf(Set set) {
Class<?> clazz;
if (set instanceof ImposterEnumSet imposter) {
set = imposter.set;
clazz = imposter.objectClass;
} else {
if (!set.isEmpty()) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = Object.class;
}
}
Set newSet = createSet(clazz);
newSet.addAll(set);
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet copyOf(Collection collection) {
Class<?> clazz;
if (collection instanceof ImposterEnumSet imposter) {
collection = imposter.set;
clazz = imposter.objectClass;
} else {
if (!collection.isEmpty()) {
clazz = (Class<?>) collection.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = Object.class;
}
}
Set newSet = createSet(clazz);
newSet.addAll(collection);
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet complementOf(Set set) {
Class<?> clazz = null;
if (set instanceof ImposterEnumSet imposter) {
set = imposter.set;
clazz = imposter.objectClass;
}
if (set instanceof EnumSet<?> enumSet) {
enumSet = EnumSet.complementOf(enumSet);
if (clazz != null) {
return new ImposterEnumSet(enumSet, clazz);
}
if (!set.isEmpty()) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
} else {
clazz = (Class<?>) enumSet.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.map(val -> (Class) val)
.findAny()
.orElse(Object.class);
}
return new ImposterEnumSet(enumSet, clazz);
}
if (set.isEmpty() && clazz == null) {
throw new IllegalStateException("Class is null and set is empty, cannot get class!");
}
if (clazz == null) {
clazz = (Class<?>) set.stream()
.filter(val -> val != null)
.map(val -> val.getClass())
.findAny()
.orElse(Object.class);
}
Registry registry = EnumEvil.getRegistry(clazz);
Set newSet = new HashSet();
for (Object value : registry) {
if (set.contains(value)) {
continue;
}
newSet.add(value);
}
return new ImposterEnumSet(newSet, clazz);
}
public static ImposterEnumSet of(Object e) {
Set set = createSet(e.getClass());
set.add(e);
return new ImposterEnumSet(set, e.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2) {
Set set = createSet(e1.getClass());
set.add(e1);
set.add(e2);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3) {
Set set = createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3, Object e4) {
Set set = createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e1, Object e2, Object e3, Object e4, Object e5) {
Set set = createSet(e1.getClass());
set.add(e1);
set.add(e2);
set.add(e3);
set.add(e4);
set.add(e5);
return new ImposterEnumSet(set, e1.getClass());
}
public static ImposterEnumSet of(Object e, Object... rest) {
Set set = createSet(e.getClass());
set.add(e);
Collections.addAll(set, rest);
return new ImposterEnumSet(set, e.getClass());
}
public static ImposterEnumSet range(Object from, Object to) {
Set set;
if (from.getClass().isEnum()) {
set = EnumSet.range((Enum) from, (Enum) to);
} else {
set = new HashSet();
Registry registry = EnumEvil.getRegistry(from.getClass());
for (Object o : registry) {
if (((OldEnum) o).ordinal() < ((OldEnum) from).ordinal()) {
continue;
}
if (((OldEnum) o).ordinal() > ((OldEnum) to).ordinal()) {
continue;
}
set.add(o);
}
}
return new ImposterEnumSet(set, from.getClass());
}
private ImposterEnumSet(Set set, Class<?> objectClass) {
this.set = set;
this.objectClass = objectClass;
}
@Override
public Iterator<Object> iterator() {
return set.iterator();
}
@Override
public int size() {
return set.size();
}
@Override
public boolean equals(Object o) {
return set.equals(o);
}
@Override
public int hashCode() {
return set.hashCode();
}
@Override
public boolean removeAll(Collection<?> c) {
return set.removeAll(c);
}
@Override
public boolean isEmpty() {
return set.isEmpty();
}
@Override
public boolean contains(Object o) {
return set.contains(o);
}
@NotNull
@Override
public Object[] toArray() {
return set.toArray();
}
@NotNull
@Override
public <T> T[] toArray(@NotNull T[] a) {
return (T[]) set.toArray(a);
}
@Override
public boolean add(Object o) {
typeCheck(o);
return set.add(o);
}
@Override
public boolean remove(Object o) {
return set.remove(o);
}
@Override
public boolean containsAll(@NotNull Collection<?> c) {
return set.containsAll(c);
}
@Override
public boolean addAll(@NotNull Collection<?> c) {
if (set instanceof EnumSet<?>) {
set.addAll(c);
}
return super.addAll(c);
}
@Override
public boolean retainAll(@NotNull Collection<?> c) {
return set.retainAll(c);
}
@Override
public void clear() {
set.clear();
}
@Override
public String toString() {
return set.toString();
}
public ImposterEnumSet clone() {
Set newSet;
if (set instanceof EnumSet<?> enumSet) {
newSet = enumSet.clone();
} else {
newSet = new HashSet();
newSet.addAll(set);
}
return new ImposterEnumSet(newSet, objectClass);
}
private void typeCheck(Object object) {
if (objectClass != DummyEnum.class) {
if (!objectClass.isAssignableFrom(object.getClass())) {
throw new ClassCastException(object.getClass() + " != " + objectClass);
}
}
}
}

View File

@@ -0,0 +1,13 @@
package org.bukkit.craftbukkit.legacy.reroute;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface InjectCompatibility {
String value();
}

View File

@@ -6,6 +6,6 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface NotInBukkit {
}

View File

@@ -6,7 +6,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequireCompatibility {
String value();

View File

@@ -0,0 +1,17 @@
package org.bukkit.craftbukkit.legacy.reroute;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface RequirePluginVersion {
String value() default "";
String minInclusive() default "";
String maxInclusive() default "";
}

View File

@@ -0,0 +1,40 @@
package org.bukkit.craftbukkit.legacy.reroute;
import org.bukkit.craftbukkit.util.ApiVersion;
public record RequirePluginVersionData(ApiVersion minInclusive, ApiVersion maxInclusive) {
public static RequirePluginVersionData create(RequirePluginVersion requirePluginVersion) {
if (!requirePluginVersion.value().isBlank()) {
if (!requirePluginVersion.minInclusive().isBlank() || !requirePluginVersion.maxInclusive().isBlank()) {
throw new RuntimeException("When setting value, min inclusive and max inclusive data is not allowed.");
}
return new RequirePluginVersionData(ApiVersion.getOrCreateVersion(requirePluginVersion.value()), ApiVersion.getOrCreateVersion(requirePluginVersion.value()));
}
ApiVersion minInclusive = null;
ApiVersion maxInclusive = null;
if (!requirePluginVersion.minInclusive().isBlank()) {
minInclusive = ApiVersion.getOrCreateVersion(requirePluginVersion.minInclusive());
}
if (!requirePluginVersion.maxInclusive().isBlank()) {
maxInclusive = ApiVersion.getOrCreateVersion(requirePluginVersion.maxInclusive());
}
return new RequirePluginVersionData(minInclusive, maxInclusive);
}
public boolean test(ApiVersion pluginVersion) {
if (minInclusive != null && pluginVersion.isOlderThan(minInclusive)) {
return false;
}
if (maxInclusive != null && pluginVersion.isNewerThan(maxInclusive)) {
return false;
}
return true;
}
}

View File

@@ -1,9 +1,10 @@
package org.bukkit.craftbukkit.legacy.reroute;
import org.jetbrains.annotations.Nullable;
import org.objectweb.asm.Opcodes;
import org.objectweb.asm.Type;
public record RerouteArgument(Type type, boolean injectPluginName, boolean injectPluginVersion) {
public record RerouteArgument(Type type, Type sourceType, boolean injectPluginName, boolean injectPluginVersion, @Nullable String injectCompatibility) {
/**
* Converts the type string to the correct load opcode.
@@ -30,8 +31,8 @@ public record RerouteArgument(Type type, boolean injectPluginName, boolean injec
* @return the opcode of the type
*/
public int instruction() {
if (injectPluginName() || injectPluginVersion()) {
throw new IllegalStateException(String.format("Cannot get instruction for plugin name / version argument: %s", this));
if (injectPluginName() || injectPluginVersion() || injectCompatibility() != null) {
throw new IllegalStateException(String.format("Cannot get instruction for plugin name / version argument / compatibility: %s", this));
}
return type.getOpcode(Opcodes.ILOAD);

View File

@@ -0,0 +1,13 @@
package org.bukkit.craftbukkit.legacy.reroute;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.PARAMETER)
public @interface RerouteArgumentType {
String value();
}

View File

@@ -54,13 +54,16 @@ public class RerouteBuilder {
for (Parameter parameter : method.getParameters()) {
Type type = Type.getType(parameter.getType());
int count = 0;
boolean injectPluginName = false;
boolean injectPluginVersion = false;
String injectCompatibility = null;
if (parameter.isAnnotationPresent(InjectPluginName.class)) {
if (parameter.getType() != String.class) {
throw new RuntimeException("Plugin name argument must be of type name, but got " + parameter.getType());
}
injectPluginName = true;
count++;
}
if (parameter.isAnnotationPresent(InjectPluginVersion.class)) {
@@ -68,17 +71,39 @@ public class RerouteBuilder {
throw new RuntimeException("Plugin version argument must be of type ApiVersion, but got " + parameter.getType());
}
injectPluginVersion = true;
count++;
}
if (injectPluginName && injectPluginVersion) {
if (parameter.isAnnotationPresent(InjectCompatibility.class)) {
if (parameter.getType() != boolean.class) {
throw new RuntimeException("Compatibility argument must be of type boolean, but got " + parameter.getType());
}
injectCompatibility = parameter.getAnnotation(InjectCompatibility.class).value();
count++;
}
if (count > 1) {
// This should not happen, since we check types,
// and those two have different types -> it would already have failed
throw new RuntimeException("Wtf?");
}
RerouteArgument argument = new RerouteArgument(type, injectPluginName, injectPluginVersion);
RerouteArgumentType rerouteArgumentType = parameter.getAnnotation(RerouteArgumentType.class);
if (count == 1 && rerouteArgumentType != null) {
// Why would you do this?
throw new RuntimeException("Wtf?");
}
Type sourceType;
if (rerouteArgumentType != null) {
sourceType = Type.getObjectType(rerouteArgumentType.value());
} else {
sourceType = type;
}
RerouteArgument argument = new RerouteArgument(type, sourceType, injectPluginName, injectPluginVersion, injectCompatibility);
arguments.add(argument);
if (!injectPluginName && !injectPluginVersion) {
if (count == 0) {
sourceArguments.add(argument);
}
}
@@ -89,10 +114,18 @@ public class RerouteBuilder {
sourceOwner = Type.getObjectType(rerouteStatic.value());
} else {
RerouteArgument argument = sourceArguments.get(0);
sourceOwner = argument.type();
sourceOwner = argument.sourceType();
sourceArguments.remove(argument);
}
Type sourceDesc = Type.getMethodType(rerouteReturn.type(), sourceArguments.stream().map(RerouteArgument::type).toArray(Type[]::new));
RerouteReturnType rerouteReturnType = method.getAnnotation(RerouteReturnType.class);
Type returnType;
if (rerouteReturnType != null) {
returnType = Type.getObjectType(rerouteReturnType.value());
} else {
returnType = rerouteReturn.type();
}
Type sourceDesc = Type.getMethodType(returnType, sourceArguments.stream().map(RerouteArgument::sourceType).toArray(Type[]::new));
RerouteMethodName rerouteMethodName = method.getAnnotation(RerouteMethodName.class);
String methodName;
@@ -110,13 +143,22 @@ public class RerouteBuilder {
Type targetType = Type.getType(method);
boolean inBukkit = !method.isAnnotationPresent(NotInBukkit.class);
boolean inBukkit = !method.isAnnotationPresent(NotInBukkit.class) && !method.getDeclaringClass().isAnnotationPresent(NotInBukkit.class);
String requiredCompatibility = null;
if (method.isAnnotationPresent(RequireCompatibility.class)) {
requiredCompatibility = method.getAnnotation(RequireCompatibility.class).value();
} else if (method.getDeclaringClass().isAnnotationPresent(RequireCompatibility.class)) {
requiredCompatibility = method.getDeclaringClass().getAnnotation(RequireCompatibility.class).value();
}
return new RerouteMethodData(methodKey, sourceDesc, sourceOwner, methodName, rerouteStatic != null, targetType, Type.getInternalName(method.getDeclaringClass()), method.getName(), arguments, rerouteReturn, inBukkit, requiredCompatibility);
RequirePluginVersionData requiredPluginVersion = null;
if (method.isAnnotationPresent(RequirePluginVersion.class)) {
requiredPluginVersion = RequirePluginVersionData.create(method.getAnnotation(RequirePluginVersion.class));
} else if (method.getDeclaringClass().isAnnotationPresent(RequirePluginVersion.class)) {
requiredPluginVersion = RequirePluginVersionData.create(method.getDeclaringClass().getAnnotation(RequirePluginVersion.class));
}
return new RerouteMethodData(methodKey, sourceDesc, sourceOwner, methodName, rerouteStatic != null, targetType, Type.getInternalName(method.getDeclaringClass()), method.getName(), arguments, rerouteReturn, inBukkit, requiredCompatibility, requiredPluginVersion);
}
}

View File

@@ -7,5 +7,5 @@ import org.objectweb.asm.Type;
public record RerouteMethodData(String source, Type sourceDesc, Type sourceOwner, String sourceName,
boolean staticReroute, Type targetType, String targetOwner, String targetName,
List<RerouteArgument> arguments, RerouteReturn rerouteReturn, boolean isInBukkit,
@Nullable String requiredCompatibility) {
@Nullable String requiredCompatibility, @Nullable RequirePluginVersionData requiredPluginVersion) {
}

View File

@@ -0,0 +1,13 @@
package org.bukkit.craftbukkit.legacy.reroute;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RerouteReturnType {
String value();
}