diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 45f5cb13d..cd57929c4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/Jenkinsfile b/Jenkinsfile index ed261ac78..f39110cd3 100644 --- a/Jenkinsfile +++ b/Jenkinsfile @@ -1,91 +1,74 @@ -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 clean yatoclip publish + mkdir -p "./target" + 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() + } + } + } + } +} diff --git a/PATCHES.md b/PATCHES.md index 66dfb55f9..51d0f1aa9 100644 --- a/PATCHES.md +++ b/PATCHES.md @@ -8,6 +8,7 @@ This is an overview over all patches that are currently used. | Side | Patch | Author | CoAuthors | | ----- | ------------- |:-------------:| -----:| +| server | (PaperPR) Inline shift direction fields | Andrew Steinborn | | | server | AFK API | William Blake Galbreath | | | api | AFK API | William Blake Galbreath | | | server | Ability to re-add farmland mechanics from Alpha | Yive | | @@ -131,6 +132,8 @@ This is an overview over all patches that are currently used. | server | Delay chunk unloads | Spottedleaf | | | server | Despawn rate config options per projectile type | jmp | | | server | Detail more information in watchdog dumps | Spottedleaf | | +| server | Detailed lag and crash reports | ishland | | +| api | Detailed lag and crash reports | ishland | | | server | Disable loot drops on death by cramming | William Blake Galbreath | | | server | Disable outdated build check | William Blake Galbreath | | | api | Disable reload command | Ivan Pekov | | @@ -185,6 +188,7 @@ This is an overview over all patches that are currently used. | api | Full netherite armor grants fire resistance | BillyGalbreath | | | server | Giants AI settings | William Blake Galbreath | | | server | Global Eula file | tr7zw | | +| server | Handle proxy online mode properly | ishland | | | server | Heavily optimize furnance fuel and recipe lookups | tr7zw | Mykyta Komarn | | server | Heavily optimize recipe lookups in CraftingManager | Mykyta Komarn | Ivan Pekov, ishland | | server | Highly optimise single and multi-AABB VoxelShapes and | Spottedleaf | | @@ -240,6 +244,7 @@ This is an overview over all patches that are currently used. | server | Multi-Threaded Server Ticking Vanilla | Spottedleaf | | | server | Multi-Threaded ticking CraftBukkit | Spottedleaf | | | server | Name craft scheduler threads according to the plugin using | Spottedleaf | | +| server | New nbt cache | Hugo Planque | | | server | Nuke streams off BlockPosition | Ivan Pekov | | | server | Nuke streams off SectionPosition | Ivan Pekov | | | server | Optimise EntityInsentient#checkDespawn | Spottedleaf | | @@ -374,3 +379,4 @@ This is an overview over all patches that are currently used. | server | lithium VoronoiBiomeAccessTypeMixin | JellySquid | | | server | lithium enum_values | JellySquid | | | server | lithium reduce allocations | JellySquid | Mykyta Komarnytskyy | +| server | lithium: cache chunk gen sea level | SuperCoder7979 | | diff --git a/Yatoclip/build.gradle.kts b/Yatoclip/build.gradle.kts new file mode 100644 index 000000000..3d1d3e7a4 --- /dev/null +++ b/Yatoclip/build.gradle.kts @@ -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") +} + diff --git a/Yatoclip/src/main/java/org/yatopia/yatoclip/PatchesMetadata.java b/Yatoclip/src/main/java/org/yatopia/yatoclip/PatchesMetadata.java new file mode 100644 index 000000000..49a3dbc68 --- /dev/null +++ b/Yatoclip/src/main/java/org/yatopia/yatoclip/PatchesMetadata.java @@ -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 patches; + public final Set relocations; + public final Set copyExcludes; + + public PatchesMetadata(Set patches, Set relocations, Set 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; + } + } +} diff --git a/Yatoclip/src/main/java/org/yatopia/yatoclip/ServerSetup.java b/Yatoclip/src/main/java/org/yatopia/yatoclip/ServerSetup.java new file mode 100644 index 000000000..d0892f38d --- /dev/null +++ b/Yatoclip/src/main/java/org/yatopia/yatoclip/ServerSetup.java @@ -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)) { + SpecialSourceLauncher.resetSpecialSourceClassloader(); + final Path buildData = cacheDirectory.resolve("BuildData"); + SpecialSourceLauncher.setSpecialSourceJar(buildData.resolve("bin").resolve("SpecialSource-2.jar").toFile()); + System.err.println("Applying class mapping..."); + SpecialSourceLauncher.runProcess( + "map", "--only", ".", "--only", "net/minecraft", "--auto-lvt", "BASIC", "--auto-member", "SYNTHETIC", + "-i", vanillaJar.toAbsolutePath().toString(), + "-m", buildData.resolve("mappings").resolve(buildDataInfo.classMappings).toAbsolutePath().toString(), + "-o", classMappedJar.toAbsolutePath().toString() + ); + System.err.println("Applying member mapping..."); + SpecialSourceLauncher.runProcess( + "map", "--only", ".", "--only", "net/minecraft", "--auto-member", "LOGGER", "--auto-member", "TOKENS", + "-i", classMappedJar.toAbsolutePath().toString(), + "-m", buildData.resolve("mappings").resolve(buildDataInfo.memberMappings).toAbsolutePath().toString(), + "-o", memberMappedJar.toAbsolutePath().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) 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 javaVersions; + + public Refs getRefs() { + return refs; + } + + public String getName() { + return name; + } + + public String getDescription() { + return description; + } + + public int getToolsVersion() { + return toolsVersion; + } + + public List 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; + } + } +} diff --git a/Yatoclip/src/main/java/org/yatopia/yatoclip/SpecialSourceLauncher.java b/Yatoclip/src/main/java/org/yatopia/yatoclip/SpecialSourceLauncher.java new file mode 100644 index 000000000..580184b8a --- /dev/null +++ b/Yatoclip/src/main/java/org/yatopia/yatoclip/SpecialSourceLauncher.java @@ -0,0 +1,90 @@ +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.Arrays; +import java.util.concurrent.atomic.AtomicReference; + +public class SpecialSourceLauncher { + + private static final AtomicReference classLoader = new AtomicReference<>(new SpecialSourceClassLoader(new URL[0], SpecialSourceLauncher.class.getClassLoader().getParent())); + private static final AtomicReference mainClass = new AtomicReference<>(""); + + static void setSpecialSourceJar(File specialSourceJar) { + synchronized (classLoader) { + System.err.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) { + if(!classLoader.get().isLoaded) return; + System.err.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(); + + System.err.println("Invoking SpecialSource with arguments: " + Arrays.toString(command)); + + AtomicReference 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); + } + } + +} diff --git a/Yatoclip/src/main/java/org/yatopia/yatoclip/Yatoclip.java b/Yatoclip/src/main/java/org/yatopia/yatoclip/Yatoclip.java new file mode 100644 index 000000000..6805083c8 --- /dev/null +++ b/Yatoclip/src/main/java/org/yatopia/yatoclip/Yatoclip.java @@ -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; + } + +} diff --git a/Yatoclip/src/main/java/org/yatopia/yatoclip/YatoclipPatcher.java b/Yatoclip/src/main/java/org/yatopia/yatoclip/YatoclipPatcher.java new file mode 100644 index 000000000..11d709078 --- /dev/null +++ b/Yatoclip/src/main/java/org/yatopia/yatoclip/YatoclipPatcher.java @@ -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 classMappedZip = ThreadLocal.withInitial(() -> { + try { + return new ZipFile(memberMappedJar.toFile()); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + final ThreadLocal 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 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 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) 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 patchedBytesFuture; + public final PatchesMetadata.PatchMetadata metadata; + + + private PatchData(CompletableFuture patchedBytesFuture, PatchesMetadata.PatchMetadata metadata) { + Objects.requireNonNull(patchedBytesFuture); + Objects.requireNonNull(metadata); + this.patchedBytesFuture = patchedBytesFuture.thenApply(Objects::requireNonNull); + this.metadata = metadata; + } + } + +} diff --git a/build.gradle.kts b/build.gradle.kts index 63ca84681..33a0a65b7 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -32,6 +32,17 @@ toothpick { project = project(":$forkNameLowercase-api") patchesDir = rootProject.projectDir.resolve("patches/api") } + + if(!System.getenv("BRANCH_NAME").isNullOrEmpty()) { + currentBranchDisplayName = System.getenv("BRANCH_NAME").replace("/${minecraftVersion}", ""); + } else if (!System.getenv("GITHUB_REF").isNullOrEmpty()) { + currentBranchDisplayName = System.getenv("GITHUB_REF").substring("refs/heads/".length).replace("/${minecraftVersion}", "") + } else { + currentBranchDisplayName = gitCmd("rev-parse", "--abbrev-ref", "HEAD").output.toString().trim().replace("/${minecraftVersion}", "") + if(currentBranchDisplayName == "HEAD") logger.warn("You are currently in \'detached HEAD\' state, branch information isn\'t available") + } + + logger.lifecycle("Configured version string: $calcVersionString") } subprojects { diff --git a/buildSrc/build.gradle.kts b/buildSrc/build.gradle.kts index 891742b3a..1991567b9 100644 --- a/buildSrc/build.gradle.kts +++ b/buildSrc/build.gradle.kts @@ -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 { + options.encoding = "UTF-8" + sourceCompatibility = "1.8" } gradlePlugin { diff --git a/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/MakePatchesTask.java b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/MakePatchesTask.java new file mode 100644 index 000000000..931162dbc --- /dev/null +++ b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/MakePatchesTask.java @@ -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 getRelocations() { + return relocations; + } + + public void setRelocations(Set relocations) { + this.relocations = relocations; + } + + @Input + public Set 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 originalZip = ThreadLocal.withInitial(() -> { + try { + return new ZipFile(originalJar); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + ThreadLocal targetZip = ThreadLocal.withInitial(() -> { + try { + return new ZipFile(targetJar); + } catch (IOException e) { + throw new RuntimeException(e); + } + }); + + Set patchMetadata = Sets.newConcurrentHashSet(); + ThreadLocal digestThreadLocal = ThreadLocal.withInitial(() -> { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + }); + ThreadLocal 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) 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 copyExcludes = new HashSet<>(); + ((Iterator) 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(); + } + +} diff --git a/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PatchesMetadata.java b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PatchesMetadata.java new file mode 100644 index 000000000..a979c1c90 --- /dev/null +++ b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PatchesMetadata.java @@ -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 patches; + public final Set relocations; + public final Set copyExcludes; + + public PatchesMetadata(Set patches, Set relocations, Set 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; + } + } +} diff --git a/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PropertiesUtils.java b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PropertiesUtils.java new file mode 100644 index 000000000..55b68f66a --- /dev/null +++ b/buildSrc/src/main/java/org/yatopia/yatoclip/gradle/PropertiesUtils.java @@ -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); + } + } + +} diff --git a/buildSrc/src/main/kotlin/ConfigureSubprojects.kt b/buildSrc/src/main/kotlin/ConfigureSubprojects.kt index e5ef2a1cd..9d6614fe7 100644 --- a/buildSrc/src/main/kotlin/ConfigureSubprojects.kt +++ b/buildSrc/src/main/kotlin/ConfigureSubprojects.kt @@ -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() + apply() + + tasks.register("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) + 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("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()) + 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() + // 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) @@ -121,9 +200,8 @@ private fun Project.configureServerProject() { extensions.configure { publications { - getByName("mavenJava") { - artifactId = rootProject.name - artifact(tasks["shadowJar"]) + create("shadow") { + artifact(project.tasks.named("shadowJar")) } } } diff --git a/buildSrc/src/main/kotlin/DependencyLoading.kt b/buildSrc/src/main/kotlin/DependencyLoading.kt index ff3b9f15d..0e256ab7b 100644 --- a/buildSrc/src/main/kotlin/DependencyLoading.kt +++ b/buildSrc/src/main/kotlin/DependencyLoading.kt @@ -32,7 +32,7 @@ fun DependencyHandlerScope.loadDependencies(project: Project) { val groupId = dependencyElem.search("groupId").first().textContent val artifactId = dependencyElem.search("artifactId").first().textContent val version = dependencyElem.search("version").first().textContent.applyReplacements( - "project.version" to project.version.toString(), + "project.version" to "${project.rootProject.toothpick.minecraftVersion}-${project.rootProject.toothpick.nmsRevision}", "minecraft.version" to project.toothpick.minecraftVersion ) val scope = dependencyElem.search("scope").firstOrNull()?.textContent diff --git a/buildSrc/src/main/kotlin/ToothpickExtension.kt b/buildSrc/src/main/kotlin/ToothpickExtension.kt index fd314511f..755ba6f91 100644 --- a/buildSrc/src/main/kotlin/ToothpickExtension.kt +++ b/buildSrc/src/main/kotlin/ToothpickExtension.kt @@ -33,6 +33,10 @@ open class ToothpickExtension(objects: ObjectFactory) { lateinit var patchCreditsOutput: String lateinit var patchCreditsTemplate: String + lateinit var currentBranchDisplayName : String + val calcVersionString + get() = if(!currentBranchDisplayName.startsWith("ver/")) { "${minecraftVersion}-${nmsRevision}-${currentBranchDisplayName.replace('/', '_')}" } else "${minecraftVersion}-${nmsRevision}" + fun server(receiver: ToothpickSubproject.() -> Unit) { serverProject = ToothpickSubproject() receiver(serverProject) diff --git a/buildSrc/src/main/kotlin/ToothpickExtensions.kt b/buildSrc/src/main/kotlin/ToothpickExtensions.kt index a4a02800e..1c524c0c5 100644 --- a/buildSrc/src/main/kotlin/ToothpickExtensions.kt +++ b/buildSrc/src/main/kotlin/ToothpickExtensions.kt @@ -10,7 +10,7 @@ fun Project.toothpick(receiver: ToothpickExtension.() -> Unit) { receiver(toothpick) allprojects { group = toothpick.groupId - version = "${toothpick.minecraftVersion}-${toothpick.nmsRevision}" + version = toothpick.calcVersionString } configureSubprojects() initToothpickTasks() diff --git a/buildSrc/src/main/kotlin/task/RebuildPatches.kt b/buildSrc/src/main/kotlin/task/RebuildPatches.kt index b44a71596..afaa2d982 100644 --- a/buildSrc/src/main/kotlin/task/RebuildPatches.kt +++ b/buildSrc/src/main/kotlin/task/RebuildPatches.kt @@ -70,7 +70,12 @@ private fun Project.updatePatches( "--no-stat", "--zero-commit", "--full-index", "--no-signature", "-N", "-o", patchPath.absolutePath, previousUpstreamName, dir = projectDir, - printOut = true + printOut = false ) ) + gitCmd( + "add", patchPath.canonicalPath, + dir = patchPath, + printOut = true + ) } diff --git a/patches/api/0008-Detailed-lag-and-crash-reports.patch b/patches/api/0008-Detailed-lag-and-crash-reports.patch new file mode 100644 index 000000000..78c06d976 --- /dev/null +++ b/patches/api/0008-Detailed-lag-and-crash-reports.patch @@ -0,0 +1,66 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: ishland +Date: Wed, 27 Jan 2021 23:35:30 +0800 +Subject: [PATCH] Detailed lag and crash reports + +Added "Suspected Plugins" to Watchdog and crash reports + +diff --git a/src/main/java/org/bukkit/plugin/SimplePluginManager.java b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +index 26685f59b235ea5b4c4fb7ae21acb5149edaa2b3..a90945e434aa3729e2ec5355a4340c995316a2f7 100644 +--- a/src/main/java/org/bukkit/plugin/SimplePluginManager.java ++++ b/src/main/java/org/bukkit/plugin/SimplePluginManager.java +@@ -905,4 +905,9 @@ public final class SimplePluginManager implements PluginManager { + } + // Paper end + ++ // Yatopia start - Accessor ++ public Collection getPluginLoaders() { ++ return new HashSet<>(fileAssociations.values()); ++ } ++ // Yatopia end + } +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +index 04fa3991f6ce4e9dad804f28fc6c947695857089..cb11eab6e13ed1c395b8f7db033c9a2817f4089c 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPlugin.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPlugin.java +@@ -111,7 +111,7 @@ public abstract class JavaPlugin extends PluginBase { + * @return File containing this plugin + */ + @NotNull +- protected File getFile() { ++ public File getFile() { // Yatopia + return file; + } + +diff --git a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +index 384edf9890dfbd1cddfdcac4db1ebe9a4d761f78..6d39959389e2eebb4acf19dfb1efc5cf692db135 100644 +--- a/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/JavaPluginLoader.java +@@ -439,4 +439,9 @@ public final class JavaPluginLoader implements PluginLoader { + } + } + } ++ // Yatopia start - Accessor ++ public List getClassLoaders() { ++ return java.util.Collections.unmodifiableList(loaders); ++ } ++ // Yatopia end + } +diff --git a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +index 7760be3e34fa20825faf145d9fb5b2855c1a4602..2e8f3efdb683c44b3e42bb0187bc907e64cde288 100644 +--- a/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java ++++ b/src/main/java/org/bukkit/plugin/java/PluginClassLoader.java +@@ -232,4 +232,13 @@ public final class PluginClassLoader extends URLClassLoader { // Spigot + '}'; + } + // Paper end ++ ++ // Yatopia start - Accessor ++ public java.util.Collection> getLoadedClasses() { ++ return java.util.Collections.unmodifiableCollection( ++ new java.util.HashSet<>(classes.values()).stream() ++ .filter(clazz -> clazz.getClassLoader() == this).collect(java.util.stream.Collectors.toSet()) ++ ); ++ } ++ // Yatopia end + } diff --git a/patches/removed/server/0060-New-Network-System.patch b/patches/removed/server/0060-New-Network-System.patch index cf9b78aa9..12463fc71 100644 --- a/patches/removed/server/0060-New-Network-System.patch +++ b/patches/removed/server/0060-New-Network-System.patch @@ -6,15 +6,11 @@ Subject: [PATCH] New Network System Co-authored-by: Ivan Pekov diff --git a/pom.xml b/pom.xml -index 8af1a91102c5cc4c230f622e6629e46e95f17d44..9b7ed0de1054285dadff6aefc95c7207079504a6 100644 +index 6109699411c349c4965de6dbdbd9f8454bc18b10..0bbc7badb1f597357d0fce7cd0f5faf5eb1d0eb5 100644 --- a/pom.xml +++ b/pom.xml -@@ -53,9 +53,17 @@ - - io.netty - netty-all -- 4.1.50.Final -+ 4.1.58.Final +@@ -56,6 +56,14 @@ + 4.1.58.Final + diff --git a/patches/server/0001-Modify-POM.patch b/patches/server/0001-Modify-POM.patch index b52b22263..5fc7cba4e 100644 --- a/patches/server/0001-Modify-POM.patch +++ b/patches/server/0001-Modify-POM.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Modify POM diff --git a/pom.xml b/pom.xml -index 752d62eb3b87ab24260ec2c029bae0d2b0e3b908..4f56aa4ae78b9d3756983cde52bc1d1adda0c9d4 100644 +index 752d62eb3b87ab24260ec2c029bae0d2b0e3b908..462aa2d9d3c4aad5d92a7b02ea9ea636ae061cf6 100644 --- a/pom.xml +++ b/pom.xml @@ -1,11 +1,11 @@ @@ -54,6 +54,73 @@ index 752d62eb3b87ab24260ec2c029bae0d2b0e3b908..4f56aa4ae78b9d3756983cde52bc1d1a ${project.version} compile +@@ -44,7 +53,7 @@ + + io.netty + netty-all +- 4.1.50.Final ++ 4.1.58.Final + + + +@@ -61,7 +70,7 @@ + + org.jline + jline-terminal-jansi +- 3.12.1 ++ 3.19.0 + runtime + + +@@ -131,7 +140,7 @@ + + mysql + mysql-connector-java +- 5.1.49 ++ 8.0.23 + runtime + + +@@ -144,14 +153,14 @@ + + org.hamcrest + hamcrest-library +- 1.3 ++ 2.2 + test + + + + io.github.classgraph + classgraph +- 4.8.47 ++ 4.8.98 + test + + @@ -161,6 +170,12 @@ 1.1.0-SNAPSHOT compile diff --git a/patches/server/0003-Utilities.patch b/patches/server/0003-Utilities.patch index f8618a3cf..892385298 100644 --- a/patches/server/0003-Utilities.patch +++ b/patches/server/0003-Utilities.patch @@ -9,7 +9,7 @@ Co-authored-by: Mykyta Komarnytskyy Co-authored-by: Ivan Pekov diff --git a/pom.xml b/pom.xml -index 4f56aa4ae78b9d3756983cde52bc1d1adda0c9d4..d8ec87143370144c04502cd7bddf57f2f5e25168 100644 +index 462aa2d9d3c4aad5d92a7b02ea9ea636ae061cf6..b259d0e9c4157049fc31a2d42bfc07d73f8f73fb 100644 --- a/pom.xml +++ b/pom.xml @@ -176,6 +176,12 @@ diff --git a/patches/server/0009-Add-NBT-API-as-a-first-class-lib.patch b/patches/server/0009-Add-NBT-API-as-a-first-class-lib.patch index 3251eb95b..64c62a107 100644 --- a/patches/server/0009-Add-NBT-API-as-a-first-class-lib.patch +++ b/patches/server/0009-Add-NBT-API-as-a-first-class-lib.patch @@ -5,7 +5,7 @@ Subject: [PATCH] Add NBT API as a first-class lib diff --git a/pom.xml b/pom.xml -index d8ec87143370144c04502cd7bddf57f2f5e25168..8af1a91102c5cc4c230f622e6629e46e95f17d44 100644 +index b259d0e9c4157049fc31a2d42bfc07d73f8f73fb..6109699411c349c4965de6dbdbd9f8454bc18b10 100644 --- a/pom.xml +++ b/pom.xml @@ -358,6 +358,10 @@ diff --git a/patches/server/0062-lithium-cache-chunk-gen-sea-level.patch b/patches/server/0062-lithium-cache-chunk-gen-sea-level.patch new file mode 100644 index 000000000..54871796a --- /dev/null +++ b/patches/server/0062-lithium-cache-chunk-gen-sea-level.patch @@ -0,0 +1,52 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: SuperCoder7979 <25208576+SuperCoder7979@users.noreply.github.com> +Date: Fri, 22 Jan 2021 16:38:19 -0500 +Subject: [PATCH] lithium: cache chunk gen sea level + +Chunk generator settings are passed to the noise chunk generator through a supplier, which retrieves a given chunk generator settings registry key from the registry. The problem arises as this setting is retrieved from the supplier to get the sea level, which is called every single time a block is placed by the generator, for a total of 65536 registry lookups per chunk. As you can imagine this isn't all that fast and removing it speeds up the chunk generation time by a moderate amount (13% of CPU time spent in populateNoise without the patch vs 10% with the patch). My solution to this was to stick the supplier into a Lazy<> so it's computed once and then cached, but I'm open to suggestions. (The sea level can't be computed ahead of time due to a quirk in the new registry system, which would have been the best option here.) + +This code was originally made by SuperCoder7979 in a Pull Request to lithium (https://github.com/jellysquid3/lithium-fabric/pull/179) + +This code was orignally licensed under the LGPL-3.0 license. + +diff --git a/src/main/java/net/minecraft/server/ChunkGeneratorAbstract.java b/src/main/java/net/minecraft/server/ChunkGeneratorAbstract.java +index fa60285c0c48147ad09b9197bfe577f504dd34bd..de469f9b4f0fecc05dca7a5aacd1308db6f80a18 100644 +--- a/src/main/java/net/minecraft/server/ChunkGeneratorAbstract.java ++++ b/src/main/java/net/minecraft/server/ChunkGeneratorAbstract.java +@@ -17,6 +17,7 @@ import javax.annotation.Nullable; + + public final class ChunkGeneratorAbstract extends ChunkGenerator { + ++ private int cachedSeaLevel; // Yatopia - lithium cache chunk gen settings + public static final Codec d = RecordCodecBuilder.create((instance) -> { + return instance.group(WorldChunkManager.a.fieldOf("biome_source").forGetter((chunkgeneratorabstract) -> { + return chunkgeneratorabstract.b; +@@ -101,6 +102,7 @@ public final class ChunkGeneratorAbstract extends ChunkGenerator { + } else { + this.v = null; + } ++ this.cachedSeaLevel = ((GeneratorSettingBase) this.h.get()).g(); // Yatopia - lithium cache chunk gen settings + + } + +@@ -688,10 +690,18 @@ public final class ChunkGeneratorAbstract extends ChunkGenerator { + return this.x; + } + ++ // Yatopia start - lithium cache chunk gen settings ++ /** ++ * Use cached sea level instead of retrieving from the registry every time. ++ * This method is called for every block in the chunk so this will save a lot of registry lookups. ++ * ++ * @author SuperCoder79 ++ */ + @Override +- public int getSeaLevel() { +- return ((GeneratorSettingBase) this.h.get()).g(); ++ public int getSeaLevel() { // Cached method of getting sealevel ++ return this.cachedSeaLevel; + } ++ // Yatopia end + + @Override + public List getMobsFor(BiomeBase biomebase, StructureManager structuremanager, EnumCreatureType enumcreaturetype, BlockPosition blockposition) { diff --git a/patches/server/0063-PaperPR-Inline-shift-direction-fields.patch b/patches/server/0063-PaperPR-Inline-shift-direction-fields.patch new file mode 100644 index 000000000..357752c2f --- /dev/null +++ b/patches/server/0063-PaperPR-Inline-shift-direction-fields.patch @@ -0,0 +1,55 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Andrew Steinborn +Date: Mon, 18 Jan 2021 20:45:25 -0500 +Subject: [PATCH] (PaperPR) Inline shift direction fields + +Removes a layer of indirection for EnumDirection.getAdjacent(X|Y|Z)(), which is in the +critical section for much of the server, including the lighting engine. + +diff --git a/src/main/java/net/minecraft/server/EnumDirection.java b/src/main/java/net/minecraft/server/EnumDirection.java +index 2f1e7f0d753aace655210e32ddec59a3c46aade9..74e95bfcae5212c77383f7b8780dce293fda02dc 100644 +--- a/src/main/java/net/minecraft/server/EnumDirection.java ++++ b/src/main/java/net/minecraft/server/EnumDirection.java +@@ -49,6 +49,11 @@ public enum EnumDirection implements INamable { + }, (enumdirection, enumdirection1) -> { + throw new IllegalArgumentException("Duplicate keys"); + }, Long2ObjectOpenHashMap::new)); ++ // Paper start ++ private final int adjX; ++ private final int adjY; ++ private final int adjZ; ++ // Paper end + + private EnumDirection(int i, int j, int k, String s, EnumDirection.EnumAxisDirection enumdirection_enumaxisdirection, EnumDirection.EnumAxis enumdirection_enumaxis, BaseBlockPosition baseblockposition) { + this.g = i; +@@ -58,6 +63,11 @@ public enum EnumDirection implements INamable { + this.k = enumdirection_enumaxis; + this.l = enumdirection_enumaxisdirection; + this.m = baseblockposition; ++ // Paper start ++ this.adjX = baseblockposition.getX(); ++ this.adjY = baseblockposition.getY(); ++ this.adjZ = baseblockposition.getZ(); ++ // Paper end + } + + public static EnumDirection[] a(Entity entity) { +@@ -151,15 +161,15 @@ public enum EnumDirection implements INamable { + } + + public int getAdjacentX() { +- return this.m.getX(); ++ return this.adjX; // Paper + } + + public int getAdjacentY() { +- return this.m.getY(); ++ return this.adjY; // Paper + } + + public int getAdjacentZ() { +- return this.m.getZ(); ++ return this.adjZ; // Paper + } + + public String m() { diff --git a/patches/server/0064-New-nbt-cache.patch b/patches/server/0064-New-nbt-cache.patch new file mode 100644 index 000000000..85b828928 --- /dev/null +++ b/patches/server/0064-New-nbt-cache.patch @@ -0,0 +1,129 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: Hugo Planque +Date: Thu, 21 Jan 2021 17:56:03 +0100 +Subject: [PATCH] New nbt cache + +The goal of this patch is to reduce I/O operations from the main thread while saving player data and also to avoid too many I/O operations while reading NBT Player file by using a cache (Which start to delete the oldest data when there is too much player compared to the map size) + +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 2a06f663ce64529842cfb0f4cf16c28ee143f110..084f7435ce50a10cd22e967f695064fb00c9b165 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -824,7 +824,9 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant dataCache = new org.yatopiamc.yatopia.server.cache.NBTCache<>(); // Yatopia - NBT Cache system ++ public final java.util.concurrent.ExecutorService executorService = java.util.concurrent.Executors.newSingleThreadExecutor(); // Yatopia - NBT Cache system + + public WorldNBTStorage(Convertable.ConversionSession convertable_conversionsession, DataFixer datafixer) { + this.a = datafixer; +@@ -30,11 +32,22 @@ public class WorldNBTStorage { + NBTTagCompound nbttagcompound = entityhuman.save(new NBTTagCompound()); + File file = File.createTempFile(entityhuman.getUniqueIDString() + "-", ".dat", this.playerDir); + +- NBTCompressedStreamTools.a(nbttagcompound, file); ++ // NBTCompressedStreamTools.a(nbttagcompound, file); // Yatopia ++ // Yatopia start - NBT Cache system ++ Runnable task = () -> { try { NBTCompressedStreamTools.a(nbttagcompound, file); + File file1 = new File(this.playerDir, entityhuman.getUniqueIDString() + ".dat"); + File file2 = new File(this.playerDir, entityhuman.getUniqueIDString() + ".dat_old"); + + SystemUtils.a(file1, file, file2); ++ } catch (Exception exception) { ++ WorldNBTStorage.LOGGER.error("Failed to save player data for {}", entityhuman.getName(), exception); // Paper ++ } ++ }; ++ synchronized (this.dataCache){ ++ this.dataCache.put(file, nbttagcompound); ++ } ++ this.executorService.execute(task); ++ // Yatopia end + } catch (Exception exception) { + WorldNBTStorage.LOGGER.error("Failed to save player data for {}", entityhuman.getName(), exception); // Paper + } +@@ -50,9 +63,18 @@ public class WorldNBTStorage { + // Spigot Start + boolean usingWrongFile = false; + boolean normalFile = file.exists() && file.isFile(); // Akarin - ensures normal file +- if ( org.bukkit.Bukkit.getOnlineMode() && !normalFile ) // Paper - Check online mode first // Akarin - ensures normal file ++ // if ( org.bukkit.Bukkit.getOnlineMode() && !normalFile ) // Paper - Check online mode first // Akarin - ensures normal file // Yatopia ++ // Yatopia start - NBT Cache system ++ NBTTagCompound playerData; ++ synchronized (this.dataCache){ ++ playerData = this.dataCache.get(file); ++ } ++ if (playerData == null && org.bukkit.Bukkit.getOnlineMode() && !normalFile ) // Paper - Check online mode first // Akarin - ensures normal file + { + file = new File( this.playerDir, java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + entityhuman.getName() ).getBytes( "UTF-8" ) ).toString() + ".dat"); ++ synchronized (this.dataCache){ ++ playerData = this.dataCache.get(file); ++ } + if ( file.exists() ) + { + usingWrongFile = true; +@@ -60,10 +82,13 @@ public class WorldNBTStorage { + } + } + // Spigot End +- +- if (normalFile) { // Akarin - avoid double I/O operation ++ // if (normalFile) { // Akarin - avoid double I/O operation // Yatopia ++ if (playerData != null) { ++ nbttagcompound = playerData; ++ } else if (normalFile) { // Akarin - avoid double I/O operation + nbttagcompound = NBTCompressedStreamTools.a(file); + } ++ // Yatopia end + // Spigot Start + if ( usingWrongFile ) + { +diff --git a/src/main/java/org/yatopiamc/yatopia/server/cache/NBTCache.java b/src/main/java/org/yatopiamc/yatopia/server/cache/NBTCache.java +new file mode 100644 +index 0000000000000000000000000000000000000000..adc5975ded5f655a7b5da59daa1ff3b63697f841 +--- /dev/null ++++ b/src/main/java/org/yatopiamc/yatopia/server/cache/NBTCache.java +@@ -0,0 +1,29 @@ ++package org.yatopiamc.yatopia.server.cache; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectLinkedOpenCustomHashMap; ++import net.minecraft.server.MinecraftServer; ++ ++public class NBTCache extends Object2ObjectLinkedOpenCustomHashMap { ++ ++ public NBTCache() { ++ super(100, 0.75F, new Strategy() { ++ @Override ++ public int hashCode(K k) { ++ return k.hashCode(); ++ } ++ ++ @Override ++ public boolean equals(K k, K k1) { ++ return k.equals(k1); ++ } ++ }); ++ } ++ ++ @Override ++ public V put(K k, V v) { ++ if (this.size() > MinecraftServer.getServer().getPlayerCount()) { ++ this.removeLast(); ++ } ++ return super.putAndMoveToFirst(k, v); ++ } ++} diff --git a/patches/server/0065-Detailed-lag-and-crash-reports.patch b/patches/server/0065-Detailed-lag-and-crash-reports.patch new file mode 100644 index 000000000..13fb5bb61 --- /dev/null +++ b/patches/server/0065-Detailed-lag-and-crash-reports.patch @@ -0,0 +1,126 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: ishland +Date: Wed, 27 Jan 2021 23:35:30 +0800 +Subject: [PATCH] Detailed lag and crash reports + +Added "Suspected Plugins" to Watchdog and crash reports + +diff --git a/src/main/java/net/minecraft/server/CrashReport.java b/src/main/java/net/minecraft/server/CrashReport.java +index cc6e6f245ee5e73bd570cf42381bf55ee0b364d3..e52eb754debd3abe02da978825e53839e87302fc 100644 +--- a/src/main/java/net/minecraft/server/CrashReport.java ++++ b/src/main/java/net/minecraft/server/CrashReport.java +@@ -87,6 +87,8 @@ public class CrashReport { + if (this.h != null && this.h.length > 0) { + stringbuilder.append("-- Head --\n"); + stringbuilder.append("Thread: ").append(Thread.currentThread().getName()).append("\n"); ++ org.yatopiamc.yatopia.server.util.StackTraceUtils.printPlugins(this.h, stringbuilder::append); // Yatopia ++ stringbuilder.append("\n"); // Yatopia + stringbuilder.append("Stacktrace:\n"); + StackTraceElement[] astacktraceelement = this.h; + int i = astacktraceelement.length; +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 084f7435ce50a10cd22e967f695064fb00c9b165..1105ee7274df3f482b816c5f6183ae8594da55be 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1040,6 +1040,8 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant log.log(Level.SEVERE, msg)); ++ log.log(Level.SEVERE, ""); ++ // Yatopia end + log.log( Level.SEVERE, "\tStack:" ); + // + for ( StackTraceElement stack : thread.getStackTrace() ) +diff --git a/src/main/java/org/yatopiamc/yatopia/server/util/StackTraceUtils.java b/src/main/java/org/yatopiamc/yatopia/server/util/StackTraceUtils.java +new file mode 100644 +index 0000000000000000000000000000000000000000..627debed74cd8400371de887c2667ff839288de3 +--- /dev/null ++++ b/src/main/java/org/yatopiamc/yatopia/server/util/StackTraceUtils.java +@@ -0,0 +1,72 @@ ++package org.yatopiamc.yatopia.server.util; ++ ++import it.unimi.dsi.fastutil.objects.Object2ObjectOpenHashMap; ++import it.unimi.dsi.fastutil.objects.ObjectOpenHashSet; ++import org.bukkit.Bukkit; ++import org.bukkit.plugin.Plugin; ++import org.bukkit.plugin.PluginLoader; ++import org.bukkit.plugin.SimplePluginManager; ++import org.bukkit.plugin.java.JavaPlugin; ++import org.bukkit.plugin.java.JavaPluginLoader; ++import org.bukkit.plugin.java.PluginClassLoader; ++ ++import java.util.Collection; ++import java.util.HashSet; ++import java.util.List; ++import java.util.Map; ++import java.util.Set; ++import java.util.function.Consumer; ++ ++public class StackTraceUtils { ++ ++ public static void printPlugins(StackTraceElement[] stackTrace, Consumer out) { ++ Map>> loadedClasses = scanForPluginClasses(); ++ ++ Set suspectedPlugins = new HashSet<>(); ++ for (StackTraceElement stackTraceElement : stackTrace) { ++ for (Map.Entry>> pluginSetEntry : loadedClasses.entrySet()) { ++ if (pluginSetEntry.getValue().stream().anyMatch(clazz -> clazz.getName().equals(stackTraceElement.getClassName()))) ++ suspectedPlugins.add(pluginSetEntry.getKey()); ++ } ++ } ++ ++ if (!suspectedPlugins.isEmpty()) { ++ out.accept("Suspected Plugins: "); ++ for (Plugin plugin : suspectedPlugins) { ++ StringBuilder builder = new StringBuilder("\t"); ++ builder.append(plugin.getName()) ++ .append("{") ++ .append(plugin.isEnabled() ? "enabled" : "disabled") ++ .append(",").append("ver=").append(plugin.getDescription().getVersion()); ++ if (!plugin.isNaggable()) ++ builder.append(",").append("nag"); ++ if (plugin instanceof JavaPlugin) ++ builder.append(",").append("path=").append(((JavaPlugin) plugin).getFile()); ++ ++ builder.append("}"); ++ out.accept(builder.toString()); ++ } ++ } else { ++ out.accept("Suspected Plugins: None"); ++ } ++ } ++ ++ private static Map>> scanForPluginClasses() { ++ Map>> loadedClasses = new Object2ObjectOpenHashMap<>(); ++ if (Bukkit.getPluginManager() instanceof SimplePluginManager) { ++ final SimplePluginManager pluginManager = (SimplePluginManager) Bukkit.getPluginManager(); ++ final Collection pluginLoaders = pluginManager.getPluginLoaders(); ++ for (PluginLoader pluginLoader : pluginLoaders) { ++ if (pluginLoader instanceof JavaPluginLoader) { ++ JavaPluginLoader javaPluginLoader = (JavaPluginLoader) pluginLoader; ++ final List classLoaders = javaPluginLoader.getClassLoaders(); ++ for (PluginClassLoader classLoader : classLoaders) { ++ loadedClasses.put(classLoader.getPlugin(), new ObjectOpenHashSet<>(classLoader.getLoadedClasses())); ++ } ++ } ++ } ++ } ++ return loadedClasses; ++ } ++ ++} diff --git a/patches/server/0066-Handle-proxy-online-mode-properly.patch b/patches/server/0066-Handle-proxy-online-mode-properly.patch new file mode 100644 index 000000000..28b0badfc --- /dev/null +++ b/patches/server/0066-Handle-proxy-online-mode-properly.patch @@ -0,0 +1,102 @@ +From 0000000000000000000000000000000000000000 Mon Sep 17 00:00:00 2001 +From: ishland +Date: Thu, 28 Jan 2021 16:46:34 +0800 +Subject: [PATCH] Handle proxy online mode properly + + +diff --git a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java +index 44132e902b653b07b21fd01d13a88870290af439..50d2b0c8b99f30bd294b07c2f608882a7b25c45f 100644 +--- a/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java ++++ b/src/main/java/com/destroystokyo/paper/profile/CraftPlayerProfile.java +@@ -137,7 +137,7 @@ public class CraftPlayerProfile implements PlayerProfile { + @Override + public boolean completeFromCache() { + MinecraftServer server = MinecraftServer.getServer(); +- return completeFromCache(false, server.getOnlineMode() || (SpigotConfig.bungee && PaperConfig.bungeeOnlineMode)); ++ return completeFromCache(false, server.getOnlineMode() || PaperConfig.isProxyOnlineMode()); + } + + public boolean completeFromCache(boolean onlineMode) { +@@ -182,7 +182,7 @@ public class CraftPlayerProfile implements PlayerProfile { + + public boolean complete(boolean textures) { + MinecraftServer server = MinecraftServer.getServer(); +- return complete(textures, server.getOnlineMode() || (SpigotConfig.bungee && PaperConfig.bungeeOnlineMode)); ++ return complete(textures, server.getOnlineMode() || PaperConfig.isProxyOnlineMode()); // Yatopia - handle proxy online mode + } + public boolean complete(boolean textures, boolean onlineMode) { + MinecraftServer server = MinecraftServer.getServer(); +diff --git a/src/main/java/net/minecraft/server/DedicatedServer.java b/src/main/java/net/minecraft/server/DedicatedServer.java +index 2473eb88ec7be3f4935debe04eeabcc0815b3233..f45b11a978addb7f8cf2d659115164e23161cbc9 100644 +--- a/src/main/java/net/minecraft/server/DedicatedServer.java ++++ b/src/main/java/net/minecraft/server/DedicatedServer.java +@@ -231,7 +231,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer + DedicatedServer.LOGGER.warn("**** SERVER IS RUNNING IN OFFLINE/INSECURE MODE!"); + DedicatedServer.LOGGER.warn("The server will make no attempt to authenticate usernames. Beware."); + // Spigot start +- if (org.spigotmc.SpigotConfig.bungee) { ++ if (org.spigotmc.SpigotConfig.bungee || com.destroystokyo.paper.PaperConfig.velocitySupport) { // Yatopia - Add velocity + DedicatedServer.LOGGER.warn("Whilst this makes it possible to use BungeeCord, unless access to your server is properly restricted, it also opens up the ability for hackers to connect with any username they choose."); + DedicatedServer.LOGGER.warn("Please see http://www.spigotmc.org/wiki/firewall-guide/ for further information."); + } else { +@@ -254,7 +254,7 @@ public class DedicatedServer extends MinecraftServer implements IMinecraftServer + this.c(dedicatedserverproperties.maxBuildHeight); + TileEntitySkull.a(this.getUserCache()); + TileEntitySkull.a(this.getMinecraftSessionService()); +- UserCache.a(this.getOnlineMode()); ++ UserCache.a(this.getOnlineMode() || com.destroystokyo.paper.PaperConfig.isProxyOnlineMode()); + DedicatedServer.LOGGER.info("Preparing level \"{}\"", this.getWorld()); + this.loadWorld(convertable.getLevelName()); // CraftBukkit + long j = SystemUtils.getMonotonicNanos() - i; +diff --git a/src/main/java/net/minecraft/server/MinecraftServer.java b/src/main/java/net/minecraft/server/MinecraftServer.java +index 1105ee7274df3f482b816c5f6183ae8594da55be..c08941c5c3e94b0825081ac0ce444678195f9cb6 100644 +--- a/src/main/java/net/minecraft/server/MinecraftServer.java ++++ b/src/main/java/net/minecraft/server/MinecraftServer.java +@@ -1693,7 +1693,7 @@ public abstract class MinecraftServer extends IAsyncTaskHandlerReentrant list = Lists.newArrayList(); + ProfileLookupCallback profilelookupcallback = new ProfileLookupCallback() { + public void onProfileLookupSucceeded(GameProfile gameprofile1) { +diff --git a/src/main/java/net/minecraft/server/WorldNBTStorage.java b/src/main/java/net/minecraft/server/WorldNBTStorage.java +index ab43806fc4edbf2d07291f3b147563ba1de48f5d..c9c30621e886b47ec636a05c795c9d9aff467591 100644 +--- a/src/main/java/net/minecraft/server/WorldNBTStorage.java ++++ b/src/main/java/net/minecraft/server/WorldNBTStorage.java +@@ -69,7 +69,7 @@ public class WorldNBTStorage { + synchronized (this.dataCache){ + playerData = this.dataCache.get(file); + } +- if (playerData == null && org.bukkit.Bukkit.getOnlineMode() && !normalFile ) // Paper - Check online mode first // Akarin - ensures normal file ++ if (playerData == null && (org.bukkit.Bukkit.getOnlineMode() || com.destroystokyo.paper.PaperConfig.isProxyOnlineMode()) && !normalFile ) // Paper - Check online mode first // Akarin - ensures normal file // Yatopia - handle proxy online mode + { + file = new File( this.playerDir, java.util.UUID.nameUUIDFromBytes( ( "OfflinePlayer:" + entityhuman.getName() ).getBytes( "UTF-8" ) ).toString() + ".dat"); + synchronized (this.dataCache){ +diff --git a/src/main/java/org/bukkit/craftbukkit/CraftServer.java b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +index 7d0f73c83a7f7f3c6becf3ed4348c6b2938a86b1..fda6780cd9f1af93f50eedc0bf915025e656a2d0 100644 +--- a/src/main/java/org/bukkit/craftbukkit/CraftServer.java ++++ b/src/main/java/org/bukkit/craftbukkit/CraftServer.java +@@ -1557,7 +1557,7 @@ public final class CraftServer implements Server { + GameProfile profile; + // Only fetch an online UUID in online mode + if (net.minecraft.server.MinecraftServer.getServer().getOnlineMode() +- || (org.spigotmc.SpigotConfig.bungee && com.destroystokyo.paper.PaperConfig.bungeeOnlineMode)) { ++ || (com.destroystokyo.paper.PaperConfig.isProxyOnlineMode())) { // Yatopia - handle proxy online mode + profile = console.getUserCache().getProfile( name ); + } else { + // Make an OfflinePlayer using an offline mode UUID since the name has no profile diff --git a/settings.gradle.kts b/settings.gradle.kts index 2b6949d2a..d05e93733 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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) diff --git a/subprojects/server.gradle.kts b/subprojects/server.gradle.kts index 5e84f929e..855fb4bdf 100644 --- a/subprojects/server.gradle.kts +++ b/subprojects/server.gradle.kts @@ -7,12 +7,6 @@ dependencies { } publishing { - publications { - create("server") { - from(components.getByName("java")) - } - } - repositories { maven { url = uri("https://repo.codemc.org/repository/nms-local/")