Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion paper-server/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -176,10 +176,11 @@ dependencies {
implementation("net.neoforged:AutoRenamingTool:2.0.3") // Remap plugins

// Remap reflection
val reflectionRewriterVersion = "0.0.3"
val reflectionRewriterVersion = "0.0.4-SNAPSHOT"
implementation("io.papermc:reflection-rewriter:$reflectionRewriterVersion")
implementation("io.papermc:reflection-rewriter-runtime:$reflectionRewriterVersion")
implementation("io.papermc:reflection-rewriter-proxy-generator:$reflectionRewriterVersion")
implementation("io.papermc:asm-utils:$reflectionRewriterVersion")

// Spark
implementation("me.lucko:spark-api:0.1-20240720.200737-2")
Expand Down
181 changes: 181 additions & 0 deletions paper-server/src/main/java/io/papermc/paper/plugin/ApiVersion.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,181 @@
package io.papermc.paper.plugin;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.AnnotatedType;
import java.util.Comparator;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.function.Predicate;
import org.bukkit.craftbukkit.util.Versioning;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;

@NullMarked
public sealed interface ApiVersion extends io.papermc.asm.versioned.ApiVersion<ApiVersion> permits ApiVersion.Legacy, ApiVersion.Modern, ApiVersion.None {

Serializer SERIALIZER = new Serializer();

ApiVersion CURRENT = getOrCreateVersion(Versioning.getCurrentApiVersion());
ApiVersion FLATTENING = getOrCreateVersion("1.13");
ApiVersion FIELD_NAME_PARITY = getOrCreateVersion("1.20.5");
ApiVersion ABSTRACT_COW = getOrCreateVersion("1.21.5");
ApiVersion NONE = getOrCreateVersion("none");

String getVersionString();

@Override
default int compareTo(final ApiVersion o) {
return switch (o) {
case final Legacy oLegacy -> switch (this) {
case final Legacy thisLegacy -> thisLegacy.compareSameType(oLegacy);
case final Modern ignored -> 1;
case final None ignored -> -1;
};
case final Modern oModern -> switch (this) {
case final Legacy ignored -> -1;
case final Modern thisModern -> thisModern.compareSameType(oModern);
case final None ignored -> -1;
};
case final None ignored -> switch (this) {
case final Legacy ignored2 -> 1;
case final Modern ignored2 -> 1;
case final None ignored2 -> 0;
};
};
}

record Legacy(int major, int minor, int patch) implements ApiVersion {

private static final Comparator<Legacy> COMPARATOR = Comparator
.comparingInt(Legacy::major)
.thenComparingInt(Legacy::minor)
.thenComparingInt(Legacy::patch);

int compareSameType(final Legacy other) {
return COMPARATOR.compare(this, other);
}

@Override
public String getVersionString() {
return this.major() + "." + this.minor() + '.' + this.patch();
}

@Override
public String toString() {
return this.getVersionString();
}
}

record Modern(int year, int major, int minor) implements ApiVersion {

private static final Comparator<Modern> COMPARATOR = Comparator
.comparingInt(Modern::year)
.thenComparingInt(Modern::major)
.thenComparingInt(Modern::minor);

int compareSameType(final Modern other) {
return COMPARATOR.compare(this, other);
}

@Override
public String getVersionString() {
return this.year() + "." + this.major() + '.' + this.minor();
}

@Override
public String toString() {
return this.getVersionString();
}
}

record None() implements ApiVersion {

private static final None INSTANCE = new None();

@Override
public String getVersionString() {
return "none";
}

@Override
public String toString() {
return this.getVersionString();
}
}

final class Serializer extends ScalarSerializer.Annotated<ApiVersion> {

Serializer() {
super(ApiVersion.class);
}

@Override
public ApiVersion deserialize(final AnnotatedType type, final Object obj) throws SerializationException {
try {
final ApiVersion version = getOrCreateVersion(obj.toString());
final Minimum min = type.getAnnotation(Minimum.class);
if (min != null) {
final ApiVersion minVersion = getOrCreateVersion(min.value());
if (version.isOlderThan(minVersion)) {
throw new SerializationException(ApiVersion.class, version + " is too old for a paper plugin!");
}
}
return version;
} catch (final IllegalArgumentException ex) {
throw new SerializationException(ApiVersion.class, "Could not parse version string", ex);
}
}

@Override
protected Object serialize(final AnnotatedType type, final ApiVersion item, final Predicate<Class<?>> typeSupported) {
return item.getVersionString();
}
}

static ApiVersion getOrCreateVersion(final @Nullable String versionString) {
class Holder {
private static final Map<String, ApiVersion> cache = new ConcurrentHashMap<>();
}
if (versionString == null || versionString.isBlank() || versionString.equalsIgnoreCase("none")) {
return None.INSTANCE;
}
final ApiVersion parsed = parse(versionString);
Holder.cache.putIfAbsent(parsed.getVersionString(), parsed);
return Holder.cache.get(parsed.getVersionString());
}

private static ApiVersion parse(final String versionString) {
final String[] parts = versionString.split("\\.");

if (parts.length < 2 || parts.length > 3) {
throw new IllegalArgumentException("Version string must have 2 or 3 numbers");
}

final int first = Integer.parseInt(parts[0]);
final int second = Integer.parseInt(parts[1]);
final int third = parts.length == 3 ? Integer.parseInt(parts[2]) : 0;

if (first == 1) {
if (second > 21) {
throw new IllegalArgumentException("Legacy version string must be 1.21.xx or lower");
}
return new Legacy(first, second, third);
} else {
if (first < 26) {
throw new IllegalArgumentException("Modern version string must be 26.xx.xx or higher");
}
return new Modern(first, second, third);
}
}

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.FIELD)
@interface Minimum {
String value();
}
}

