Introducing: Yatoclip (#360)

This commit is contained in:
ishland 2021-01-26 00:15:36 +08:00 committed by GitHub
parent 2669a91c6a
commit 224376504d
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
14 changed files with 1340 additions and 93 deletions

View File

@ -80,12 +80,12 @@ jobs:
- name: Build Yatopia
run: |
./gradlew paperclip
./gradlew yatoclip
- name: Upload Artifact
if: github.ref != 'refs/heads/ver/1.16.4'
uses: actions/upload-artifact@v2
with:
name: Yatopia-${{ matrix.java }}
path: yatopia-1.16.5-paperclip.jar
path: yatopia-1.16.5-yatoclip.jar

182
Jenkinsfile vendored
View File

@ -1,91 +1,91 @@
pipeline {
agent { label 'slave' }
options { timestamps() }
stages {
stage('Cleanup') {
steps {
scmSkip(deleteBuild: true, skipPattern:'.*\\[CI-SKIP\\].*')
sh 'rm -rf ./target'
sh 'rm -rf ./Paper/Paper-API ./Paper/Paper-Server ./Paper/work/Spigot/Spigot-API ./Paper/work/Spigot/Spigot-Server'
sh 'rm -rf ./Yatopia-API ./Yatopia-Server'
sh 'chmod +x ./gradlew'
}
}
stage('Init project & submodules') {
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT',
) {
sh './gradlew initGitSubmodules'
}
}
}
stage('Decompile & apply patches') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT',
) {
sh '''
./gradlew setupUpstream
./gradlew applyPatches
'''
}
}
}
stage('Build') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT'
) {
withCredentials([usernamePassword(credentialsId: 'jenkins-deploy', usernameVariable: 'ORG_GRADLE_PROJECT_mavenUsername', passwordVariable: 'ORG_GRADLE_PROJECT_mavenPassword')]) {
sh '''
./gradlew build
./gradlew publish
'''
}
}
}
}
stage('Build Launcher') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT'
) {
sh '''
mkdir -p "./target"
./gradlew paperclip
basedir=$(pwd)
paperworkdir="$basedir/Paper/work"
mcver=$(cat "$paperworkdir/BuildData/info.json" | grep minecraftVersion | cut -d '"' -f 4)
cp "yatopia-$mcver-paperclip.jar" "./target/yatopia-$mcver-paperclip-b$BUILD_NUMBER.jar"
'''
}
}
post {
success {
archiveArtifacts "target/*.jar"
}
failure {
cleanWs()
}
}
}
}
}
pipeline {
agent { label 'slave' }
options { timestamps() }
stages {
stage('Cleanup') {
steps {
scmSkip(deleteBuild: true, skipPattern:'.*\\[CI-SKIP\\].*')
sh 'rm -rf ./target'
sh 'rm -rf ./Paper/Paper-API ./Paper/Paper-Server ./Paper/work/Spigot/Spigot-API ./Paper/work/Spigot/Spigot-Server'
sh 'rm -rf ./Yatopia-API ./Yatopia-Server'
sh 'chmod +x ./gradlew'
}
}
stage('Init project & submodules') {
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT',
) {
sh './gradlew initGitSubmodules'
}
}
}
stage('Decompile & apply patches') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT',
) {
sh '''
./gradlew setupUpstream
./gradlew applyPatches
'''
}
}
}
stage('Build') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT'
) {
withCredentials([usernamePassword(credentialsId: 'jenkins-deploy', usernameVariable: 'ORG_GRADLE_PROJECT_mavenUsername', passwordVariable: 'ORG_GRADLE_PROJECT_mavenPassword')]) {
sh '''
./gradlew build
./gradlew publish
'''
}
}
}
}
stage('Build Launcher') {
tools {
jdk "OpenJDK 8"
}
steps {
withMaven(
maven: '3',
mavenLocalRepo: '.repository',
publisherStrategy: 'EXPLICIT'
) {
sh '''
mkdir -p "./target"
./gradlew yatoclip
basedir=$(pwd)
paperworkdir="$basedir/Paper/work"
mcver=$(cat "$paperworkdir/BuildData/info.json" | grep minecraftVersion | cut -d '"' -f 4)
cp "yatopia-$mcver-yatoclip.jar" "./target/yatopia-$mcver-yatoclip-b$BUILD_NUMBER.jar"
'''
}
}
post {
success {
archiveArtifacts "target/*.jar"
}
failure {
cleanWs()
}
}
}
}
}

11
Yatoclip/build.gradle.kts Normal file
View File

@ -0,0 +1,11 @@
repositories {
mavenCentral()
maven("https://jitpack.io/")
}
dependencies {
implementation("com.github.ishlandbukkit:jbsdiff:deff66b794")
implementation("com.google.code.gson:gson:2.8.6")
implementation("commons-io:commons-io:2.8.0")
}

View File

@ -0,0 +1,51 @@
package org.yatopia.yatoclip;
import java.io.Serializable;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
public class PatchesMetadata {
public final Set<PatchMetadata> patches;
public final Set<Relocation> relocations;
public final Set<String> copyExcludes;
public PatchesMetadata(Set<PatchMetadata> patches, Set<Relocation> relocations, Set<String> copyExcludes) {
Objects.requireNonNull(copyExcludes);
this.copyExcludes = Collections.unmodifiableSet(copyExcludes);
Objects.requireNonNull(relocations);
this.relocations = Collections.unmodifiableSet(relocations);
Objects.requireNonNull(patches);
this.patches = Collections.unmodifiableSet(patches);
}
public static class PatchMetadata {
public final String name;
public final String originalHash;
public final String targetHash;
public final String patchHash;
public PatchMetadata(String name, String originalHash, String targetHash, String patchHash) {
this.name = name;
this.originalHash = originalHash;
this.targetHash = targetHash;
this.patchHash = patchHash;
}
}
public static class Relocation implements Serializable {
public final String from;
public final String to;
public final boolean includeSubPackages;
public Relocation(String from, String to, boolean includeSubPackages) {
Objects.requireNonNull(from);
Objects.requireNonNull(to);
this.from = from.replaceAll("\\.", "/");
this.to = to.replaceAll("\\.", "/");
this.includeSubPackages = includeSubPackages;
}
}
}

