Skip to content

Support unobfuscated versions + bug fixes & improvements#12

Open
SizableShrimp wants to merge 15 commits intoneoforged:mainfrom
SizableShrimp:main
Open

Support unobfuscated versions + bug fixes & improvements#12
SizableShrimp wants to merge 15 commits intoneoforged:mainfrom
SizableShrimp:main

Conversation

@SizableShrimp
Copy link
Contributor

Snowblower as it currently stands has a number of deficiencies which this PR attempts to address.

Main features:

  • Support unobfuscated versions, like 26.1-snapshot-1. This is also set up to work automatically with future versions, like 26.1-pre-1 or 26.1-rc-1.
    • Additionally, Minecraft versions are now parsed using a custom class rather than depending on SRGUtils. The main benefit of this system is allowing any future Minecraft version format to be parsed even if it is not explicitly accounted for; it just may be parsed as a "special" version and need to be explicitly included using a branch config.
    • All previously-released _unobfuscated version variants are also supported, but every unobfuscated version except 1.21.11_unobfuscated require explicit inclusion using a branch config. (The complete list of versions with unobfuscated variants is 25w45a, 25w46a, 1.21.11-pre1, 1.21.11-pre2, 1.21.11-pre3, 1.21.11-pre4, 1.21.11-pre5, 1.21.11-rc1, 1.21.11-rc2, 1.21.11-rc3, and 1.21.11.)
      • To support cleaner diffs when generating versions between 1.21.11 and earlier <-> 26.1 and later, 1.21.11_unobfuscated is included in the version list by default. Since this version includes names for EVERY local variable and parameter in the game that were previously autogenerated, the 1.21.11 <-> 1.21.11_unobfuscated diff is extremely large (see picture below). Unfortunately, there isn't really any way around this. It means that tracking down changes across this 1.21.11 <-> 26.1 boundary will become a lot more annoying when using git blame. But hey, at least we have names for everything now!
        Change summary for version diff from 1.21.11 to 1.21.11_unobfuscated
  • Fix bugs and general improvements around when to start over and when to push to remote. There was a bug with the detection logic for whether the branch is considered "up-to-date" when determining whether to push old commits, leading to the current version of Snowblower re-pushing every commit on startup. Fixing this alone speeds up runtime by a lot.
    • Other improvements have been added around the --start-over-if-required flag, making it smarter for detecting more complicated incompatible changes and starting over the branch where appropriate. (This part needs more testing to be honest, but I don't feel like writing a test harness at the moment.)
  • Improve logging to both log more things (e.g., computed start and end version, whether branch was checked out from remote, started over, etc.) and, when a GitHub Actions environment is detected, group log lines together (can also manually enable/disable using --github-actions [enabled] flag).
    • Now uses SLF4J's Mapped Diagnostic Context (MDC) system to set a Minecraft version property that is prepended to all log lines when generating a specific Minecraft version. This makes it easy to see exactly which log lines are associated with which versions.
  • Discover and download all artifacts ahead-of-time using multiple threads (includes all mappings, client/server JARs, and libraries) to speed up runtime and minimize downloads during the middle of generation. This supersedes feat: ahead of time downloads #11.
  • Overhaul the README to provide more background on what the project is, what it can be used for, how to use it, and where the name "Snowblower" came from (with a history lesson)!

Closes #8.
Supersedes #11.

Right now, the code doesn't work because the new 26.1 snapshot also brought with it Java 25, and MergeTool uses an outdated version of ASM.
Use ProcessMinecraftJar task from InstallerTools to unify merging and remapping code
Fix bug where old commits would be pushed even if the --push flag was not set
Fix bug where all commits would be pushed again if the branch was up-to-date with the remote
When generating 1.21.11, Snowblower will now additionally generate 1.21.11_unobfuscated in addition, to support cleaner diffs for unobfuscated versions after 1.21.11 (e.g., 26.1-snapshot-1).
… versions

All unobfuscated versions except for 1.21.11_unobfuscated are excluded by default; they can be included by explicitly adding them to the included versions list.
Improved detection for cases when starting over the branch is necessary (rather than forcing an error on the user or starting over too often).
* Rather than restarting every time a new version of Snowblower is released, now a manually incremented version ID is used, so branches are only restarted when actually necessary.
* Snowblower will now detect if the latest committed version on a branch is newer than the target and skip generating.
* Snowblower will accurately report if the latest version either doesn't exist, is older than the start version, or filtered out by the current branch configuration (or start over with --start-over-if-required).
Improved error reporting to use Logger#error in more cases, rather than throwing an exception.
Added more logging during generation/setup so that the computed configuration is more transparent to users.
Also fix general bugs with JAR file iteration
Update InstallerTools to a PR build to fix support for pre-1.16 versions
Overhauled partial cache checking to be smarter; the partial cache for the decompiled jar will now be skipped if any individual task is outdated in the partial cache.
Extracted decompilation from Generator to DecompileTask
Use MergeTool for obfuscated game versions to respect dist annotations on class members
Changed unobfuscated game versions to add dist annotations with ProcessMinecraftJar instead of the OnlyInPlugin provided by neoforged's VineflowerPlugins, since VF's plugin system currently has bugs (and the plugin itself does not respect dist annotations on nested classes)
@neoforged-pr-publishing
Copy link

neoforged-pr-publishing bot commented Dec 24, 2025

  • Publish PR to GitHub Packages

Last commit published: f8596b32349b31ead01191191e1cc4c4ba21665c - version: 2.0.46

PR Publishing

The artifacts published by this PR:

Repository Declaration

In order to use the artifacts published by the PR, add the following repository to your buildscript:

repositories {
    maven {
        name = "Maven for PR #12" // https://github.com/neoforged/snowblower/pull/12
        url = uri("https://prmaven.neoforged.net/snowblower/pr12")
        content {
            includeModule("net.neoforged", "snowblower")
        }
    }
}

}