This file was deleted.

Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;

import com.google.common.collect.Iterators;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
import net.minecraft.util.Util;
import org.objectweb.asm.Opcodes;

public class PaperClassloaderBytecodeModifier implements ClassloaderBytecodeModifier {

private static final Map<ApiVersion, List<ModifierFactory>> MODIFIERS = Util.make(new LinkedHashMap<>(), map -> {
});

private final Map<ApiVersion, List<VersionedClassloaderBytecodeModifier>> constructedModifiers = MODIFIERS.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey, entry -> {
return entry.getValue().stream().map(factory -> factory.create(Opcodes.ASM9)).toList();
}));

@Override
public byte[] modify(final PluginMeta configuration, byte[] bytecode) {
int start = -1;
if (configuration.getAPIVersion() != null) {
int i = 0;
for (final Map.Entry<ApiVersion, List<VersionedClassloaderBytecodeModifier>> entry : this.constructedModifiers.entrySet()) {
final ApiVersion apiVersion = ApiVersion.getOrCreateVersion(configuration.getAPIVersion());
final ApiVersion modifierApiVersion = entry.getKey();
if (apiVersion.isOlderThanOrSameAs(modifierApiVersion)) {
start = i;
break;
}
i++;
}
} else {
start = 0;
}
if (start == -1) {
return bytecode; // no modification needed. The plugin version is newer than all versioned modifiers
}

final Iterator<Map.Entry<ApiVersion, List<VersionedClassloaderBytecodeModifier>>> iter = this.constructedModifiers.entrySet().iterator();
Iterators.advance(iter, start);
while (iter.hasNext()) {
for (final VersionedClassloaderBytecodeModifier modifier : iter.next().getValue()) {
bytecode = modifier.modify(configuration, bytecode);
}
}
return bytecode;
}

