mirror of https://github.com/YatopiaMC/Yatopia.git
Introducing: Yatoclip (#360)
This commit is contained in:
parent
2669a91c6a
commit
224376504d
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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 {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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)
|
||||
|
|
|
@ -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)
|
||||
|
|
Loading…
Reference in New Issue