public boolean isUnobfuscated() {
return FIRST_UNOBFUSCATED_RELEASE_DATE.compareTo(this.releaseTime) <= 0 || this.id.version().endsWith("_unobfuscated");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think this is a guarantee and I'd instead compare the version against 26.1-snapshot-1. There could always be a 1.21.12 until 26.1 is released.

Copy link
Contributor Author

@SizableShrimp SizableShrimp Dec 24, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, this poses a bit of a problem because by rewriting MinecraftVersion to use a custom class, I no longer have access to comparisons, at least not without consulting the version manifest. (And the manifest would do no better here.)

The main reason for this change is it simplifies the MinecraftVersion class a lot, and I don't need a huge if block for the snapshots that needs to be maintained to determine sorting. And it hasn't really been necessary, since the version manifest gives sorting.

While I understand your point, I think it's unlikely to come up. How often do they release new versions for the current version when they have started the next development cycle? Also, it's debatable whether or not they will designate 1.21.12 (if such a version were to come out) as unobfuscated by default or not. Thus, I think it's reasonable to keep this as-is for now and reevaluate if it turns out not to be the case.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This far into the development cycle, I think Mojang is only going to release a new hotfix when there's a security issue or massive game-breaking flaw. I think we can proceed with this heuristic for now, and cross that bridge when we get to it as Shrimp says.

mappings = IMappingFile.load(in).reverse();
}

try (var inFs = FileSystems.newFileSystem(serverJar);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ZipInputStream/ZipOutputStream is probably going to faster given that NIO isn't needed here (it's a case of simply listing all files and copying some of them over). But this is a nitpick.

LOGGER.info("Discovering and downloading artifacts for {} versions", versions.size());
GitHubActions.logStartGroup("Discovering and downloading artifacts");

try (ExecutorService executor = Executors.newFixedThreadPool(Runtime.getRuntime().availableProcessors())) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be a virtual thread pool without a limit

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I guess I haven't compared them, but would an unbounded number of threads not be slowed down by high CPU contention? I can test and come back to this.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we do go virtual threads here (which I'm pretty sure is a good fit at least on its face), what I'm not sure is if it would be better to have each download be a separate virtual thread rather than each version (with multiple downloads) on a virtual thread.

It would require some refactoring to pass the executor around, so each call to Util.downloadFile is its own task.

However, what I will say is that because Snowblower runs on Java 21, it doesn't have JEP 491: Synchronize Virtual Threads without Pinning (delivered in Java 24). So, if we move to virtual threads, that synchronized block below will have to become a Semaphore or other java.util.concurrent lock, to avoid pinning virtual threads to platform threads.


var githubAppId = parser.accepts("github-app-id", "The ID of a GitHub app to use for git auth").withRequiredArg().ofType(String.class);
var githubInstallationRepo = parser.accepts("github-installation-repo", "The name of the repository to use as the installation target of the GitHub app").availableIf(githubAppId).withRequiredArg();
var githubActionsO = parser.accepts("github-actions", "Whether Snowblower is being executed in a GitHub Actions environment (to enhance logging); defaults to the parsed boolean value of the \"GITHUB_ACTIONS\" environment variable if available")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this configurable? Are there cases where it's run in a GHA environment without the env var set? If so, that sounds like a bug. Besides, I'd argue that the env var is itself a config option and this is redundant.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The main reason is simply the fact that I originally coded it to use the flag without accounting for the environment variable. I'll change this accordingly.

java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
languageVersion = JavaLanguageVersion.of(21)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Given 25 is the newest LTS, I'd go the full mile and bump to 25.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm fine with either, but the code wouldn't be using any features of J25. I only bumped it to J21 to begin with since the latest InstallerTools version required. Keeping it at J21 means less headache for CI environments, Snowblower's main target, or users in general since J25 is still relatively new.

Copy link
Member

@sciwhiz12 sciwhiz12 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A read-through of it looks good. Mostly found some small details and some other comments.

We should update Gradle, the plugins (particularly to switch to net.neoforged.licenser), and the license header as it still says "Forge Development LLC" even for new files.

Comment on lines +59 to +71
private final boolean special;

