├── settings.gradle ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── src ├── lib │ └── java │ │ └── com │ │ └── github │ │ └── winplay02 │ │ └── gitcraft │ │ ├── pipeline │ │ ├── key │ │ │ ├── DirectoryKey.java │ │ │ ├── StorageKey.java │ │ │ ├── KeyInformation.java │ │ │ └── ArtifactKey.java │ │ ├── StepInput.java │ │ ├── IStepConfig.java │ │ ├── IStep.java │ │ ├── ParallelismPolicy.java │ │ ├── DependencyRelation.java │ │ ├── IPipelineFilesystemRoot.java │ │ ├── IStepWorker.java │ │ ├── StepStatus.java │ │ ├── IStepContext.java │ │ ├── StepResults.java │ │ ├── StepOutput.java │ │ ├── PipelineExecutionGraph.java │ │ └── PipelineFilesystemStorage.java │ │ ├── manifest │ │ ├── metadata │ │ │ ├── ArtifactMetadata.java │ │ │ └── GithubRepositoryBlobContent.java │ │ ├── VersionsManifest.java │ │ └── MetadataSources.java │ │ ├── graph │ │ ├── Vertex.java │ │ ├── AbstractVersion.java │ │ └── AbstractVersionGraph.java │ │ ├── util │ │ ├── DescribedURL.java │ │ ├── Tuple2.java │ │ ├── MavenCache.java │ │ ├── LazyValue.java │ │ └── ImmutableMultiSetView.java │ │ ├── integrity │ │ ├── SHA1Algorithm.java │ │ └── GitBlobSHA1Algorithm.java │ │ ├── LibraryPaths.java │ │ ├── config │ │ └── IntegrityConfiguration.java │ │ └── Library.java ├── main │ └── groovy │ │ └── com │ │ └── github │ │ └── winplay02 │ │ └── gitcraft │ │ ├── unpick │ │ ├── UnpickDescriptionFile.java │ │ ├── NoneUnpick.java │ │ ├── UnpickFlavour.java │ │ └── Unpick.java │ │ ├── pipeline │ │ ├── key │ │ │ └── MinecraftJar.java │ │ ├── GitCraftStepWorker.java │ │ ├── workers │ │ │ ├── RepoGarbageCollector.java │ │ │ ├── UnpickProvider.java │ │ │ ├── MappingsProvider.java │ │ │ ├── ExceptionsProvider.java │ │ │ ├── SignaturesProvider.java │ │ │ ├── NestsProvider.java │ │ │ ├── LibrariesFetcher.java │ │ │ ├── ArtifactsUnpacker.java │ │ │ ├── AssetsFetcher.java │ │ │ ├── Preener.java │ │ │ ├── ArtifactsFetcher.java │ │ │ ├── Remapper.java │ │ │ ├── JarsNester.java │ │ │ ├── JarsExceptor.java │ │ │ ├── JarsSignatureChanger.java │ │ │ ├── LvtPatcher.java │ │ │ ├── JarsMerger.java │ │ │ └── Resetter.java │ │ ├── GitCraftPipelineFilesystemRoot.java │ │ └── GitCraftStepConfig.java │ │ ├── types │ │ ├── ServerDistribution.java │ │ ├── AssetsIndex.java │ │ └── Artifact.java │ │ ├── meta │ │ ├── VersionMetaSource.java │ │ ├── SimpleVersionMeta.java │ │ ├── VersionMeta.java │ │ ├── GameVersionBuildMeta.java │ │ ├── MetaUrls.java │ │ └── RemoteVersionMetaSource.java │ │ ├── manifest │ │ ├── vanilla │ │ │ └── MojangLauncherManifest.java │ │ ├── metadata │ │ │ ├── VersionDetails.java │ │ │ ├── AssetsIndexMetadata.java │ │ │ ├── LibraryMetadata.java │ │ │ └── VersionInfo.java │ │ ├── skyrising │ │ │ └── SkyrisingManifest.java │ │ ├── ornithe │ │ │ └── OrnitheMetadataProvider.java │ │ ├── omniarchive │ │ │ └── OmniarchiveMetadataProvider.java │ │ ├── ManifestSource.java │ │ └── MetadataProvider.java │ │ ├── migration │ │ ├── MetadataStoreUpgrade.java │ │ └── Transition0_1_0To0_2_0.java │ │ ├── util │ │ ├── FabricHelper.java │ │ ├── FFNIODirectoryResultSaver.java │ │ └── SerializationTypes.java │ │ ├── exceptions │ │ ├── NoneExceptions.java │ │ ├── ExceptionsFlavour.java │ │ └── ExceptionsPatch.java │ │ ├── signatures │ │ ├── NoneSignatures.java │ │ ├── SignaturesFlavour.java │ │ └── SignaturesPatch.java │ │ ├── nests │ │ ├── NoneNests.java │ │ └── NestsFlavour.java │ │ ├── launcher │ │ ├── LauncherConfig.java │ │ ├── GitCraftLauncher.java │ │ └── LauncherUtils.java │ │ ├── mappings │ │ ├── IdentityMappings.java │ │ └── MappingFlavour.java │ │ ├── config │ │ ├── RepositoryConfiguration.java │ │ ├── DataConfiguration.java │ │ └── TransientApplicationConfiguration.java │ │ ├── GitCraft.java │ │ └── GitCraftQuirks.java └── test │ └── groovy │ └── com │ └── github │ └── winplay02 │ └── gitcraft │ └── GitCraftTestingFs.java ├── .editorconfig ├── .gitignore ├── launcher_agent ├── src │ └── main │ │ └── java │ │ └── com │ │ └── github │ │ └── winplay02 │ │ └── gitcraft │ │ └── launchagent │ │ ├── GitCraftAgent.java │ │ ├── GitCraftResourceDownloaderTransformer.java │ │ └── GitCraftLauncherTransformer.java └── build.gradle ├── .github └── workflows │ └── gradle.yml ├── gradle.properties ├── changelog.md └── gradlew.bat /settings.gradle: -------------------------------------------------------------------------------- 1 | rootProject.name = 'GitCraft' 2 | 3 | include ':launcher_agent' 4 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/WinPlay02/GitCraft/HEAD/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/key/DirectoryKey.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.key; 2 | 3 | public record DirectoryKey(String type) implements StorageKey { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/unpick/UnpickDescriptionFile.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.unpick; 2 | 3 | public record UnpickDescriptionFile(int version, String namespace) { 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/manifest/metadata/ArtifactMetadata.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | public record ArtifactMetadata(String sha1, long size, String url) { 4 | } 5 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/key/StorageKey.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.key; 2 | 3 | public sealed interface StorageKey permits ArtifactKey, DirectoryKey, KeyInformation { 4 | } 5 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/key/MinecraftJar.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.key; 2 | 3 | public enum MinecraftJar implements KeyInformation { 4 | CLIENT, SERVER, MERGED; 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/StepInput.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | public interface StepInput { 4 | record Empty() implements StepInput { 5 | } 6 | 7 | StepInput EMPTY = new Empty(); 8 | } 9 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/key/KeyInformation.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.key; 2 | 3 | public non-sealed interface KeyInformation> extends StorageKey, Comparable { 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = tab 6 | indent_size = 4 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.{yml,yaml,json,toml,md}] 12 | indent_size = 2 13 | 14 | [*.bat] 15 | end_of_line = crlf 16 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | distributionBase=GRADLE_USER_HOME 2 | distributionPath=wrapper/dists 3 | distributionUrl=https\://services.gradle.org/distributions/gradle-9.2.1-bin.zip 4 | networkTimeout=10000 5 | validateDistributionUrl=true 6 | zipStoreBase=GRADLE_USER_HOME 7 | zipStorePath=wrapper/dists 8 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/manifest/metadata/GithubRepositoryBlobContent.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | public record GithubRepositoryBlobContent(String name, String path, String sha, long size, String url, 4 | String download_url, String type) { 5 | } 6 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/graph/Vertex.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.graph; 2 | 3 | /** 4 | * Vertex Type 5 | * @param Concrete Type 6 | */ 7 | public interface Vertex> extends Comparable { 8 | default String description() { 9 | return toString(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/util/DescribedURL.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | /** 4 | * URL with description. 5 | * 6 | * @param url URL 7 | * @param description Description (human-readable) of the URL 8 | */ 9 | public record DescribedURL(String url, String description) { 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/IStepConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.key.KeyInformation; 4 | 5 | public interface IStepConfig { 6 | 7 | String createArtifactComponentString(KeyInformation dist, KeyInformation... matchingFlavours); 8 | 9 | } 10 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/types/ServerDistribution.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.types; 2 | 3 | public record ServerDistribution(Artifact serverJar, Artifact windowsServer, Artifact serverZip) { 4 | public boolean hasServerCode() { 5 | return this.serverJar() != null || this.windowsServer() != null || this.serverZip() != null; 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/VersionMetaSource.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | 6 | public interface VersionMetaSource> { 7 | 8 | M getLatest(String clas) throws IOException, URISyntaxException, InterruptedException; 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/IStep.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | 5 | public interface IStep, S extends StepInput, C extends IStepContext, D extends IStepConfig> { 6 | 7 | String getName(); 8 | 9 | ParallelismPolicy getParallelismPolicy(); 10 | 11 | IStepWorker createWorker(D config); 12 | } 13 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # GitCraft 2 | artifact-store/ 3 | minecraft\-repo*/ 4 | minecraft\-repo*/** 5 | remote-cache/ 6 | config.json 7 | 8 | # Gradle 9 | .gradle/ 10 | build/ 11 | bin/ 12 | out/ 13 | classes/ 14 | 15 | # IntelliJ Idea 16 | .idea/ 17 | *.iml 18 | *.ipr 19 | *.iws 20 | 21 | # Eclipse 22 | .eclipse/ 23 | *.launch 24 | 25 | # VS Code 26 | .vscode/ 27 | 28 | # Fleet 29 | .fleet/ 30 | 31 | # MacOS 32 | *.DS_Store 33 | 34 | # Java 35 | hs_err_*.log 36 | replay_*.log 37 | *.hprof 38 | *.jfr 39 | -------------------------------------------------------------------------------- /launcher_agent/src/main/java/com/github/winplay02/gitcraft/launchagent/GitCraftAgent.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launchagent; 2 | 3 | import java.lang.instrument.Instrumentation; 4 | 5 | public class GitCraftAgent { 6 | public static void premain(String agentArgs, Instrumentation inst) { 7 | System.out.println("[GitCraft Agent]: Initializing GitCraft Agent"); 8 | inst.addTransformer(new GitCraftLauncherTransformer()); 9 | inst.addTransformer(new GitCraftResourceDownloaderTransformer()); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/vanilla/MojangLauncherManifest.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.vanilla; 2 | 3 | import java.util.List; 4 | 5 | import com.github.winplay02.gitcraft.manifest.VersionsManifest; 6 | 7 | public record MojangLauncherManifest(LatestVersions latest, List versions) implements VersionsManifest { 8 | public record LatestVersions(String release, String snapshot) { 9 | } 10 | 11 | public record VersionEntry(String id, String url, String sha1) implements VersionsManifest.VersionEntry { 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/ParallelismPolicy.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | public enum ParallelismPolicy { 4 | /** 5 | * Execution is allowed to happen fully in parallel. 6 | */ 7 | SAFELY_FULLY_PARALLEL, 8 | 9 | /** 10 | * Execution is restricted to one instance at a time, making it sequential. 11 | */ 12 | UNSAFE_RESTRICTED_TO_SEQUENTIAL; 13 | 14 | public boolean isParallel() { 15 | return this == SAFELY_FULLY_PARALLEL; 16 | } 17 | 18 | public boolean isRestrictedToSequential() { 19 | return this == UNSAFE_RESTRICTED_TO_SEQUENTIAL; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/metadata/VersionDetails.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | import java.time.ZonedDateTime; 4 | import java.util.List; 5 | 6 | public record VersionDetails(String id, String normalizedVersion, List next, List previous, 7 | List manifests, boolean client, boolean server, boolean sharedMappings) { 8 | public record ManifestEntry(String url, String type, ZonedDateTime time, ZonedDateTime lastModified, String hash, int downloadsId, String downloads, String assetIndex, String assetHash) { 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/GitCraftStepWorker.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 4 | import com.github.winplay02.gitcraft.types.OrderedVersion; 5 | 6 | import java.util.Optional; 7 | 8 | public interface GitCraftStepWorker extends IStepWorker, GitCraftStepConfig> { 9 | 10 | record JarTupleInput(Optional mergedJar, Optional clientJar, Optional serverJar) implements StepInput { 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/DependencyRelation.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | public enum DependencyRelation { 4 | 5 | /** 6 | * does not depend on the step at all 7 | */ 8 | NONE, 9 | 10 | /** 11 | * requires the step to be present and run first 12 | */ 13 | REQUIRED, 14 | 15 | /** 16 | * requires the step to run first only if it is present 17 | */ 18 | NOT_REQUIRED; 19 | 20 | /** 21 | * @return Whether the type describes a dependency relationship 22 | */ 23 | public boolean isDependency() { 24 | return this == REQUIRED || this == NOT_REQUIRED; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /.github/workflows/gradle.yml: -------------------------------------------------------------------------------- 1 | name: GitCraft-Next CI 2 | 3 | on: 4 | push: 5 | branches: [ "master", "experimental" ] 6 | pull_request: 7 | branches: [ "master", "experimental" ] 8 | 9 | permissions: 10 | contents: read 11 | 12 | jobs: 13 | test: 14 | runs-on: ubuntu-latest 15 | steps: 16 | - uses: actions/checkout@v6 17 | - name: Set up JDK 25 18 | uses: actions/setup-java@v5 19 | with: 20 | java-version: '25' 21 | distribution: 'temurin' 22 | - name: Setup Gradle 23 | uses: gradle/actions/setup-gradle@v5 24 | - name: Test GitCraft-Next 25 | run: ./gradlew test 26 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/util/Tuple2.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import java.util.Iterator; 4 | import java.util.RandomAccess; 5 | import java.util.stream.Stream; 6 | 7 | public record Tuple2(V1 v1, V2 v2) implements Iterable, RandomAccess { 8 | public static Tuple2 tuple(V1 v1, V2 v2) { 9 | return new Tuple2<>(v1, v2); 10 | } 11 | 12 | public V1 getV1() { 13 | return this.v1; 14 | } 15 | 16 | public V2 getV2() { 17 | return this.v2; 18 | } 19 | 20 | @Override 21 | public Iterator iterator() { 22 | return Stream.of(this.v1(), this.v2()).iterator(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/metadata/AssetsIndexMetadata.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | import com.github.winplay02.gitcraft.types.AssetsIndex; 4 | 5 | import java.util.Map; 6 | 7 | public record AssetsIndexMetadata(Map objects, boolean map_to_resources) { 8 | public record Asset(String hash, int size, String url) { 9 | public Asset(String hash, int size) { 10 | this(hash, size, null); 11 | } 12 | 13 | public Asset(String hash, int size, String url) { 14 | this.hash = hash; 15 | this.size = size; 16 | this.url = (url != null) ? url : AssetsIndex.makeMinecraftAssetUrl(hash); 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/SimpleVersionMeta.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | public record SimpleVersionMeta(String maven, String version, boolean stable) implements VersionMeta { 4 | @Override 5 | public int compareTo(SimpleVersionMeta o) { 6 | return 0; 7 | } 8 | 9 | @Override 10 | public String makeMavenUrl(String baseUrl, String ext) { 11 | return baseUrl + maven.substring(0, maven.indexOf(':')).replace('.', '/') + "/" + maven.substring(maven.indexOf(':') + 1, maven.lastIndexOf(':')) + "/" + maven.substring(maven.lastIndexOf(':') + 1) + "/" + maven.substring(maven.indexOf(':') + 1).replace(':', '-') + ext; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/migration/MetadataStoreUpgrade.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.migration; 2 | 3 | import com.github.winplay02.gitcraft.util.GitCraftPaths; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Path; 7 | import java.util.List; 8 | 9 | public interface MetadataStoreUpgrade { 10 | String sourceVersion(); 11 | 12 | String targetVersion(); 13 | 14 | void upgrade() throws IOException; 15 | 16 | default List upgradeInfo() { 17 | return List.of(); 18 | } 19 | 20 | default Path getLostAndFoundDirectory() { 21 | return GitCraftPaths.LOST_AND_FOUND.resolve(String.format("%s-%s", sourceVersion(), targetVersion())); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/key/ArtifactKey.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.key; 2 | 3 | import com.github.winplay02.gitcraft.util.MiscHelper; 4 | 5 | public record ArtifactKey(String... keys) implements StorageKey { 6 | public ArtifactKey(DirectoryKey type, String... keys) { 7 | this(MiscHelper.concatArrays(new String[]{type.type()}, keys)); 8 | } 9 | } 10 | 11 | /*public record ArtifactKey(String type, MinecraftJar minecraftJar, MinecraftDist minecraftDist) implements StorageKey { 12 | public ArtifactKey(DirectoryKey type, MinecraftJar minecraftJar, MinecraftDist minecraftDist) { 13 | this(type.type(), minecraftJar, minecraftDist); 14 | } 15 | } 16 | */ 17 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/manifest/VersionsManifest.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest; 2 | 3 | /** 4 | * Represents a versions manifest json that provides metadata for available versions. 5 | * 6 | * @param VersionsManifest.VersionEntry 7 | */ 8 | public interface VersionsManifest { 9 | /** 10 | * @return Available versions 11 | */ 12 | Iterable versions(); 13 | 14 | /** 15 | * Represents a manifest entry that provides metadata for a specific version. 16 | * This version has to be identifiable by an id. 17 | */ 18 | interface VersionEntry { 19 | /** 20 | * @return Version ID 21 | */ 22 | String id(); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /gradle.properties: -------------------------------------------------------------------------------- 1 | # Gradle Properties 2 | org.gradle.jvmargs = -XX:+HeapDumpOnOutOfMemoryError 3 | 4 | # GitCraft Properties 5 | group = com.github.winplay02.gitcraft 6 | version = 0.3.0-SNAPSHOT 7 | 8 | # GitCraft Dependencies 9 | groovy_version = 5.0.+ 10 | gson_version = 2.+ 11 | fabric_loader_version = 0.18.+ 12 | asm_version = 9.+ 13 | stitch_version = 0.22.+ 14 | tiny_remapper_version = 0.12.+ 15 | access_widener_version = 2.+ 16 | mappingio_version = 0.8.+ 17 | lorenz_tiny_version = 4.+ 18 | condor_version = 1.4.+ 19 | exceptor_version = 1.1.+ 20 | signature_changer_version = 1.0+ 21 | preen_version = 1.1+ 22 | nester_version = 1.4.+ 23 | jgit_version = 7.+ 24 | vineflower_version = 1.11.+ 25 | loom_version = 1.+ 26 | unpick_version = 3.+ 27 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/VersionMeta.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | public interface VersionMeta> extends Comparable { 4 | 5 | default String makeJarMavenUrl(String mavenUrl) { 6 | return makeMavenUrl(mavenUrl, ".jar"); 7 | } 8 | 9 | default String makeV2JarMavenUrl(String mavenUrl) { 10 | return makeMavenUrl(mavenUrl, "-v2.jar"); 11 | } 12 | 13 | default String makeMergedV2JarMavenUrl(String mavenUrl) { 14 | return makeMavenUrl(mavenUrl, "-mergedv2.jar"); 15 | } 16 | 17 | default String makeConstantsJarMavenUrl(String mavenUrl) { 18 | return makeMavenUrl(mavenUrl, "-constants.jar"); 19 | } 20 | 21 | String makeMavenUrl(String baseUrl, String ext); 22 | 23 | } 24 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/skyrising/SkyrisingManifest.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.skyrising; 2 | 3 | import java.time.ZonedDateTime; 4 | import java.util.List; 5 | 6 | import com.github.winplay02.gitcraft.manifest.VersionsManifest; 7 | 8 | public record SkyrisingManifest(LatestVersions latest, List versions) implements VersionsManifest { 9 | public record LatestVersions(String old_alpha, String classic_server, String alpha_server, String old_beta, String release, String snapshot, String pending) { 10 | } 11 | 12 | public record VersionEntry(String id, String type, String url, ZonedDateTime time, ZonedDateTime releaseTime, String details) implements VersionsManifest.VersionEntry { 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/GameVersionBuildMeta.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | public record GameVersionBuildMeta(String gameVersion, String separator, int build, String maven, String version, boolean stable) implements VersionMeta { 4 | @Override 5 | public int compareTo(GameVersionBuildMeta o) { 6 | return Integer.compare(this.build, o.build); 7 | } 8 | 9 | @Override 10 | public String makeMavenUrl(String baseUrl, String ext) { 11 | return baseUrl + maven.substring(0, maven.indexOf(':')).replace('.', '/') + "/" + maven.substring(maven.indexOf(':') + 1, maven.lastIndexOf(':')) + "/" + maven.substring(maven.lastIndexOf(':') + 1) + "/" + maven.substring(maven.indexOf(':') + 1).replace(':', '-') + ext; 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/IPipelineFilesystemRoot.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import java.nio.file.Path; 4 | import java.util.function.Function; 5 | import java.util.function.Supplier; 6 | 7 | public interface IPipelineFilesystemRoot { 8 | Path getRoot(); 9 | 10 | Path getByIndex(String index); 11 | 12 | static Function getPathIndexed(String path) { 13 | return src -> src.getByIndex(path); 14 | } 15 | 16 | record SimpleSuppliedPipelineFilesystemRoot(Supplier root) implements IPipelineFilesystemRoot { 17 | public Path getRoot() { 18 | return this.root.get(); 19 | } 20 | 21 | @Override 22 | public Path getByIndex(String index) { 23 | return this.getRoot().resolve(index); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/ornithe/OrnitheMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.ornithe; 2 | import com.github.winplay02.gitcraft.manifest.ManifestSource; 3 | import com.github.winplay02.gitcraft.manifest.skyrising.SkyrisingMetadataProvider; 4 | 5 | public class OrnitheMetadataProvider extends SkyrisingMetadataProvider { 6 | 7 | public OrnitheMetadataProvider() { 8 | super("https://ornithemc.net/mc-versions/version_manifest.json"); 9 | } 10 | 11 | @Override 12 | public ManifestSource getSource() { 13 | return ManifestSource.ORNITHEMC; 14 | } 15 | 16 | @Override 17 | public String getName() { 18 | return "OrnitheMC Version Metadata (https://ornithemc.github.io/mc-versions/)"; 19 | } 20 | 21 | @Override 22 | public String getInternalName() { 23 | return "ornithemc"; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/IStepWorker.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | 5 | public interface IStepWorker, S extends StepInput, C extends IStepContext, D extends IStepConfig> { 6 | D config(); 7 | 8 | StepOutput run(IPipeline pipeline, C context, S input, StepResults results) throws Exception; 9 | 10 | default StepOutput runGeneric(IPipeline pipeline, C context, StepInput input, StepResults results) throws Exception { 11 | @SuppressWarnings("unchecked") 12 | S castInput = (S) input; 13 | return this.run(pipeline, context, castInput, results); 14 | } 15 | 16 | default boolean shouldExecute(IPipeline pipeline, C context) { 17 | return true; 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /launcher_agent/build.gradle: -------------------------------------------------------------------------------- 1 | apply plugin: 'java' 2 | 3 | java { 4 | sourceCompatibility = JavaVersion.VERSION_24 5 | targetCompatibility = JavaVersion.VERSION_24 6 | } 7 | 8 | tasks.withType(JavaCompile).configureEach { 9 | it.options.encoding = "UTF-8" 10 | it.options.release = 24 11 | } 12 | 13 | jar { 14 | manifest { 15 | attributes ("Premain-Class": "com.github.winplay02.gitcraft.launchagent.GitCraftAgent") 16 | } 17 | metadataCharset = "UTF-8" 18 | preserveFileTimestamps = false 19 | reproducibleFileOrder = true 20 | from { 21 | configurations.runtimeClasspath.collect { it.isDirectory() ? it : zipTree(it) } 22 | } 23 | } 24 | 25 | jar.dependsOn(configurations.runtimeClasspath) 26 | 27 | project(':').runLauncher { 28 | dependsOn jar 29 | environment("GITCRAFT_LAUNCH_AGENT", jar.archiveFile.get().getAsFile().getAbsolutePath()) 30 | } 31 | 32 | dependencies {} 33 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/util/MavenCache.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import java.io.FileNotFoundException; 4 | import java.io.IOException; 5 | import java.net.URISyntaxException; 6 | import java.net.URL; 7 | import java.util.HashMap; 8 | 9 | public record MavenCache(HashMap shaUrlMap) { 10 | public MavenCache() { 11 | this(new HashMap<>()); 12 | } 13 | 14 | public String getSha1ForURL(String urlSha1) throws IOException { 15 | if (!this.shaUrlMap.containsKey(urlSha1)) { 16 | String sha1 = null; 17 | try { 18 | sha1 = FileSystemNetworkManager.fetchAllFromURLSync(new URL(urlSha1)); 19 | } catch (FileNotFoundException ignored) { 20 | } catch (URISyntaxException | InterruptedException e) { 21 | throw new IOException(e); 22 | } 23 | this.shaUrlMap.put(urlSha1, sha1); 24 | } 25 | 26 | return this.shaUrlMap.get(urlSha1); 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/types/AssetsIndex.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.types; 2 | 3 | import java.util.ArrayList; 4 | import java.util.Collections; 5 | import java.util.List; 6 | 7 | import com.github.winplay02.gitcraft.manifest.metadata.AssetsIndexMetadata; 8 | 9 | public record AssetsIndex(AssetsIndexMetadata assetsIndex, List assets) { 10 | public static AssetsIndex from(AssetsIndexMetadata assetsIndex) { 11 | List assets = new ArrayList<>(assetsIndex.objects().size()); 12 | for (AssetsIndexMetadata.Asset info : assetsIndex.objects().values()) { 13 | assets.add(new Artifact(info.url(), info.hash(), info.hash())); 14 | } 15 | return new AssetsIndex(assetsIndex, Collections.unmodifiableList(assets)); 16 | } 17 | 18 | public static String makeMinecraftAssetUrl(String hash) { 19 | return String.format("https://resources.download.minecraft.net/%s/%s", hash.substring(0, 2), hash); 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/manifest/MetadataSources.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest; 2 | 3 | import java.nio.file.Path; 4 | 5 | public final class MetadataSources { 6 | 7 | public record RemoteVersionsManifest, E extends VersionsManifest.VersionEntry>(String url, Class manifestClass) { 8 | public static , E extends VersionsManifest.VersionEntry> RemoteVersionsManifest of(String url, Class manifestClass) { 9 | return new RemoteVersionsManifest<>(url, manifestClass); 10 | } 11 | } 12 | 13 | public record RemoteMetadata(E versionEntry) { 14 | public static RemoteMetadata of(E versionEntry) { 15 | return new RemoteMetadata<>(versionEntry); 16 | } 17 | } 18 | 19 | public record LocalRepository(Path directory) { 20 | public static LocalRepository of(Path directory) { 21 | return new LocalRepository(directory); 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/util/FabricHelper.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import com.github.winplay02.gitcraft.GitCraftQuirks; 4 | import net.fabricmc.loader.api.SemanticVersion; 5 | import net.fabricmc.loader.api.Version; 6 | import net.fabricmc.loader.api.VersionParsingException; 7 | import net.fabricmc.loader.impl.FabricLoaderImpl; 8 | 9 | public class FabricHelper { 10 | public static void checkFabricLoaderVersion() { 11 | try { 12 | SemanticVersion actualFabricLoaderVersion = SemanticVersion.parse(FabricLoaderImpl.VERSION); 13 | SemanticVersion minRequiredVersion = SemanticVersion.parse(GitCraftQuirks.MIN_SUPPORTED_FABRIC_LOADER); 14 | if (actualFabricLoaderVersion.compareTo((Version) minRequiredVersion) < 0) { 15 | MiscHelper.panic("Fabric loader is out of date. Min required version: %s, Actual provided version: %s", GitCraftQuirks.MIN_SUPPORTED_FABRIC_LOADER, FabricLoaderImpl.VERSION); 16 | } 17 | } catch (VersionParsingException e) { 18 | throw new RuntimeException(e); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/graph/AbstractVersion.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.graph; 2 | 3 | /** 4 | * A version that can be ordered inside a graph. 5 | * 6 | * @param Concrete Version 7 | */ 8 | public interface AbstractVersion> extends Vertex { 9 | 10 | /** 11 | * @return Semantic version string, usable for ordering versions 12 | */ 13 | String semanticVersion(); 14 | 15 | /** 16 | * @return Friendly version string, mostly in a human-readable format 17 | */ 18 | String friendlyVersion(); 19 | 20 | /** 21 | * Convert this version into a uniquely identifiable commit message. 22 | * 23 | * @return Commit Message identifying this version 24 | */ 25 | String toCommitMessage(); 26 | 27 | @Override 28 | default String description() { 29 | return String.format("%s (%s)", friendlyVersion(), semanticVersion()); 30 | } 31 | 32 | /** 33 | * @return This version as part of the path used on a filesystem 34 | */ 35 | default String pathName() { 36 | return friendlyVersion(); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/StepStatus.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import java.util.Arrays; 4 | import java.util.Collection; 5 | import java.util.Comparator; 6 | import java.util.Objects; 7 | 8 | public enum StepStatus implements Comparator { 9 | 10 | NOT_RUN, 11 | UP_TO_DATE, 12 | SUCCESS, 13 | FAILED; 14 | 15 | public static StepStatus merge(StepStatus... statuses) { 16 | return Arrays.stream(statuses).filter(Objects::nonNull).max(StepStatus::compareTo).orElse(NOT_RUN); 17 | } 18 | 19 | public static StepStatus merge(Collection statuses) { 20 | return statuses.stream().filter(Objects::nonNull).max(StepStatus::compareTo).orElse(NOT_RUN); 21 | } 22 | 23 | @Override 24 | public int compare(StepStatus o1, StepStatus o2) { 25 | if (o1 == null) { 26 | o1 = NOT_RUN; 27 | } 28 | if (o2 == null) { 29 | o2 = NOT_RUN; 30 | } 31 | return o2.ordinal() - o1.ordinal(); 32 | } 33 | 34 | public boolean hasRun() { 35 | return this != NOT_RUN; 36 | } 37 | 38 | public boolean isSuccessful() { 39 | return this == SUCCESS || this == UP_TO_DATE; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/omniarchive/OmniarchiveMetadataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.omniarchive; 2 | 3 | import java.util.List; 4 | 5 | import com.github.winplay02.gitcraft.manifest.ManifestSource; 6 | import com.github.winplay02.gitcraft.manifest.vanilla.MojangLauncherMetadataProvider; 7 | 8 | public class OmniarchiveMetadataProvider extends MojangLauncherMetadataProvider { 9 | 10 | public OmniarchiveMetadataProvider() { 11 | super("https://meta.omniarchive.uk/v1/manifest.json"); 12 | } 13 | 14 | @Override 15 | public ManifestSource getSource() { 16 | return ManifestSource.OMNIARCHIVE; 17 | } 18 | 19 | @Override 20 | public String getName() { 21 | return "Omniarchive Version Metadata (https://omniarchive.uk/)"; 22 | } 23 | 24 | @Override 25 | public String getInternalName() { 26 | return "omniarchive"; 27 | } 28 | 29 | @Override 30 | public List getParentVersionIds(String versionId) { 31 | switch (versionId) { 32 | case "3D Shareware v1.34" -> { 33 | return List.of("19w13b+1653"); 34 | } 35 | } 36 | 37 | return super.getParentVersionIds(versionId); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/IStepContext.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | import com.github.winplay02.gitcraft.graph.AbstractVersionGraph; 5 | import com.github.winplay02.gitcraft.util.RepoWrapper; 6 | 7 | import java.util.concurrent.ExecutorService; 8 | 9 | public interface IStepContext, T extends AbstractVersion> { 10 | RepoWrapper repository(); 11 | 12 | AbstractVersionGraph versionGraph(); 13 | 14 | T targetVersion(); 15 | 16 | ExecutorService executorService(); 17 | 18 | C withDifferingVersion(T targetVersion); 19 | 20 | record SimpleStepContext>(RepoWrapper repository, AbstractVersionGraph versionGraph, T targetVersion, ExecutorService executorService) implements IStepContext, T> { 21 | 22 | public SimpleStepContext withDifferingVersion(T targetVersion) { 23 | return new SimpleStepContext<>(repository, versionGraph, targetVersion, executorService); 24 | } 25 | 26 | @Override 27 | public String toString() { 28 | return "version: %s".formatted(targetVersion.friendlyVersion()); 29 | } 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/MetaUrls.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | public class MetaUrls { 4 | 5 | private static final String FABRIC_META_URL = "https://meta.fabricmc.net/"; 6 | private static final String ORNITHE_META_URL = "https://meta.ornithemc.net/"; 7 | 8 | public static final String FABRIC_YARN = fabricV2("yarn"); 9 | 10 | public static final String ORNITHE_RAVEN = ornitheV3("raven"); 11 | public static final String ORNITHE_SPARROW = ornitheV3("sparrow"); 12 | public static final String ORNITHE_NESTS = ornitheV3("nests"); 13 | 14 | public static String ornitheCalamusIntermediary(int generation) { 15 | return ornitheV3(generation, "intermediary"); 16 | } 17 | 18 | public static String ornitheFeather(int generation) { 19 | return ornitheV3(generation, "feather"); 20 | } 21 | 22 | public static String fabricV2(String endpoint) { 23 | return FABRIC_META_URL + "v2/versions/" + endpoint; 24 | } 25 | 26 | public static String ornitheV3(String endpoint) { 27 | return ORNITHE_META_URL + "v3/versions/" + endpoint; 28 | } 29 | 30 | public static String ornitheV3(int generation, String endpoint) { 31 | return ORNITHE_META_URL + "v3/versions/gen" + generation + "/" + endpoint; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/unpick/NoneUnpick.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.unpick; 2 | 3 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 6 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 7 | import com.github.winplay02.gitcraft.types.OrderedVersion; 8 | 9 | import java.io.IOException; 10 | 11 | public class NoneUnpick implements Unpick { 12 | @Override 13 | public StepStatus provideUnpick(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException { 14 | return StepStatus.NOT_RUN; 15 | } 16 | 17 | @Override 18 | public UnpickContext getContext(OrderedVersion targetVersion, MinecraftJar minecraftJar) throws IOException { 19 | return null; 20 | } 21 | 22 | @Override 23 | public boolean doesUnpickInformationExist(OrderedVersion mcVersion) { 24 | return true; 25 | } 26 | 27 | @Override 28 | public MappingFlavour applicableMappingFlavour(UnpickDescriptionFile unpickDescription) { 29 | return null; 30 | } 31 | 32 | @Override 33 | public boolean supportsUnpickRemapping(UnpickDescriptionFile unpickDescription) { 34 | return false; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/integrity/SHA1Algorithm.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.integrity; 2 | 3 | import com.github.winplay02.gitcraft.config.IntegrityConfiguration; 4 | 5 | import java.security.MessageDigest; 6 | import java.security.NoSuchAlgorithmException; 7 | import java.util.function.BiConsumer; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * Integrity algorithm using plain SHA1. 12 | */ 13 | public class SHA1Algorithm extends IntegrityAlgorithm { 14 | 15 | /** 16 | * Construct this algorithm with a configuration. 17 | * 18 | * @param configuration Configuration 19 | */ 20 | public SHA1Algorithm(IntegrityConfiguration configuration) { 21 | super(configuration); 22 | } 23 | 24 | @Override 25 | public String getAlgorithmName() { 26 | return "SHA1"; 27 | } 28 | 29 | @Override 30 | protected byte[] calculateChecksum(T object, BiConsumer> objectBytesExtractor, Function objectLengthExtractor) { 31 | try { 32 | MessageDigest digest = MessageDigest.getInstance("SHA1"); 33 | objectBytesExtractor.accept(object, (bytes, length) -> digest.update(bytes, 0, length)); 34 | return digest.digest(); 35 | } catch (NoSuchAlgorithmException e) { 36 | throw new RuntimeException(e); 37 | } 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/metadata/LibraryMetadata.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | import com.github.winplay02.gitcraft.util.RemoteHelper; 4 | 5 | import java.io.IOException; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public record LibraryMetadata(String name, Downloads downloads, List rules, Extract extract, Map natives) { 11 | public List getArtifact() { 12 | if (this.downloads() != null) { 13 | List artifacts = new ArrayList<>(); 14 | if (this.downloads().artifact() != null) { 15 | artifacts.add(this.downloads().artifact()); 16 | } 17 | if (this.downloads().classifiers() != null) { 18 | artifacts.addAll(this.downloads().classifiers().values()); 19 | } 20 | return artifacts; 21 | } 22 | final String mavenUrl = RemoteHelper.createMavenURLFromMavenArtifact("https://libraries.minecraft.net", this.name()); 23 | try { 24 | return List.of(RemoteHelper.createMavenURLFromMavenArtifact(mavenUrl)); 25 | } catch (IOException e) { 26 | throw new RuntimeException(e); 27 | } 28 | } 29 | 30 | public record Downloads(ArtifactMetadata artifact, Map classifiers) { 31 | } 32 | 33 | public record Extract(List exclude) {} 34 | } 35 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/migration/Transition0_1_0To0_2_0.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.migration; 2 | 3 | import com.github.winplay02.gitcraft.util.GitCraftPaths; 4 | 5 | import java.io.IOException; 6 | import java.nio.file.Files; 7 | import java.util.List; 8 | 9 | public class Transition0_1_0To0_2_0 implements MetadataStoreUpgrade { 10 | @Override 11 | public String sourceVersion() { 12 | return "0.1.0"; 13 | } 14 | 15 | @Override 16 | public String targetVersion() { 17 | return "0.2.0"; 18 | } 19 | 20 | @Override 21 | public void upgrade() throws IOException { 22 | if (Files.exists(GitCraftPaths.LEGACY_METADATA_STORE)) { 23 | Files.delete(GitCraftPaths.LEGACY_METADATA_STORE); 24 | } 25 | } 26 | 27 | @Override 28 | public List upgradeInfo() { 29 | return List.of( 30 | "WARNING: There were breaking changes to existing repos", 31 | "- More known versions are automatically downloaded (like experimental snapshots)", 32 | "- Commits may now be merges of multiple previous versions", 33 | "- Datagen is executed by default (registry reports, NBT -> SNBT)", 34 | "- Vanilla worldgen datapack is now downloaded for versions, where there are no other ways of obtaining these files", 35 | "- Comments are enabled for yarn generation", 36 | "- Constant unpicking is now done for yarn generation", 37 | "More information in --help" 38 | ); 39 | } 40 | 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/util/LazyValue.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import java.util.Objects; 4 | import java.util.function.Supplier; 5 | 6 | /** 7 | * A value that is calculated on first access. 8 | * 9 | * @param Any type 10 | */ 11 | public final class LazyValue implements Supplier { 12 | /** 13 | * Supplier that create the value 14 | */ 15 | public Supplier valueSupplier; 16 | 17 | /** 18 | * Actual value 19 | */ 20 | public T value; 21 | 22 | /** 23 | * Constructs a new lazy value with a supplier. 24 | * 25 | * @param valueSupplier Supplier that create the value. The supplied value may also be null, the supplier may not. 26 | */ 27 | private LazyValue(final Supplier valueSupplier) { 28 | Objects.requireNonNull(valueSupplier); 29 | this.valueSupplier = valueSupplier; 30 | this.value = null; 31 | } 32 | 33 | /** 34 | * Constructs a new lazy value with a supplier. 35 | * 36 | * @param valueSupplier Supplier that create the value. The supplied value may also be null, the supplier may not. 37 | */ 38 | public static LazyValue of(final Supplier valueSupplier) { 39 | return new LazyValue<>(valueSupplier); 40 | } 41 | 42 | @Override 43 | public T get() { 44 | if (this.valueSupplier != null) { 45 | this.value = this.valueSupplier.get(); 46 | this.valueSupplier = null; 47 | } 48 | return this.value; 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/exceptions/NoneExceptions.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.exceptions; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | 6 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 7 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 8 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 9 | import com.github.winplay02.gitcraft.types.OrderedVersion; 10 | 11 | import net.ornithemc.exceptor.io.ExceptionsFile; 12 | 13 | public class NoneExceptions extends ExceptionsPatch { 14 | 15 | @Override 16 | public String getName() { 17 | return "None"; 18 | } 19 | 20 | @Override 21 | public boolean doExceptionsExist(OrderedVersion mcVersion) { 22 | return true; 23 | } 24 | 25 | @Override 26 | public boolean doExceptionsExist(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 27 | return true; 28 | } 29 | 30 | @Override 31 | public boolean canExceptionsBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 32 | return true; 33 | } 34 | 35 | @Override 36 | public StepStatus provideExceptions(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException { 37 | return StepStatus.SUCCESS; 38 | } 39 | 40 | @Override 41 | protected Path getExceptionsPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 42 | return null; 43 | } 44 | 45 | @Override 46 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, ExceptionsFile visitor) throws IOException { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/LibraryPaths.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft; 2 | 3 | import java.io.File; 4 | import java.io.IOException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.Paths; 8 | 9 | public class LibraryPaths { 10 | public static Path lookupCurrentWorkingDirectory() throws IOException { 11 | return Paths.get(new File(".").getCanonicalPath()); 12 | } 13 | 14 | public static Path CURRENT_WORKING_DIRECTORY = null; 15 | public static Path MAIN_ARTIFACT_STORE = null; 16 | public static Path MAVEN_CACHE = null; 17 | public static Path TMP_DIR = null; 18 | 19 | public static void init(Path currentWorkingDirectory) throws IOException { 20 | if (CURRENT_WORKING_DIRECTORY != null) { 21 | return; 22 | } 23 | CURRENT_WORKING_DIRECTORY = currentWorkingDirectory; 24 | MAIN_ARTIFACT_STORE = CURRENT_WORKING_DIRECTORY.resolve("artifact-store"); 25 | MAVEN_CACHE = MAIN_ARTIFACT_STORE.resolve("maven-cache.json"); 26 | TMP_DIR = CURRENT_WORKING_DIRECTORY.resolve("tmp"); 27 | Files.createDirectories(MAIN_ARTIFACT_STORE); 28 | Files.createDirectories(TMP_DIR); 29 | } 30 | 31 | public record TmpFileGuard(Path filePath) implements AutoCloseable { 32 | @Override 33 | public void close() throws IOException { 34 | Files.deleteIfExists(filePath); 35 | } 36 | } 37 | 38 | public static TmpFileGuard getTmpFile(String prefix, String suffix) throws IOException { 39 | return new TmpFileGuard(TMP_DIR.resolve(prefix + "-" + System.nanoTime() + "-" + suffix)); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/signatures/NoneSignatures.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.signatures; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | 6 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 7 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 8 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 9 | import com.github.winplay02.gitcraft.types.OrderedVersion; 10 | 11 | import io.github.gaming32.signaturechanger.tree.SigsFile; 12 | 13 | public class NoneSignatures extends SignaturesPatch { 14 | 15 | @Override 16 | public String getName() { 17 | return "None"; 18 | } 19 | 20 | @Override 21 | public boolean doSignaturesExist(OrderedVersion mcVersion) { 22 | return true; 23 | } 24 | 25 | @Override 26 | public boolean doSignaturesExist(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 27 | return true; 28 | } 29 | 30 | @Override 31 | public boolean canSignaturesBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 32 | return true; 33 | } 34 | 35 | @Override 36 | public StepStatus provideSignatures(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException { 37 | return StepStatus.SUCCESS; 38 | } 39 | 40 | @Override 41 | protected Path getSignaturesPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 42 | return null; 43 | } 44 | 45 | @Override 46 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, SigsFile visitor) throws IOException { 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/config/IntegrityConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.config; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import static com.github.winplay02.gitcraft.config.Configuration.Utils.prim; 9 | 10 | /** 11 | * Configuration for integrity verifying algorithms. 12 | * 13 | * @param verifyChecksums Whether checksums should be verified. If this is false, no checksum is actually calculated. 14 | * @param cacheChecksums Whether checksums should be cached. If this is false, no checksum is cached and every calculation will start from scratch. When dealing in a malicious environment, this should be disabled as last-modified timestamps can be forged. 15 | */ 16 | public record IntegrityConfiguration(boolean verifyChecksums, 17 | boolean cacheChecksums) 18 | implements Configuration { 19 | 20 | @Override 21 | public Map serialize() { 22 | return Map.of( 23 | "verifyChecksums", prim(this.verifyChecksums()), 24 | "cacheChecksums", prim(this.cacheChecksums()) 25 | ); 26 | } 27 | 28 | @Override 29 | public List generateInfo() { 30 | return List.of( 31 | String.format("Checksum verification is: %s", verifyChecksums ? "enabled" : "disabled") 32 | ); 33 | } 34 | 35 | public static IntegrityConfiguration deserialize(Map map) { 36 | return new IntegrityConfiguration( 37 | Utils.getBoolean(map, "verifyChecksums", true), 38 | Utils.getBoolean(map, "cacheChecksums", true) 39 | ); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/RepoGarbageCollector.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.GitCraft; 4 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 5 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 6 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 7 | import com.github.winplay02.gitcraft.pipeline.StepInput; 8 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 9 | import com.github.winplay02.gitcraft.pipeline.StepResults; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record RepoGarbageCollector(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | if (GitCraft.getTransientApplicationConfiguration().noRepo()) { 24 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 25 | } 26 | context.repository().gc(); 27 | return StepOutput.ofEmptyResultSet(StepStatus.SUCCESS); 28 | } 29 | } 30 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/nests/NoneNests.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.nests; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | 6 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 7 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 8 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 9 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 10 | import com.github.winplay02.gitcraft.types.OrderedVersion; 11 | 12 | import net.ornithemc.nester.nest.Nests; 13 | 14 | public class NoneNests extends Nest { 15 | 16 | @Override 17 | public String getName() { 18 | return "None"; 19 | } 20 | 21 | @Override 22 | public boolean doNestsExist(OrderedVersion mcVersion) { 23 | return true; 24 | } 25 | 26 | @Override 27 | public boolean doNestsExist(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 28 | return true; 29 | } 30 | 31 | @Override 32 | public boolean canNestsBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) { 33 | return true; 34 | } 35 | 36 | @Override 37 | public StepStatus provideNests(IStepContext versionContext, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) throws IOException { 38 | return StepStatus.SUCCESS; 39 | } 40 | 41 | @Override 42 | protected Path getNestsPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) { 43 | return null; 44 | } 45 | 46 | @Override 47 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour, Nests visitor) throws IOException { 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/ManifestSource.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest; 2 | 3 | import com.github.winplay02.gitcraft.manifest.historic.HistoricMojangLauncherMetadataProvider; 4 | import com.github.winplay02.gitcraft.manifest.omniarchive.OmniarchiveMetadataProvider; 5 | import com.github.winplay02.gitcraft.manifest.skyrising.SkyrisingMetadataProvider; 6 | import com.github.winplay02.gitcraft.manifest.ornithe.OrnitheMetadataProvider; 7 | import com.github.winplay02.gitcraft.manifest.vanilla.MojangLauncherMetadataProvider; 8 | import com.github.winplay02.gitcraft.types.OrderedVersion; 9 | import com.github.winplay02.gitcraft.util.LazyValue; 10 | 11 | import java.util.Locale; 12 | import java.util.function.Supplier; 13 | 14 | public enum ManifestSource { 15 | 16 | MOJANG(MojangLauncherMetadataProvider::new), 17 | SKYRISING(SkyrisingMetadataProvider::new), 18 | ORNITHEMC(OrnitheMetadataProvider::new), 19 | OMNIARCHIVE(OmniarchiveMetadataProvider::new), 20 | MOJANG_HISTORIC(() -> new HistoricMojangLauncherMetadataProvider((MojangLauncherMetadataProvider) MOJANG.getMetadataProvider(), (SkyrisingMetadataProvider) SKYRISING.getMetadataProvider())); 21 | 22 | private final LazyValue> metadataProvider; 23 | 24 | ManifestSource(Supplier> metadataProvider) { 25 | this.metadataProvider = LazyValue.of(metadataProvider); 26 | } 27 | 28 | public MetadataProvider getMetadataProvider() { 29 | return this.metadataProvider.get(); 30 | } 31 | 32 | @Override 33 | public String toString() { 34 | return super.toString().toLowerCase(Locale.ROOT); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/launcher/LauncherConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launcher; 2 | 3 | import com.github.winplay02.gitcraft.config.Configuration; 4 | import com.google.gson.JsonElement; 5 | import groovy.lang.Tuple2; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.UUID; 10 | 11 | public record LauncherConfig( 12 | String username, 13 | UUID uuid, 14 | boolean launchDemo, 15 | Tuple2 customResolution, 16 | String quickPlayPath, 17 | String quickPlaySingleplayer, 18 | String quickPlayMultiplayer, 19 | String quickPlayRealms) implements Configuration { 20 | 21 | public static final LauncherConfig DEFAULT = new LauncherConfig( 22 | "TestingUser", 23 | UUID.randomUUID(), 24 | false, 25 | null, 26 | null, 27 | null, 28 | null, 29 | null 30 | ); 31 | 32 | public static LauncherConfig deserialize(Map map) { 33 | return DEFAULT; // transient, do not deserialize 34 | } 35 | 36 | @Override 37 | public Map serialize() { 38 | return Map.of(); // transient, do not serialize 39 | } 40 | 41 | @Override 42 | public List generateInfo() { 43 | return List.of( 44 | String.format("Username: %s (%s)", this.username(), this.uuid()), 45 | String.format("Launch in demo mode: %s", this.launchDemo()), 46 | String.format("Custom resolution: %s", this.customResolution() != null ? String.format("%sx%s", this.customResolution().getV1(), this.customResolution().getV2()) : "(not set)"), 47 | String.format("Quickplay (path, singleplayer, multiplayer, realms): %s, %s, %s, %s", this.quickPlayPath(), this.quickPlaySingleplayer(), this.quickPlayMultiplayer(), this.quickPlayRealms()) 48 | ); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/meta/RemoteVersionMetaSource.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.meta; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.net.URL; 6 | import java.util.Comparator; 7 | import java.util.List; 8 | import java.util.Map; 9 | import java.util.Optional; 10 | import java.util.function.Function; 11 | import java.util.stream.Collectors; 12 | 13 | import com.github.winplay02.gitcraft.util.FileSystemNetworkManager; 14 | import com.github.winplay02.gitcraft.util.SerializationHelper; 15 | import com.google.gson.reflect.TypeToken; 16 | 17 | public class RemoteVersionMetaSource> implements VersionMetaSource { 18 | 19 | private final String url; 20 | private final TypeToken> metaType; 21 | private final Function classifier; 22 | 23 | private Map latestVersions; 24 | 25 | public RemoteVersionMetaSource(String url, TypeToken> metaType, Function classifier) { 26 | this.url = url; 27 | this.metaType = metaType; 28 | this.classifier = classifier; 29 | } 30 | 31 | public M getLatest(String clas) throws IOException, URISyntaxException, InterruptedException { 32 | if (latestVersions == null) { 33 | List allVersions = SerializationHelper.deserialize(FileSystemNetworkManager.fetchAllFromURLSync(new URL(url)), metaType); 34 | Map> groupedVersions = allVersions.stream().collect(Collectors.groupingBy(classifier)); 35 | 36 | latestVersions = groupedVersions.values().stream().map(versions -> versions.stream().max(Comparator.naturalOrder())).filter(Optional::isPresent).map(Optional::get).collect(Collectors.toMap(classifier, Function.identity())); 37 | } 38 | 39 | return latestVersions.get(clas); 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/integrity/GitBlobSHA1Algorithm.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.integrity; 2 | 3 | import com.github.winplay02.gitcraft.config.IntegrityConfiguration; 4 | 5 | import java.nio.charset.StandardCharsets; 6 | import java.security.MessageDigest; 7 | import java.security.NoSuchAlgorithmException; 8 | import java.util.function.BiConsumer; 9 | import java.util.function.Function; 10 | 11 | /** 12 | * Integrity algorithm using SHA1 in the same way blobs in git are hashed. 13 | * This includes a {@code "blob "} prefix followed by the amount of bytes and terminated with {@code "\0"}. 14 | */ 15 | public class GitBlobSHA1Algorithm extends IntegrityAlgorithm { 16 | private static final byte[] BLOB_BYTES = new byte[]{(byte) 'b', (byte) 'l', (byte) 'o', (byte) 'b', (byte) ' '}; 17 | 18 | /** 19 | * Construct this algorithm with a configuration. 20 | * 21 | * @param configuration Configuration 22 | */ 23 | public GitBlobSHA1Algorithm(IntegrityConfiguration configuration) { 24 | super(configuration); 25 | } 26 | 27 | @Override 28 | public String getAlgorithmName() { 29 | return "Git-Blob-SHA1"; 30 | } 31 | 32 | @Override 33 | protected byte[] calculateChecksum(T object, BiConsumer> objectBytesExtractor, Function objectLengthExtractor) { 34 | try { 35 | MessageDigest digest = MessageDigest.getInstance("SHA1"); 36 | digest.update(BLOB_BYTES); 37 | digest.update(String.valueOf(objectLengthExtractor.apply(object)).getBytes(StandardCharsets.US_ASCII)); 38 | digest.update((byte) 0); 39 | objectBytesExtractor.accept(object, (bytes, length) -> digest.update(bytes, 0, length)); 40 | return digest.digest(); 41 | } catch (NoSuchAlgorithmException e) { 42 | throw new RuntimeException(e); 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /changelog.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## 0.3 4 | 5 | #### Breaking changes 6 | 7 | - intermediate artifacts are now fully qualified, using all steps that modified them 8 | - all artifacts now record (part of) the hash of their source artifact(s) 9 | - multiple meta sources can use the same version id, but referencing different artifacts 10 | - asset indexes now qualify their names even more fully 11 | 12 | > [!WARNING] 13 | > Unpicking is no longer enabled by default 14 | 15 | > [!NOTE] 16 | > **More information in --help** 17 | 18 | #### Other changes 19 | 20 | - new meta sources: skyrising, mojang_historic 21 | - new mappings: Ornithe {calamus, feather}, Mojmap+Yarn 22 | - unpicking (mostly) independent of mappings; support for unpick-v3 23 | - new steps for legacy versions: patching local variable tables, nesting (correct nested classes), apply signature patches (to handle generics), applying exception patches, preening (undo merging of specialized and bridge methods) 24 | - multithreading 25 | - garbage collecting for repositories can now be enabled (and is enabled by default) 26 | - updated dependencies 27 | 28 | > [!CAUTION] 29 | > Most of the metadata store will be unusable after this upgrade. 30 | > It is recommended to remove most of the artifact-store. Redundant data won't be used but will still use storage space. 31 | 32 | ## 0.2 33 | 34 | - More known versions are automatically downloaded (like experimental snapshots) 35 | - Commits may now be merges of multiple previous versions 36 | - Datagen is executed by default (registry reports, NBT -> SNBT) 37 | - Vanilla worldgen datapack is now downloaded for versions, where there are no other ways of obtaining these files 38 | - Comments are enabled for yarn generation 39 | - Constant unpicking is now done for yarn generation 40 | 41 | > [!NOTE] 42 | > **More information in --help** 43 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/UnpickProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 6 | import com.github.winplay02.gitcraft.pipeline.StepInput; 7 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 8 | import com.github.winplay02.gitcraft.pipeline.StepResults; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 11 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record UnpickProvider(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | StepStatus mergedStatus = config.unpickFlavour().provide(context, MinecraftJar.MERGED); 24 | if (mergedStatus.hasRun()) { 25 | return StepOutput.ofEmptyResultSet(mergedStatus); 26 | } 27 | StepStatus clientStatus = config.unpickFlavour().provide(context, MinecraftJar.CLIENT); 28 | StepStatus serverStatus = config.unpickFlavour().provide(context, MinecraftJar.SERVER); 29 | return StepOutput.ofEmptyResultSet(StepStatus.merge(clientStatus, serverStatus)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/MappingsProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 4 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 5 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 6 | import com.github.winplay02.gitcraft.pipeline.StepInput; 7 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 8 | import com.github.winplay02.gitcraft.pipeline.StepResults; 9 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record MappingsProvider(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | StepStatus mergedStatus = config.mappingFlavour().provide(context, MinecraftJar.MERGED); 24 | if (mergedStatus.hasRun()) { 25 | return StepOutput.ofEmptyResultSet(mergedStatus); 26 | } 27 | StepStatus clientStatus = config.mappingFlavour().provide(context, MinecraftJar.CLIENT); 28 | StepStatus serverStatus = config.mappingFlavour().provide(context, MinecraftJar.SERVER); 29 | return StepOutput.ofEmptyResultSet(StepStatus.merge(clientStatus, serverStatus)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/ExceptionsProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 6 | import com.github.winplay02.gitcraft.pipeline.StepInput; 7 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 8 | import com.github.winplay02.gitcraft.pipeline.StepResults; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 11 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record ExceptionsProvider(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | StepStatus mergedStatus = config.exceptionsFlavour().provide(context, MinecraftJar.MERGED); 24 | if (mergedStatus.hasRun()) { 25 | return StepOutput.ofEmptyResultSet(mergedStatus); 26 | } 27 | StepStatus clientStatus = config.exceptionsFlavour().provide(context, MinecraftJar.CLIENT); 28 | StepStatus serverStatus = config.exceptionsFlavour().provide(context, MinecraftJar.SERVER); 29 | return StepOutput.ofEmptyResultSet(StepStatus.merge(clientStatus, serverStatus)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/SignaturesProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 6 | import com.github.winplay02.gitcraft.pipeline.StepInput; 7 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 8 | import com.github.winplay02.gitcraft.pipeline.StepResults; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 11 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record SignaturesProvider(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | StepStatus mergedStatus = config.signaturesFlavour().provide(context, MinecraftJar.MERGED); 24 | if (mergedStatus.hasRun()) { 25 | return StepOutput.ofEmptyResultSet(mergedStatus); 26 | } 27 | StepStatus clientStatus = config.signaturesFlavour().provide(context, MinecraftJar.CLIENT); 28 | StepStatus serverStatus = config.signaturesFlavour().provide(context, MinecraftJar.SERVER); 29 | return StepOutput.ofEmptyResultSet(StepStatus.merge(clientStatus, serverStatus)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/NestsProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 6 | import com.github.winplay02.gitcraft.pipeline.StepInput; 7 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 8 | import com.github.winplay02.gitcraft.pipeline.StepResults; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 11 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | 14 | public record NestsProvider(GitCraftStepConfig config) implements GitCraftStepWorker { 15 | 16 | @Override 17 | public StepOutput, GitCraftStepConfig> run( 18 | IPipeline, GitCraftStepConfig> pipeline, 19 | IStepContext.SimpleStepContext context, 20 | StepInput.Empty input, 21 | StepResults, GitCraftStepConfig> results 22 | ) throws Exception { 23 | StepStatus mergedStatus = config.nestsFlavour().provide(context, MinecraftJar.MERGED, config.mappingFlavour()); 24 | if (mergedStatus.hasRun()) { 25 | return StepOutput.ofEmptyResultSet(mergedStatus); 26 | } 27 | StepStatus clientStatus = config.nestsFlavour().provide(context, MinecraftJar.CLIENT, config.mappingFlavour()); 28 | StepStatus serverStatus = config.nestsFlavour().provide(context, MinecraftJar.SERVER, config.mappingFlavour()); 29 | return StepOutput.ofEmptyResultSet(StepStatus.merge(clientStatus, serverStatus)); 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/unpick/UnpickFlavour.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.unpick; 2 | 3 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 6 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 7 | import com.github.winplay02.gitcraft.types.OrderedVersion; 8 | import com.github.winplay02.gitcraft.util.LazyValue; 9 | 10 | import java.io.IOException; 11 | import java.util.Locale; 12 | import java.util.function.Supplier; 13 | 14 | public enum UnpickFlavour { 15 | NONE(NoneUnpick::new), 16 | YARN(YarnUnpick::new), 17 | FEATHER(FeatherUnpick::new); 18 | 19 | private final LazyValue impl; 20 | 21 | UnpickFlavour(Supplier unpick) { 22 | this.impl = LazyValue.of(unpick); 23 | } 24 | 25 | @Override 26 | public String toString() { 27 | return super.toString().toLowerCase(Locale.ROOT); 28 | } 29 | 30 | public StepStatus provide(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException { 31 | return this.impl.get().provideUnpick(versionContext, minecraftJar); 32 | } 33 | 34 | public Unpick.UnpickContext getContext(OrderedVersion targetVersion, MinecraftJar minecraftJar) throws IOException { 35 | return this.impl.get().getContext(targetVersion, minecraftJar); 36 | } 37 | 38 | public boolean exists(OrderedVersion targetVersion) { 39 | return this.impl.get().doesUnpickInformationExist(targetVersion); 40 | } 41 | 42 | public MappingFlavour applicableMappingFlavour(UnpickDescriptionFile unpickDescription) { 43 | return this.impl.get().applicableMappingFlavour(unpickDescription); 44 | } 45 | 46 | public boolean supportsRemapping(UnpickDescriptionFile unpickDescription) { 47 | return this.impl.get().supportsUnpickRemapping(unpickDescription); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/unpick/Unpick.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.unpick; 2 | 3 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 6 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 7 | import com.github.winplay02.gitcraft.types.OrderedVersion; 8 | import daomephsta.unpick.constantmappers.datadriven.parser.v2.UnpickV2Reader; 9 | import daomephsta.unpick.constantmappers.datadriven.parser.v3.UnpickV3Reader; 10 | 11 | import java.io.IOException; 12 | import java.nio.file.Files; 13 | import java.nio.file.Path; 14 | 15 | public interface Unpick { 16 | 17 | record UnpickContext(Path unpickConstants, Path unpickDefinitions, Path unpickDescription) { 18 | } 19 | 20 | StepStatus provideUnpick(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException; 21 | 22 | UnpickContext getContext(OrderedVersion targetVersion, MinecraftJar minecraftJar) throws IOException; 23 | 24 | boolean doesUnpickInformationExist(OrderedVersion mcVersion); 25 | 26 | MappingFlavour applicableMappingFlavour(UnpickDescriptionFile unpickDescription); 27 | 28 | boolean supportsUnpickRemapping(UnpickDescriptionFile unpickDescription); 29 | 30 | static boolean validateUnpickDefinitionsV2(Path unpickDefinitionsPath) { 31 | try (UnpickV2Reader r = new UnpickV2Reader(Files.newInputStream(unpickDefinitionsPath))) { 32 | r.accept(new UnpickV2Reader.Visitor() { }); 33 | return true; 34 | } catch (IOException e) { 35 | return false; 36 | } 37 | } 38 | 39 | static boolean validateUnpickDefinitionsV3(Path unpickDefinitionsPath) { 40 | try (UnpickV3Reader r = new UnpickV3Reader(Files.newBufferedReader(unpickDefinitionsPath))) { 41 | // TODO validate 42 | return true; 43 | } catch (IOException e) { 44 | return false; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/StepResults.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 5 | 6 | import java.nio.file.Path; 7 | import java.util.Optional; 8 | import java.util.Set; 9 | import java.util.concurrent.ConcurrentHashMap; 10 | 11 | public record StepResults, C extends IStepContext, D extends IStepConfig>(Set result) { 12 | public static , C extends IStepContext, D extends IStepConfig> StepResults ofEmpty() { 13 | return new StepResults<>(ConcurrentHashMap.newKeySet()); 14 | } 15 | 16 | public void addKey(StorageKey storageKey) { 17 | this.result.add(storageKey); 18 | } 19 | 20 | public Path getPathForKeyAndAdd(IPipeline pipeline, C context, D config, StorageKey storageKey) { 21 | this.result.add(storageKey); 22 | return pipeline.getStoragePath(storageKey, context, config); 23 | } 24 | 25 | public Path getPathForDifferentVersionKeyAndAdd(IPipeline pipeline, C context, D config, StorageKey storageKey, T version) { 26 | this.result.add(storageKey); 27 | if (!version.equals(context.targetVersion())) { 28 | pipeline.relinkStoragePathToDifferentVersion(storageKey, context, config, version); 29 | } 30 | return pipeline.getStoragePath(storageKey, context, config); 31 | } 32 | 33 | public Optional getKeyIfExists(StorageKey key) { 34 | return this.result.contains(key) ? Optional.of(key) : Optional.empty(); 35 | } 36 | 37 | public Optional getKeyByPriority(StorageKey... keys) { 38 | for (StorageKey key : keys) { 39 | if (this.result.contains(key)) { 40 | return Optional.of(key); 41 | } 42 | } 43 | return Optional.empty(); 44 | } 45 | 46 | public void addAll(StepResults results) { 47 | this.result.addAll(results.result); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/LibrariesFetcher.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 4 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 5 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 6 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 7 | import com.github.winplay02.gitcraft.pipeline.StepInput; 8 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 9 | import com.github.winplay02.gitcraft.pipeline.StepResults; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | import com.github.winplay02.gitcraft.util.MiscHelper; 14 | 15 | import java.nio.file.Files; 16 | import java.nio.file.Path; 17 | import java.util.List; 18 | import java.util.concurrent.Callable; 19 | 20 | public record LibrariesFetcher(GitCraftStepConfig config) implements GitCraftStepWorker { 21 | 22 | @Override 23 | public StepOutput, GitCraftStepConfig> run( 24 | IPipeline, GitCraftStepConfig> pipeline, 25 | IStepContext.SimpleStepContext context, 26 | StepInput.Empty input, 27 | StepResults, GitCraftStepConfig> results 28 | ) throws Exception { 29 | Path librariesDir = Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.LIBRARIES)); 30 | 31 | int maxRunningTasks = 32; 32 | List statuses = MiscHelper.runTasksInParallelAndAwaitResult( 33 | maxRunningTasks, 34 | context.executorService(), 35 | context.targetVersion().libraries().stream().>map(library -> () -> library.fetchArtifact(context.executorService(), librariesDir, "library")).toList() 36 | ); 37 | 38 | return new StepOutput<>(StepStatus.merge(statuses), results); 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/mappings/IdentityMappings.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.mappings; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Path; 5 | import java.util.Collections; 6 | 7 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 8 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.types.OrderedVersion; 11 | 12 | import net.fabricmc.loom.api.mappings.layered.MappingsNamespace; 13 | import net.fabricmc.mappingio.MappingVisitor; 14 | 15 | public class IdentityMappings extends Mapping { 16 | @Override 17 | public String getName() { 18 | return "Identity (unmapped)"; 19 | } 20 | 21 | @Override 22 | public boolean isMappingFileRequired() { 23 | return false; 24 | } 25 | 26 | @Override 27 | public boolean needsPackageFixingForLaunch() { 28 | return false; 29 | } 30 | 31 | @Override 32 | public String getDestinationNS() { 33 | return MappingsNamespace.NAMED.toString(); 34 | } 35 | 36 | @Override 37 | public boolean doMappingsExist(OrderedVersion mcVersion) { 38 | return true; 39 | } 40 | 41 | @Override 42 | public boolean doMappingsExist(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 43 | return true; 44 | } 45 | 46 | @Override 47 | public boolean canMappingsBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 48 | return true; 49 | } 50 | 51 | @Override 52 | public StepStatus provideMappings(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException { 53 | return StepStatus.SUCCESS; 54 | } 55 | 56 | @Override 57 | protected Path getMappingsPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 58 | return null; 59 | } 60 | 61 | @Override 62 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingVisitor visitor) throws IOException { 63 | visitor.visitNamespaces(getSourceNS(), Collections.emptyList()); 64 | } 65 | 66 | @Override 67 | public Path executeCustomRemappingLogic(Path previousFile, OrderedVersion mcVersion, MinecraftJar minecraftJar) { 68 | return previousFile; 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/util/ImmutableMultiSetView.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import java.util.Collection; 4 | import java.util.Iterator; 5 | import java.util.List; 6 | import java.util.Set; 7 | import java.util.stream.Collectors; 8 | 9 | /** 10 | * An immutable view over multiple {@link Set}s 11 | * @param backed Collection containing sets backing this view 12 | * @param Type of elements contained in this view 13 | */ 14 | public record ImmutableMultiSetView (Collection> backed) implements Set { 15 | 16 | /** 17 | * Create an immutable {@link Set} view from multiple sets 18 | * @param backed Varargs Array of sets 19 | */ 20 | @SafeVarargs 21 | public ImmutableMultiSetView(Set... backed) { 22 | this(List.of(backed)); 23 | } 24 | 25 | @Override 26 | public int size() { 27 | return 0; 28 | } 29 | 30 | @Override 31 | public boolean isEmpty() { 32 | return backed.stream().allMatch(Set::isEmpty); 33 | } 34 | 35 | @Override 36 | public boolean contains(Object o) { 37 | return backed.stream().anyMatch(set -> set.contains(o)); 38 | } 39 | 40 | @Override 41 | public Iterator iterator() { 42 | return backed.stream().flatMap(Collection::stream).distinct().iterator(); 43 | } 44 | 45 | @Override 46 | public Object[] toArray() { 47 | return backed.stream().flatMap(Collection::stream).distinct().toArray(); 48 | } 49 | 50 | @Override 51 | public T[] toArray(T[] a) { 52 | return backed.stream().flatMap(Collection::stream).collect(Collectors.toUnmodifiableSet()).toArray(a); 53 | } 54 | 55 | @Override 56 | public boolean add(E e) { 57 | return false; 58 | } 59 | 60 | @Override 61 | public boolean remove(Object o) { 62 | return false; 63 | } 64 | 65 | @Override 66 | public boolean containsAll(Collection c) { 67 | return backed.stream().allMatch(set -> set.containsAll(c)); 68 | } 69 | 70 | @Override 71 | public boolean addAll(Collection c) { 72 | return false; 73 | } 74 | 75 | @Override 76 | public boolean retainAll(Collection c) { 77 | return false; 78 | } 79 | 80 | @Override 81 | public boolean removeAll(Collection c) { 82 | return false; 83 | } 84 | 85 | @Override 86 | public void clear() { 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/metadata/VersionInfo.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest.metadata; 2 | 3 | import com.github.winplay02.gitcraft.util.SerializationTypes; 4 | import com.google.gson.annotations.JsonAdapter; 5 | 6 | import java.time.ZonedDateTime; 7 | import java.util.List; 8 | 9 | public record VersionInfo(ArtifactMetadata assetIndex, String assets, Downloads downloads, String id, 10 | JavaVersion javaVersion, List libraries, String mainClass, 11 | ZonedDateTime releaseTime, ZonedDateTime time, String type, VersionArguments arguments, 12 | String minecraftArguments) { 13 | public record Downloads(ArtifactMetadata client, ArtifactMetadata client_mappings, ArtifactMetadata server, 14 | ArtifactMetadata server_mappings, ArtifactMetadata windows_server, ArtifactMetadata server_zip) { 15 | } 16 | 17 | public record JavaVersion(int majorVersion) { 18 | } 19 | 20 | public VersionInfo withUpdatedId(String versionId) { 21 | if (this.id().equals(versionId)) { 22 | return this; 23 | } 24 | return new VersionInfo(this.assetIndex(), this.assets(), this.downloads(), versionId, this.javaVersion(), this.libraries(), this.mainClass(), this.releaseTime(), this.time(), this.type(), this.arguments(), this.minecraftArguments()); 25 | } 26 | 27 | public record VersionArguments(List game, List jvm) { 28 | } 29 | 30 | public record VersionArgumentWithRules( 31 | @JsonAdapter(SerializationTypes.ConvertToList.class) List value, 32 | List rules) { 33 | } 34 | 35 | public record VersionArgumentRule(String action, VersionArgumentRuleFeatures features, VersionArgumentOS os) { 36 | } 37 | 38 | public record VersionArgumentRuleFeatures(boolean is_demo_user, boolean has_custom_resolution, 39 | boolean has_quick_plays_support, boolean is_quick_play_singleplayer, 40 | boolean is_quick_play_multiplayer, boolean is_quick_play_realms) { 41 | public static final VersionArgumentRuleFeatures EMPTY = new VersionArgumentRuleFeatures(false, false, false, false, false, false); 42 | } 43 | 44 | public record VersionArgumentOS(String name, String version, String arch) { 45 | } 46 | } 47 | 48 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/manifest/MetadataProvider.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.manifest; 2 | 3 | import java.io.IOException; 4 | import java.util.List; 5 | import java.util.Map; 6 | import java.util.concurrent.Executor; 7 | 8 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 9 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 10 | 11 | public interface MetadataProvider> { 12 | ManifestSource getSource(); 13 | 14 | /** 15 | * @return A human-readable string that identifies this manifest provider. 16 | */ 17 | String getName(); 18 | 19 | /** 20 | * A string that uniquely identifies this metadata provider. 21 | * This string should be usable in a filesystem path 22 | * 23 | * @return Name that identifies this manifest provider 24 | */ 25 | String getInternalName(); 26 | 27 | /** 28 | * Loads versions using given executor if they were not already loaded. Executor must be not null. 29 | * @return A map containing all available versions, keyed by a unique name (see {@linkplain VersionInfo#id VersionInfo.id}). 30 | */ 31 | Map getVersions(Executor executor) throws IOException; 32 | 33 | /** 34 | * When calling the versions must be already loaded. Crashes if not. 35 | * @return A map containing all available versions, keyed by a unique name (see {@linkplain VersionInfo#id VersionInfo.id}). 36 | */ 37 | Map getVersionsAssumeLoaded(); 38 | 39 | /** 40 | * Finds parent nodes to the provided version. Used to construct the {@link com.github.winplay02.gitcraft.graph.AbstractVersionGraph}. 41 | * 42 | * @param mcVersion Subject version 43 | * @return List of parent versions, or an empty list, if the provided version is the root version. {@code null} is returned, if the default ordering should be used (specified by {@link E}) 44 | */ 45 | List getParentVersions(E mcVersion); 46 | 47 | E getVersionByVersionID(String versionId); 48 | 49 | /** 50 | * @return whether this version should definitely not appear in a main branch of the version graph 51 | */ 52 | boolean shouldExcludeFromMainBranch(E mcVersion); 53 | 54 | /** 55 | * @return whether this version should be excluded from the version graph 56 | */ 57 | boolean shouldExclude(E mcVersion); 58 | } 59 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/types/Artifact.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.types; 2 | 3 | import com.github.winplay02.gitcraft.Library; 4 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 5 | import com.github.winplay02.gitcraft.util.FileSystemNetworkManager; 6 | import com.github.winplay02.gitcraft.util.RemoteHelper; 7 | 8 | import java.nio.file.Path; 9 | import java.util.concurrent.Executor; 10 | 11 | /** 12 | * Single artifact 13 | * 14 | * @param url Download URL 15 | * @param name Artifact File name 16 | * @param sha1sum SHA1 Checksum 17 | */ 18 | public record Artifact(String url, String name, String sha1sum) { 19 | public Artifact(String url, String name) { 20 | this(url, name, null); 21 | } 22 | 23 | public static Artifact fromURL(String url, String sha1sum) { 24 | return new Artifact(url, nameFromUrl(url), sha1sum); 25 | } 26 | 27 | public Path resolve(Path containingPath) { 28 | return containingPath.resolve(name); 29 | } 30 | 31 | public StepStatus fetchArtifact(Executor executor, Path containingPath) { 32 | return fetchArtifact(executor, containingPath, "artifact"); 33 | } 34 | 35 | public StepStatus fetchArtifact(Executor executor, Path containingPath, String artifactKind) { 36 | Path path = resolve(containingPath); 37 | return RemoteHelper.downloadToFileWithChecksumIfNotExists(executor, url, new FileSystemNetworkManager.LocalFileInfo(path, sha1sum, Library.IA_SHA1, artifactKind, name)); 38 | } 39 | 40 | public StepStatus fetchArtifactToFile(Executor executor, Path filePath, String artifactKind) { 41 | return RemoteHelper.downloadToFileWithChecksumIfNotExists(executor, url, new FileSystemNetworkManager.LocalFileInfo(filePath, sha1sum, Library.IA_SHA1, artifactKind, name)); 42 | } 43 | 44 | public static String nameFromUrl(String url) { 45 | if (url == null) { 46 | return ""; 47 | } 48 | String[] urlParts = url.split("/"); 49 | return urlParts[urlParts.length - 1]; 50 | } 51 | 52 | public record DependencyArtifact(String sha1sum, String name, String url) { 53 | public DependencyArtifact(Artifact artifact) { 54 | this(artifact.sha1sum(), artifact.name(), artifact.url()); 55 | } 56 | 57 | public static DependencyArtifact ofVirtual(String name) { 58 | return new DependencyArtifact(null, name, null); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /launcher_agent/src/main/java/com/github/winplay02/gitcraft/launchagent/GitCraftResourceDownloaderTransformer.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launchagent; 2 | 3 | import java.lang.classfile.ClassFile; 4 | import java.lang.classfile.ClassModel; 5 | import java.lang.classfile.ClassTransform; 6 | import java.lang.classfile.constantpool.PoolEntry; 7 | import java.lang.classfile.constantpool.Utf8Entry; 8 | import java.lang.classfile.instruction.ConstantInstruction; 9 | import java.lang.instrument.ClassFileTransformer; 10 | import java.lang.instrument.IllegalClassFormatException; 11 | import java.security.ProtectionDomain; 12 | 13 | public class GitCraftResourceDownloaderTransformer implements ClassFileTransformer { 14 | public static final String DEAD_RESOURCE_URL = "http://s3.amazonaws.com/MinecraftResources/"; 15 | public static final String REPLACEMENT_RESOURCE_URL = "https://web.archive.org/web/20220916212449if_/https://s3.amazonaws.com/MinecraftResources"; 16 | 17 | @Override 18 | public byte[] transform(ClassLoader loader, String fullyQualifiedClassName, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 19 | ClassFile classFile = ClassFile.of(); 20 | ClassModel classModel = classFile.parse(classfileBuffer); 21 | if (!isClassRelevantToTransform(classModel)) { 22 | return null; 23 | } 24 | System.out.printf("[GitCraft Agent]: Transforming %s (by ResourceDownloaderTransformer)%n", fullyQualifiedClassName); 25 | return transform_class_with_resource_downloader(classFile, classModel); 26 | } 27 | 28 | private boolean isClassRelevantToTransform(ClassModel classModel) { 29 | for (PoolEntry poolEntry : classModel.constantPool()) { 30 | if (poolEntry instanceof Utf8Entry utf8Entry) { 31 | if (utf8Entry.equalsString(DEAD_RESOURCE_URL)) { 32 | return true; 33 | } 34 | } 35 | } 36 | return false; 37 | } 38 | 39 | private byte[] transform_class_with_resource_downloader(ClassFile classFile, ClassModel classModel) { 40 | return classFile.transformClass(classModel, ClassTransform.transformingMethodBodies((codeBuilder, codeElement) -> { 41 | if (codeElement instanceof ConstantInstruction.LoadConstantInstruction ldc) { 42 | if (ldc.constantValue().equals(DEAD_RESOURCE_URL)) { 43 | codeBuilder.loadConstant(REPLACEMENT_RESOURCE_URL); 44 | return; 45 | } 46 | } 47 | codeBuilder.accept(codeElement); 48 | })); 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/launcher/GitCraftLauncher.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launcher; 2 | 3 | import com.github.winplay02.gitcraft.GitCraftApplication; 4 | import com.github.winplay02.gitcraft.config.ApplicationConfiguration; 5 | import com.github.winplay02.gitcraft.config.Configuration; 6 | import com.github.winplay02.gitcraft.config.DataConfiguration; 7 | import com.github.winplay02.gitcraft.config.RepositoryConfiguration; 8 | import com.github.winplay02.gitcraft.config.TransientApplicationConfiguration; 9 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineDescription; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 12 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 13 | import com.github.winplay02.gitcraft.util.SerializationHelper; 14 | import com.github.winplay02.gitcraft.util.SerializationTypes; 15 | 16 | public class GitCraftLauncher extends GitCraftApplication { 17 | 18 | public static void main(String... args) throws Exception { 19 | new GitCraftLauncher().mainEntrypoint(args); 20 | } 21 | 22 | public static LauncherConfig getLauncherConfig() { 23 | return Configuration.getConfiguration(LauncherConfig.class); 24 | } 25 | 26 | @Override 27 | public boolean initialize(String... args) { 28 | Configuration.register("gitcraft_repository", RepositoryConfiguration.class, RepositoryConfiguration::deserialize); 29 | Configuration.register("gitcraft_dataimport", DataConfiguration.class, DataConfiguration::deserialize); 30 | Configuration.register("gitcraft_application", ApplicationConfiguration.class, ApplicationConfiguration::deserialize); 31 | Configuration.register("gitcraft_application_transient", TransientApplicationConfiguration.class, TransientApplicationConfiguration::deserialize); 32 | Configuration.register("gitcraft_launcher", LauncherConfig.class, LauncherConfig::deserialize); 33 | SerializationHelper.registerTypeAdapter(VersionInfo.VersionArgumentWithRules.class, SerializationTypes.VersionArgumentWithRulesAdapter::new); 34 | if (!GitCraftLauncherCli.handleCliArgs(args)) { 35 | return false; 36 | } 37 | return true; 38 | } 39 | 40 | @Override 41 | public void run() throws Exception { 42 | versionGraph = doVersionGraphOperations(versionGraph); 43 | IPipeline.run(GitCraftPipelineDescription.LAUNCH_PIPELINE, GitCraftPipelineFilesystemStorage.DEFAULT.get(), null, versionGraph); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/exceptions/ExceptionsFlavour.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.exceptions; 2 | 3 | import com.github.winplay02.gitcraft.exceptions.ornithe.RavenExceptions; 4 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 5 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 6 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 7 | import com.github.winplay02.gitcraft.types.OrderedVersion; 8 | import com.github.winplay02.gitcraft.util.LazyValue; 9 | 10 | import net.ornithemc.exceptor.io.ExceptionsFile; 11 | 12 | import java.io.IOException; 13 | import java.net.URISyntaxException; 14 | import java.nio.file.Path; 15 | import java.util.Locale; 16 | import java.util.Optional; 17 | import java.util.function.Supplier; 18 | 19 | public enum ExceptionsFlavour { 20 | RAVEN(RavenExceptions::new), 21 | NONE(NoneExceptions::new); 22 | 23 | private final LazyValue impl; 24 | 25 | ExceptionsFlavour(Supplier exceptions) { 26 | this.impl = LazyValue.of(exceptions); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return super.toString().toLowerCase(Locale.ROOT); 32 | } 33 | 34 | public String getName() { 35 | return impl.get().getName(); 36 | } 37 | 38 | public boolean exists(OrderedVersion mcVersion) { 39 | return impl.get().doExceptionsExist(mcVersion); 40 | } 41 | 42 | public boolean exists(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 43 | return impl.get().doExceptionsExist(mcVersion, minecraftJar); 44 | } 45 | 46 | public boolean canBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 47 | return impl.get().canExceptionsBeUsedOn(mcVersion, minecraftJar); 48 | } 49 | 50 | public StepStatus provide(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException, URISyntaxException, InterruptedException { 51 | return impl.get().provideExceptions(versionContext, minecraftJar); 52 | } 53 | 54 | public Optional getPath(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 55 | return impl.get().getExceptionsPath(mcVersion, minecraftJar); 56 | } 57 | 58 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, ExceptionsFile visitor) throws IOException { 59 | impl.get().visit(mcVersion, minecraftJar, visitor); 60 | } 61 | 62 | public ExceptionsFile getExceptions(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 63 | return impl.get().getExceptions(mcVersion, minecraftJar); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/signatures/SignaturesFlavour.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.signatures; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 4 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 5 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 6 | import com.github.winplay02.gitcraft.signatures.ornithe.SparrowSignatures; 7 | import com.github.winplay02.gitcraft.types.OrderedVersion; 8 | import com.github.winplay02.gitcraft.util.LazyValue; 9 | 10 | import io.github.gaming32.signaturechanger.tree.SigsFile; 11 | 12 | import java.io.IOException; 13 | import java.net.URISyntaxException; 14 | import java.nio.file.Path; 15 | import java.util.Locale; 16 | import java.util.Optional; 17 | import java.util.function.Supplier; 18 | 19 | public enum SignaturesFlavour { 20 | SPARROW(SparrowSignatures::new), 21 | NONE(NoneSignatures::new); 22 | 23 | private final LazyValue impl; 24 | 25 | SignaturesFlavour(Supplier signatures) { 26 | this.impl = LazyValue.of(signatures); 27 | } 28 | 29 | @Override 30 | public String toString() { 31 | return super.toString().toLowerCase(Locale.ROOT); 32 | } 33 | 34 | public String getName() { 35 | return impl.get().getName(); 36 | } 37 | 38 | public boolean exists(OrderedVersion mcVersion) { 39 | return impl.get().doSignaturesExist(mcVersion); 40 | } 41 | 42 | public boolean exists(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 43 | return impl.get().doSignaturesExist(mcVersion, minecraftJar); 44 | } 45 | 46 | public boolean canBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 47 | return impl.get().canSignaturesBeUsedOn(mcVersion, minecraftJar); 48 | } 49 | 50 | public StepStatus provide(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException, URISyntaxException, InterruptedException { 51 | return impl.get().provideSignatures(versionContext, minecraftJar); 52 | } 53 | 54 | public Optional getPath(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 55 | return impl.get().getSignaturesPath(mcVersion, minecraftJar); 56 | } 57 | 58 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, SigsFile visitor) throws IOException { 59 | impl.get().visit(mcVersion, minecraftJar, visitor); 60 | } 61 | 62 | public SigsFile getSignatures(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 63 | return impl.get().getSignatures(mcVersion, minecraftJar); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/nests/NestsFlavour.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.nests; 2 | 3 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 4 | import com.github.winplay02.gitcraft.nests.ornithe.OrnitheNests; 5 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 6 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 7 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 8 | import com.github.winplay02.gitcraft.types.OrderedVersion; 9 | import com.github.winplay02.gitcraft.util.LazyValue; 10 | 11 | import net.ornithemc.nester.nest.Nests; 12 | 13 | import java.io.IOException; 14 | import java.net.URISyntaxException; 15 | import java.nio.file.Path; 16 | import java.util.Locale; 17 | import java.util.Optional; 18 | import java.util.function.Supplier; 19 | 20 | public enum NestsFlavour { 21 | ORNITHE_NESTS(OrnitheNests::new), 22 | NONE(NoneNests::new); 23 | 24 | private final LazyValue impl; 25 | 26 | NestsFlavour(Supplier nests) { 27 | this.impl = LazyValue.of(nests); 28 | } 29 | 30 | @Override 31 | public String toString() { 32 | return super.toString().toLowerCase(Locale.ROOT); 33 | } 34 | 35 | public String getName() { 36 | return impl.get().getName(); 37 | } 38 | 39 | public boolean exists(OrderedVersion mcVersion) { 40 | return impl.get().doNestsExist(mcVersion); 41 | } 42 | 43 | public boolean exists(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 44 | return impl.get().doNestsExist(mcVersion, minecraftJar); 45 | } 46 | 47 | public boolean canBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) { 48 | return impl.get().canNestsBeUsedOn(mcVersion, minecraftJar, mappingFlavour); 49 | } 50 | 51 | public StepStatus provide(IStepContext versionContext, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) throws IOException, URISyntaxException, InterruptedException { 52 | return impl.get().provideNests(versionContext, minecraftJar, mappingFlavour); 53 | } 54 | 55 | public Optional getPath(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) { 56 | return impl.get().getNestsPath(mcVersion, minecraftJar, mappingFlavour); 57 | } 58 | 59 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour, Nests visitor) throws IOException { 60 | impl.get().visit(mcVersion, minecraftJar, mappingFlavour, visitor); 61 | } 62 | 63 | public Nests getNests(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingFlavour mappingFlavour) { 64 | return impl.get().getNests(mcVersion, minecraftJar, mappingFlavour); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/config/RepositoryConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.config; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | import static com.github.winplay02.gitcraft.config.Configuration.Utils.prim; 9 | 10 | /** 11 | * Repository Configuration 12 | * 13 | * @param gitUser Name of User-Identity used for git 14 | * @param gitMail Mail of User-Identity used for git 15 | * @param gitMainlineLinearBranch Branch name used for 'mainline' versions 16 | * @param createVersionBranches Whether branches should be created for all versions 17 | * @param createStableVersionBranches Whether branches should be created for stable versions 18 | * @param gcAfterRun Whether garbage-collection should be run after completing a run 19 | */ 20 | public record RepositoryConfiguration(String gitUser, 21 | String gitMail, 22 | String gitMainlineLinearBranch, 23 | boolean createVersionBranches, 24 | boolean createStableVersionBranches, 25 | boolean gcAfterRun) 26 | implements Configuration { 27 | 28 | public static final RepositoryConfiguration DEFAULT = new RepositoryConfiguration( 29 | "Mojang", 30 | "gitcraft@decompiled.mc", 31 | "master", 32 | false, 33 | false, 34 | true 35 | ); 36 | 37 | @Override 38 | public Map serialize() { 39 | return Map.of( 40 | "gitUser", prim(this.gitUser()), 41 | "gitMail", prim(this.gitMail()), 42 | "gitMainlineLinearBranch", prim(this.gitMainlineLinearBranch()), 43 | "createVersionBranches", prim(this.createVersionBranches()), 44 | "createStableVersionBranches", prim(this.createStableVersionBranches()), 45 | "gcAfterRun", prim(this.gcAfterRun()) 46 | ); 47 | } 48 | 49 | @Override 50 | public List generateInfo() { 51 | if (createVersionBranches) { 52 | return List.of("A separate branch will be created for each version."); 53 | } else if (createStableVersionBranches) { 54 | return List.of("A separate branch will be created for each stable version."); 55 | } 56 | return List.of(); 57 | } 58 | 59 | public static RepositoryConfiguration deserialize(Map map) { 60 | return new RepositoryConfiguration( 61 | Utils.getString(map, "gitUser", DEFAULT.gitUser()), 62 | Utils.getString(map, "gitMail", DEFAULT.gitMail()), 63 | Utils.getString(map, "gitMainlineLinearBranch", DEFAULT.gitMainlineLinearBranch()), 64 | Utils.getBoolean(map, "createVersionBranches", DEFAULT.createVersionBranches()), 65 | Utils.getBoolean(map, "createStableVersionBranches", DEFAULT.createStableVersionBranches()), 66 | Utils.getBoolean(map, "gcAfterRun", DEFAULT.gcAfterRun()) 67 | ); 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/StepOutput.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 5 | import com.github.winplay02.gitcraft.util.MiscHelper; 6 | 7 | import java.util.Arrays; 8 | import java.util.Collection; 9 | import java.util.HashSet; 10 | import java.util.Objects; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | import java.util.stream.Stream; 14 | 15 | public record StepOutput, C extends IStepContext, D extends IStepConfig>(StepStatus status, StepResults results) { 16 | public static , C extends IStepContext, D extends IStepConfig> StepOutput ofSingle(StepStatus status, StorageKey key) { 17 | return new StepOutput<>(status, new StepResults(Set.of(key))); 18 | } 19 | 20 | public static , C extends IStepContext, D extends IStepConfig> StepOutput ofEmptyResultSet(StepStatus status) { 21 | return new StepOutput<>(status, new StepResults(Set.of())); 22 | } 23 | 24 | @SafeVarargs 25 | public static , C extends IStepContext, D extends IStepConfig> StepOutput merge(StepOutput... outputs) { 26 | return new StepOutput<>( 27 | StepStatus.merge( 28 | Arrays.stream(outputs).filter(Objects::nonNull).map(StepOutput::status).collect(Collectors.toList())), 29 | new StepResults(MiscHelper.mergeSetsUnion( 30 | new HashSet<>(), Arrays.stream(outputs).filter(Objects::nonNull).map(output -> output.results().result()).toList() 31 | )) 32 | ); 33 | } 34 | 35 | public static , C extends IStepContext, D extends IStepConfig> StepOutput merge(Collection> outputs) { 36 | return new StepOutput<>( 37 | StepStatus.merge( 38 | outputs.stream().filter(Objects::nonNull).map(StepOutput::status).collect(Collectors.toList())), 39 | new StepResults(MiscHelper.mergeSetsUnion( 40 | new HashSet<>(), outputs.stream().filter(Objects::nonNull).map(output -> output.results().result()).toList() 41 | )) 42 | ); 43 | } 44 | 45 | @SafeVarargs 46 | public static , C extends IStepContext, D extends IStepConfig> StepOutput merge(StepResults otherResults, StepOutput... outputs) { 47 | return new StepOutput<>( 48 | StepStatus.merge( 49 | Arrays.stream(outputs).filter(Objects::nonNull).map(StepOutput::status).collect(Collectors.toList())), 50 | new StepResults(MiscHelper.mergeSetsUnion( 51 | new HashSet<>(), Stream.concat(Stream.of(otherResults.result()), Arrays.stream(outputs).filter(Objects::nonNull).map(output -> output.results().result())).toList() 52 | )) 53 | ); 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/test/groovy/com/github/winplay02/gitcraft/GitCraftTestingFs.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft; 2 | 3 | import com.github.winplay02.gitcraft.config.ApplicationConfiguration; 4 | import com.github.winplay02.gitcraft.config.Configuration; 5 | import com.github.winplay02.gitcraft.config.DataConfiguration; 6 | import com.github.winplay02.gitcraft.config.RepositoryConfiguration; 7 | import com.github.winplay02.gitcraft.config.TransientApplicationConfiguration; 8 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 9 | import com.github.winplay02.gitcraft.util.GitCraftPaths; 10 | import com.github.winplay02.gitcraft.util.MiscHelper; 11 | import com.github.winplay02.gitcraft.util.SerializationHelper; 12 | import com.github.winplay02.gitcraft.util.SerializationTypes; 13 | import org.junit.jupiter.api.extension.BeforeAllCallback; 14 | import org.junit.jupiter.api.extension.ExtensionContext; 15 | 16 | import java.io.IOException; 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | 20 | public class GitCraftTestingFs implements BeforeAllCallback, AutoCloseable { 21 | 22 | protected static Path temporaryTestingFsPath = null; 23 | //protected static FileSystemUtil.Delegate temporaryTestingFs; 24 | 25 | @Override 26 | public void beforeAll(ExtensionContext extensionContext) throws Exception { 27 | if (temporaryTestingFsPath == null) { 28 | temporaryTestingFsPath = Files.createTempDirectory("gitcraft"); 29 | LibraryPaths.init(temporaryTestingFsPath); 30 | Library.initialize(); 31 | Configuration.register("gitcraft_repository", RepositoryConfiguration.class, RepositoryConfiguration::deserialize); 32 | Configuration.register("gitcraft_dataimport", DataConfiguration.class, DataConfiguration::deserialize); 33 | Configuration.register("gitcraft_application", ApplicationConfiguration.class, ApplicationConfiguration::deserialize); 34 | Configuration.register("gitcraft_application_transient", TransientApplicationConfiguration.class, TransientApplicationConfiguration::deserialize); 35 | SerializationHelper.registerTypeAdapter(VersionInfo.VersionArgumentWithRules.class, SerializationTypes.VersionArgumentWithRulesAdapter::new); 36 | Library.applyConfiguration(); 37 | 38 | //temporaryTestingFs = FileSystemUtil.getJarFileSystem(temporaryTestingFsPath.resolve("testingfs.jar"), true); 39 | //GitCraftPaths.initializePaths(temporaryTestingFs.getPath(".")); 40 | //GitCraftPaths.initializePaths(temporaryTestingFsPath); 41 | GitCraftPaths.initializePaths(); 42 | extensionContext.getRoot().getStore(ExtensionContext.Namespace.GLOBAL).put("GitCraftTestingFs", this); 43 | } 44 | } 45 | 46 | @Override 47 | public void close() throws IOException { 48 | if (temporaryTestingFsPath != null) { 49 | // temporaryTestingFs.close(); 50 | // Files.delete(temporaryTestingFsPath.resolve("testingfs.jar")); 51 | // MiscHelper.deleteDirectory(temporaryTestingFsPath); 52 | temporaryTestingFsPath = null; 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/ArtifactsUnpacker.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.Optional; 6 | 7 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 8 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 9 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 11 | import com.github.winplay02.gitcraft.pipeline.StepInput; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 15 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 16 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 17 | import com.github.winplay02.gitcraft.types.OrderedVersion; 18 | import com.github.winplay02.gitcraft.util.MiscHelper; 19 | import net.fabricmc.loom.util.FileSystemUtil; 20 | 21 | public record ArtifactsUnpacker(GitCraftStepConfig config) implements GitCraftStepWorker { 22 | 23 | static final String SERVER_ZIP_JAR_NAME = "minecraft-server.jar"; 24 | 25 | @Override 26 | public StepOutput, GitCraftStepConfig> run( 27 | IPipeline, GitCraftStepConfig> pipeline, 28 | IStepContext.SimpleStepContext context, 29 | ArtifactsUnpacker.Inputs input, 30 | StepResults, GitCraftStepConfig> results 31 | ) throws Exception { 32 | if (input.serverZip().isEmpty()) { 33 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 34 | } 35 | // If an actual JAR exists, do not proceed 36 | Optional fetchedServerJarKey = results.getKeyIfExists(GitCraftPipelineFilesystemStorage.ARTIFACTS_SERVER_JAR); 37 | if (fetchedServerJarKey.isPresent()) { 38 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 39 | } 40 | Path serverZip = pipeline.getStoragePath(input.serverZip().orElseThrow(), context, this.config); 41 | Path unpackedServerJar = results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.UNPACKED_SERVER_JAR); 42 | 43 | if (Files.exists(unpackedServerJar) && !MiscHelper.isJarEmpty(unpackedServerJar)) { 44 | return new StepOutput<>(StepStatus.UP_TO_DATE, results); 45 | } 46 | if (Files.exists(unpackedServerJar)) { 47 | Files.delete(unpackedServerJar); 48 | } 49 | 50 | try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(serverZip)) { 51 | Files.copy(Files.newInputStream(fs.get().getPath(SERVER_ZIP_JAR_NAME)), unpackedServerJar); 52 | } 53 | return new StepOutput<>(StepStatus.SUCCESS, results); 54 | } 55 | 56 | public record Inputs(Optional serverZip) implements StepInput { 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/GitCraft.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft; 2 | 3 | import com.github.winplay02.gitcraft.config.ApplicationConfiguration; 4 | import com.github.winplay02.gitcraft.config.Configuration; 5 | import com.github.winplay02.gitcraft.config.DataConfiguration; 6 | import com.github.winplay02.gitcraft.config.RepositoryConfiguration; 7 | import com.github.winplay02.gitcraft.config.TransientApplicationConfiguration; 8 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 9 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineDescription; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 11 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 12 | import com.github.winplay02.gitcraft.util.MiscHelper; 13 | import com.github.winplay02.gitcraft.util.RepoWrapper; 14 | import com.github.winplay02.gitcraft.util.SerializationHelper; 15 | import com.github.winplay02.gitcraft.util.SerializationTypes; 16 | 17 | public class GitCraft extends GitCraftApplication { 18 | public static final String NAME = "GitCraft"; 19 | public static final String VERSION = "0.3.0"; 20 | 21 | public static final String FABRIC_MAVEN = "https://maven.fabricmc.net/"; 22 | public static final String ORNITHE_MAVEN = "https://maven.ornithemc.net/releases/"; 23 | 24 | public static void main(String... args) throws Exception { 25 | new GitCraft().mainEntrypoint(args); 26 | } 27 | 28 | @Override 29 | public boolean initialize(String... args) { 30 | Configuration.register("gitcraft_repository", RepositoryConfiguration.class, RepositoryConfiguration::deserialize); 31 | Configuration.register("gitcraft_dataimport", DataConfiguration.class, DataConfiguration::deserialize); 32 | Configuration.register("gitcraft_application", ApplicationConfiguration.class, ApplicationConfiguration::deserialize); 33 | Configuration.register("gitcraft_application_transient", TransientApplicationConfiguration.class, TransientApplicationConfiguration::deserialize); 34 | SerializationHelper.registerTypeAdapter(VersionInfo.VersionArgumentWithRules.class, SerializationTypes.VersionArgumentWithRulesAdapter::new); 35 | if (!GitCraftCli.handleCliArgs(args)) { 36 | return false; 37 | } 38 | return true; 39 | } 40 | 41 | @Override 42 | public void run() throws Exception { 43 | MiscHelper.println("Decompiler log output is suppressed!"); 44 | versionGraph = doVersionGraphOperations(versionGraph); 45 | resetVersionGraph = doVersionGraphOperationsForReset(versionGraph); 46 | try (RepoWrapper repo = getRepository()) { 47 | if (getTransientApplicationConfiguration().refreshDecompilation()) { 48 | IPipeline.run(GitCraftPipelineDescription.RESET_PIPELINE, GitCraftPipelineFilesystemStorage.DEFAULT.get(), repo, versionGraph); 49 | } 50 | IPipeline.run(GitCraftPipelineDescription.DEFAULT_PIPELINE, GitCraftPipelineFilesystemStorage.DEFAULT.get(), repo, versionGraph); 51 | if (getRepositoryConfiguration().gcAfterRun()) { 52 | IPipeline.run(GitCraftPipelineDescription.GC_PIPELINE, GitCraftPipelineFilesystemStorage.DEFAULT.get(), repo, versionGraph); 53 | } 54 | if (repo != null) { 55 | MiscHelper.println("Repo can be found at: %s", repo.getRootPath().toString()); 56 | } 57 | } 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @rem 2 | @rem Copyright 2015 the original author or authors. 3 | @rem 4 | @rem Licensed under the Apache License, Version 2.0 (the "License"); 5 | @rem you may not use this file except in compliance with the License. 6 | @rem You may obtain a copy of the License at 7 | @rem 8 | @rem https://www.apache.org/licenses/LICENSE-2.0 9 | @rem 10 | @rem Unless required by applicable law or agreed to in writing, software 11 | @rem distributed under the License is distributed on an "AS IS" BASIS, 12 | @rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 | @rem See the License for the specific language governing permissions and 14 | @rem limitations under the License. 15 | @rem 16 | @rem SPDX-License-Identifier: Apache-2.0 17 | @rem 18 | 19 | @if "%DEBUG%"=="" @echo off 20 | @rem ########################################################################## 21 | @rem 22 | @rem Gradle startup script for Windows 23 | @rem 24 | @rem ########################################################################## 25 | 26 | @rem Set local scope for the variables with windows NT shell 27 | if "%OS%"=="Windows_NT" setlocal 28 | 29 | set DIRNAME=%~dp0 30 | if "%DIRNAME%"=="" set DIRNAME=. 31 | @rem This is normally unused 32 | set APP_BASE_NAME=%~n0 33 | set APP_HOME=%DIRNAME% 34 | 35 | @rem Resolve any "." and ".." in APP_HOME to make it shorter. 36 | for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi 37 | 38 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 39 | set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" 40 | 41 | @rem Find java.exe 42 | if defined JAVA_HOME goto findJavaFromJavaHome 43 | 44 | set JAVA_EXE=java.exe 45 | %JAVA_EXE% -version >NUL 2>&1 46 | if %ERRORLEVEL% equ 0 goto execute 47 | 48 | echo. 1>&2 49 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 50 | echo. 1>&2 51 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 52 | echo location of your Java installation. 1>&2 53 | 54 | goto fail 55 | 56 | :findJavaFromJavaHome 57 | set JAVA_HOME=%JAVA_HOME:"=% 58 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 59 | 60 | if exist "%JAVA_EXE%" goto execute 61 | 62 | echo. 1>&2 63 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 64 | echo. 1>&2 65 | echo Please set the JAVA_HOME variable in your environment to match the 1>&2 66 | echo location of your Java installation. 1>&2 67 | 68 | goto fail 69 | 70 | :execute 71 | @rem Setup the command line 72 | 73 | 74 | 75 | @rem Execute Gradle 76 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* 77 | 78 | :end 79 | @rem End local scope for the variables with windows NT shell 80 | if %ERRORLEVEL% equ 0 goto mainEnd 81 | 82 | :fail 83 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 84 | rem the _cmd.exe /c_ return code! 85 | set EXIT_CODE=%ERRORLEVEL% 86 | if %EXIT_CODE% equ 0 set EXIT_CODE=1 87 | if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% 88 | exit /b %EXIT_CODE% 89 | 90 | :mainEnd 91 | if "%OS%"=="Windows_NT" endlocal 92 | 93 | :omega 94 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/PipelineExecutionGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | import com.github.winplay02.gitcraft.graph.AbstractVersionGraph; 5 | import com.github.winplay02.gitcraft.util.MiscHelper; 6 | 7 | import java.util.Collections; 8 | import java.util.HashMap; 9 | import java.util.HashSet; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.stream.Collectors; 13 | 14 | public record PipelineExecutionGraph, C extends IStepContext, D extends IStepConfig> 15 | (Set> stepVersionSubsetVertices, Map, Set>> stepVersionSubsetEdges) { 16 | 17 | public static , C extends IStepContext, D extends IStepConfig> PipelineExecutionGraph populate(PipelineDescription description, AbstractVersionGraph versionGraph) { 18 | Set> stepVersionSubsetVertices = new HashSet<>(); 19 | // directed: (target, source) 20 | Map, Set>> stepVersionSubsetEdges = new HashMap<>(); 21 | for (T version : versionGraph) { 22 | for (IStep step : description.steps()) { 23 | IPipeline.TupleVersionStep node = new IPipeline.TupleVersionStep<>(step, version); 24 | stepVersionSubsetVertices.add(node); 25 | stepVersionSubsetEdges.computeIfAbsent(node, __ -> new HashSet<>()); 26 | // Inter-Version dependency: depend on previous version only; logically should depend on all previous versions 27 | // but this is not necessary as this dependency applies transitively in a valid pipeline description (step depending on itself) 28 | for (IStep interVersionDependencyStep : description.getInterVersionDependencies(step)) { 29 | for (T previousVersion : versionGraph.getPreviousVertices(version)) { 30 | stepVersionSubsetEdges.get(node).add(new IPipeline.TupleVersionStep<>(interVersionDependencyStep, previousVersion)); 31 | } 32 | } 33 | // Intra-Version dependency 34 | for (IStep intraVersionDependencyStep : description.getIntraVersionDependencies(step)) { 35 | DependencyRelation depType = description.getDependencyType(step, intraVersionDependencyStep); 36 | if (depType != null && depType.isDependency()) { 37 | stepVersionSubsetEdges.get(node).add(new IPipeline.TupleVersionStep<>(intraVersionDependencyStep, version)); 38 | } 39 | } 40 | } 41 | } 42 | // TODO validate execution graph 43 | return new PipelineExecutionGraph<>(Collections.unmodifiableSet(stepVersionSubsetVertices), Collections.unmodifiableMap(stepVersionSubsetEdges)); 44 | } 45 | 46 | protected Set> nextTuples(Set> completedSubset) { 47 | return stepVersionSubsetEdges.entrySet().stream().filter(entry -> !completedSubset.contains(entry.getKey())).filter(entry -> MiscHelper.calculateAsymmetricSetDifference(entry.getValue(), completedSubset).isEmpty()).map(java.util.Map.Entry::getKey).collect(Collectors.toUnmodifiableSet()); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/GitCraftPipelineFilesystemRoot.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.function.Function; 7 | 8 | public final class GitCraftPipelineFilesystemRoot { 9 | 10 | private GitCraftPipelineFilesystemRoot() {} 11 | 12 | public static Function getDecompiled() { 13 | return root -> root.getByIndex("decompiled"); 14 | } 15 | 16 | public static Function getMappings() { 17 | return root -> root.getByIndex("mappings"); 18 | } 19 | 20 | public static Function getDefaultRepository() { 21 | return root -> root.getByIndex("minecraft-repo"); 22 | } 23 | 24 | public static Function getMcVersionStore() { 25 | return root -> root.getByIndex("mc-versions"); 26 | } 27 | 28 | public static Function getMcMetaStore() { 29 | return root -> root.getByIndex("mc-meta"); 30 | } 31 | 32 | public static Function getMcExtraVersionStore() { 33 | return root -> root.getByIndex("extra-versions"); 34 | } 35 | 36 | public static Function getMcMetaDownloads() { 37 | return root -> root.getByIndex("mc-meta-download"); 38 | } 39 | 40 | public static Function getLibraryStore() { 41 | return root -> root.getByIndex("libraries"); 42 | } 43 | 44 | public static Function getRemapped() { 45 | return root -> root.getByIndex("remapped-mc"); 46 | } 47 | 48 | public static Function getAssetsIndex() { 49 | return root -> root.getByIndex("assets-index"); 50 | } 51 | 52 | public static Function getAssetsObjects() { 53 | return root -> root.getByIndex("assets-objects"); 54 | } 55 | 56 | public static Function getPatchesStore() { 57 | return root -> root.getByIndex("patches"); 58 | } 59 | 60 | public static Function getPatchedStore() { 61 | return root -> root.getByIndex("patched"); 62 | } 63 | 64 | public static Function getRuntimeDirectory() { 65 | return root -> root.getByIndex("runtime"); 66 | } 67 | 68 | public static void initialize(IPipelineFilesystemRoot fsRoot) throws IOException { 69 | Files.createDirectories(getDecompiled().apply(fsRoot)); 70 | Files.createDirectories(getMappings().apply(fsRoot)); 71 | Files.createDirectories(getMcVersionStore().apply(fsRoot)); 72 | Files.createDirectories(getMcMetaStore().apply(fsRoot)); 73 | Files.createDirectories(getMcMetaDownloads().apply(fsRoot)); 74 | Files.createDirectories(getMcExtraVersionStore().apply(fsRoot)); 75 | Files.createDirectories(getLibraryStore().apply(fsRoot)); 76 | Files.createDirectories(getRemapped().apply(fsRoot)); 77 | Files.createDirectories(getAssetsIndex().apply(fsRoot)); 78 | Files.createDirectories(getAssetsObjects().apply(fsRoot)); 79 | Files.createDirectories(getPatchesStore().apply(fsRoot)); 80 | Files.createDirectories(getPatchedStore().apply(fsRoot)); 81 | Files.createDirectories(getRuntimeDirectory().apply(fsRoot)); 82 | } 83 | } 84 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/config/DataConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.config; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | import static com.github.winplay02.gitcraft.config.Configuration.Utils.prim; 10 | 11 | /** 12 | * Data Import Configuration 13 | * 14 | * @param loadIntegratedDatapack Whether the integrated datapack should be included in versioning 15 | * @param loadAssets Whether any integrated assets/resources should be included in versioning 16 | * @param loadAssetsExtern Whether any external (assigned via manifest) assets/resources should be included in versioning 17 | * @param readableNbt Whether raw NBT files should be converted to the readable SNBT format and additionally included in versioning 18 | * @param loadDatagenRegistry Whether datagenerated registry artifacts should be included in versioning 19 | * @param sortJsonObjects Whether JSON files should be sorted in a deterministic order to make them more comparable 20 | */ 21 | public record DataConfiguration(boolean loadIntegratedDatapack, 22 | boolean loadAssets, 23 | boolean loadAssetsExtern, 24 | boolean readableNbt, 25 | boolean loadDatagenRegistry, 26 | boolean sortJsonObjects) 27 | implements Configuration { 28 | 29 | public static final DataConfiguration DEFAULT = new DataConfiguration( 30 | true, 31 | true, 32 | true, 33 | true, 34 | true, 35 | false 36 | ); 37 | 38 | @Override 39 | public Map serialize() { 40 | return Map.of( 41 | "loadIntegratedDatapack", prim(this.loadIntegratedDatapack()), 42 | "loadAssets", prim(this.loadAssets()), 43 | "loadAssetsExtern", prim(this.loadAssetsExtern()), 44 | "readableNbt", prim(this.readableNbt()), 45 | "loadDatagenRegistry", prim(this.loadDatagenRegistry()), 46 | "sortJsonObjects", prim(this.sortJsonObjects()) 47 | ); 48 | } 49 | 50 | @Override 51 | public List generateInfo() { 52 | List info = new ArrayList<>(List.of( 53 | String.format("Integrated datapack versioning is: %s", this.loadIntegratedDatapack() ? "enabled" : "disabled"), 54 | String.format("Asset versioning is: %s", this.loadAssets() ? "enabled" : "disabled"), 55 | String.format("External asset versioning is: %s", this.loadAssetsExtern() ? (this.loadAssets() ? "enabled" : "implicitely disabled") : "disabled"), 56 | String.format("Conversion of NBT data is: %s", this.readableNbt() ? "enabled" : "disabled"), 57 | String.format("Data-generation from registries is: %s", this.loadDatagenRegistry() ? "enabled" : "disabled") 58 | )); 59 | if (this.sortJsonObjects()) { 60 | info.add("JSON files (JSON objects) will be sorted in natural order."); 61 | } 62 | return info; 63 | } 64 | 65 | public static DataConfiguration deserialize(Map map) { 66 | return new DataConfiguration( 67 | Utils.getBoolean(map, "loadIntegratedDatapack", DEFAULT.loadIntegratedDatapack()), 68 | Utils.getBoolean(map, "loadAssets", DEFAULT.loadAssets()), 69 | Utils.getBoolean(map, "loadAssetsExtern", DEFAULT.loadAssetsExtern()), 70 | Utils.getBoolean(map, "readableNbt", DEFAULT.readableNbt()), 71 | Utils.getBoolean(map, "loadDatagenRegistry", DEFAULT.loadDatagenRegistry()), 72 | Utils.getBoolean(map, "sortJsonObjects", DEFAULT.sortJsonObjects()) 73 | ); 74 | } 75 | } 76 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/AssetsFetcher.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.nio.file.Files; 4 | import java.nio.file.Path; 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.concurrent.Callable; 8 | 9 | import com.github.winplay02.gitcraft.GitCraft; 10 | import com.github.winplay02.gitcraft.manifest.metadata.AssetsIndexMetadata; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 12 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 13 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 14 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 15 | import com.github.winplay02.gitcraft.pipeline.StepInput; 16 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 17 | import com.github.winplay02.gitcraft.pipeline.StepResults; 18 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 19 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 20 | import com.github.winplay02.gitcraft.types.AssetsIndex; 21 | import com.github.winplay02.gitcraft.types.OrderedVersion; 22 | import com.github.winplay02.gitcraft.util.MiscHelper; 23 | import com.github.winplay02.gitcraft.util.SerializationHelper; 24 | 25 | public record AssetsFetcher(GitCraftStepConfig config) implements GitCraftStepWorker { 26 | 27 | @Override 28 | public StepOutput, GitCraftStepConfig> run( 29 | IPipeline, GitCraftStepConfig> pipeline, 30 | IStepContext.SimpleStepContext context, 31 | StepInput.Empty input, 32 | StepResults, GitCraftStepConfig> results 33 | ) throws Exception { 34 | if (!GitCraft.getDataConfiguration().loadAssetsExtern() || !GitCraft.getDataConfiguration().loadAssets()) { 35 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 36 | } 37 | if (context.targetVersion().assetsIndex() == null) { 38 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 39 | } 40 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.ASSETS_INDEX)); 41 | Path assetsObjectsDir = Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.ASSETS_OBJECTS)); 42 | Path assetsIndexPath = results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.ASSETS_INDEX_JSON); 43 | List, GitCraftStepConfig>> statuses = new ArrayList<>(); 44 | statuses.add(ArtifactsFetcher.fetchArtifact(pipeline, context, this.config, context.targetVersion().assetsIndex(), GitCraftPipelineFilesystemStorage.ASSETS_INDEX_JSON, "assets index")); 45 | AssetsIndex assetsIndex = AssetsIndex.from(SerializationHelper.deserialize(SerializationHelper.fetchAllFromPath(assetsIndexPath), AssetsIndexMetadata.class)); 46 | 47 | int maxRunningTasks = 32; 48 | statuses.addAll( 49 | MiscHelper.runTasksInParallelAndAwaitResult( 50 | maxRunningTasks, 51 | context.executorService(), 52 | assetsIndex.assets().stream()., GitCraftStepConfig>>>map(assetObject -> () -> StepOutput.ofEmptyResultSet(assetObject.fetchArtifact(context.executorService(), assetsObjectsDir, "asset"))).toList() 53 | ) 54 | ); 55 | return StepOutput.merge(statuses); 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/GitCraftQuirks.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft; 2 | 3 | import groovy.lang.Tuple2; 4 | 5 | import java.time.ZonedDateTime; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class GitCraftQuirks { 10 | /// Mapping quirks 11 | public static final String MIN_SUPPORTED_FABRIC_LOADER = "0.17.0"; 12 | 13 | public static final String FABRIC_INTERMEDIARY_MAPPINGS_START_VERSION_ID = "18w43b"; // 1.14 snapshot 14 | public static final String YARN_MAPPINGS_START_VERSION_ID = "18w49a"; // 1.14 snapshot 15 | public static final String YARN_CORRECTLY_ORIENTATED_MAPPINGS_VERSION_ID = "1.14.3"; 16 | public static final String PARCHMENT_START_VERSION_ID = "1.16.5"; 17 | public static final String YARN_UNPICK_START_VERSION_ID = "21w11a"; // build 22; 1.17 snapshot 18 | public static final String YARN_UNPICK_NO_CONSTANTS_JAR_VERSION_ID = "25w32a"; // build 7; 1.21.9? snapshot 19 | 20 | public static final ZonedDateTime RELEASE_TIME_1_3 = ZonedDateTime.parse("2012-07-25T22:00:00+00:00"); 21 | public static final ZonedDateTime RELEASE_TIME_B1_0 = ZonedDateTime.parse("2010-12-20T17:28:00+00:00"); 22 | public static final ZonedDateTime RELEASE_TIME_A1_0_15 = ZonedDateTime.parse("2010-08-04T00:00:00+00:00"); 23 | 24 | public static List intermediaryMissingVersions = List.of("1.16_combat-1", "1.16_combat-2", "1.16_combat-4", "1.16_combat-5", "1.16_combat-6"); 25 | 26 | public static List yarnBrokenVersions = List.of("19w13a", "19w13b", "19w14a", "19w14b"); 27 | 28 | public static List yarnMissingVersions = List.of("1.16_combat-1", "1.16_combat-2", "1.16_combat-4", "1.16_combat-5", "1.16_combat-6"); 29 | 30 | public static List yarnMissingReuploadedVersions = List.of("23w13a_or_b_original", "24w14potato_original"); 31 | 32 | // Maps (version, [broken] build) -> [working] build or -1 33 | public static Map, Integer> yarnBrokenBuildOverride = Map.of( 34 | // FIXUP Version 19w04b Build 9 is broken 35 | Tuple2.tuple("19w04b", 9), 8, /* 19w04b.9 -> Mapping source name conflicts detected: (3 instances) */ 36 | // FIXUP Version 19w08a Build 6-9 is broken 37 | Tuple2.tuple("19w08a", 9), 5, /* 19w08a -> Mapping target name conflicts detected: (1 instance) */ 38 | // FIXUP Version 19w12b Build 7-12 is broken 39 | Tuple2.tuple("19w12b", 12), 6, /* 19w12b -> Mapping target name conflicts detected: (1 instance); Mapping source name conflicts detected: (7 instances) */ 40 | // FIXUP Version 19w13a Build 1-10 is broken 41 | Tuple2.tuple("19w13a", 10), -1, /* 19w13a -> Mapping target name conflicts detected: (1 instance) */ // No working build found 42 | // FIXUP Version 19w13b Build 1-12 is broken 43 | Tuple2.tuple("19w13b", 12), -1, /* 19w13b -> Mapping target name conflicts detected: (1 instance) */ // No working build found 44 | // FIXUP Version 19w14a Build 1-9 is broken 45 | Tuple2.tuple("19w14a", 9), -1, /* 19w14a -> Mapping target name conflicts detected: (1 instance) */ // No working build found 46 | // FIXUP Version 19w14b Build 1-9 is broken 47 | Tuple2.tuple("19w14b", 9), -1 /* 19w14b -> Mapping target name conflicts detected: (2 instances) */ // No working build found 48 | ); 49 | 50 | public static Map yarnInconsistentVersionNaming = Map.of( 51 | "1.15_combat-6", "1_15_combat-6", // 1.15_combat-6 52 | "1.16_combat-0", "1_16_combat-0" // 1.16_combat-0 53 | ); 54 | 55 | // There are no releases for these parchment versions (yet) 56 | public static List parchmentMissingVersions = List.of("1.18", "1.19", "1.19.1", "1.20", "1.20.5", "1.21.2"); 57 | } 58 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/GitCraftStepConfig.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.exceptions.ExceptionsFlavour; 4 | import com.github.winplay02.gitcraft.mappings.MappingFlavour; 5 | import com.github.winplay02.gitcraft.nests.NestsFlavour; 6 | import com.github.winplay02.gitcraft.pipeline.key.KeyInformation; 7 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 8 | import com.github.winplay02.gitcraft.signatures.SignaturesFlavour; 9 | import com.github.winplay02.gitcraft.unpick.UnpickFlavour; 10 | 11 | import java.util.ArrayList; 12 | import java.util.Arrays; 13 | import java.util.EnumSet; 14 | import java.util.LinkedHashMap; 15 | import java.util.List; 16 | import java.util.Map; 17 | import java.util.stream.Collectors; 18 | 19 | public record GitCraftStepConfig(Map identifier, MappingFlavour mappingFlavour, 20 | UnpickFlavour unpickFlavour, ExceptionsFlavour exceptionsFlavour, 21 | SignaturesFlavour signaturesFlavour, NestsFlavour nestsFlavour, boolean lvtPatch, 22 | boolean preen) implements IStepConfig { 23 | 24 | public enum FlavourMatcher implements KeyInformation { 25 | MAPPING, UNPICK, EXCEPTIONS, SIGNATURES, NESTS, LVT, PREEN; 26 | } 27 | 28 | @Override 29 | public String toString() { 30 | List flavours = new ArrayList<>(); 31 | /*if (mappingFlavour != MappingFlavour.IDENTITY_UNMAPPED)*/ 32 | flavours.add("mappings: %s".formatted(mappingFlavour)); // prevent empty config information 33 | if (unpickFlavour != UnpickFlavour.NONE) flavours.add("unpick: %s".formatted(unpickFlavour)); 34 | if (exceptionsFlavour != ExceptionsFlavour.NONE) flavours.add("exceptions: %s".formatted(exceptionsFlavour)); 35 | if (signaturesFlavour != SignaturesFlavour.NONE) flavours.add("signatures: %s".formatted(signaturesFlavour)); 36 | if (nestsFlavour != NestsFlavour.NONE) flavours.add("nests: %s".formatted(nestsFlavour)); 37 | if (lvtPatch) flavours.add("lvt-patch"); 38 | if (preen) flavours.add("preen"); 39 | return String.join(", ", flavours); 40 | } 41 | 42 | public String createArtifactComponentString(KeyInformation dist, KeyInformation... matchingFlavours) { 43 | return createArtifactComponentString((MinecraftJar) dist, Arrays.stream(matchingFlavours).map(FlavourMatcher.class::cast).toArray(FlavourMatcher[]::new)); 44 | } 45 | 46 | public String createArtifactComponentString(MinecraftJar dist, FlavourMatcher... matchingFlavours) { 47 | Map flavours = new LinkedHashMap<>(); 48 | if (matchingFlavours.length != 0) { 49 | EnumSet flavoursSet = EnumSet.copyOf(Arrays.stream(matchingFlavours).toList()); 50 | if (flavoursSet.contains(FlavourMatcher.MAPPING)) flavours.put("map", String.valueOf(mappingFlavour)); 51 | if (flavoursSet.contains(FlavourMatcher.UNPICK)) flavours.put("un", String.valueOf(unpickFlavour)); 52 | if (flavoursSet.contains(FlavourMatcher.EXCEPTIONS)) flavours.put("exc", String.valueOf(exceptionsFlavour)); 53 | if (flavoursSet.contains(FlavourMatcher.SIGNATURES)) flavours.put("sig", String.valueOf(signaturesFlavour)); 54 | if (flavoursSet.contains(FlavourMatcher.NESTS)) flavours.put("nests", String.valueOf(nestsFlavour)); 55 | if (flavoursSet.contains(FlavourMatcher.LVT) && this.lvtPatch()) flavours.put("lvt", "patch"); 56 | if (flavoursSet.contains(FlavourMatcher.PREEN) && this.preen()) flavours.put("preen", "1"); 57 | } 58 | flavours.put("id", this.identifier().get(dist)); 59 | return flavours.entrySet().stream().map(entry -> String.format("%s_%s", entry.getKey(), entry.getValue())).collect(Collectors.joining("-")); 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/config/TransientApplicationConfiguration.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.config; 2 | 3 | import com.google.gson.JsonElement; 4 | 5 | import java.nio.file.Path; 6 | import java.util.ArrayList; 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | /** 11 | * GitCraft Transient Application Configuration (not loaded from the configuration file) 12 | * 13 | * @param noRepo Whether the committing to the repository step should be skipped 14 | * @param overrideRepositoryPath Path to a repository that should be used instead of the calculated repository path 15 | * @param refreshDecompilation Whether existing artifacts should be deleted and generated (useful, e.g. if there are decompiler updates) 16 | * @param refreshOnlyVersion Whether a specific versions should be refreshed 17 | * @param refreshMinVersion A min version that should be refreshed (all versions greater than this version are also refreshed) 18 | * @param refreshMaxVersion A max version that should be refreshed (all versions less than this version are also refreshed) 19 | */ 20 | public record TransientApplicationConfiguration(boolean noRepo, 21 | Path overrideRepositoryPath, 22 | boolean refreshDecompilation, 23 | String[] refreshOnlyVersion, 24 | String refreshMinVersion, 25 | String refreshMaxVersion) 26 | implements Configuration { 27 | 28 | public static final TransientApplicationConfiguration DEFAULT = new TransientApplicationConfiguration( 29 | false, 30 | null, 31 | false, 32 | null, 33 | null, 34 | null 35 | ); 36 | 37 | @Override 38 | public Map serialize() { 39 | return Map.of(); // transient, do not serialize 40 | } 41 | 42 | @Override 43 | public List generateInfo() { 44 | List info = new ArrayList<>(); 45 | info.add(String.format("Repository creation and versioning is: %s", this.noRepo() ? "skipped" : "enabled")); 46 | if (this.overrideRepositoryPath() != null) { 47 | info.add(String.format("Repository path is overridden. This may lead to various errors (see help). Proceed with caution. Target: %s", this.overrideRepositoryPath())); 48 | } 49 | if (this.refreshDecompilation() && !this.isRefreshOnlyVersion() && !this.isRefreshMinVersion() && !this.isRefreshMaxVersion()) { 50 | info.add(String.format("All / specified version(s) will be: %s", this.refreshDecompilation() ? "deleted and decompiled again" : "reused if existing")); 51 | } else { 52 | if (this.isRefreshOnlyVersion()) { 53 | info.add(String.format("Versions to refresh artifacts: %s", String.join(", ", this.refreshOnlyVersion()))); 54 | } else if (this.isRefreshMinVersion() && this.isRefreshMaxVersion()) { 55 | info.add(String.format("Versions to refresh artifacts: from %s to %s", this.refreshMinVersion(), this.refreshMaxVersion())); 56 | } else if (this.isRefreshMinVersion()) { 57 | info.add(String.format("Versions to refresh artifacts: all from %s", this.refreshMinVersion())); 58 | } else if (this.isRefreshMaxVersion()) { 59 | info.add(String.format("Versions to refresh artifacts: all up to %s", this.refreshMaxVersion())); 60 | } 61 | } 62 | return info; 63 | } 64 | 65 | public boolean isRefreshOnlyVersion() { 66 | return this.refreshOnlyVersion() != null; 67 | } 68 | 69 | public boolean isRefreshMinVersion() { 70 | return this.refreshMinVersion() != null; 71 | } 72 | 73 | public boolean isRefreshMaxVersion() { 74 | return this.refreshMaxVersion() != null; 75 | } 76 | 77 | public static TransientApplicationConfiguration deserialize(Map map) { 78 | return DEFAULT; // transient, do not deserialize 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/Preener.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 8 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 9 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 11 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 12 | import com.github.winplay02.gitcraft.pipeline.StepResults; 13 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 14 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 15 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 16 | import com.github.winplay02.gitcraft.types.OrderedVersion; 17 | import com.github.winplay02.gitcraft.util.MiscHelper; 18 | import net.ornithemc.preen.Preen; 19 | 20 | public record Preener(GitCraftStepConfig config) implements GitCraftStepWorker { 21 | 22 | @Override 23 | public boolean shouldExecute(IPipeline, GitCraftStepConfig> pipeline, IStepContext.SimpleStepContext context) { 24 | return config.preen(); 25 | } 26 | 27 | @Override 28 | public StepOutput, GitCraftStepConfig> run( 29 | IPipeline, GitCraftStepConfig> pipeline, 30 | IStepContext.SimpleStepContext context, 31 | GitCraftStepWorker.JarTupleInput input, 32 | StepResults, GitCraftStepConfig> results 33 | ) throws Exception { 34 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.REMAPPED)); // this directory might be confusing? 35 | StepOutput, GitCraftStepConfig> mergedStatus = preenJar(pipeline, context, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.PREENED_MERGED_JAR); 36 | if (mergedStatus.status().isSuccessful()) { 37 | return mergedStatus; 38 | } 39 | StepOutput, GitCraftStepConfig> clientStatus = preenJar(pipeline, context, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.PREENED_CLIENT_JAR); 40 | StepOutput, GitCraftStepConfig> serverStatus = preenJar(pipeline, context, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.PREENED_SERVER_JAR); 41 | return StepOutput.merge(clientStatus, serverStatus); 42 | } 43 | 44 | private StepOutput, GitCraftStepConfig> preenJar(IPipeline, GitCraftStepConfig> pipeline, 45 | IStepContext.SimpleStepContext context, StorageKey inputFile, StorageKey outputFile) throws IOException { 46 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 47 | if (jarIn == null) { 48 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 49 | } 50 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 51 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 52 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 53 | } 54 | Files.deleteIfExists(jarOut); 55 | Files.copy(jarIn, jarOut); 56 | Preen.splitMergedBridgeMethods(jarOut); 57 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 58 | } 59 | } 60 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/ArtifactsFetcher.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 4 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 5 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 6 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 7 | import com.github.winplay02.gitcraft.pipeline.StepInput; 8 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 9 | import com.github.winplay02.gitcraft.pipeline.StepResults; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 12 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 13 | import com.github.winplay02.gitcraft.types.Artifact; 14 | import com.github.winplay02.gitcraft.types.OrderedVersion; 15 | import com.github.winplay02.gitcraft.util.MiscHelper; 16 | 17 | import java.nio.file.Files; 18 | import java.nio.file.Path; 19 | import java.util.ArrayList; 20 | import java.util.List; 21 | import java.util.concurrent.Callable; 22 | 23 | public record ArtifactsFetcher(GitCraftStepConfig config) implements GitCraftStepWorker { 24 | 25 | @Override 26 | public StepOutput, GitCraftStepConfig> run( 27 | IPipeline, GitCraftStepConfig> pipeline, 28 | IStepContext.SimpleStepContext context, 29 | StepInput.Empty input, 30 | StepResults, GitCraftStepConfig> results 31 | ) throws Exception { 32 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.ARTIFACTS)); 33 | 34 | OrderedVersion mcVersion = context.targetVersion(); 35 | 36 | List, GitCraftStepConfig>>> outputTasks = new ArrayList<>(4); 37 | 38 | if (mcVersion.hasClientCode()) { 39 | outputTasks.add(() -> fetchArtifact(pipeline, context, this.config, mcVersion.clientJar(), GitCraftPipelineFilesystemStorage.ARTIFACTS_CLIENT_JAR, "client jar")); 40 | } 41 | if (mcVersion.hasServerJar()) { 42 | outputTasks.add(() -> fetchArtifact(pipeline, context, this.config, mcVersion.serverDist().serverJar(), GitCraftPipelineFilesystemStorage.ARTIFACTS_SERVER_JAR, "server jar")); 43 | } 44 | if (mcVersion.hasServerWindows()) { 45 | outputTasks.add(() -> fetchArtifact(pipeline, context, this.config, mcVersion.serverDist().windowsServer(), GitCraftPipelineFilesystemStorage.ARTIFACTS_SERVER_EXE, "server exe")); 46 | } 47 | if (mcVersion.hasServerZip()) { 48 | outputTasks.add(() -> fetchArtifact(pipeline, context, this.config, mcVersion.serverDist().serverZip(), GitCraftPipelineFilesystemStorage.ARTIFACTS_SERVER_ZIP, "server zip")); 49 | } 50 | return StepOutput.merge(results, StepOutput.merge(MiscHelper.runTasksInParallelAndAwaitResult( 51 | 32, 52 | context.executorService(), 53 | outputTasks 54 | ))); 55 | } 56 | 57 | static StepOutput, GitCraftStepConfig> fetchArtifact( 58 | IPipeline, GitCraftStepConfig> pipeline, 59 | IStepContext.SimpleStepContext context, 60 | GitCraftStepConfig config, 61 | Artifact artifact, 62 | StorageKey resultFile, 63 | String artifactKind) 64 | { 65 | Path resultPath = pipeline.getStoragePath(resultFile, context, config); 66 | StepStatus status = artifact.fetchArtifactToFile(context.executorService(), resultPath, artifactKind); 67 | if (status.hasRun()) { 68 | return StepOutput.ofSingle(status, resultFile); 69 | } 70 | return StepOutput.ofEmptyResultSet(status); 71 | } 72 | } 73 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/exceptions/ExceptionsPatch.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.exceptions; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.nio.file.Path; 6 | import java.util.Optional; 7 | 8 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 9 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 10 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 11 | import com.github.winplay02.gitcraft.types.OrderedVersion; 12 | import com.github.winplay02.gitcraft.util.MiscHelper; 13 | 14 | import net.ornithemc.exceptor.io.ExceptionsFile; 15 | import net.ornithemc.exceptor.io.ExceptorIo; 16 | 17 | public abstract class ExceptionsPatch { 18 | 19 | public abstract String getName(); 20 | 21 | /** 22 | * @return whether exceptions of this flavour exist for the given minecraft version. 23 | */ 24 | public abstract boolean doExceptionsExist(OrderedVersion mcVersion); 25 | 26 | /** 27 | * @return whether exceptions of this flavour exist for the given jar for the given minecraft version. 28 | */ 29 | public abstract boolean doExceptionsExist(OrderedVersion mcVersion, MinecraftJar minecraftJar); 30 | 31 | /** 32 | * @return whether exceptions of this flavour can be used on the given jar for the given minecraft version. 33 | */ 34 | public abstract boolean canExceptionsBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar); 35 | 36 | /** 37 | * After calling this method, a path returned by {@link #getExceptionsPath(OrderedVersion, MinecraftJar)} should be valid. 38 | * This is only true if {@link #doExceptionsExist(OrderedVersion, MinecraftJar)} returns true for the version. 39 | * 40 | * @param versionContext Version Context 41 | * @param minecraftJar Minecraft jar 42 | * @return A result 43 | */ 44 | public abstract StepStatus provideExceptions(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException, URISyntaxException, InterruptedException; 45 | 46 | /** 47 | * Should return a path to a exceptions file, created by {@link #provideExceptions(IStepContext, MinecraftJar)} 48 | * 49 | * @param mcVersion Version 50 | * @param minecraftJar Minecraft jar 51 | * @return path to exceptions file 52 | */ 53 | public final Optional getExceptionsPath(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 54 | return Optional.ofNullable(getExceptionsPathInternal(mcVersion, minecraftJar)); 55 | } 56 | 57 | protected abstract Path getExceptionsPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar); 58 | 59 | /** 60 | * Visits exceptions of this flavour for the given jar for the given minecraft version, using the given visitor. 61 | * The visitor will only be called if {@link #canExceptionsBeUsedOn(OrderedVersion, MinecraftJar)} returns true for the version. 62 | */ 63 | public abstract void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, ExceptionsFile visitor) throws IOException; 64 | 65 | public final ExceptionsFile getExceptions(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 66 | if (!canExceptionsBeUsedOn(mcVersion, minecraftJar)) { 67 | MiscHelper.panic("Tried to use %s-exceptions for version %s, %s jar. These exceptions can not be used for this version.", this, mcVersion.launcherFriendlyVersionName(), minecraftJar.name().toLowerCase()); 68 | } 69 | ExceptionsFile exceptions = new ExceptionsFile(); 70 | try { 71 | visit(mcVersion, minecraftJar, exceptions); 72 | } catch (IOException e) { 73 | MiscHelper.panicBecause(e, "An error occurred while getting exceptions information for %s (version %s)", this, mcVersion.launcherFriendlyVersionName()); 74 | } 75 | return exceptions; 76 | } 77 | 78 | protected static boolean validateExceptions(Path exceptionsPath) { 79 | try { 80 | ExceptorIo.read(exceptionsPath); 81 | return true; 82 | } catch (IOException e) { 83 | return false; 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/util/FFNIODirectoryResultSaver.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import net.fabricmc.loom.util.FileSystemUtil; 4 | import org.jetbrains.java.decompiler.main.DecompilerContext; 5 | import org.jetbrains.java.decompiler.main.extern.IResultSaver; 6 | 7 | import java.io.BufferedWriter; 8 | import java.io.IOException; 9 | import java.io.OutputStream; 10 | import java.nio.file.Files; 11 | import java.nio.file.Path; 12 | import java.nio.file.Paths; 13 | import java.util.jar.Manifest; 14 | 15 | public class FFNIODirectoryResultSaver implements IResultSaver { 16 | private final Path root; 17 | private final AutoCloseable closeable; 18 | 19 | public FFNIODirectoryResultSaver(Path root, AutoCloseable closeable) { 20 | this.root = root; 21 | this.closeable = closeable; 22 | } 23 | 24 | @Override 25 | public void saveClassEntry(String path, String archiveName, String qualifiedName, String entryName, String content) { 26 | Path entryPath = this.root.resolve(entryName); 27 | 28 | try (BufferedWriter writer = Files.newBufferedWriter(entryPath)) { 29 | if (content != null) { 30 | writer.write(content); 31 | } 32 | } catch (IOException e) { 33 | throw new RuntimeException("Failed to save class", e); 34 | } 35 | } 36 | 37 | @Override 38 | public void saveDirEntry(String path, String archiveName, String entryName) { 39 | Path entryPath = this.root.resolve(entryName); 40 | try { 41 | Files.createDirectories(entryPath); 42 | } catch (IOException e) { 43 | throw new RuntimeException("Failed to save directory", e); 44 | } 45 | } 46 | 47 | @Override 48 | public void createArchive(String path, String archiveName, Manifest manifest) { 49 | 50 | } 51 | 52 | @Override 53 | public void saveFolder(String path) { 54 | Path entryPath = this.root.resolve(path); 55 | try { 56 | Files.createDirectories(entryPath); 57 | } catch (IOException e) { 58 | throw new RuntimeException("Failed to save directory", e); 59 | } 60 | } 61 | 62 | @Override 63 | public void copyFile(String source, String path, String entryName) { 64 | try { 65 | Files.copy(Paths.get(source), this.root.resolve(entryName)); 66 | } catch (IOException ex) { 67 | DecompilerContext.getLogger().writeMessage("Cannot copy " + source + " to " + entryName, ex); 68 | } 69 | } 70 | 71 | @Override 72 | public void saveClassFile(String path, String qualifiedName, String entryName, String content, int[] mapping) { 73 | Path entryPath = this.root.resolve(path).resolve(entryName); 74 | try (BufferedWriter writer = Files.newBufferedWriter(entryPath)) { 75 | if (content != null) { 76 | writer.write(content); 77 | } 78 | } catch (IOException e) { 79 | throw new RuntimeException("Failed to save class", e); 80 | } 81 | } 82 | 83 | @Override 84 | public void copyEntry(String source, String path, String archiveName, String entryName) { 85 | Path srcPath = Path.of(source); 86 | Path targetPath = this.root.resolve(path).resolve(entryName); 87 | try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(srcPath)) { 88 | Path zipEntry = fs.getPath(entryName); 89 | if (zipEntry != null) { 90 | try (OutputStream output = Files.newOutputStream(targetPath)) { 91 | Files.copy(zipEntry, output); 92 | } 93 | } 94 | } catch (IOException ex) { 95 | String message = "Cannot copy entry " + entryName + " from " + source; 96 | DecompilerContext.getLogger().writeMessage(message, ex); 97 | } 98 | } 99 | 100 | @Override 101 | public void closeArchive(String path, String archiveName) { 102 | 103 | } 104 | 105 | @Override 106 | public void close() throws IOException { 107 | IResultSaver.super.close(); 108 | if (closeable != null) { 109 | try { 110 | closeable.close(); 111 | } catch (Exception e) { 112 | throw new IOException(e); 113 | } 114 | } 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/signatures/SignaturesPatch.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.signatures; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.util.Optional; 8 | 9 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 12 | import com.github.winplay02.gitcraft.types.OrderedVersion; 13 | import com.github.winplay02.gitcraft.util.MiscHelper; 14 | 15 | import io.github.gaming32.signaturechanger.tree.SigsFile; 16 | import io.github.gaming32.signaturechanger.visitor.SigsReader; 17 | 18 | public abstract class SignaturesPatch { 19 | 20 | public abstract String getName(); 21 | 22 | /** 23 | * @return whether signatures of this flavour exist for the given minecraft version. 24 | */ 25 | public abstract boolean doSignaturesExist(OrderedVersion mcVersion); 26 | 27 | /** 28 | * @return whether signatures of this flavour exist for the given jar for the given minecraft version. 29 | */ 30 | public abstract boolean doSignaturesExist(OrderedVersion mcVersion, MinecraftJar minecraftJar); 31 | 32 | /** 33 | * @return whether signatures of this flavour can be used on the given jar for the given minecraft version. 34 | */ 35 | public abstract boolean canSignaturesBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar); 36 | 37 | /** 38 | * After calling this method, a path returned by {@link #getSignaturesPath(OrderedVersion, MinecraftJar)} should be valid. 39 | * This is only true if {@link #doSignaturesExist(OrderedVersion, MinecraftJar)} returns true for the version. 40 | * 41 | * @param versionContext Version Context 42 | * @param minecraftJar Minecraft jar 43 | * @return A result 44 | */ 45 | public abstract StepStatus provideSignatures(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException, URISyntaxException, InterruptedException; 46 | 47 | /** 48 | * Should return a path to a signatures file, created by {@link #provideSignatures(IStepContext, MinecraftJar)} 49 | * 50 | * @param mcVersion Version 51 | * @param minecraftJar Minecraft jar 52 | * @return path to signatures file 53 | */ 54 | public final Optional getSignaturesPath(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 55 | return Optional.ofNullable(getSignaturesPathInternal(mcVersion, minecraftJar)); 56 | } 57 | 58 | protected abstract Path getSignaturesPathInternal(OrderedVersion mcVersion, MinecraftJar minecraftJar); 59 | 60 | /** 61 | * Visits signatures of this flavour for the given jar for the given minecraft version, using the given visitor. 62 | * The visitor will only be called if {@link #canSignaturesBeUsedOn(OrderedVersion, MinecraftJar)} returns true for the version. 63 | */ 64 | public abstract void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, SigsFile visitor) throws IOException; 65 | 66 | public final SigsFile getSignatures(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 67 | if (!canSignaturesBeUsedOn(mcVersion, minecraftJar)) { 68 | MiscHelper.panic("Tried to use %s-signatures for version %s, %s jar. These signatures can not be used for this version.", this, mcVersion.launcherFriendlyVersionName(), minecraftJar.name().toLowerCase()); 69 | } 70 | SigsFile signatures = new SigsFile(); 71 | try { 72 | visit(mcVersion, minecraftJar, signatures); 73 | } catch (IOException e) { 74 | MiscHelper.panicBecause(e, "An error occurred while getting signatures information for %s (version %s)", this, mcVersion.launcherFriendlyVersionName()); 75 | } 76 | return signatures; 77 | } 78 | 79 | protected static boolean validateSignatures(Path signaturesPath) { 80 | try (SigsReader sr = new SigsReader(Files.newBufferedReader(signaturesPath))) { 81 | sr.accept(new SigsFile()); 82 | return true; 83 | } catch (IOException e) { 84 | return false; 85 | } 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/graph/AbstractVersionGraph.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.graph; 2 | 3 | import com.github.winplay02.gitcraft.util.MiscHelper; 4 | 5 | import java.util.Arrays; 6 | import java.util.HashMap; 7 | import java.util.HashSet; 8 | import java.util.Set; 9 | import java.util.TreeSet; 10 | import java.util.function.Function; 11 | import java.util.function.Predicate; 12 | import java.util.stream.Collectors; 13 | 14 | public abstract class AbstractVersionGraph> extends Graph { 15 | 16 | public HashSet repoTags = new HashSet<>(); 17 | /** 18 | * root nodes of the graph, mapped to the path lengths to the tips of those main branches 19 | */ 20 | public Set roots = new HashSet<>(); 21 | 22 | protected AbstractVersionGraph() { 23 | super(); 24 | } 25 | 26 | protected AbstractVersionGraph(AbstractVersionGraph previous, Predicate predicate, String... tags) { 27 | super(new HashMap<>(previous.edgesBack.keySet().stream().filter(predicate).collect(Collectors.toMap(Function.identity(), key -> new TreeSet()))), new HashMap<>(previous.edgesFw.keySet().stream().filter(predicate).collect(Collectors.toMap(Function.identity(), key -> new TreeSet())))); 28 | this.repoTags = new HashSet<>(previous.repoTags); 29 | this.repoTags.addAll(Arrays.asList(tags)); 30 | this.reconnectGraph(previous); 31 | this.validateNoCycles(); 32 | } 33 | 34 | protected void reconnectGraph(AbstractVersionGraph previous) { 35 | TreeSet allVersions = new TreeSet<>(this.edgesBack.keySet()); 36 | for (T version : allVersions) { 37 | TreeSet nearestPreviousNodes = new TreeSet<>(previous.getPreviousVertices(version)); 38 | TreeSet calculatedPreviousNodes = new TreeSet<>(); 39 | //while (nearestPreviousNodes.stream().anyMatch(anyPreviousVersion -> !this.containsVersion(anyPreviousVersion))) { 40 | while (!nearestPreviousNodes.isEmpty()) { 41 | TreeSet nextBestNodes = new TreeSet<>(); 42 | for (T nearestPreviousNode : nearestPreviousNodes) { 43 | if (allVersions.contains(nearestPreviousNode)) { 44 | calculatedPreviousNodes.add(nearestPreviousNode); 45 | } else { 46 | nextBestNodes.addAll(previous.getPreviousVertices(nearestPreviousNode)); 47 | } 48 | } 49 | nextBestNodes.removeIf(calculatedPreviousNodes::contains); 50 | nearestPreviousNodes = nextBestNodes; 51 | } 52 | // ADD 53 | this.edgesBack.computeIfAbsent(version, value -> new TreeSet<>()).addAll(calculatedPreviousNodes); 54 | for (T prevVersion : calculatedPreviousNodes) { 55 | this.edgesFw.computeIfAbsent(prevVersion, value -> new TreeSet<>()).add(version); 56 | } 57 | } 58 | 59 | for (T version : allVersions) { 60 | TreeSet nearestNextNodes = new TreeSet<>(previous.getFollowingVertices(version)); 61 | TreeSet calculatedNextNodes = new TreeSet<>(); 62 | //while (nearestNextNodes.stream().anyMatch(anyNextVersion -> !this.containsVersion(anyNextVersion))) { 63 | while (!nearestNextNodes.isEmpty()) { 64 | TreeSet nextBestNodes = new TreeSet<>(); 65 | for (T nearestNextNode : nearestNextNodes) { 66 | if (allVersions.contains(nearestNextNode)) { 67 | calculatedNextNodes.add(nearestNextNode); 68 | } else { 69 | nextBestNodes.addAll(previous.getFollowingVertices(nearestNextNode)); 70 | } 71 | } 72 | nextBestNodes.removeIf(calculatedNextNodes::contains); 73 | nearestNextNodes = nextBestNodes; 74 | } 75 | // ADD 76 | //this.edgesFw.computeIfAbsent(version, value -> new TreeSet<>()).addAll(calculatedNextNodes); 77 | //for (OrderedVersion nextVersion : calculatedNextNodes) { 78 | //this.edgesBack.computeIfAbsent(nextVersion, value -> new TreeSet<>()).add(version); 79 | //} 80 | } 81 | this.testGraphConnectivity(); 82 | } 83 | 84 | public Set getRootVersions() { 85 | if (this.roots.isEmpty()) { 86 | MiscHelper.panic("Graph does not contain a root node"); 87 | } 88 | return this.roots; 89 | } 90 | 91 | public boolean containsVersion(T version) { 92 | return version != null && this.edgesBack.containsKey(version); 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/Remapper.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | import com.github.winplay02.gitcraft.mappings.MappingUtils; 8 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 9 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 10 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 15 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 16 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 17 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 18 | import com.github.winplay02.gitcraft.types.OrderedVersion; 19 | import com.github.winplay02.gitcraft.util.MiscHelper; 20 | import net.fabricmc.tinyremapper.IMappingProvider; 21 | import net.fabricmc.tinyremapper.TinyRemapper; 22 | 23 | public record Remapper(GitCraftStepConfig config) implements GitCraftStepWorker { 24 | 25 | @Override 26 | public StepOutput, GitCraftStepConfig> run( 27 | IPipeline, GitCraftStepConfig> pipeline, 28 | IStepContext.SimpleStepContext context, 29 | GitCraftStepWorker.JarTupleInput input, 30 | StepResults, GitCraftStepConfig> results 31 | ) throws Exception { 32 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.REMAPPED)); 33 | StepOutput, GitCraftStepConfig> mergedStatus = remapJar(pipeline, context, MinecraftJar.MERGED, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.REMAPPED_MERGED_JAR); 34 | if (mergedStatus.status().isSuccessful()) { 35 | return mergedStatus; 36 | } 37 | StepOutput, GitCraftStepConfig> clientStatus = remapJar(pipeline, context, MinecraftJar.CLIENT, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.REMAPPED_CLIENT_JAR); 38 | StepOutput, GitCraftStepConfig> serverStatus = remapJar(pipeline, context, MinecraftJar.SERVER, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.REMAPPED_SERVER_JAR); 39 | return StepOutput.merge(clientStatus, serverStatus); 40 | } 41 | 42 | private StepOutput, GitCraftStepConfig> remapJar(IPipeline, GitCraftStepConfig> pipeline, 43 | IStepContext.SimpleStepContext context, MinecraftJar type, StorageKey inputFile, StorageKey outputFile) throws IOException { 44 | if (inputFile == null) { 45 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 46 | } 47 | if (!config.mappingFlavour().canBeUsedOn(context.targetVersion(), type)) { 48 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 49 | } 50 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 51 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 52 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 53 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 54 | } 55 | if (Files.exists(jarOut)) { 56 | Files.delete(jarOut); 57 | } 58 | IMappingProvider mappingProvider = config.mappingFlavour().getProvider(context.targetVersion(), type); 59 | TinyRemapper remapper = MappingUtils.createTinyRemapper(mappingProvider); 60 | MappingUtils.remapJar(remapper, jarIn, jarOut); 61 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/pipeline/PipelineFilesystemStorage.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline; 2 | 3 | import com.github.winplay02.gitcraft.graph.AbstractVersion; 4 | import com.github.winplay02.gitcraft.pipeline.key.KeyInformation; 5 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 6 | import com.github.winplay02.gitcraft.util.MiscHelper; 7 | 8 | import java.nio.file.Path; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.function.BiFunction; 13 | import java.util.function.Function; 14 | 15 | public record PipelineFilesystemStorage, C extends IStepContext, D extends IStepConfig>(IPipelineFilesystemRoot rootFilesystem, 16 | Set resettableKeys, 17 | Map> paths) { 18 | @FunctionalInterface 19 | public interface PathDeriver, C extends IStepContext, D extends IStepConfig> { 20 | Path derive(PipelineFilesystemStorage storage, C context, D config); 21 | } 22 | 23 | @SafeVarargs 24 | public PipelineFilesystemStorage(IPipelineFilesystemRoot rootFilesystem, Set resettableKeys, Map>... paths) { 25 | this(rootFilesystem, resettableKeys, MiscHelper.mergeMaps(new HashMap<>(), paths)); 26 | } 27 | 28 | public Path getPath(StorageKey key, C context, D config) { 29 | if (key == null || !this.paths.containsKey(key)) { 30 | return null; 31 | } 32 | return this.paths.get(key).derive(this, context, config); 33 | } 34 | 35 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver rootPathConst(Function rootPathConstFunction) { 36 | return (root, _context, _config) -> rootPathConstFunction.apply(root.rootFilesystem()); 37 | } 38 | 39 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver rootPathConstSubDir(Function rootPathConstFunction, String subDir1, String... subDirs) { 40 | return (root, _context, _config) -> rootPathConstFunction.apply(root.rootFilesystem()).resolve(subDir1, subDirs); 41 | } 42 | 43 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver rootPathVersioned(Function rootPathConstFunction) { 44 | return (root, context, _config) -> rootPathConstFunction.apply(root.rootFilesystem()).resolve(context.targetVersion().pathName()); 45 | } 46 | 47 | public Path resolvePath(StorageKey key, C context, D config, String toResolveFirst, String... toResolve) { 48 | return this.paths.get(key).derive(this, context, config).resolve(toResolveFirst, toResolve); 49 | } 50 | 51 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver createFromKey(StorageKey key, String toResolveFirst, String... toResolve) { 52 | return (root, context, config) -> root.resolvePath(key, context, config, toResolveFirst, toResolve); 53 | } 54 | 55 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver createFromKey(StorageKey key, Function toResolve) { 56 | return (root, context, config) -> root.resolvePath(key, context, config, toResolve.apply(context)); 57 | } 58 | 59 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver createFromKey(StorageKey key, BiFunction toResolve) { 60 | return (root, context, config) -> root.resolvePath(key, context, config, toResolve.apply(context, config)); 61 | } 62 | 63 | public static , C extends IStepContext, D extends IStepConfig> PathDeriver createFromKeyWithConfig(StorageKey key, String pattern, KeyInformation dist, KeyInformation... flavours) { 64 | return (root, context, config) -> root.resolvePath(key, context, config, String.format(pattern, config.createArtifactComponentString(dist, flavours))); 65 | } 66 | } 67 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/JarsNester.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | import com.github.winplay02.gitcraft.nests.NestsFlavour; 8 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 9 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 10 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 15 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 16 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 17 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 18 | import com.github.winplay02.gitcraft.types.OrderedVersion; 19 | import com.github.winplay02.gitcraft.util.MiscHelper; 20 | import net.ornithemc.nester.Nester; 21 | 22 | public record JarsNester(GitCraftStepConfig config) implements GitCraftStepWorker { 23 | 24 | @Override 25 | public boolean shouldExecute(IPipeline, GitCraftStepConfig> pipeline, IStepContext.SimpleStepContext context) { 26 | return this.config().nestsFlavour() != NestsFlavour.NONE; // optimization 27 | } 28 | 29 | @Override 30 | public StepOutput, GitCraftStepConfig> run( 31 | IPipeline, GitCraftStepConfig> pipeline, 32 | IStepContext.SimpleStepContext context, 33 | GitCraftStepWorker.JarTupleInput input, 34 | StepResults, GitCraftStepConfig> results 35 | ) throws Exception { 36 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.REMAPPED)); 37 | StepOutput, GitCraftStepConfig> mergedStatus = nestJar(pipeline, context, MinecraftJar.MERGED, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.NESTED_MERGED_JAR); 38 | if (mergedStatus.status().isSuccessful()) { 39 | return mergedStatus; 40 | } 41 | StepOutput, GitCraftStepConfig> clientStatus = nestJar(pipeline, context, MinecraftJar.CLIENT, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.NESTED_CLIENT_JAR); 42 | StepOutput, GitCraftStepConfig> serverStatus = nestJar(pipeline, context, MinecraftJar.SERVER, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.NESTED_SERVER_JAR); 43 | return StepOutput.merge(clientStatus, serverStatus); 44 | } 45 | 46 | private StepOutput, GitCraftStepConfig> nestJar(IPipeline, GitCraftStepConfig> pipeline, 47 | IStepContext.SimpleStepContext context, MinecraftJar inFile, StorageKey inputFile, StorageKey outputFile) throws IOException { 48 | if (!config.nestsFlavour().canBeUsedOn(context.targetVersion(), inFile, config.mappingFlavour())) { 49 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 50 | } 51 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 52 | if (jarIn == null) { 53 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 54 | } 55 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 56 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 57 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 58 | } 59 | Files.deleteIfExists(jarOut); 60 | Nester.nestJar(jarIn, jarOut, config.nestsFlavour().getNests(context.targetVersion(), inFile, config.mappingFlavour())); 61 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/JarsExceptor.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | 7 | import com.github.winplay02.gitcraft.exceptions.ExceptionsFlavour; 8 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 9 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 10 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 15 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 16 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 17 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 18 | import com.github.winplay02.gitcraft.types.OrderedVersion; 19 | import com.github.winplay02.gitcraft.util.MiscHelper; 20 | import net.ornithemc.exceptor.Exceptor; 21 | 22 | public record JarsExceptor(GitCraftStepConfig config) implements GitCraftStepWorker { 23 | 24 | @Override 25 | public boolean shouldExecute(IPipeline, GitCraftStepConfig> pipeline, IStepContext.SimpleStepContext context) { 26 | return this.config().exceptionsFlavour() != ExceptionsFlavour.NONE; // optimization 27 | } 28 | 29 | @Override 30 | public StepOutput, GitCraftStepConfig> run( 31 | IPipeline, GitCraftStepConfig> pipeline, 32 | IStepContext.SimpleStepContext context, 33 | GitCraftStepWorker.JarTupleInput input, 34 | StepResults, GitCraftStepConfig> results 35 | ) throws Exception { 36 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.PATCHED)); 37 | StepOutput, GitCraftStepConfig> mergedStatus = patchJar(pipeline, context, MinecraftJar.MERGED, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.EXCEPTIONS_PATCHED_MERGED_JAR); 38 | if (mergedStatus.status().isSuccessful()) { 39 | return mergedStatus; 40 | } 41 | StepOutput, GitCraftStepConfig> clientStatus = patchJar(pipeline, context, MinecraftJar.CLIENT, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.EXCEPTIONS_PATCHED_CLIENT_JAR); 42 | StepOutput, GitCraftStepConfig> serverStatus = patchJar(pipeline, context, MinecraftJar.SERVER, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.EXCEPTIONS_PATCHED_SERVER_JAR); 43 | return StepOutput.merge(clientStatus, serverStatus); 44 | } 45 | 46 | private StepOutput, GitCraftStepConfig> patchJar(IPipeline, GitCraftStepConfig> pipeline, 47 | IStepContext.SimpleStepContext context, MinecraftJar inFile, StorageKey inputFile, StorageKey outputFile) throws IOException { 48 | if (!config.exceptionsFlavour().canBeUsedOn(context.targetVersion(), inFile)) { 49 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 50 | } 51 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 52 | if (jarIn == null) { 53 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 54 | } 55 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 56 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 57 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 58 | } 59 | Files.deleteIfExists(jarOut); 60 | Files.copy(jarIn, jarOut); 61 | Exceptor.apply(jarOut, config.exceptionsFlavour().getExceptions(context.targetVersion(), inFile)); 62 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/JarsSignatureChanger.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.List; 7 | 8 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 9 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 10 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 15 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 16 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 17 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 18 | import com.github.winplay02.gitcraft.signatures.SignaturesFlavour; 19 | import com.github.winplay02.gitcraft.types.OrderedVersion; 20 | import com.github.winplay02.gitcraft.util.MiscHelper; 21 | import io.github.gaming32.signaturechanger.cli.ApplyAction; 22 | 23 | public record JarsSignatureChanger(GitCraftStepConfig config) implements GitCraftStepWorker { 24 | 25 | @Override 26 | public boolean shouldExecute(IPipeline, GitCraftStepConfig> pipeline, IStepContext.SimpleStepContext context) { 27 | return this.config().signaturesFlavour() != SignaturesFlavour.NONE; // optimization 28 | } 29 | 30 | @Override 31 | public StepOutput, GitCraftStepConfig> run( 32 | IPipeline, GitCraftStepConfig> pipeline, 33 | IStepContext.SimpleStepContext context, 34 | GitCraftStepWorker.JarTupleInput input, 35 | StepResults, GitCraftStepConfig> results 36 | ) throws Exception { 37 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.PATCHED)); 38 | StepOutput, GitCraftStepConfig> mergedStatus = patchJar(pipeline, context, MinecraftJar.MERGED, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.SIGNATURES_PATCHED_MERGED_JAR); 39 | if (mergedStatus.status().isSuccessful()) { 40 | return mergedStatus; 41 | } 42 | StepOutput, GitCraftStepConfig> clientStatus = patchJar(pipeline, context, MinecraftJar.CLIENT, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.SIGNATURES_PATCHED_CLIENT_JAR); 43 | StepOutput, GitCraftStepConfig> serverStatus = patchJar(pipeline, context, MinecraftJar.SERVER, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.SIGNATURES_PATCHED_SERVER_JAR); 44 | return StepOutput.merge(clientStatus, serverStatus); 45 | } 46 | 47 | private StepOutput, GitCraftStepConfig> patchJar(IPipeline, GitCraftStepConfig> pipeline, 48 | IStepContext.SimpleStepContext context, MinecraftJar inFile, StorageKey inputFile, StorageKey outputFile) throws IOException { 49 | if (!config.signaturesFlavour().canBeUsedOn(context.targetVersion(), inFile)) { 50 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 51 | } 52 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 53 | if (jarIn == null) { 54 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 55 | } 56 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 57 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 58 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 59 | } 60 | Files.deleteIfExists(jarOut); 61 | Files.copy(jarIn, jarOut); 62 | ApplyAction.run(config.signaturesFlavour().getSignatures(context.targetVersion(), inFile), List.of(jarOut)); 63 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/LvtPatcher.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.nio.file.Files; 5 | import java.nio.file.Path; 6 | import java.util.List; 7 | 8 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 9 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 10 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 12 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 13 | import com.github.winplay02.gitcraft.pipeline.StepResults; 14 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 15 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 16 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 17 | import com.github.winplay02.gitcraft.types.OrderedVersion; 18 | import com.github.winplay02.gitcraft.util.MiscHelper; 19 | import net.ornithemc.condor.Condor; 20 | import net.ornithemc.condor.Options; 21 | 22 | public record LvtPatcher(GitCraftStepConfig config) implements GitCraftStepWorker { 23 | 24 | @Override 25 | public boolean shouldExecute(IPipeline, GitCraftStepConfig> pipeline, IStepContext.SimpleStepContext context) { 26 | return config.lvtPatch(); 27 | } 28 | 29 | @Override 30 | public StepOutput, GitCraftStepConfig> run( 31 | IPipeline, GitCraftStepConfig> pipeline, 32 | IStepContext.SimpleStepContext context, 33 | GitCraftStepWorker.JarTupleInput input, 34 | StepResults, GitCraftStepConfig> results 35 | ) throws Exception { 36 | Files.createDirectories(results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.PATCHED)); 37 | Path librariesDir = pipeline.getStoragePath(GitCraftPipelineFilesystemStorage.LIBRARIES, context, config); 38 | if (librariesDir == null) { 39 | return StepOutput.ofEmptyResultSet(StepStatus.FAILED); 40 | } 41 | List libraries = context.targetVersion().libraries().stream().map(artifact -> artifact.resolve(librariesDir)).toList(); 42 | StepOutput, GitCraftStepConfig> mergedStatus = patchLocalVariableTables(pipeline, context, input.mergedJar().orElse(null), GitCraftPipelineFilesystemStorage.LVT_PATCHED_MERGED_JAR, libraries); 43 | if (mergedStatus.status().isSuccessful()) { 44 | return mergedStatus; 45 | } 46 | StepOutput, GitCraftStepConfig> clientStatus = patchLocalVariableTables(pipeline, context, input.clientJar().orElse(null), GitCraftPipelineFilesystemStorage.LVT_PATCHED_CLIENT_JAR, libraries); 47 | StepOutput, GitCraftStepConfig> serverStatus = patchLocalVariableTables(pipeline, context, input.serverJar().orElse(null), GitCraftPipelineFilesystemStorage.LVT_PATCHED_SERVER_JAR, libraries); 48 | return StepOutput.merge(clientStatus, serverStatus); 49 | } 50 | 51 | private StepOutput, GitCraftStepConfig> patchLocalVariableTables(IPipeline, GitCraftStepConfig> pipeline, 52 | IStepContext.SimpleStepContext context, StorageKey inputFile, StorageKey outputFile, List libraries) throws IOException { 53 | Path jarIn = pipeline.getStoragePath(inputFile, context, this.config); 54 | if (jarIn == null) { 55 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 56 | } 57 | Path jarOut = pipeline.getStoragePath(outputFile, context, this.config); 58 | if (Files.exists(jarOut) && !MiscHelper.isJarEmpty(jarOut)) { 59 | return StepOutput.ofSingle(StepStatus.UP_TO_DATE, outputFile); 60 | } 61 | Files.deleteIfExists(jarOut); 62 | Files.copy(jarIn, jarOut); 63 | // this step is applied before remapping, so obfuscate variable names 64 | // that way Tiny Remapper will take care of fixing them 65 | Condor.run(jarOut, libraries, Options.builder().removeInvalidEntries().obfuscateNames().build()); 66 | return StepOutput.ofSingle(StepStatus.SUCCESS, outputFile); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/JarsMerger.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import java.io.IOException; 4 | import java.io.InputStream; 5 | import java.nio.file.Files; 6 | import java.nio.file.Path; 7 | import java.nio.file.StandardCopyOption; 8 | import java.util.Optional; 9 | 10 | import com.github.winplay02.gitcraft.pipeline.GitCraftPipelineFilesystemStorage; 11 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 12 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 13 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 14 | import com.github.winplay02.gitcraft.pipeline.StepInput; 15 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 16 | import com.github.winplay02.gitcraft.pipeline.StepResults; 17 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 18 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 19 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 20 | import com.github.winplay02.gitcraft.types.OrderedVersion; 21 | 22 | import com.github.winplay02.gitcraft.util.MiscHelper; 23 | import net.fabricmc.loom.configuration.providers.BundleMetadata; 24 | import net.fabricmc.loom.util.FileSystemUtil; 25 | import net.fabricmc.stitch.merge.JarMerger; 26 | 27 | public record JarsMerger(boolean obfuscated, GitCraftStepConfig config) implements GitCraftStepWorker { 28 | 29 | @Override 30 | public StepOutput, GitCraftStepConfig> run( 31 | IPipeline, GitCraftStepConfig> pipeline, 32 | IStepContext.SimpleStepContext context, 33 | JarsMerger.Inputs input, 34 | StepResults, GitCraftStepConfig> results 35 | ) throws Exception { 36 | OrderedVersion mcVersion = context.targetVersion(); 37 | if (input.clientJar().isEmpty() || input.serverJar().isEmpty()) { 38 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 39 | } 40 | // obfuscated jars for versions older than 1.3 cannot be merged 41 | // those versions must be merged after remapping, if the mapping flavour allows it 42 | if (this.obfuscated != mcVersion.hasSharedObfuscation()) { 43 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 44 | } 45 | if (!this.obfuscated && !config.mappingFlavour().supportsMergingPre1_3Versions()) { 46 | return StepOutput.ofEmptyResultSet(StepStatus.NOT_RUN); 47 | } 48 | Path mergedJarPath = results.getPathForKeyAndAdd(pipeline, context, this.config, this.obfuscated ? GitCraftPipelineFilesystemStorage.ARTIFACTS_MERGED_JAR : GitCraftPipelineFilesystemStorage.REMAPPED_MERGED_JAR); 49 | if (Files.exists(mergedJarPath) && !MiscHelper.isJarEmpty(mergedJarPath)) { 50 | return new StepOutput<>(StepStatus.UP_TO_DATE, results); 51 | } 52 | Files.deleteIfExists(mergedJarPath); 53 | Path clientJar = pipeline.getStoragePath(input.clientJar().orElseThrow(), context, this.config); 54 | Path serverJar = pipeline.getStoragePath(input.serverJar().orElseThrow(), context, this.config); 55 | 56 | // unbundle if bundled 57 | if (this.obfuscated) { 58 | BundleMetadata sbm = BundleMetadata.fromJar(serverJar); 59 | if (sbm != null) { 60 | Path unbundledServerJar = results.getPathForKeyAndAdd(pipeline, context, this.config, GitCraftPipelineFilesystemStorage.UNBUNDLED_SERVER_JAR); 61 | 62 | if (sbm.versions().size() != 1) { 63 | throw new UnsupportedOperationException("Expected only 1 version in META-INF/versions.list, but got %d".formatted(sbm.versions().size())); 64 | } 65 | 66 | unpackJarEntry(sbm.versions().getFirst(), serverJar, unbundledServerJar); 67 | serverJar = unbundledServerJar; 68 | } 69 | } 70 | 71 | try (JarMerger jarMerger = new JarMerger(clientJar.toFile(), serverJar.toFile(), mergedJarPath.toFile())) { 72 | jarMerger.enableSyntheticParamsOffset(); 73 | jarMerger.merge(); 74 | } 75 | return new StepOutput<>(StepStatus.SUCCESS, results); 76 | } 77 | 78 | public record Inputs(Optional clientJar, Optional serverJar) implements StepInput { 79 | } 80 | 81 | private void unpackJarEntry(BundleMetadata.Entry entry, Path jar, Path dest) throws IOException { 82 | try (FileSystemUtil.Delegate fs = FileSystemUtil.getJarFileSystem(jar); InputStream is = Files.newInputStream(fs.get().getPath(entry.path()))) { 83 | Files.copy(is, dest, StandardCopyOption.REPLACE_EXISTING); 84 | } 85 | } 86 | } 87 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/mappings/MappingFlavour.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.mappings; 2 | 3 | import java.io.IOException; 4 | import java.net.URISyntaxException; 5 | import java.nio.file.Path; 6 | import java.util.Locale; 7 | import java.util.Map; 8 | import java.util.Optional; 9 | import java.util.function.Supplier; 10 | 11 | import com.github.winplay02.gitcraft.mappings.ornithe.CalamusIntermediaryMappings; 12 | import com.github.winplay02.gitcraft.mappings.ornithe.FeatherMappings; 13 | import com.github.winplay02.gitcraft.mappings.yarn.FabricIntermediaryMappings; 14 | import com.github.winplay02.gitcraft.mappings.yarn.YarnMappings; 15 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 16 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 17 | import com.github.winplay02.gitcraft.pipeline.key.MinecraftJar; 18 | import com.github.winplay02.gitcraft.types.OrderedVersion; 19 | import com.github.winplay02.gitcraft.util.LazyValue; 20 | 21 | import net.fabricmc.mappingio.MappingVisitor; 22 | import net.fabricmc.tinyremapper.IMappingProvider; 23 | 24 | public enum MappingFlavour { 25 | MOJMAP(MojangMappings::new), 26 | FABRIC_INTERMEDIARY(FabricIntermediaryMappings::new), 27 | YARN(() -> new YarnMappings((FabricIntermediaryMappings) FABRIC_INTERMEDIARY.impl.get())), 28 | MOJMAP_PARCHMENT(() -> new ParchmentMappings((MojangMappings) MOJMAP.impl.get())), 29 | CALAMUS_INTERMEDIARY(CalamusIntermediaryMappings::new), 30 | FEATHER(FeatherMappings::new), 31 | IDENTITY_UNMAPPED(IdentityMappings::new), 32 | MOJMAP_YARN(() -> new MojangPlusYarnMappings((MojangMappings) MOJMAP.impl.get(), (YarnMappings) YARN.impl.get())); 33 | 34 | private final LazyValue impl; 35 | 36 | MappingFlavour(Supplier mapping) { 37 | this.impl = LazyValue.of(mapping); 38 | } 39 | 40 | @Override 41 | public String toString() { 42 | return super.toString().toLowerCase(Locale.ROOT); 43 | } 44 | 45 | public String getName() { 46 | return impl.get().getName(); 47 | } 48 | 49 | public boolean supportsComments() { 50 | return impl.get().supportsComments(); 51 | } 52 | 53 | public boolean needsPackageFixingForLaunch() { 54 | return impl.get().needsPackageFixingForLaunch(); 55 | } 56 | 57 | public boolean supportsConstantUnpicking() { 58 | return impl.get().supportsConstantUnpicking(); 59 | } 60 | 61 | public boolean supportsMergingPre1_3Versions() { 62 | return impl.get().supportsMergingPre1_3Versions(); 63 | } 64 | 65 | public boolean isFileRequired() { 66 | return impl.get().isMappingFileRequired(); 67 | } 68 | 69 | public String getSourceNS() { 70 | return impl.get().getSourceNS(); 71 | } 72 | 73 | public String getDestinationNS() { 74 | return impl.get().getDestinationNS(); 75 | } 76 | 77 | public boolean exists(OrderedVersion mcVersion) { 78 | return impl.get().doMappingsExist(mcVersion); 79 | } 80 | 81 | public boolean exists(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 82 | return impl.get().doMappingsExist(mcVersion, minecraftJar); 83 | } 84 | 85 | public boolean canBeUsedOn(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 86 | return impl.get().canMappingsBeUsedOn(mcVersion, minecraftJar); 87 | } 88 | 89 | public StepStatus provide(IStepContext versionContext, MinecraftJar minecraftJar) throws IOException, URISyntaxException, InterruptedException { 90 | return impl.get().provideMappings(versionContext, minecraftJar); 91 | } 92 | 93 | public Optional getPath(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 94 | return impl.get().getMappingsPath(mcVersion, minecraftJar); 95 | } 96 | 97 | public Map getAdditionalInformation(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 98 | return impl.get().getAdditionalMappingInformation(mcVersion, minecraftJar); 99 | } 100 | 101 | public void visit(OrderedVersion mcVersion, MinecraftJar minecraftJar, MappingVisitor visitor) throws IOException { 102 | impl.get().visit(mcVersion, minecraftJar, visitor); 103 | } 104 | 105 | public IMappingProvider getProvider(OrderedVersion mcVersion, MinecraftJar minecraftJar) { 106 | return impl.get().getMappingsProvider(mcVersion, minecraftJar); 107 | } 108 | 109 | public Path executeCustomLogic(Path previousFile, OrderedVersion mcVersion, MinecraftJar minecraftJar) { 110 | return impl.get().executeCustomRemappingLogic(previousFile, mcVersion, minecraftJar); 111 | } 112 | 113 | protected Mapping getImpl() { 114 | return impl.get(); 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/lib/java/com/github/winplay02/gitcraft/Library.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft; 2 | 3 | import com.github.winplay02.gitcraft.config.Configuration; 4 | import com.github.winplay02.gitcraft.config.GlobalConfiguration; 5 | import com.github.winplay02.gitcraft.config.IntegrityConfiguration; 6 | import com.github.winplay02.gitcraft.integrity.GitBlobSHA1Algorithm; 7 | import com.github.winplay02.gitcraft.integrity.IntegrityAlgorithm; 8 | import com.github.winplay02.gitcraft.integrity.SHA1Algorithm; 9 | import com.github.winplay02.gitcraft.util.MiscHelper; 10 | import com.github.winplay02.gitcraft.util.RemoteHelper; 11 | 12 | import java.io.IOException; 13 | import java.io.PrintWriter; 14 | import java.io.StringWriter; 15 | import java.time.ZoneId; 16 | import java.time.ZonedDateTime; 17 | import java.util.logging.ConsoleHandler; 18 | import java.util.logging.Formatter; 19 | import java.util.logging.Level; 20 | import java.util.logging.LogManager; 21 | import java.util.logging.LogRecord; 22 | import java.util.logging.Logger; 23 | import java.util.logging.SimpleFormatter; 24 | 25 | public class Library { 26 | public static GlobalConfiguration CONF_GLOBAL = null; 27 | public static IntegrityConfiguration CONF_INTEGRITY = null; 28 | 29 | public static IntegrityAlgorithm IA_SHA1 = null; 30 | public static IntegrityAlgorithm IA_GIT_BLOB_SHA1 = null; 31 | 32 | public static Logger LIBRARY_LOGGER = null; 33 | 34 | public static void initialize() { 35 | LIBRARY_LOGGER = Logger.getLogger("GitCraft/Library"); 36 | LIBRARY_LOGGER.setLevel(Level.ALL); 37 | LIBRARY_LOGGER.setUseParentHandlers(false); 38 | { 39 | ConsoleHandler consoleHandler = new ConsoleHandler(); 40 | consoleHandler.setFormatter(new SimpleFormatter() { 41 | protected String format = "[%1$tF %1$tT - %3$s / %4$s] %5$s %n"; 42 | 43 | @Override 44 | public String format(LogRecord record) { 45 | ZonedDateTime zdt = ZonedDateTime.ofInstant( 46 | record.getInstant(), ZoneId.systemDefault()); 47 | String source; 48 | if (record.getSourceClassName() != null) { 49 | source = record.getSourceClassName(); 50 | if (record.getSourceMethodName() != null) { 51 | source += " " + record.getSourceMethodName(); 52 | } 53 | } else { 54 | source = record.getLoggerName(); 55 | } 56 | String message = formatMessage(record); 57 | String throwable = ""; 58 | if (record.getThrown() != null) { 59 | StringWriter sw = new StringWriter(); 60 | PrintWriter pw = new PrintWriter(sw); 61 | pw.println(); 62 | record.getThrown().printStackTrace(pw); 63 | pw.close(); 64 | throwable = sw.toString(); 65 | } 66 | return String.format(format, 67 | zdt, 68 | source, 69 | record.getLoggerName(), 70 | record.getLevel().getLocalizedName(), 71 | message, 72 | throwable); 73 | } 74 | }); 75 | LIBRARY_LOGGER.addHandler(consoleHandler); 76 | } 77 | try { 78 | LibraryPaths.init(LibraryPaths.lookupCurrentWorkingDirectory()); 79 | // Maven startup 80 | RemoteHelper.loadMavenCache(); 81 | } catch (IOException e) { 82 | MiscHelper.panicBecause(e, "Could not initialize base library"); 83 | } 84 | Configuration.register("global", GlobalConfiguration.class, GlobalConfiguration::deserialize); 85 | Configuration.register("integrity", IntegrityConfiguration.class, IntegrityConfiguration::deserialize); 86 | } 87 | 88 | public static void applyConfiguration() throws IOException { 89 | Configuration.loadConfiguration(); 90 | CONF_GLOBAL = Configuration.getConfiguration(GlobalConfiguration.class); 91 | CONF_INTEGRITY = Configuration.getConfiguration(IntegrityConfiguration.class); 92 | IA_SHA1 = new SHA1Algorithm(CONF_INTEGRITY); 93 | IA_GIT_BLOB_SHA1 = new GitBlobSHA1Algorithm(CONF_INTEGRITY); 94 | System.setProperty("jdk.httpclient.connectionPoolSize", String.valueOf(CONF_GLOBAL.maxConcurrentHttpConnections())); 95 | System.setProperty("jdk.httpclient.maxstreams", String.valueOf(CONF_GLOBAL.maxConcurrentHttpStreams())); 96 | System.setProperty("jdk.httpclient.bufsize", String.valueOf(Short.MAX_VALUE * 2)); 97 | } 98 | 99 | public static Logger getSubLogger(String name) { 100 | Logger logger = Logger.getLogger(name); 101 | logger.setParent(LIBRARY_LOGGER); 102 | return logger; 103 | } 104 | 105 | public static Logger getSubLogger(String name, Level logLevel) { 106 | Logger logger = Logger.getLogger(name); 107 | logger.setParent(LIBRARY_LOGGER); 108 | logger.setLevel(logLevel); 109 | return logger; 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/pipeline/workers/Resetter.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.pipeline.workers; 2 | 3 | import com.github.winplay02.gitcraft.GitCraft; 4 | import com.github.winplay02.gitcraft.pipeline.IPipeline; 5 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepConfig; 6 | import com.github.winplay02.gitcraft.pipeline.IStepContext; 7 | import com.github.winplay02.gitcraft.pipeline.StepInput; 8 | import com.github.winplay02.gitcraft.pipeline.StepOutput; 9 | import com.github.winplay02.gitcraft.pipeline.StepResults; 10 | import com.github.winplay02.gitcraft.pipeline.StepStatus; 11 | import com.github.winplay02.gitcraft.pipeline.GitCraftStepWorker; 12 | import com.github.winplay02.gitcraft.pipeline.key.StorageKey; 13 | import com.github.winplay02.gitcraft.types.OrderedVersion; 14 | import com.github.winplay02.gitcraft.util.MiscHelper; 15 | import org.eclipse.jgit.lib.Constants; 16 | import org.eclipse.jgit.lib.Ref; 17 | import org.eclipse.jgit.revwalk.RevCommit; 18 | 19 | import java.nio.file.Path; 20 | import java.util.NavigableSet; 21 | 22 | public record Resetter(GitCraftStepConfig config) implements GitCraftStepWorker { 23 | 24 | @Override 25 | public StepOutput, GitCraftStepConfig> run( 26 | IPipeline, GitCraftStepConfig> pipeline, 27 | IStepContext.SimpleStepContext context, 28 | StepInput.Empty input, 29 | StepResults, GitCraftStepConfig> results 30 | ) throws Exception { 31 | if (GitCraft.resetVersionGraph.containsVersion(context.targetVersion())) { // only reset version artifacts of versions which are specified 32 | // delete all mc jars other than the initial artifacts 33 | // resettable artifacts are described by the storage layer 34 | // initial artifacts won't ever be different, so those mc jars can stay 35 | for (StorageKey storageKey : pipeline.getFilesystemStorage().resettableKeys()) { 36 | Path subjectPath = pipeline.getStoragePath(storageKey, context, this.config); 37 | MiscHelper.deleteFile(subjectPath); 38 | } 39 | } 40 | if (context.repository() == null) { // do not refresh repo, when no-repo is set 41 | return StepOutput.ofEmptyResultSet(StepStatus.SUCCESS); 42 | } 43 | if (context.repository().isHeadless()) { 44 | return StepOutput.ofEmptyResultSet(StepStatus.SUCCESS); 45 | } 46 | // Always refresh repo, if any refresh flag is set 47 | // delete all non-main refs that contain this commit (including remotes, tags, ...) 48 | RevCommit commitToRemove = context.repository().findRevByCommitMessage(context.targetVersion().toCommitMessage()); 49 | if (commitToRemove != null) { 50 | context.repository().checkoutBranch(GitCraft.getRepositoryConfiguration().gitMainlineLinearBranch()); 51 | for (Ref ref : context.repository().getRefsContainingCommit(commitToRemove)) { 52 | if (!ref.getName().equals(Constants.R_HEADS + GitCraft.getRepositoryConfiguration().gitMainlineLinearBranch()) && !ref.getName().equals(Constants.HEAD)) { 53 | context.repository().deleteRef(ref.getName()); 54 | } 55 | } 56 | } 57 | if (context.versionGraph().getRootVersions().contains(context.targetVersion())) { // if root node is about to be refreshed, delete main branch and HEAD 58 | context.repository().deleteRef(Constants.HEAD); 59 | context.repository().deleteRef(GitCraft.getRepositoryConfiguration().gitMainlineLinearBranch()); 60 | context.repository().createSymbolicHEAD(Constants.R_HEADS + GitCraft.getRepositoryConfiguration().gitMainlineLinearBranch()); 61 | return StepOutput.ofEmptyResultSet(StepStatus.SUCCESS); 62 | } 63 | if (GitCraft.resetVersionGraph.getRootVersions().contains(context.targetVersion())) { // Min-Version reached 64 | NavigableSet previousVersions = context.versionGraph().getPreviousVertices(context.targetVersion()); 65 | if (previousVersions.isEmpty() || previousVersions.stream().noneMatch(GitCraft.versionGraph::isOnMainBranch)) { 66 | MiscHelper.panic("Previous mainline version for '%s' does not exist, but it is not a root node. This should never happen", context.targetVersion()); 67 | } 68 | OrderedVersion newMainlineRoot = previousVersions.stream().filter(GitCraft.versionGraph::isOnMainBranch).findFirst().orElseThrow(); 69 | RevCommit commit = context.repository().findRevByCommitMessage(newMainlineRoot.toCommitMessage()); 70 | context.repository().resetRef(GitCraft.getRepositoryConfiguration().gitMainlineLinearBranch(), commit); 71 | } 72 | return StepOutput.ofEmptyResultSet(StepStatus.SUCCESS); 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/launcher/LauncherUtils.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launcher; 2 | 3 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 4 | 5 | import java.util.ArrayList; 6 | import java.util.List; 7 | import java.util.Locale; 8 | import java.util.Map; 9 | import java.util.function.Function; 10 | import java.util.regex.Matcher; 11 | import java.util.regex.Pattern; 12 | import java.util.stream.Collectors; 13 | 14 | public class LauncherUtils { 15 | protected static Pattern LAUNCH_PARAMETER_REGEX = Pattern.compile("\\$\\{.+?\\}"); 16 | 17 | public static String getOperatingSystem() { 18 | String osName = System.getProperty("os.name", "").toLowerCase(Locale.ENGLISH); 19 | if ((osName.contains("mac")) || (osName.contains("darwin"))) { 20 | return "osx"; 21 | } else if (osName.contains("win")) { 22 | return "windows"; 23 | } else if (osName.contains("nux")) { 24 | return "linux"; 25 | } else { 26 | return "unknown"; 27 | } 28 | } 29 | 30 | public static String getArch() { 31 | return System.getProperty("os.arch", "").toLowerCase(Locale.ENGLISH); 32 | } 33 | 34 | public static String getShortArch() { 35 | return System.getProperty("sun.arch.data.model", "").toLowerCase(Locale.ENGLISH); 36 | } 37 | 38 | public static boolean evaluateLauncherRuleFast(VersionInfo.VersionArgumentRule rule) { 39 | return evaluateLauncherRule(rule, getOperatingSystem(), getArch()); 40 | } 41 | 42 | public static boolean evaluateLauncherRule(VersionInfo.VersionArgumentRule rule, String os, String arch) { 43 | if (rule.os() != null) { 44 | if (!rule.action().equalsIgnoreCase("allow") && 45 | (rule.os().name() == null || rule.os().name().equalsIgnoreCase(os)) && 46 | (rule.os().arch() == null || rule.os().arch().equalsIgnoreCase(arch))) { 47 | return false; 48 | } 49 | if (rule.action().equalsIgnoreCase("allow") && 50 | (rule.os().name() != null && !rule.os().name().equalsIgnoreCase(os) || 51 | rule.os().arch() != null && !rule.os().arch().equalsIgnoreCase(arch))) { 52 | return false; 53 | } 54 | } 55 | if (rule.features() != null) { 56 | if (rule.features().is_demo_user() && !GitCraftLauncher.getLauncherConfig().launchDemo()) { 57 | return false; 58 | } 59 | if (rule.features().has_custom_resolution() && (GitCraftLauncher.getLauncherConfig().customResolution() == null)) { 60 | return false; 61 | } 62 | if (rule.features().has_quick_plays_support() && (GitCraftLauncher.getLauncherConfig().quickPlayPath() == null)) { 63 | return false; 64 | } 65 | if (rule.features().is_quick_play_singleplayer() && (GitCraftLauncher.getLauncherConfig().quickPlaySingleplayer() == null)) { 66 | return false; 67 | } 68 | if (rule.features().is_quick_play_multiplayer() && (GitCraftLauncher.getLauncherConfig().quickPlayMultiplayer() == null)) { 69 | return false; 70 | } 71 | if (rule.features().is_quick_play_realms() && (GitCraftLauncher.getLauncherConfig().quickPlayRealms() == null)) { 72 | return false; 73 | } 74 | } 75 | return true; 76 | } 77 | 78 | public static String replaceLambda(String src, Pattern regex, Function replacement) { 79 | Matcher m = regex.matcher(src); 80 | boolean result = m.find(); 81 | if (result) { 82 | StringBuilder sb = new StringBuilder(src.length()); 83 | int index = 0; 84 | do { 85 | sb.append(src, index, m.start()); 86 | sb.append(replacement.apply(m.group())); 87 | index = m.end(); 88 | } while (m.find()); 89 | sb.append(src, index, src.length()); 90 | return sb.toString(); 91 | } 92 | return src; 93 | } 94 | 95 | public static List evaluateArgs(List protoArgs, Map args, String os, String arch) { 96 | return protoArgs.stream() 97 | .filter(arg -> arg.rules().isEmpty() || 98 | arg.rules().stream().map(rule -> LauncherUtils.evaluateLauncherRule(rule, os, arch)).reduce(true, Boolean::logicalAnd)) 99 | .map(arg -> arg.value().stream().map(value -> evaluateLauncherString(value, args)).collect(Collectors.toList())).reduce(new ArrayList<>(), (list, next) -> { 100 | list.addAll(next); 101 | return list; 102 | }); 103 | } 104 | 105 | public static String evaluateLauncherString(String toEvaluate, Map args) { 106 | return LauncherUtils.replaceLambda(toEvaluate, LAUNCH_PARAMETER_REGEX, (replaced) -> { 107 | String argsVar = replaced.substring(2, replaced.length() - 1); 108 | if (args.containsKey(argsVar)) { 109 | return args.get(argsVar); 110 | } 111 | return replaced; 112 | }); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/groovy/com/github/winplay02/gitcraft/util/SerializationTypes.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.util; 2 | 3 | import com.github.winplay02.gitcraft.manifest.metadata.VersionInfo; 4 | import com.github.winplay02.gitcraft.meta.GameVersionBuildMeta; 5 | import com.github.winplay02.gitcraft.meta.SimpleVersionMeta; 6 | import com.github.winplay02.gitcraft.types.OrderedVersion; 7 | import com.google.gson.Gson; 8 | import com.google.gson.TypeAdapter; 9 | import com.google.gson.TypeAdapterFactory; 10 | import com.google.gson.reflect.TypeToken; 11 | import com.google.gson.stream.JsonReader; 12 | import com.google.gson.stream.JsonToken; 13 | import com.google.gson.stream.JsonWriter; 14 | import com.google.gson.stream.MalformedJsonException; 15 | 16 | import java.io.IOException; 17 | import java.lang.reflect.ParameterizedType; 18 | import java.lang.reflect.Type; 19 | import java.util.ArrayList; 20 | import java.util.Collections; 21 | import java.util.LinkedHashMap; 22 | import java.util.List; 23 | import java.util.TreeMap; 24 | 25 | public class SerializationTypes { 26 | 27 | public static final TypeToken> TYPE_LINKED_HASH_MAP_STRING_VERSION = new TypeToken>() { 28 | }; 29 | public static final TypeToken> TYPE_TREE_MAP_STRING_STRING = new TypeToken>() { 30 | }; 31 | public static final TypeToken> TYPE_LIST_SIMPLE_VERSION_META = new TypeToken>() { 32 | }; 33 | public static final TypeToken> TYPE_LIST_GAME_VERSION_BUILD_META = new TypeToken>() { 34 | }; 35 | 36 | public static final class ConvertToList implements TypeAdapterFactory { 37 | @Override 38 | public TypeAdapter create(Gson gson, TypeToken type) { 39 | if (!List.class.isAssignableFrom(type.getRawType())) { 40 | return null; 41 | } 42 | Type valueType = getTypeArgument(type.getType()); 43 | TypeAdapter elementTypeAdapter = (TypeAdapter) gson.getAdapter(TypeToken.get(valueType)); 44 | return (TypeAdapter) new TypeAdapterValueToList<>(elementTypeAdapter).nullSafe(); 45 | } 46 | 47 | private static Type getTypeArgument(Type type) { 48 | if (!(type instanceof ParameterizedType parameterizedType)) { 49 | return Object.class; 50 | } 51 | return parameterizedType.getActualTypeArguments()[0]; 52 | } 53 | } 54 | 55 | public static final class TypeAdapterValueToList extends TypeAdapter> { 56 | 57 | private final TypeAdapter valueAdapter; 58 | 59 | private TypeAdapterValueToList(final TypeAdapter valueAdapter) { 60 | this.valueAdapter = valueAdapter; 61 | } 62 | 63 | @Override 64 | public void write(JsonWriter out, List value) throws IOException { 65 | out.beginArray(); 66 | for (V v : value) { 67 | valueAdapter.write(out, v); 68 | } 69 | out.endArray(); 70 | } 71 | 72 | @Override 73 | public List read(JsonReader in) throws IOException { 74 | List list = new ArrayList<>(); 75 | JsonToken token = in.peek(); 76 | switch (token) { 77 | case BEGIN_ARRAY -> { 78 | in.beginArray(); 79 | while (in.hasNext()) { 80 | list.add(this.valueAdapter.read(in)); 81 | } 82 | in.endArray(); 83 | } 84 | case BEGIN_OBJECT, STRING, NUMBER, BOOLEAN, NULL -> { 85 | list.add(this.valueAdapter.read(in)); 86 | } 87 | case NAME, END_ARRAY, END_OBJECT, END_DOCUMENT -> { 88 | throw new MalformedJsonException(String.format("Unexpected %s", token)); 89 | } 90 | } 91 | return list; 92 | } 93 | } 94 | 95 | public static final class VersionArgumentWithRulesAdapter extends TypeAdapter { 96 | 97 | private final TypeAdapter delegate; 98 | 99 | public VersionArgumentWithRulesAdapter(TypeAdapter delegate) { 100 | this.delegate = delegate; 101 | } 102 | 103 | @Override 104 | public void write(JsonWriter out, VersionInfo.VersionArgumentWithRules value) throws IOException { 105 | delegate.write(out, value); 106 | } 107 | 108 | @Override 109 | public VersionInfo.VersionArgumentWithRules read(JsonReader in) throws IOException { 110 | JsonToken token = in.peek(); 111 | switch (token) { 112 | case STRING, NUMBER -> { 113 | return new VersionInfo.VersionArgumentWithRules(List.of(in.nextString()), Collections.emptyList()); 114 | } 115 | default -> { 116 | return delegate.read(in); 117 | } 118 | } 119 | } 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /launcher_agent/src/main/java/com/github/winplay02/gitcraft/launchagent/GitCraftLauncherTransformer.java: -------------------------------------------------------------------------------- 1 | package com.github.winplay02.gitcraft.launchagent; 2 | 3 | import java.lang.classfile.ClassFile; 4 | import java.lang.classfile.ClassModel; 5 | import java.lang.classfile.ClassTransform; 6 | import java.lang.classfile.CodeTransform; 7 | import java.lang.constant.ClassDesc; 8 | import java.lang.constant.MethodTypeDesc; 9 | import java.lang.instrument.ClassFileTransformer; 10 | import java.lang.instrument.IllegalClassFormatException; 11 | import java.security.ProtectionDomain; 12 | import java.util.HashMap; 13 | import java.util.Map; 14 | import java.util.function.Function; 15 | 16 | public class GitCraftLauncherTransformer implements ClassFileTransformer { 17 | 18 | protected Map> transformations = new HashMap<>(); 19 | 20 | public GitCraftLauncherTransformer() { 21 | registerTransformer("net/minecraft/launchwrapper/Launch", this::transform_net$minecraft$launchwrapper$Launch); 22 | } 23 | 24 | @Override 25 | public byte[] transform(ClassLoader loader, String fullyQualifiedClassName, Class classBeingRedefined, ProtectionDomain protectionDomain, byte[] classfileBuffer) throws IllegalClassFormatException { 26 | if (transformations.containsKey(fullyQualifiedClassName)) { 27 | System.out.printf("[GitCraft Agent]: Transforming %s (by LauncherTransformer)%n", fullyQualifiedClassName); 28 | return transformations.get(fullyQualifiedClassName).apply(classfileBuffer); 29 | } 30 | return null; 31 | } 32 | 33 | protected void registerTransformer(String fullyQualifiedClassName, Function transformation) { 34 | transformations.put(fullyQualifiedClassName, transformation); 35 | } 36 | 37 | protected byte[] transform_net$minecraft$launchwrapper$Launch(byte[] classfileBuffer) { 38 | ClassFile classFile = ClassFile.of(); 39 | ClassModel classModel = classFile.parse(classfileBuffer); 40 | return classFile.transformClass(classModel, ClassTransform.transformingMethodBodies(methodModel -> methodModel.methodType().toString().equalsIgnoreCase("()V"), CodeTransform.ofStateful(() -> (builder, element) -> { 41 | // constructor 42 | builder.aload(0); 43 | builder.invokespecial(classModel.superclass().orElseThrow().asSymbol(), "", MethodTypeDesc.ofDescriptor("()V")); 44 | // get class loader 45 | builder.aload(0); 46 | builder.invokevirtual(ClassDesc.ofInternalName("java/lang/Object"), "getClass", MethodTypeDesc.ofDescriptor("()Ljava/lang/Class;")); 47 | builder.invokevirtual(ClassDesc.ofInternalName("java/lang/Class"), "getClassLoader", MethodTypeDesc.ofDescriptor("()Ljava/lang/ClassLoader;")); 48 | builder.checkcast(ClassDesc.ofInternalName("jdk/internal/loader/BuiltinClassLoader")); 49 | builder.astore(1); 50 | // find field using methodhandles 51 | builder.ldc(ClassDesc.ofInternalName("jdk/internal/loader/BuiltinClassLoader")); 52 | builder.invokestatic(ClassDesc.ofInternalName("java/lang/invoke/MethodHandles"), "lookup", MethodTypeDesc.ofDescriptor("()Ljava/lang/invoke/MethodHandles$Lookup;")); 53 | builder.invokestatic(ClassDesc.ofInternalName("java/lang/invoke/MethodHandles"), "privateLookupIn", MethodTypeDesc.ofDescriptor("(Ljava/lang/Class;Ljava/lang/invoke/MethodHandles$Lookup;)Ljava/lang/invoke/MethodHandles$Lookup;")); 54 | builder.ldc(ClassDesc.ofInternalName("jdk/internal/loader/BuiltinClassLoader")); 55 | builder.ldc("ucp"); 56 | builder.ldc(ClassDesc.ofInternalName("jdk/internal/loader/URLClassPath")); 57 | builder.invokevirtual(ClassDesc.ofInternalName("java/lang/invoke/MethodHandles$Lookup"), "findVarHandle", MethodTypeDesc.ofDescriptor("(Ljava/lang/Class;Ljava/lang/String;Ljava/lang/Class;)Ljava/lang/invoke/VarHandle;")); 58 | builder.astore(2); 59 | // access field 60 | builder.aload(2); 61 | builder.aload(1); 62 | builder.invokevirtual(ClassDesc.ofInternalName("java/lang/invoke/VarHandle"), "get", MethodTypeDesc.ofDescriptor("(Ljdk/internal/loader/BuiltinClassLoader;)Ljdk/internal/loader/URLClassPath;")); 63 | // builder.checkcast(ClassDesc.ofInternalName("jdk/internal/loader/URLClassPath")); 64 | builder.astore(3); 65 | // access URLs 66 | builder.aload(3); 67 | builder.invokevirtual(ClassDesc.ofInternalName("jdk/internal/loader/URLClassPath"), "getURLs", MethodTypeDesc.ofDescriptor("()[Ljava/net/URL;")); 68 | builder.astore(4); 69 | builder.new_(ClassDesc.ofInternalName("net/minecraft/launchwrapper/LaunchClassLoader")); 70 | builder.dup(); 71 | builder.aload(4); 72 | builder.invokespecial(ClassDesc.ofInternalName("net/minecraft/launchwrapper/LaunchClassLoader"), "", MethodTypeDesc.ofDescriptor("([Ljava/net/URL;)V")); 73 | builder.putstatic(ClassDesc.ofInternalName("net/minecraft/launchwrapper/Launch"), "classLoader", ClassDesc.ofInternalName("net/minecraft/launchwrapper/LaunchClassLoader")); 74 | builder.return_(); 75 | }))); 76 | } 77 | } 78 | --------------------------------------------------------------------------------