private interface ModifierFactory {

VersionedClassloaderBytecodeModifier create(int api);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;

import io.papermc.asm.AbstractRewriteRuleVisitorFactory;
import io.papermc.asm.ClassInfoProvider;
import io.papermc.asm.rules.builder.RuleFactoryConfiguration;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.entrypoint.classloader.ClassloaderBytecodeModifier;
import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassWriter;

import static io.papermc.asm.util.DescriptorUtils.desc;

public abstract class VersionedClassloaderBytecodeModifier extends AbstractRewriteRuleVisitorFactory implements ClassloaderBytecodeModifier, RuleFactoryConfiguration.Holder {

protected VersionedClassloaderBytecodeModifier(final int api) {
super(api, ClassInfoProvider.basic());
}

@Override
public final byte[] modify(final PluginMeta config, final byte[] bytecode) {
final ClassReader cr = new ClassReader(bytecode);
final ClassWriter cw = new ClassWriter(cr, ClassWriter.COMPUTE_FRAMES); // need to compute frames because of instruction removal in ctor rewriting

cr.accept(this.createVisitor(cw), 0);
return cw.toByteArray();
}

@Override
public final RuleFactoryConfiguration configuration() {
return RuleFactoryConfiguration.create(desc(this.getClass()));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@NullMarked
package io.papermc.paper.plugin.entrypoint.classloader.bytecode;

import org.jspecify.annotations.NullMarked;
Original file line number Diff line number Diff line change
Expand Up @@ -5,15 +5,13 @@
import io.papermc.paper.configuration.constraint.Constraint;
import io.papermc.paper.configuration.serializer.ComponentSerializer;
import io.papermc.paper.configuration.serializer.EnumValueSerializer;
import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.plugin.configuration.PluginMeta;
import io.papermc.paper.plugin.provider.configuration.serializer.PermissionConfigurationSerializer;
import io.papermc.paper.plugin.provider.configuration.serializer.constraints.PluginConfigConstraints;
import io.papermc.paper.plugin.provider.configuration.type.DependencyConfiguration;
import io.papermc.paper.plugin.provider.configuration.type.PermissionConfiguration;
import io.papermc.paper.plugin.provider.configuration.type.PluginDependencyLifeCycle;
import java.lang.reflect.Type;
import java.util.function.Predicate;
import org.bukkit.craftbukkit.util.ApiVersion;
import org.bukkit.permissions.Permission;
import org.bukkit.permissions.PermissionDefault;
import org.bukkit.plugin.PluginLoadOrder;
Expand All @@ -26,8 +24,6 @@
import org.spongepowered.configurate.objectmapping.ConfigSerializable;
import org.spongepowered.configurate.objectmapping.ObjectMapper;
import org.spongepowered.configurate.objectmapping.meta.Required;
import org.spongepowered.configurate.serialize.ScalarSerializer;
import org.spongepowered.configurate.serialize.SerializationException;
import org.spongepowered.configurate.yaml.NodeStyle;
import org.spongepowered.configurate.yaml.YamlConfigurationLoader;

Expand Down Expand Up @@ -63,43 +59,23 @@ public class PaperPluginMeta implements PluginMeta {
@FlattenedResolver
private PermissionConfiguration permissionConfiguration = new PermissionConfiguration(PermissionDefault.OP, List.of());
@Required
@ApiVersion.Minimum("1.19")
private ApiVersion apiVersion;

private Map<PluginDependencyLifeCycle, Map<String, DependencyConfiguration>> dependencies = new EnumMap<>(PluginDependencyLifeCycle.class);

public PaperPluginMeta() {
}

static final ApiVersion MINIMUM = ApiVersion.getOrCreateVersion("1.19");
public static PaperPluginMeta create(BufferedReader reader) throws ConfigurateException {
YamlConfigurationLoader loader = YamlConfigurationLoader.builder()
.indent(2)
.nodeStyle(NodeStyle.BLOCK)
.headerMode(HeaderMode.NONE)
.source(() -> reader)
.defaultOptions((options) -> {

return options.serializers((serializers) -> {
serializers
.register(new ScalarSerializer<>(ApiVersion.class) {
@Override
public ApiVersion deserialize(final Type type, final Object obj) throws SerializationException {
try {
final ApiVersion version = ApiVersion.getOrCreateVersion(obj.toString());
if (version.isOlderThan(MINIMUM)) {
throw new SerializationException(version + " is too old for a paper plugin!");
}
return version;
} catch (final IllegalArgumentException e) {
throw new SerializationException(e);
}
}

@Override
protected Object serialize(final ApiVersion item, final Predicate<Class<?>> typeSupported) {
return item.getVersionString();
}
})
.register(new EnumValueSerializer())
.register(PermissionConfiguration.class, PermissionConfigurationSerializer.SERIALIZER)
.register(new ComponentSerializer())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package io.papermc.paper.registry.entry;

import io.papermc.paper.plugin.ApiVersion;
import io.papermc.paper.registry.PaperRegistryBuilder;
import io.papermc.paper.registry.RegistryKey;
import java.util.function.BiFunction;
Expand All @@ -10,7 +11,6 @@
import net.minecraft.resources.ResourceKey;
import org.bukkit.Keyed;
import org.bukkit.NamespacedKey;
import org.bukkit.craftbukkit.util.ApiVersion;

import static io.papermc.paper.registry.entry.RegistryEntryMeta.RegistryModificationApiSupport.ADDABLE;
import static io.papermc.paper.registry.entry.RegistryEntryMeta.RegistryModificationApiSupport.MODIFIABLE;
Expand Down
Loading
Loading