diff --git a/AccessWidener/build.gradle.kts b/AccessWidener/build.gradle.kts new file mode 100644 index 00000000..87a25b4d --- /dev/null +++ b/AccessWidener/build.gradle.kts @@ -0,0 +1,45 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2025 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +plugins { + `java-library` + id("com.github.johnrengelman.shadow") version "8.1.1" +} + +dependencies { + implementation("org.ow2.asm:asm:9.7") + implementation("org.ow2.asm:asm-commons:9.7") +} + +tasks.shadowJar { + manifest { + attributes( + "Manifest-Version" to "1.0", + "Build-Jdk-Spec" to "21", + "Main-Class" to "de.steamwar.Main", + "Premain-Class" to "de.steamwar.Agent", + "Can-Retransform-Classes" to "true", + "Can-Redefine-Classes" to "true", + ) + } +} + +tasks.build { + dependsOn(tasks.shadowJar) +} diff --git a/AccessWidener/src/main/java/de/steamwar/AccessWidenerEntry.java b/AccessWidener/src/main/java/de/steamwar/AccessWidenerEntry.java new file mode 100644 index 00000000..5b5ff9a0 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/AccessWidenerEntry.java @@ -0,0 +1,49 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +/** + * A single parsed line from a .accesswidener file. + *

+ * Examples: + * accessible class net/minecraft/server/level/ServerPlayer + * accessible method net/minecraft/server/level/ServerPlayer getStats ()V + * mutable field net/minecraft/world/entity/Entity id I + * extendable class net/minecraft/world/level/chunk/LevelChunk + */ +public record AccessWidenerEntry( + /** accessible | mutable | extendable (may have "transitive-" prefix) */ + String directive, + /** class | method | field */ + String memberType, + /** Internal class name, e.g. net/minecraft/server/level/ServerPlayer */ + String target, + /** Method/field name, null for class entries */ + String name, + /** Descriptor, null for class entries */ + String descriptor) { + /** + * Returns true if this entry targets the class with the given internal name. + */ + public boolean targets(String internalName) { + return target.equals(internalName); + } +} + diff --git a/AccessWidener/src/main/java/de/steamwar/AccessWidenerParser.java b/AccessWidener/src/main/java/de/steamwar/AccessWidenerParser.java new file mode 100644 index 00000000..d962e0dc --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/AccessWidenerParser.java @@ -0,0 +1,104 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.charset.StandardCharsets; +import java.util.ArrayList; +import java.util.List; + +/** + * Parses Fabric-compatible .accesswidener files. + *

+ * Supported format: + *

+ *   accessWidener v2 named
+ *
+ *   # comments are supported
+ *   accessible class  net/minecraft/Foo
+ *   accessible method net/minecraft/Foo someMethod ()V
+ *   accessible field  net/minecraft/Foo someField I
+ *   mutable    field  net/minecraft/Foo someField I
+ *   extendable class  net/minecraft/Foo
+ *   extendable method net/minecraft/Foo someMethod ()V
+ *
+ *   # transitive variants (expose widening to dependents)
+ *   transitive-accessible class net/minecraft/Foo
+ * 
+ */ +public final class AccessWidenerParser { + + private AccessWidenerParser() { + } + + public static List parse(InputStream in) throws IOException { + List entries = new ArrayList<>(); + + try (BufferedReader reader = new BufferedReader(new InputStreamReader(in, StandardCharsets.UTF_8))) { + + String line; + boolean headerSeen = false; + + while ((line = reader.readLine()) != null) { + // Strip inline comments + int commentIdx = line.indexOf('#'); + if (commentIdx >= 0) line = line.substring(0, commentIdx); + line = line.strip(); + + if (line.isEmpty()) continue; + + if (!headerSeen) { + // First non-blank, non-comment line must be the header + if (!line.startsWith("accessWidener")) { + throw new IOException("Missing accessWidener header, got: " + line); + } + headerSeen = true; + continue; + } + + AccessWidenerEntry entry = parseLine(line); + if (entry != null) entries.add(entry); + } + } + + return entries; + } + + private static AccessWidenerEntry parseLine(String line) { + String[] parts = line.split("\\s+"); + if (parts.length < 3) return null; + + String directive = parts[0]; // accessible / mutable / extendable / transitive-* + String memberType = parts[1]; // class / method / field + String target = parts[2]; // internal class name + + return switch (memberType) { + case "class" -> new AccessWidenerEntry(directive, "class", target, null, null); + case "method", "field" -> { + if (parts.length < 5) yield null; + yield new AccessWidenerEntry(directive, memberType, target, parts[3], parts[4]); + } + default -> null; + }; + } +} \ No newline at end of file diff --git a/AccessWidener/src/main/java/de/steamwar/AccessWidenerScanner.java b/AccessWidener/src/main/java/de/steamwar/AccessWidenerScanner.java new file mode 100644 index 00000000..70d2c0c9 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/AccessWidenerScanner.java @@ -0,0 +1,145 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.lang.instrument.Instrumentation; +import java.lang.reflect.Field; +import java.net.URL; +import java.net.URLClassLoader; +import java.nio.file.Path; +import java.util.*; +import java.util.logging.Logger; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +/** + * Scans all plugin ClassLoaders for "plugin.accesswidener" resources + * and returns the parsed entries. + */ +public final class AccessWidenerScanner { + + private static final Logger LOG = Logger.getLogger("AccessWidenerScanner"); + + private AccessWidenerScanner() { + } + + /** + * Scans every ClassLoader visible from the already-loaded classes for + */ + public static List scanAll(Instrumentation inst) { + List allEntries = new ArrayList<>(); + Set seen = Collections.newSetFromMap(new IdentityHashMap<>()); + + for (Class clazz : inst.getAllLoadedClasses()) { + ClassLoader cl = clazz.getClassLoader(); + if (cl == null) continue; // bootstrap — skip + if (!seen.add(cl)) continue; // already processed + if (!isPluginClassLoader(cl)) continue; + + allEntries.addAll(scanClassLoader(cl)); + } + + return allEntries; + } + + /** + * Uses getResources() so it finds ALL matching files on the loader's classpath. + */ + public static List scanClassLoader(ClassLoader cl) { + List entries = new ArrayList<>(); + + try { + for (URL url : findAccessWideners(cl)) { + try (InputStream in = url.openStream()) { + List parsed = AccessWidenerParser.parse(in); + LOG.info("[AccessWidener] Loaded " + parsed.size() + " entries from " + url); + entries.addAll(parsed); + } catch (IOException e) { + LOG.warning("[AccessWidener] Failed to read " + url + ": " + e.getMessage()); + } + } + } catch (IOException e) { + LOG.warning("[AccessWidener] Failed to scan " + cl + ": " + e.getMessage()); + } + + return entries; + } + + public static List findAccessWideners(ClassLoader cl) throws IOException { + List results = new ArrayList<>(); + + // Standard path — works on most Paper versions + if (cl instanceof URLClassLoader urlCl) { + return scanUrls(urlCl.getURLs()); + } + + // Newer Paper — PluginClassLoader stores the jar as a field + try { + // Try common field names used across Paper versions + for (String fieldName : List.of("file", "jarFile", "pluginJar", "source")) { + try { + Field f = cl.getClass().getDeclaredField(fieldName); + f.setAccessible(true); + Object value = f.get(cl); + if (value instanceof File file) { + return scanUrls(new URL[]{ file.toURI().toURL() }); + } + if (value instanceof Path path) { + return scanUrls(new URL[]{ path.toUri().toURL() }); + } + } catch (NoSuchFieldException ignored) {} + } + } catch (Exception e) { + LOG.warning("[AccessWidener] Could not extract JAR path from " + cl + ": " + e.getMessage()); + } + + return results; + } + + private static List scanUrls(URL[] urls) throws IOException { + List results = new ArrayList<>(); + for (URL url : urls) { + if (!url.getPath().endsWith(".jar")) continue; + try (ZipInputStream zip = new ZipInputStream(url.openStream())) { + ZipEntry entry; + while ((entry = zip.getNextEntry()) != null) { + if (entry.getName().endsWith(".accesswidener")) { + results.add(new URL("jar:" + url + "!/" + entry.getName())); + } + } + } + } + return results; + } + + /** + * Returns true if the classloader looks like a Paper plugin classloader. + * Checks by name so we don't need a direct dependency on Paper internals. + */ + public static boolean isPluginClassLoader(ClassLoader cl) { + String name = cl.getClass().getName(); + return name.contains("PluginClassLoader") // legacy Paper / Spigot + || name.contains("PaperPluginLoader") // Paper 1.20.5+ + || name.contains("PaperSimplePluginManager"); // just in case + } +} diff --git a/AccessWidener/src/main/java/de/steamwar/Agent.java b/AccessWidener/src/main/java/de/steamwar/Agent.java new file mode 100644 index 00000000..a786fed8 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/Agent.java @@ -0,0 +1,67 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import java.lang.instrument.Instrumentation; +import java.util.logging.Logger; + +/** + * Java agent entry point. + * + * Can be used two ways: + * 1. At JVM startup: java -javaagent:paper-access-widener-agent.jar -jar server.jar + * 2. Late attach: from inside a Paper plugin via the Attach API + * + * On attach the agent: + * 1. Scans all existing plugin ClassLoaders for "plugin.accesswidener" resources + * 2. Registers a ClassFileTransformer that: + * a. Applies widening to every class as it loads + * b. Detects new plugin ClassLoaders and scans them automatically + * 3. Retransforms any Minecraft/server classes that are already loaded + */ +public class Agent { + private Agent() { + /* This utility class should not be instantiated */ + } + + private static final Logger LOG = Logger.getLogger("AccessWidenerAgent"); + + // Exposed so tests or other code can inspect the live transformer + static volatile WideningTransformer transformer; + + // -javaagent: startup + public static void premain(String args, Instrumentation inst) { + init(inst); + } + + private static void init(Instrumentation inst) { + LOG.info("[AccessWidener] Agent initialising."); + + WideningTransformer t = new WideningTransformer(inst); + transformer = t; + + // --- Phase 2: register transformer for future class loads --- + // canRetransform=true so we can call retransformClasses() later + inst.addTransformer(t, true); + + LOG.info("[AccessWidener] Agent ready."); + } +} + diff --git a/AccessWidener/src/main/java/de/steamwar/ClassPatcher.java b/AccessWidener/src/main/java/de/steamwar/ClassPatcher.java new file mode 100644 index 00000000..3e640694 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/ClassPatcher.java @@ -0,0 +1,63 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import org.objectweb.asm.*; + +import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Uses ASM to patch class bytecode according to a list of access widener entries. + * + * Returns {@code null} if the class is not targeted by any entry (no-op signal + * to the caller so it can skip the write). + */ +public class ClassPatcher { + + private final List entries; + + /** Pre-computed set of targeted internal names for fast filtering. */ + private final Set targets; + + public ClassPatcher(List entries) { + this.entries = entries; + this.targets = entries.stream() + .map(AccessWidenerEntry::target) + .collect(Collectors.toSet()); + } + + /** + * Patches {@code classBytes} if {@code internalName} is targeted. + * + * @return patched bytes, or {@code null} if no changes were needed + */ + public byte[] patch(String internalName, byte[] classBytes) { + if (!targets.contains(internalName)) return null; + + ClassReader cr = new ClassReader(classBytes); + // COMPUTE_FRAMES would require the full classpath; we only touch flags so 0 is fine + ClassWriter cw = new ClassWriter(cr, 0); + cr.accept(new ClassTransformer(cw, internalName, entries), 0); + return cw.toByteArray(); + } +} + diff --git a/AccessWidener/src/main/java/de/steamwar/ClassTransformer.java b/AccessWidener/src/main/java/de/steamwar/ClassTransformer.java new file mode 100644 index 00000000..c329b966 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/ClassTransformer.java @@ -0,0 +1,98 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.FieldVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Opcodes; + +import java.util.List; + +public class ClassTransformer extends ClassVisitor { + + private final String internalName; + private final List entries; + + public ClassTransformer(ClassVisitor cv, String internalName, List entries) { + super(Opcodes.ASM9, cv); + this.internalName = internalName; + this.entries = entries; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + int newAccess = access; + for (AccessWidenerEntry e : entries) { + if (!e.targets(internalName) || !"class".equals(e.memberType())) continue; + newAccess = applyDirective(e.directive(), newAccess, false); + } + super.visit(version, newAccess, name, signature, superName, interfaces); + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + int newAccess = access; + for (AccessWidenerEntry e : entries) { + if (!e.targets(internalName) || !"method".equals(e.memberType())) continue; + if (!name.equals(e.name()) || !descriptor.equals(e.descriptor())) continue; + newAccess = applyDirective(e.directive(), newAccess, false); + } + return super.visitMethod(newAccess, name, descriptor, signature, exceptions); + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + int newAccess = access; + for (AccessWidenerEntry e : entries) { + if (!e.targets(internalName) || !"field".equals(e.memberType())) continue; + if (!name.equals(e.name())) continue; + newAccess = applyDirective(e.directive(), newAccess, true); + } + return super.visitField(newAccess, name, descriptor, signature, value); + } + + /** + * Apply a directive to an access bitmask. + * + * @param directive accessible / mutable / extendable (with optional "transitive-" prefix) + * @param access current access flags + * @param isField true when processing a field (mutable removes final) + */ + private static int applyDirective(String directive, int access, boolean isField) { + // Strip transitive- prefix — the widening itself is the same + String effective = directive.startsWith("transitive-") ? directive.substring("transitive-".length()) : directive; + + return switch (effective) { + case "accessible" -> makePublic(access); + case "extendable" -> makePublic(removeFinal(access)); + case "mutable" -> isField ? removeFinal(access) : access; + default -> access; + }; + } + + private static int makePublic(int access) { + return (access & ~(Opcodes.ACC_PRIVATE | Opcodes.ACC_PROTECTED)) | Opcodes.ACC_PUBLIC; + } + + private static int removeFinal(int access) { + return access & ~Opcodes.ACC_FINAL; + } +} diff --git a/AccessWidener/src/main/java/de/steamwar/Main.java b/AccessWidener/src/main/java/de/steamwar/Main.java new file mode 100644 index 00000000..8dc81875 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/Main.java @@ -0,0 +1,125 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import java.io.IOException; +import java.io.InputStream; +import java.nio.file.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Command-line tool that produces a widened copy of a JAR for use as a + * compile-time stub in IntelliJ / Gradle. + * + * Usage: + * java -jar jar-widener.jar [file.accesswidener ...] + * + * The output JAR is identical to the input JAR except that every class + * targeted by the access widener entries has its access flags patched: + * accessible → public + * extendable → public + non-final + * mutable → non-final field + * + * Intended for use as a Gradle task so IntelliJ sees the already-widened + * class when you Ctrl+click NMS code, and javac compiles without complaints. + */ +public class Main { + + public static void main(String[] args) throws Exception { + if (args.length < 2) { + System.err.println("Usage: jar-widener [*.accesswidener ...]"); + System.exit(1); + } + + Path inputJar = Path.of(args[0]); + Path outputJar = Path.of(args[1]); + + if (!Files.exists(inputJar)) { + System.err.println("Input JAR not found: " + inputJar); + System.exit(1); + } + + // --- Collect all access widener entries --- + List entries = new ArrayList<>(); + + if (args.length > 2) { + for (int i = 2; i < args.length; i++) { + Path awFile = Path.of(args[i]); + if (!Files.exists(awFile)) { + System.err.println("Warning: access widener file not found, skipping: " + awFile); + continue; + } + try (InputStream in = Files.newInputStream(awFile)) { + List parsed = AccessWidenerParser.parse(in); + System.out.println("Loaded " + parsed.size() + " entries from " + awFile.getFileName()); + entries.addAll(parsed); + } + } + } + + if (entries.isEmpty()) { + System.out.println("No access widener entries found — copying JAR unchanged."); + Files.createDirectories(outputJar.getParent()); + Files.copy(inputJar, outputJar, StandardCopyOption.REPLACE_EXISTING); + return; + } + + System.out.println("Widening " + inputJar.getFileName() + + " with " + entries.size() + " total entr" + + (entries.size() == 1 ? "y" : "ies") + "..."); + + // --- Copy input → output, transforming .class files in place --- + Files.createDirectories(outputJar.getParent()); + Files.copy(inputJar, outputJar, StandardCopyOption.REPLACE_EXISTING); + + ClassPatcher patcher = new ClassPatcher(entries); + + try (FileSystem fs = FileSystems.newFileSystem(outputJar)) { + // Walk every .class entry in the JAR + try (var stream = Files.walk(fs.getPath("/"))) { + stream.filter(p -> p.toString().endsWith(".class")) + .forEach(classPath -> patchClass(fs, classPath, patcher)); + } + } + + System.out.println("Done. Widened JAR written to " + outputJar); + } + + private static void patchClass(FileSystem fs, Path classPath, ClassPatcher patcher) { + // Derive internal class name from path e.g. /net/minecraft/Foo.class → net/minecraft/Foo + String internalName = classPath.toString() + .replaceFirst("^/", "") + .replace(".class", ""); + + try { + byte[] original = Files.readAllBytes(classPath); + byte[] patched = patcher.patch(internalName, original); + + if (patched != null) { + Files.write(classPath, patched); + System.out.println(" Widened: " + internalName); + } + } catch (IOException e) { + System.err.println(" Warning: failed to patch " + internalName + ": " + e.getMessage()); + } + } +} + diff --git a/AccessWidener/src/main/java/de/steamwar/WideningTransformer.java b/AccessWidener/src/main/java/de/steamwar/WideningTransformer.java new file mode 100644 index 00000000..dd14a9a0 --- /dev/null +++ b/AccessWidener/src/main/java/de/steamwar/WideningTransformer.java @@ -0,0 +1,149 @@ +/* + * This file is a part of the SteamWar software. + * + * Copyright (C) 2026 SteamWar.de-Serverteam + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package de.steamwar; + +import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassWriter; + +import java.lang.instrument.ClassFileTransformer; +import java.lang.instrument.Instrumentation; +import java.lang.instrument.UnmodifiableClassException; +import java.security.ProtectionDomain; +import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; +import java.util.logging.Logger; +import java.util.stream.Collectors; + +/** + * Transforms class bytecode to apply access widening rules. + *

+ * Also monitors for new plugin ClassLoaders appearing (when plugins load after + * the agent attaches) and automatically picks up their .accesswidener files. + */ +public class WideningTransformer implements ClassFileTransformer { + + private static final Logger LOG = Logger.getLogger("WidenerTransformer"); + + private final Instrumentation instrumentation; + + /** + * All entries collected across all plugins. Thread-safe for concurrent plugin loads. + */ + private final List entries = new CopyOnWriteArrayList<>(); + + /** + * ClassLoaders we have already scanned, to avoid re-scanning. + */ + private final Set scannedLoaders = Collections.synchronizedSet(Collections.newSetFromMap(new IdentityHashMap<>())); + + public WideningTransformer(Instrumentation instrumentation) { + this.instrumentation = instrumentation; + } + + /** + * Add entries — used during initial scan before the transformer is registered. + */ + public void addEntries(List newEntries) { + entries.addAll(newEntries); + } + + // ------------------------------------------------------------------------- + // ClassFileTransformer + // ------------------------------------------------------------------------- + + @Override + public byte[] transform(ClassLoader loader, String className, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) { + + // Check for new plugin classloaders we haven't seen yet + if (loader != null && AccessWidenerScanner.isPluginClassLoader(loader) && scannedLoaders.add(loader)) { + onNewPluginClassLoader(loader); + } + + // No entries for this class — skip transformation entirely + if (className == null) return classfileBuffer; + boolean relevant = entries.stream().anyMatch(e -> e.targets(className)); + if (!relevant) return classfileBuffer; + + // Apply widening via ASM + try { + ClassReader cr = new ClassReader(classfileBuffer); + ClassWriter cw = new ClassWriter(cr, 0); + cr.accept(new ClassTransformer(cw, className, entries), 0); + return cw.toByteArray(); + } catch (Exception e) { + LOG.warning("[AccessWidener] Failed to transform " + className + ": " + e.getMessage()); + return classfileBuffer; + } + } + + // ------------------------------------------------------------------------- + // New ClassLoader detection + // ------------------------------------------------------------------------- + + private void onNewPluginClassLoader(ClassLoader loader) { + List newEntries = AccessWidenerScanner.scanClassLoader(loader); + if (newEntries.isEmpty()) return; + + entries.addAll(newEntries); + LOG.info("[AccessWidener] Picked up " + newEntries.size() + " new entr" + (newEntries.size() == 1 ? "y" : "ies") + " from " + loader); + + // Retransform already-loaded classes that are now targeted + scheduleRetransform(newEntries); + } + + /** + * Retransformation cannot be called from within a transform() call, + * so we dispatch it to a short-lived daemon thread. + */ + private void scheduleRetransform(List newEntries) { + Set targets = newEntries.stream().map(e -> e.target().replace('/', '.')).collect(Collectors.toSet()); + + Thread t = new Thread(() -> retransform(targets), "access-widener-retransform"); + t.setDaemon(true); + t.start(); + } + + private void retransform(Set dotNames) { + List toRetransform = Arrays.stream(instrumentation.getAllLoadedClasses()) + .filter(c -> dotNames.contains(c.getName())) + .filter(instrumentation::isModifiableClass) + .collect(Collectors.toList()); + + if (toRetransform.isEmpty()) return; + + LOG.info("[AccessWidener] Retransforming " + toRetransform.size() + " class(es)."); + List failed = new ArrayList<>(); + + for (Class clazz : toRetransform) { + try { + instrumentation.retransformClasses(clazz); + } catch (UnmodifiableClassException | UnsupportedOperationException e) { + failed.add(clazz.getName()); + } + } + + if (!failed.isEmpty()) { + LOG.warning("[AccessWidener] The following classes were already loaded before the agent" + + " and cannot be retransformed. Add -javaagent: to your start script" + + " to fix this:"); + failed.forEach(name -> LOG.warning(" - " + name)); + } + } +} diff --git a/BauSystem/BauSystem_Main/src/bausystem.accesswidener b/BauSystem/BauSystem_Main/src/bausystem.accesswidener new file mode 100644 index 00000000..7a0f195d --- /dev/null +++ b/BauSystem/BauSystem_Main/src/bausystem.accesswidener @@ -0,0 +1,3 @@ +accessWidener v2 named + +accessible field net/minecraft/server/level/ServerPlayerGameMode gameModeForPlayer Lnet/minecraft/world/level/GameType; diff --git a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java index 0b10e163..6ead52d4 100644 --- a/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java +++ b/BauSystem/BauSystem_Main/src/de/steamwar/bausystem/features/util/NoClipCommand.java @@ -21,7 +21,6 @@ package de.steamwar.bausystem.features.util; import com.comphenix.tinyprotocol.TinyProtocol; import com.mojang.authlib.GameProfile; -import de.steamwar.Reflection; import de.steamwar.bausystem.BauSystem; import de.steamwar.bausystem.features.tpslimit.TPSUtils; import de.steamwar.bausystem.utils.BauMemberUpdateEvent; @@ -30,7 +29,6 @@ import de.steamwar.core.ProtocolWrapper; import de.steamwar.core.SWPlayer; import de.steamwar.linkage.Linked; import net.minecraft.network.protocol.game.*; -import net.minecraft.server.level.ServerPlayerGameMode; import net.minecraft.world.entity.player.Abilities; import net.minecraft.world.level.GameType; import org.bukkit.Bukkit; @@ -103,10 +101,8 @@ public class NoClipCommand extends SWCommand implements Listener { TinyProtocol.instance.addFilter(ServerboundSetCreativeModeSlotPacket.class, third); } - private static final Reflection.Field playerGameMode = Reflection.getField(ServerPlayerGameMode.class, GameType.class, 0); - private void setInternalGameMode(Player player, GameMode gameMode) { - playerGameMode.set(((CraftPlayer) player).getHandle().gameMode, GameType.byId(gameMode.getValue())); + // ((CraftPlayer) player).getHandle().gameMode.gameModeForPlayer = GameType.byId(gameMode.getValue()); } @Register(help = true) diff --git a/BauSystem/build.gradle.kts b/BauSystem/build.gradle.kts index de9fe1b8..b6180f3f 100644 --- a/BauSystem/build.gradle.kts +++ b/BauSystem/build.gradle.kts @@ -20,15 +20,22 @@ plugins { `java-library` alias(libs.plugins.shadow) + id("io.papermc.paperweight.userdev") version "1.7.1" } tasks.build { finalizedBy(tasks.shadowJar) } +repositories { + maven("https://repo.papermc.io/repository/maven-public/") +} + dependencies { implementation(project(":BauSystem:BauSystem_RegionFixed")) implementation(project(":BauSystem:BauSystem_Main")) + + paperweight.paperDevBundle("1.21.1-R0.1-SNAPSHOT") } tasks.register("DevBau21") { @@ -37,5 +44,7 @@ tasks.register("DevBau21") { dependsOn(":SpigotCore:shadowJar") dependsOn(":BauSystem:shadowJar") dependsOn(":SchematicSystem:shadowJar") + dependsOn(":AccessWidener:shadowJar") template = "Bau21" + jvmArgs = "-javaagent:/home/yoyonow/Bau21/plugins/AccessWidener.jar=start" } diff --git a/SpigotCore/SpigotCore_Main/src/de/steamwar/core/AccessWidenerAttacher.java b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/AccessWidenerAttacher.java new file mode 100644 index 00000000..ccf61c40 --- /dev/null +++ b/SpigotCore/SpigotCore_Main/src/de/steamwar/core/AccessWidenerAttacher.java @@ -0,0 +1,117 @@ +package de.steamwar.core; + +import java.lang.reflect.Method; +import java.nio.file.Path; +import java.util.logging.Logger; + +/** + * Public API for Paper plugins that want to attach the access widener agent. + * + *

Usage

+ * Call {@link #ensureAttached(Path)} from your plugin's {@code onLoad()} method + * (not {@code onEnable()} — onLoad fires before classes start loading): + * + *
{@code
+ * public class MyPlugin extends JavaPlugin {
+ *     @Override
+ *     public void onLoad() {
+ *         try {
+ *             AccessWidenerAttacher.ensureAttached(getDataFolder());
+ *         } catch (Exception e) {
+ *             getSLF4JLogger().error("Failed to attach access widener agent", e);
+ *         }
+ *     }
+ * }
+ * }
+ * + *

Resource file

+ * Drop a {@code plugin.accesswidener} file in your plugin's resources: + * + *
+ *   accessWidener v2 named
+ *
+ *   accessible method net/minecraft/server/level/ServerPlayer getStats ()V
+ *   mutable    field  net/minecraft/world/entity/Entity id I
+ * 
+ * + * The agent discovers it automatically via your plugin's ClassLoader. + */ +public final class AccessWidenerAttacher { + + private static final Logger LOG = Logger.getLogger("AccessWidenerAttacher"); + private static final Object LOCK = new Object(); + + private static volatile boolean attached = false; + + private AccessWidenerAttacher() {} + + /** + * Attaches the access widener agent to the running JVM if it has not already + * been attached. Safe to call from multiple plugins — the agent is only + * attached once. + * + * @param agentJar the agentJar + * @throws AccessWidenerException if the agent could not be attached + */ + public static void ensureAttached(Path agentJar) throws AccessWidenerException { + if (attached) return; // fast path — no locking needed after first attach + + synchronized (LOCK) { + if (attached) return; // double-checked + + try { + attachAgent(agentJar); + attached = true; + LOG.info("[AccessWidener] Agent attached successfully."); + } catch (Exception e) { + throw new AccessWidenerException("Failed to attach access widener agent", e); + } + } + } + + /** Returns true if the agent has been successfully attached. */ + public static boolean isAttached() { + return attached; + } + + private static void attachAgent(Path agentJar) throws Exception { + // Verify the Attach API is available before trying + try { + Class.forName("com.sun.tools.attach.VirtualMachine"); + } catch (ClassNotFoundException e) { + throw new AccessWidenerException( + "The JDK Attach API is not available. " + + "Make sure you are running on a JDK (not a JRE) " + + "and add '--add-opens jdk.attach/sun.tools.attach=ALL-UNNAMED' " + + "to your JVM flags if on Java 9+.", e); + } + + String pid = String.valueOf(ProcessHandle.current().pid()); + + // Use reflection so the Attach API is not a hard compile-time dependency + Class vmClass = Class.forName("com.sun.tools.attach.VirtualMachine"); + Method attachMethod = vmClass.getMethod("attach", String.class); + Method loadAgentMethod = vmClass.getMethod("loadAgent", String.class); + Method detachMethod = vmClass.getMethod("detach"); + + Object vm = attachMethod.invoke(null, pid); + try { + loadAgentMethod.invoke(vm, agentJar.toAbsolutePath().toString()); + } finally { + detachMethod.invoke(vm); + } + } + + // ------------------------------------------------------------------------- + // Exception type + // ------------------------------------------------------------------------- + + public static class AccessWidenerException extends Exception { + public AccessWidenerException(String message, Throwable cause) { + super(message, cause); + } + public AccessWidenerException(String message) { + super(message); + } + } +} diff --git a/buildSrc/src/steamwar.devserver.gradle b/buildSrc/src/steamwar.devserver.gradle index 59e6cfbb..af6d224e 100644 --- a/buildSrc/src/steamwar.devserver.gradle +++ b/buildSrc/src/steamwar.devserver.gradle @@ -27,7 +27,6 @@ plugins { class DevServer extends DefaultTask { @Input - @Optional boolean debug = false @Input diff --git a/settings.gradle.kts b/settings.gradle.kts index b84535d4..c3da5d8d 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -162,6 +162,10 @@ dependencyResolutionManagement { } } +include( + "AccessWidener" +) + include( "BauSystem", "BauSystem:BauSystem_Main",