View File

@ -0,0 +1,360 @@
package org.yatopia.yatoclip;
import com.google.gson.Gson;
import com.google.gson.annotations.SerializedName;
import java.io.IOException;
import java.io.InputStream;
import java.io.Reader;
import java.net.URL;
import java.nio.channels.Channels;
import java.nio.channels.FileChannel;
import java.nio.channels.ReadableByteChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.util.Iterator;
import java.util.List;
import java.util.Properties;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
import static java.nio.file.StandardOpenOption.CREATE;
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
public class ServerSetup {
private static final String minecraftVersion;
private static final Path cacheDirectory;
private static final Gson gson = new Gson();
private static VersionInfo versionInfo = null;
private static BuildDataInfo buildDataInfo = null;
static {
Properties prop = new Properties();
try (InputStream inputStream = ServerSetup.class.getClassLoader().getResourceAsStream("yatoclip-launch.properties")) {
prop.load(inputStream);
} catch (IOException e) {
throw new RuntimeException(e);
}
minecraftVersion = prop.getProperty("minecraftVersion");
cacheDirectory = Paths.get("cache", minecraftVersion);
cacheDirectory.toFile().mkdirs();
}
public static Path setup() throws IOException {
long startTime = System.nanoTime();
checkBuildData();
applyMappingsAndPatches();
System.err.println(String.format("Yatoclip server setup completed in %.2fms", (System.nanoTime() - startTime) / 1_000_000.0));
return cacheDirectory.resolve("Minecraft").resolve(minecraftVersion + "-patched.jar");
}
private static void applyMappingsAndPatches() throws IOException {
final Path minecraftDir = cacheDirectory.resolve("Minecraft");
minecraftDir.toFile().mkdirs();
final Path vanillaJar = minecraftDir.resolve(minecraftVersion + ".jar");
if (!isValidZip(vanillaJar)) {
System.err.println("Downloading vanilla jar...");
download(new URL(buildDataInfo.serverUrl), vanillaJar);
if (!isValidZip(vanillaJar)) throw new RuntimeException("Invalid vanilla jar");
}
final Path classMappedJar = minecraftDir.resolve(minecraftVersion + "-cl.jar");
final Path memberMappedJar = minecraftDir.resolve(minecraftVersion + "-m.jar");
final Path patchedJar = minecraftDir.resolve(minecraftVersion + "-patched.jar");
if (!isValidZip(classMappedJar) || !isValidZip(memberMappedJar)) {
System.err.println("Applying class mapping...");
SpecialSourceLauncher.resetSpecialSourceClassloader();
final Path buildData = cacheDirectory.resolve("BuildData");
SpecialSourceLauncher.setSpecialSourceJar(buildData.resolve("bin").resolve("SpecialSource-2.jar").toFile());
SpecialSourceLauncher.runProcess(
"map", "--only", ".", "--only", "net/minecraft", "--auto-lvt", "BASIC", "--auto-member", "SYNTHETIC",
"-i", Paths.get(".").relativize(vanillaJar).toString(),
"-m", Paths.get(".").relativize(buildData.resolve("mappings").resolve(buildDataInfo.classMappings)).toString(),
"-o", Paths.get(".").relativize(classMappedJar).toString()
);
System.err.println("Applying member mapping...");
SpecialSourceLauncher.runProcess(
"map", "--only", ".", "--only", "net/minecraft", "--auto-member", "LOGGER", "--auto-member", "TOKENS",
"-i", Paths.get(".").relativize(classMappedJar).toString(),
"-m", Paths.get(".").relativize(buildData.resolve("mappings").resolve(buildDataInfo.memberMappings)).toString(),
"-o", Paths.get(".").relativize(memberMappedJar).toString()
);
SpecialSourceLauncher.resetSpecialSourceClassloader();
if (!isValidZip(classMappedJar) || !isValidZip(memberMappedJar))
throw new RuntimeException("Unable to apply mappings");
}
if (!YatoclipPatcher.isJarUpToDate(patchedJar)){
System.err.println("Applying patches...");
YatoclipPatcher.patchJar(memberMappedJar, patchedJar);
if(!YatoclipPatcher.isJarUpToDate(patchedJar))
throw new RuntimeException("Unable to apply patches");
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean isValidZip(Path zipPath) {
try {
ZipFile zipFile = new ZipFile(zipPath.toFile());
zipFile.close();
} catch (Throwable t) {
return false;
}
return true;
}
private static void checkBuildData() throws IOException {
final Path buildDataDir = cacheDirectory.resolve("BuildData");
buildDataDir.toFile().mkdirs();
final Path versionInfoFile = buildDataDir.resolve("version.json");
if (!tryParseVersionInfo(versionInfoFile)) {
System.err.println("Downloading version.json...");
final URL versionInfoURI = new URL("https://hub.spigotmc.org/versions/" + minecraftVersion + ".json");
download(versionInfoURI, versionInfoFile);
if (!tryParseVersionInfo(versionInfoFile)) throw new RuntimeException("Unable to parse versionInfo");
}
final Path buildDataArchive = buildDataDir.resolve("BuildData.zip");
if (!tryParseBuildData(buildDataArchive)) {
System.err.println("Downloading BuildData...");
final URL buildDataURL = new URL("https://hub.spigotmc.org/stash/rest/api/latest/projects/SPIGOT/repos/builddata/archive?at=" + ServerSetup.versionInfo.refs.buildData + "&format=zip");
download(buildDataURL, buildDataArchive);
if (!tryParseBuildData(buildDataArchive)) throw new RuntimeException("Unable to parse BuildData");
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean tryParseBuildData(Path buildData) {
try {
ZipFile zipFile = new ZipFile(buildData.toFile());
((Iterator<ZipEntry>) zipFile.entries()).forEachRemaining(zipEntry -> {
if (zipEntry.isDirectory()) return;
buildData.getParent().resolve(zipEntry.getName()).getParent().toFile().mkdirs();
try (
final ReadableByteChannel source = Channels.newChannel(zipFile.getInputStream(zipEntry));
final FileChannel fileChannel = FileChannel.open(buildData.getParent().resolve(zipEntry.getName()), CREATE, WRITE, TRUNCATE_EXISTING)
) {
fileChannel.transferFrom(source, 0, Long.MAX_VALUE);
} catch (Throwable t) {
throw new RuntimeException(t);
}
});
zipFile.close();
try (Reader reader = Files.newBufferedReader(buildData.getParent().resolve("info.json"))){
ServerSetup.buildDataInfo = gson.fromJson(reader, BuildDataInfo.class);
}
} catch (Throwable t) {
return false;
}
return true;
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
private static boolean tryParseVersionInfo(Path versionInfo) {
try (Reader reader = Files.newBufferedReader(versionInfo)) {
ServerSetup.versionInfo = gson.fromJson(reader, VersionInfo.class);
} catch (Throwable t) {
return false;
}
return true;
}
private static void download(URL url, Path downloadTo) throws IOException {
try (
final ReadableByteChannel source = Channels.newChannel(url.openStream());
final FileChannel fileChannel = FileChannel.open(downloadTo, CREATE, WRITE, TRUNCATE_EXISTING)
) {
downloadTo.getParent().toFile().mkdirs();
fileChannel.transferFrom(source, 0, Long.MAX_VALUE);
}
}
static String toHex(final byte[] hash) {
final StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte aHash : hash) {
sb.append(String.format("%02X", aHash & 0xFF));
}
return sb.toString();
}
public static class VersionInfo {
@SerializedName("refs")
private Refs refs;
@SerializedName("name")
private String name;
@SerializedName("description")
private String description;
@SerializedName("toolsVersion")
private int toolsVersion;
@SerializedName("javaVersions")
private List<Integer> javaVersions;
public Refs getRefs() {
return refs;
}
public String getName() {
return name;
}
public String getDescription() {
return description;
}
public int getToolsVersion() {
return toolsVersion;
}
public List<Integer> getJavaVersions() {
return javaVersions;
}
@Override
public String toString() {
return
"VersionInfo{" +
"refs = '" + refs + '\'' +
",name = '" + name + '\'' +
",description = '" + description + '\'' +
",toolsVersion = '" + toolsVersion + '\'' +
",javaVersions = '" + javaVersions + '\'' +
"}";
}
public static class Refs {
@SerializedName("BuildData")
private String buildData;
@SerializedName("CraftBukkit")
private String craftBukkit;
@SerializedName("Bukkit")
private String bukkit;
@SerializedName("Spigot")
private String spigot;
public String getBuildData() {
return buildData;
}
public String getCraftBukkit() {
return craftBukkit;
}
public String getBukkit() {
return bukkit;
}
public String getSpigot() {
return spigot;
}
@Override
public String toString() {
return
"Refs{" +
"buildData = '" + buildData + '\'' +
",craftBukkit = '" + craftBukkit + '\'' +
",bukkit = '" + bukkit + '\'' +
",spigot = '" + spigot + '\'' +
"}";
}
}
}
public static class BuildDataInfo {
@SerializedName("memberMapCommand")
private String memberMapCommand;
@SerializedName("packageMappings")
private String packageMappings;
@SerializedName("classMapCommand")
private String classMapCommand;
@SerializedName("finalMapCommand")
private String finalMapCommand;
@SerializedName("serverUrl")
private String serverUrl;
@SerializedName("toolsVersion")
private int toolsVersion;
@SerializedName("minecraftHash")
private String minecraftHash;
@SerializedName("minecraftVersion")
private String minecraftVersion;
@SerializedName("accessTransforms")
private String accessTransforms;
@SerializedName("memberMappings")
private String memberMappings;
@SerializedName("decompileCommand")
private String decompileCommand;
@SerializedName("classMappings")
private String classMappings;
public String getMemberMapCommand() {
return memberMapCommand;
}
public String getPackageMappings() {
return packageMappings;
}
public String getClassMapCommand() {
return classMapCommand;
}
public String getFinalMapCommand() {
return finalMapCommand;
}
public String getServerUrl() {
return serverUrl;
}
public int getToolsVersion() {
return toolsVersion;
}
public String getMinecraftHash() {
return minecraftHash;
}
public String getMinecraftVersion() {
return minecraftVersion;
}
public String getAccessTransforms() {
return accessTransforms;
}
public String getMemberMappings() {
return memberMappings;
}
public String getDecompileCommand() {
return decompileCommand;
}
public String getClassMappings() {
return classMappings;
}
}
}

View File

@ -0,0 +1,119 @@
/*
Copyright (c) 2014, SpigotMC. All rights reserved.
Redistribution and use in source and binary forms, with or without
modification, are permitted provided that the following conditions are met:
Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
The name of the author may not be used to endorse or promote products derived
from this software without specific prior written permission.
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE
LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
POSSIBILITY OF SUCH DAMAGE.
*/
package org.yatopia.yatoclip;
import java.io.File;
import java.io.IOException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLClassLoader;
import java.util.concurrent.atomic.AtomicReference;
public class SpecialSourceLauncher {
private static final AtomicReference<SpecialSourceClassLoader> classLoader = new AtomicReference<>(new SpecialSourceClassLoader(new URL[0], SpecialSourceLauncher.class.getClassLoader().getParent()));
private static final AtomicReference<String> mainClass = new AtomicReference<>("");
static void setSpecialSourceJar(File specialSourceJar) {
synchronized (classLoader) {
System.out.println("Setting up SpecialSource: " + specialSourceJar);
try {
classLoader.get().addURL(specialSourceJar.toURI().toURL());
mainClass.set(Yatoclip.getMainClass(specialSourceJar.toPath()));
} catch (IOException e) {
throw new IllegalArgumentException(e);
}
}
}
static void resetSpecialSourceClassloader() {
synchronized (classLoader) {
System.out.println("Releasing SpecialSource");
try {
classLoader.get().close();
classLoader.set(new SpecialSourceClassLoader(new URL[0], SpecialSourceLauncher.class.getClassLoader().getParent()));
mainClass.set("");
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
public static void runProcess(String... command) throws IOException {
if (!(command != null && command.length > 0)) throw new IllegalArgumentException();
if (command[0].equals("java")) {
command[0] = System.getProperty("java.home") + "/bin/" + command[0];
}
AtomicReference<Throwable> thrown = new AtomicReference<>(null);
final Thread thread = new Thread(() -> {
try {
final Class<?> mainClass = Class.forName(SpecialSourceLauncher.mainClass.get(), true, classLoader.get());
final Method mainMethod = mainClass.getMethod("main", String[].class);
if (!Modifier.isStatic(mainMethod.getModifiers()) || !Modifier.isPublic(mainMethod.getModifiers()))
throw new IllegalArgumentException();
mainMethod.invoke(null, new Object[]{command});
} catch (Throwable t) {
thrown.set(t);
}
});
thread.setName("SpecialSource Thread");
thread.setContextClassLoader(classLoader.get());
thread.start();
while (thread.isAlive())
try {
thread.join();
} catch (InterruptedException ignored) {
}
if (thrown.get() != null)
throw new RuntimeException(thrown.get());
}
private static class SpecialSourceClassLoader extends URLClassLoader {
private volatile boolean isLoaded = false;
public SpecialSourceClassLoader(URL[] urls, ClassLoader parent) {
super(urls, parent);
}
@Override
protected synchronized void addURL(URL url) {
if (isLoaded) throw new IllegalStateException();
this.isLoaded = true;
super.addURL(url);
}
}
}

View File

@ -0,0 +1,37 @@
package org.yatopia.yatoclip;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.net.URI;
import java.net.URL;
import java.net.URLClassLoader;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.jar.JarInputStream;
public class Yatoclip {
public static void main(String... args) throws IOException, ClassNotFoundException, NoSuchMethodException, InvocationTargetException, IllegalAccessException {
final Path setup = ServerSetup.setup();
final URLClassLoader classLoader = new URLClassLoader(new URL[]{new URL(null, setup.toUri().toString())}, Yatoclip.class.getClassLoader().getParent());
final Class<?> mainClassInstance = Class.forName("org.bukkit.craftbukkit.Main", true, classLoader);
final Method mainMethod = mainClassInstance.getMethod("main", String[].class);
if(!Modifier.isPublic(mainMethod.getModifiers()) || !Modifier.isStatic(mainMethod.getModifiers())) throw new IllegalArgumentException();
mainMethod.invoke(null, new Object[]{args});
}
static String getMainClass(Path jarPath) throws IOException {
final String mainClass;
try (
InputStream inputStream = Files.newInputStream(jarPath);
JarInputStream jar = new JarInputStream(inputStream)
) {
mainClass = jar.getManifest().getMainAttributes().getValue("Main-Class");
}
return mainClass;
}
}

View File

@ -0,0 +1,237 @@
package org.yatopia.yatoclip;
import com.google.gson.Gson;
import io.sigpipe.jbsdiff.InvalidHeaderException;
import io.sigpipe.jbsdiff.Patch;
import org.apache.commons.compress.compressors.CompressorException;
import org.apache.commons.io.IOUtils;
import java.io.ByteArrayOutputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.nio.file.Path;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Objects;
import java.util.Set;
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.stream.Collectors;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipException;
import java.util.zip.ZipFile;
import java.util.zip.ZipOutputStream;
import static java.util.Objects.requireNonNull;
import static org.yatopia.yatoclip.ServerSetup.toHex;
public class YatoclipPatcher {
private static final PatchesMetadata patchesMetadata;
static {
try (
final InputStream in = YatoclipPatcher.class.getClassLoader().getResourceAsStream("patches/metadata.json");
final InputStreamReader reader = new InputStreamReader(in);
) {
patchesMetadata = new Gson().fromJson(reader, PatchesMetadata.class);
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
@SuppressWarnings("BooleanMethodIsAlwaysInverted")
static boolean isJarUpToDate(Path patchedJar) {
requireNonNull(patchedJar);
if (!patchedJar.toFile().isFile()) return false;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
try (ZipFile patchedZip = new ZipFile(patchedJar.toFile())) {
for (PatchesMetadata.PatchMetadata patchMetadata : patchesMetadata.patches) {
ZipEntry zipEntry = patchedZip.getEntry(patchMetadata.name);
if (zipEntry == null || !patchMetadata.targetHash.equals(toHex(digest.digest(IOUtils.toByteArray(patchedZip.getInputStream(zipEntry))))))
return false;
}
}
return true;
} catch (Throwable t) {
System.out.println(t.toString());
return false;
}
}
static void patchJar(Path memberMappedJar, Path patchedJar) {
requireNonNull(memberMappedJar);
requireNonNull(patchedJar);
if(!memberMappedJar.toFile().isFile()) throw new IllegalArgumentException(new FileNotFoundException());
try {
patchedJar.toFile().getParentFile().mkdirs();
final ThreadLocal<ZipFile> classMappedZip = ThreadLocal.withInitial(() -> {
try {
return new ZipFile(memberMappedJar.toFile());
} catch (IOException e) {
throw new RuntimeException(e);
}
});
final ThreadLocal<MessageDigest> digest = ThreadLocal.withInitial(() -> {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(), new ThreadFactory() {
private AtomicInteger serial = new AtomicInteger(0);
@Override
public Thread newThread(Runnable r) {
Thread thread = new Thread(() -> {
try {
r.run();
} finally {
try {
classMappedZip.get().close();
} catch (IOException e) {
e.printStackTrace();
}
}
});
thread.setName("YatoClip Worker #" + serial.incrementAndGet());
thread.setDaemon(true);
return thread;
}
});
try {
final Set<PatchData> patchDataSet = patchesMetadata.patches.stream().map((PatchesMetadata.PatchMetadata metadata) -> new PatchData(CompletableFuture.supplyAsync(() -> {
try {
return getPatchedBytes(classMappedZip.get(), digest.get(), metadata);
} catch (IOException | CompressorException | InvalidHeaderException e) {
throw new RuntimeException(e);
}
}, executorService), metadata)).collect(Collectors.toSet());
try (ZipOutputStream patchedZip = new ZipOutputStream(new FileOutputStream(patchedJar.toFile()))) {
patchedZip.setMethod(ZipOutputStream.DEFLATED);
patchedZip.setLevel(Deflater.BEST_SPEED);
Set<String> processed = new HashSet<>();
for (PatchData patchData : patchDataSet) {
putNextEntrySafe(patchedZip, patchData.metadata.name);
final byte[] patchedBytes = patchData.patchedBytesFuture.join();
patchedZip.write(patchedBytes);
patchedZip.closeEntry();
processed.add(patchData.metadata.name);
}
((Iterator<ZipEntry>) classMappedZip.get().entries()).forEachRemaining(zipEntry -> {
if (zipEntry.isDirectory() || processed.contains(applyRelocations(zipEntry.getName())) || patchesMetadata.copyExcludes.contains(zipEntry.getName()))
return;
try {
InputStream in = classMappedZip.get().getInputStream(zipEntry);
putNextEntrySafe(patchedZip, zipEntry.getName());
patchedZip.write(IOUtils.toByteArray(in));
patchedZip.closeEntry();
} catch (Throwable t) {
throw new RuntimeException(t);
}
});
}
} catch (IOException e) {
throw new RuntimeException(e);
}
executorService.shutdown();
} catch (Throwable t) {
throw new RuntimeException(t);
}
}
private static byte[] getPatchedBytes(ZipFile classMappedZip, MessageDigest digest, PatchesMetadata.PatchMetadata patchMetadata) throws IOException, CompressorException, InvalidHeaderException {
final byte[] originalBytes;
final ZipEntry originalEntry = classMappedZip.getEntry(applyRelocationsReverse(patchMetadata.name));
if (originalEntry != null)
try (final InputStream in = classMappedZip.getInputStream(originalEntry)) {
originalBytes = IOUtils.toByteArray(in);
}
else originalBytes = new byte[0];
final byte[] patchBytes;
try (final InputStream in = YatoclipPatcher.class.getClassLoader().getResourceAsStream("patches/" + patchMetadata.name + ".patch")) {
if (in == null)
throw new FileNotFoundException();
patchBytes = IOUtils.toByteArray(in);
}
if (!patchMetadata.originalHash.equals(toHex(digest.digest(originalBytes))) || !patchMetadata.patchHash.equals(toHex(digest.digest(patchBytes))))
throw new FileNotFoundException("Hash do not match");
ByteArrayOutputStream byteOut = new ByteArrayOutputStream();
Patch.patch(originalBytes, patchBytes, byteOut);
final byte[] patchedBytes = byteOut.toByteArray();
if (!patchMetadata.targetHash.equals(toHex(digest.digest(patchedBytes))))
throw new FileNotFoundException("Hash do not match");
return patchedBytes;
}
private static void putNextEntrySafe(ZipOutputStream patchedZip, String name) throws IOException {
String[] split = name.split("/");
split = Arrays.copyOfRange(split, 0, split.length - 1);
StringBuilder sb = new StringBuilder();
for (String s : split) {
sb.append(s).append("/");
try {
patchedZip.putNextEntry(new ZipEntry(sb.toString()));
} catch (ZipException e) {
if (e.getMessage().startsWith("duplicate entry"))
continue;
throw e;
}
}
final ZipEntry entry = new ZipEntry(name);
patchedZip.putNextEntry(entry);
}
private static String applyRelocations(String name) {
if (!name.endsWith(".class")) return name;
if (name.indexOf('/') == -1)
name = "/" + name;
for (PatchesMetadata.Relocation relocation : patchesMetadata.relocations) {
if (name.startsWith(relocation.from) && (relocation.includeSubPackages || name.split("/").length == name.split("/").length - 1)) {
return relocation.to + name.substring(relocation.from.length());
}
}
return name;
}
private static String applyRelocationsReverse(String name) {
if (!name.endsWith(".class")) return name;
if (name.indexOf('/') == -1)
name = "/" + name;
for (PatchesMetadata.Relocation relocation : patchesMetadata.relocations) {
if (name.startsWith(relocation.to) && (relocation.includeSubPackages || name.split("/").length == name.split("/").length - 1)) {
return relocation.from + name.substring(relocation.to.length());
}
}
return name;
}
private static class PatchData {
public final CompletableFuture<byte[]> patchedBytesFuture;
public final PatchesMetadata.PatchMetadata metadata;
private PatchData(CompletableFuture<byte[]> patchedBytesFuture, PatchesMetadata.PatchMetadata metadata) {
Objects.requireNonNull(patchedBytesFuture);
Objects.requireNonNull(metadata);
this.patchedBytesFuture = patchedBytesFuture.thenApply(Objects::requireNonNull);
this.metadata = metadata;
}
}
}

View File

@ -11,6 +11,7 @@ repositories {
mavenCentral()
jcenter()
maven("https://plugins.gradle.org/m2/")
maven("https://jitpack.io/")
}
dependencies {
@ -18,6 +19,15 @@ dependencies {
implementation("com.github.jengelman.gradle.plugins:shadow:$shadowVersion")
implementation("com.github.spullara.mustache.java:compiler:$mustacheVersion")
implementation("javax.mail:mail:$javaxMailVersion")
implementation("com.github.ishlandbukkit:jbsdiff:deff66b794")
implementation("com.google.code.gson:gson:2.8.6")
implementation("com.google.guava:guava:30.0-jre")
implementation("commons-io:commons-io:2.8.0")
}
tasks.withType<JavaCompile> {
options.encoding = "UTF-8"
sourceCompatibility = "1.8"
}
gradlePlugin {

View File

@ -0,0 +1,269 @@
package org.yatopia.yatoclip.gradle;
import com.google.common.base.Preconditions;
import com.google.common.base.Throwables;
import com.google.common.collect.Sets;
import com.google.common.util.concurrent.ThreadFactoryBuilder;
import com.google.gson.Gson;
import io.sigpipe.jbsdiff.Diff;
import org.apache.commons.io.FileUtils;
import org.apache.commons.io.IOUtils;
import org.gradle.api.DefaultTask;
import org.gradle.api.internal.project.ProjectInternal;
import org.gradle.api.tasks.Copy;
import org.gradle.api.tasks.Input;
import org.gradle.api.tasks.InputFile;
import org.gradle.api.tasks.OutputDirectory;
import org.gradle.api.tasks.TaskAction;
import org.gradle.internal.logging.progress.ProgressLogger;
import org.gradle.internal.logging.progress.ProgressLoggerFactory;
import org.gradle.work.Incremental;
import org.gradle.workers.WorkerExecutor;
import javax.inject.Inject;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.Arrays;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.zip.ZipEntry;
import java.util.zip.ZipFile;
public class MakePatchesTask extends DefaultTask {
@OutputDirectory
private final File outputDir = ((Copy) getProject().getTasks().getByPath("processResources")).getDestinationDir().toPath().resolve("patches").toFile();
@InputFile
@Incremental
public File originalJar = null;
@InputFile
@Incremental
public File targetJar = null;
public Set<PatchesMetadata.Relocation> getRelocations() {
return relocations;
}
public void setRelocations(Set<PatchesMetadata.Relocation> relocations) {
this.relocations = relocations;
}
@Input
public Set<PatchesMetadata.Relocation> relocations;
public File getOriginalJar() {
return originalJar;
}
public void setOriginalJar(File originalJar) {
this.originalJar = originalJar;
}
public File getTargetJar() {
return targetJar;
}
public void setTargetJar(File targetJar) {
this.targetJar = targetJar;
}
public File getOutputDir() {
return outputDir;
}
private ProgressLoggerFactory getProgressLoggerFactory() {
return ((ProjectInternal) getProject()).getServices().get(ProgressLoggerFactory.class);
}
@Inject
public WorkerExecutor getWorkerExecutor() {
throw new UnsupportedOperationException();
}
@TaskAction
public void genPatches() throws IOException, InterruptedException {
Preconditions.checkNotNull(originalJar);
Preconditions.checkNotNull(targetJar);
getLogger().lifecycle("Generating patches for " + originalJar + " -> " + targetJar);
final ProgressLogger genPatches = getProgressLoggerFactory().newOperation(getClass()).setDescription("Generate patches");
genPatches.started();
genPatches.progress("Cleanup");
outputDir.mkdirs();
FileUtils.cleanDirectory(outputDir);
genPatches.progress("Reading files");
ThreadLocal<ZipFile> originalZip = ThreadLocal.withInitial(() -> {
try {
return new ZipFile(originalJar);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
ThreadLocal<ZipFile> targetZip = ThreadLocal.withInitial(() -> {
try {
return new ZipFile(targetJar);
} catch (IOException e) {
throw new RuntimeException(e);
}
});
Set<PatchesMetadata.PatchMetadata> patchMetadata = Sets.newConcurrentHashSet();
ThreadLocal<MessageDigest> digestThreadLocal = ThreadLocal.withInitial(() -> {
try {
return MessageDigest.getInstance("SHA-256");
} catch (NoSuchAlgorithmException e) {
throw new RuntimeException(e);
}
});
ThreadLocal<ProgressLogger> progressLoggerThreadLocal = ThreadLocal.withInitial(() -> {
final ProgressLogger progressLogger = getProgressLoggerFactory().newOperation(this.getClass());
progressLogger.setDescription("Patch worker");
progressLogger.started("Idle");
return progressLogger;
});
final ExecutorService executorService = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors(),
new ThreadFactoryBuilder().setNameFormat("MakePatches-%d").setThreadFactory(r -> new Thread(() -> {
boolean isExceptionOccurred = false;
try {
r.run();
} catch (Throwable t) {
isExceptionOccurred = true;
progressLoggerThreadLocal.get().completed(t.toString(), true);
throw t;
} finally {
digestThreadLocal.remove();
if (!isExceptionOccurred)
progressLoggerThreadLocal.get().completed();
progressLoggerThreadLocal.remove();
try {
originalZip.get().close();
targetZip.get().close();
} catch (IOException e) {
e.printStackTrace();
}
}
})).build());
AtomicInteger current = new AtomicInteger(0);
final int size = targetZip.get().size();
((Iterator<ZipEntry>) targetZip.get().entries()).forEachRemaining(zipEntryT -> {
genPatches.progress("Submitting tasks (" + current.incrementAndGet() + "/" + size + ")");
if (zipEntryT.isDirectory()) return;
executorService.execute(() -> {
ZipEntry zipEntry = targetZip.get().getEntry(zipEntryT.getName());
final String child = zipEntry.getName();
progressLoggerThreadLocal.get().progress("Reading " + zipEntry.getName());
File outputFile = new File(outputDir, child + ".patch");
outputFile.getParentFile().mkdirs();
final byte[] originalBytes;
final byte[] targetBytes;
final ZipEntry oEntry = originalZip.get().getEntry(applyRelocationsReverse(child));
try (
final InputStream oin = oEntry != null ? originalZip.get().getInputStream(oEntry) : null;
final InputStream tin = targetZip.get().getInputStream(zipEntry);
) {
originalBytes = oin != null ? IOUtils.toByteArray(oin) : new byte[0];
targetBytes = IOUtils.toByteArray(tin);
} catch (Throwable e) {
Throwables.throwIfUnchecked(e);
throw new RuntimeException(e);
}
if (Arrays.equals(originalBytes, targetBytes)) return;
progressLoggerThreadLocal.get().progress("GenPatch " + zipEntry.getName());
try (final OutputStream out = new FileOutputStream(outputFile)) {
ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();
Diff.diff(originalBytes, targetBytes, byteArrayOutputStream);
patchMetadata.add(new PatchesMetadata.PatchMetadata(child, toHex(digestThreadLocal.get().digest(originalBytes)), toHex(digestThreadLocal.get().digest(targetBytes)), toHex(digestThreadLocal.get().digest(byteArrayOutputStream.toByteArray()))));
out.write(byteArrayOutputStream.toByteArray());
} catch (Throwable t) {
Throwables.throwIfUnchecked(t);
throw new RuntimeException(t);
}
progressLoggerThreadLocal.get().progress("Idle");
});
});
genPatches.progress("Calculating exclusions");
Set<String> copyExcludes = new HashSet<>();
((Iterator<ZipEntry>) originalZip.get().entries()).forEachRemaining(zipEntry -> {
if(targetZip.get().getEntry(applyRelocations(zipEntry.getName())) == null)
copyExcludes.add(zipEntry.getName());
});
originalZip.get().close();
targetZip.get().close();
genPatches.progress("Waiting for patching to finish");
executorService.shutdown();
while (!executorService.awaitTermination(1, TimeUnit.SECONDS)) ;
digestThreadLocal.remove();
genPatches.progress("Writing patches metadata");
try (final OutputStream out = new FileOutputStream(new File(outputDir, "metadata.json"));
final Writer writer = new OutputStreamWriter(out)) {
new Gson().toJson(new PatchesMetadata(patchMetadata, relocations, copyExcludes), writer);
}
/*
genPatches.progress("Reading jar files into memory");
byte[] origin = Files.readAllBytes(originalJar.toPath());
byte[] target = Files.readAllBytes(targetJar.toPath());
genPatches.progress("Generating patch");
try(final OutputStream out = new BufferedOutputStream(new FileOutputStream(output))){
Diff.diff(origin, target, out);
}
*/
genPatches.completed();
}
private String applyRelocations(String name) {
if(!name.endsWith(".class")) return name;
if (name.indexOf('/') == -1)
name = "/" + name;
for (PatchesMetadata.Relocation relocation : relocations) {
if (name.startsWith(relocation.from) && (relocation.includeSubPackages || name.split("/").length == name.split("/").length - 1)) {
return relocation.to + name.substring(relocation.from.length());
}
}
return name;
}
private String applyRelocationsReverse(String name) {
if(!name.endsWith(".class")) return name;
if (name.indexOf('/') == -1)
name = "/" + name;
for (PatchesMetadata.Relocation relocation : relocations) {
if (name.startsWith(relocation.to) && (relocation.includeSubPackages || name.split("/").length == name.split("/").length - 1)) {
return relocation.from + name.substring(relocation.to.length());
}
}
return name;
}
public static String toHex(final byte[] hash) {
final StringBuilder sb = new StringBuilder(hash.length * 2);
for (byte aHash : hash) {
sb.append(String.format("%02X", aHash & 0xFF));
}
return sb.toString();
}
}

View File

@ -0,0 +1,51 @@
package org.yatopia.yatoclip.gradle;
import java.io.Serializable;
import java.util.Collections;
import java.util.Objects;
import java.util.Set;
public class PatchesMetadata {
public final Set<PatchMetadata> patches;
public final Set<Relocation> relocations;
public final Set<String> copyExcludes;
public PatchesMetadata(Set<PatchMetadata> patches, Set<Relocation> relocations, Set<String> copyExcludes) {
Objects.requireNonNull(copyExcludes);
this.copyExcludes = Collections.unmodifiableSet(copyExcludes);
Objects.requireNonNull(relocations);
this.relocations = Collections.unmodifiableSet(relocations);
Objects.requireNonNull(patches);
this.patches = Collections.unmodifiableSet(patches);
}
public static class PatchMetadata {
public final String name;
public final String originalHash;
public final String targetHash;
public final String patchHash;
public PatchMetadata(String name, String originalHash, String targetHash, String patchHash) {
this.name = name;
this.originalHash = originalHash;
this.targetHash = targetHash;
this.patchHash = patchHash;
}
}
public static class Relocation implements Serializable {
public final String from;
public final String to;
public final boolean includeSubPackages;
public Relocation(String from, String to, boolean includeSubPackages) {
Objects.requireNonNull(from);
Objects.requireNonNull(to);
this.from = from.replaceAll("\\.", "/");
this.to = to.replaceAll("\\.", "/");
this.includeSubPackages = includeSubPackages;
}
}
}

View File

@ -0,0 +1,22 @@
package org.yatopia.yatoclip.gradle;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.Properties;
public class PropertiesUtils {
public static void saveProperties(Properties prop, Path file, String comments){
System.out.println("Saving properties file to " + file);
file.toFile().getParentFile().mkdirs();
file.toFile().delete();
try(final OutputStream out = new FileOutputStream(file.toFile())) {
prop.store(out, comments);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}

View File

@ -5,19 +5,26 @@ import kotlinx.dom.elements
import kotlinx.dom.parseXml
import kotlinx.dom.search
import org.gradle.api.Project
import org.gradle.api.Task
import org.gradle.api.UnknownDomainObjectException
import org.gradle.api.plugins.JavaLibraryPlugin
import org.gradle.api.publish.PublishingExtension
import org.gradle.api.publish.maven.MavenPublication
import org.gradle.api.publish.maven.plugins.MavenPublishPlugin
import org.gradle.api.publish.maven.tasks.GenerateMavenPom
import org.gradle.api.tasks.Copy
import org.gradle.api.tasks.bundling.Jar
import org.gradle.api.tasks.compile.JavaCompile
import org.gradle.api.tasks.javadoc.Javadoc
import org.gradle.api.tasks.testing.Test
import org.gradle.kotlin.dsl.*
import org.yatopia.yatoclip.gradle.MakePatchesTask
import org.yatopia.yatoclip.gradle.PatchesMetadata
import org.yatopia.yatoclip.gradle.PropertiesUtils
import java.nio.charset.StandardCharsets.UTF_8
import java.text.SimpleDateFormat
import java.util.*
import kotlin.collections.HashSet
internal fun Project.configureSubprojects() {
subprojects {
@ -49,6 +56,73 @@ internal fun Project.configureSubprojects() {
project.name.endsWith("api") -> configureApiProject()
}
}
rootProject.project("Yatoclip") {
configureYatoclipProject()
}
}
private fun Project.configureYatoclipProject() {
try {
rootProject.toothpick.serverProject.project.extensions.getByName("relocations")
} catch (e: UnknownDomainObjectException) {
return
}
apply<JavaLibraryPlugin>()
apply<ShadowPlugin>()
tasks.register<MakePatchesTask>("genPatches") {
originalJar = rootProject.toothpick.paperDir.resolve("work").resolve("Minecraft")
.resolve(rootProject.toothpick.minecraftVersion).resolve("${rootProject.toothpick.minecraftVersion}-m.jar")
targetJar = rootProject.toothpick.serverProject.project.tasks.getByName("shadowJar").outputs.files.singleFile
setRelocations(rootProject.toothpick.serverProject.project.extensions.getByName("relocations") as HashSet<PatchesMetadata.Relocation>)
dependsOn(rootProject.toothpick.serverProject.project.tasks.getByName("shadowJar"))
doLast {
val prop = Properties()
prop.setProperty("minecraftVersion", rootProject.toothpick.minecraftVersion)
PropertiesUtils.saveProperties(
prop,
outputDir.toPath().parent.resolve("yatoclip-launch.properties"),
"Yatoclip launch values"
)
}
}
val shadowJar by tasks.getting(ShadowJar::class) {
manifest {
attributes(
"Main-Class" to "org.yatopia.yatoclip.Yatoclip"
)
}
}
tasks.register<Copy>("copyJar") {
val targetName = "yatopia-${rootProject.toothpick.minecraftVersion}-yatoclip.jar"
from(shadowJar.outputs.files.singleFile) {
rename { targetName }
}
into(rootProject.projectDir)
doLast {
logger.lifecycle(">>> $targetName saved to root project directory")
}
dependsOn(shadowJar)
}
tasks.getByName("processResources").dependsOn(tasks.getByName("genPatches"))
tasks.getByName("assemble").dependsOn(tasks.getByName("copyJar"))
tasks.getByName("jar").enabled = false
val buildTask = tasks.getByName("build")
val buildTaskDependencies = HashSet(buildTask.dependsOn)
buildTask.setDependsOn(HashSet<Task>())
buildTask.onlyIf { false }
tasks.register("yatoclip") {
buildTaskDependencies.forEach {
dependsOn(it)
}
}
}
private fun Project.configureServerProject() {
@ -85,11 +159,14 @@ private fun Project.configureServerProject() {
into("META-INF/maven/io.papermc.paper/paper")
}
val relocationSet = HashSet<PatchesMetadata.Relocation>()
// Don't like to do this but sadly have to do this for compatibility reasons
relocate("org.bukkit.craftbukkit", "org.bukkit.craftbukkit.v${toothpick.nmsPackage}") {
exclude("org.bukkit.craftbukkit.Main*")
}
relocate("net.minecraft.server", "net.minecraft.server.v${toothpick.nmsPackage}")
relocationSet.add(PatchesMetadata.Relocation("", "net.minecraft.server.v${toothpick.nmsPackage}", false))
// Make sure we relocate deps the same as Paper et al.
val pomFile = project.projectDir.resolve("pom.xml")
@ -111,9 +188,11 @@ private fun Project.configureServerProject() {
if (pattern != "org.bukkit.craftbukkit" && pattern != "net.minecraft.server") { // We handle these ourselves above
logger.debug("Imported relocation to server project shadowJar from ${pomFile.absolutePath}: $pattern to $shadedPattern")
relocate(pattern, shadedPattern)
relocationSet.add(PatchesMetadata.Relocation(pattern, shadedPattern, true))
}
}
}
project.extensions.add("relocations", relocationSet)
}
tasks.getByName("build") {
dependsOn(shadowJar)

View File

@ -13,6 +13,7 @@ setupSubproject("$forkNameLowercase-server") {
projectDir = File("$forkName-Server")
buildFileName = "../subprojects/server.gradle.kts"
}
setupSubproject("Yatoclip") { }
inline fun setupSubproject(name: String, block: ProjectDescriptor.() -> Unit) {
include(name)