Type() {
this(false);
}

Type(boolean special) {
this.special = special;
}

public boolean isSpecial() {
return this.special;
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, you could reduce isSpecial to just be a check on this == SPECIAL.

import java.util.regex.Pattern;

public record MinecraftVersion(Type type, String version) {
// Version formats per slicedlime - https://x.com/slicedlime/status/1995886660417192442
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For completeness' sake (and to avoid having to refer to Twitter/X just to check), you should include the pattern here: <year>.<drop>[.<hotfix>][-<snapshot-type>-<build>], where <snapshot-type> can be snapshot, pre, or rc.

}

public boolean isUnobfuscated() {
return FIRST_UNOBFUSCATED_RELEASE_DATE.compareTo(this.releaseTime) <= 0 || this.id.version().endsWith("_unobfuscated");
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This far into the development cycle, I think Mojang is only going to release a new hotfix when there's a security issue or massive game-breaking flaw. I think we can proceed with this heuristic for now, and cross that bridge when we get to it as Shrimp says.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

A thought: this could be expanded in the future to also encompass other CI systems that have equivalent functionality. TeamCity's service messages comes to mind in this regard. But that can be implemented in the future.


try (var fs = FileSystems.newFileSystem(getOurJar(), (ClassLoader) null)) {
try (var fs = Util.isDev() ? null : FileSystems.newFileSystem(getOurJar(), (ClassLoader) null)) {
Path copyParentFolder = Util.isDev() ? Util.getSourcePath() : fs.getRootDirectories().iterator().next();
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This ought to be a null check now, since fs is null when running from a development environment.


### April Fools' Day versions

Snowblower also supports generating branches for April Fools' Day versions, separate from the mainline releases. Snowblower includes default support for `20w14infinite`, `22w13oneblockatatime`, `23w13a_or_b`, `24w14potato`, and `25w14craftmine` under the branch name `april-fools/<version>`. These branches will generate exactly two versions: the base version that the given April Fools' Day version is believed to have been forked from, and the April Fools' Day version itself.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Going to be fun to remember to update this with more April Fools versions as they release and get added here. 😆

Comment on lines +70 to +71
// If making changes to generation that affect the output (e.g., updating the decompiler or adding/removing decompiler args), increment this number.
public static final int VERSION_ID = 2;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Perhaps we should have either a CONTRIBUTING.md or a section in the README which mentions this. We might forget otherwise 😅

// Allow resuming by finding the last thing we generated
int skipCount = this.getSkipCount(versions, filteredVersions, toGenerate, range[0]);
if (skipCount == -1) {
return;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd like to have a brief comment here to note that -1 means an error occured, so no further processing should be done. (getSkipCount already logs the error.)

exclude.addAll(UnobfuscatedVersions.getVersionsToExclude());
if (this.branch.includeVersions() != null)
exclude.removeAll(this.branch.includeVersions());
this.branch.includeVersions().forEach(exclude::remove);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This confused me for a bit, until I opened IDEA and tried reverting this change. Apparently, IDEA has an inspection for calling removeAll for a Set with a given List argument, since AbstractSet and its descendants call List#contains for each set element (linear search for each element).

targetVer = filteredVersions.getLast().id();
} else {
var exclude = versions.stream().filter(v -> v.id().getType().isSpecial()).map(VersionInfo::id).collect(Collectors.toList());
var exclude = filteredVersions.stream().map(VersionInfo::id).filter(id -> id.type().isSpecial()).collect(Collectors.toSet());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Only now do I realize that Collectors#toSet has no guarantees if the returned collection is mutable. It may be wise to either copy the resulting set or use Collections#toCollection(LinkedHashSet::new). (Just in case.)

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

While the written contract specifies no guarantee, after over a decade it's clear this isn't changing anytime soon, too much software relies on it. Additionally Collectors#toUnmodifiableSet was added in java 10, and Stream#toList() was added for immutable lists too, so precedent also supports that they're more likely to just add alternative methods than to touch one that has existed for a decade and so many people rely on.

@Pablete1234
Copy link

After running this, it appears something about obf/unobf params is wrong, as the decompile output for 1.21.11 has $$0 as lambda parameters everywhere:
image

(ran with java -jar snowblower.jar --output ./output --branch release --exclude "**.nbt" --exclude "**.png" --start-over-if-required)

@ApexModder
Copy link
Member

ApexModder commented Jan 25, 2026

@Pablete1234 that is actually intended as the release branch does not yet generate 21.11-unobf the source your actually seeing is 21.11-obf

afaik unobf versions are only included with the first unobf release which would be 26.1
you can see this by running the dev branch which includes snapshots, so includes the 21.11-unobf as a bridge between obf and unobf (21.11->21.11-unobf->26.1-snapshot-1)

@Pablete1234
Copy link

Pablete1234 commented Jan 25, 2026

Shouldn't the 1.21.11-obf be decompiled with vineflower flag to rename invalid param names, such that it's not all invalid source?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature Request] Implement various improvements

5 participants

Comments