diff options
Diffstat (limited to 'docs/handbook/meta/neoforge-metadata.md')
| -rw-r--r-- | docs/handbook/meta/neoforge-metadata.md | 334 |
1 files changed, 334 insertions, 0 deletions
diff --git a/docs/handbook/meta/neoforge-metadata.md b/docs/handbook/meta/neoforge-metadata.md new file mode 100644 index 0000000000..63a45c36bb --- /dev/null +++ b/docs/handbook/meta/neoforge-metadata.md @@ -0,0 +1,334 @@ +# Meta — NeoForge Metadata + +## Overview + +NeoForge is a fork of Forge that emerged in 2023. Its metadata pipeline closely mirrors Forge's, but with key differences in version numbering and Maven repository structure. NeoForge exclusively uses the build-system installer format (equivalent to Forge's v2 installer profile), so there are no legacy paths to handle. + +--- + +## Phase 1: Update — `update_neoforge.py` + +### Fetching Version Lists + +NeoForge publishes versions under two Maven artifacts: + +```python +# Legacy artifact (1.20.1 era, when NeoForge still used Forge's naming) +r = sess.get( + "https://maven.neoforged.net/api/maven/versions/releases/net%2Fneoforged%2Fforge" +) +main_json = r.json()["versions"] + +# New artifact (post-1.20.1, NeoForge's own naming) +r = sess.get( + "https://maven.neoforged.net/api/maven/versions/releases/net%2Fneoforged%2Fneoforge" +) +new_main_json = r.json()["versions"] + +main_json += new_main_json # Merge both lists +``` + +### Version Parsing + +Two regex patterns handle the two naming schemes: + +```python +# Legacy format: "1.20.1-47.1.100" +version_expression = re.compile( + r"^(?P<mc>[0-9a-zA-Z_\.]+)-(?P<ver>[0-9\.]+\.(?P<build>[0-9]+))(-(?P<branch>[a-zA-Z0-9\.]+))?$" +) + +# New NeoForge format: "20.4.237" or "21.0.0-beta" +neoforge_version_re = re.compile( + r"^(?P<mcminor>\d+)\.(?:(?P<mcpatch>\d+)|(?P<snapshot>[0-9a-z]+))\.(?P<number>\d+)(?:\.(?P<build>\d+))?(?:-(?P<tag>[0-9A-Za-z][0-9A-Za-z.+-]*))?$" +) +``` + +For the new format, the Minecraft version is reconstructed from the NeoForge version number: + +```python +if match_nf: + mc_version = match_nf.group("snapshot") + if not mc_version: + mc_version = f"1.{match_nf.group('mcminor')}" + if match_nf.group("mcpatch") != "0": + mc_version += f".{match_nf.group('mcpatch')}" + artifact = "neoforge" +``` + +### File Manifest from Maven API + +Unlike Forge which uses its own `meta.json`, NeoForge file manifests come from the Maven API: + +```python +def get_single_forge_files_manifest(longversion, artifact: str): + file_url = ( + f"https://maven.neoforged.net/api/maven/details/releases/net%2Fneoforged%2F{artifact}%2F" + + urllib.parse.quote(longversion) + ) + r = sess.get(file_url) + files_json = r.json() + + for file in files_json.get("files"): + name = file["name"] + prefix = f"{artifact}-{longversion}" + file_name = name[len(prefix):] + if file_name.startswith("."): + continue # Skip top-level extension files + classifier, ext = os.path.splitext(file_name) + if ext in [".md5", ".sha1", ".sha256", ".sha512"]: + continue # Skip checksum files + + file_obj = NeoForgeFile( + artifact=artifact, classifier=classifier, extension=ext[1:] + ) + ret_dict[classifier] = file_obj +``` + +### NeoForgeFile Model + +```python +class NeoForgeFile(MetaBase): + artifact: str # "forge" or "neoforge" + classifier: str # "installer", "universal" + extension: str # "jar" + + def filename(self, long_version): + return "%s-%s-%s.%s" % ( + self.artifact, long_version, self.classifier, self.extension, + ) + + def url(self, long_version): + return "https://maven.neoforged.net/releases/net/neoforged/%s/%s/%s" % ( + self.artifact, long_version, self.filename(long_version), + ) +``` + +### Installer Processing + +The processing is virtually identical to Forge: + +```python +def process_neoforge_version(key, entry): + version = NeoForgeVersion(entry) + if version.url() is None or not version.uses_installer(): + return + + jar_path = os.path.join(UPSTREAM_DIR, JARS_DIR, version.filename()) + + # SHA-1 verification, download, extract version.json and install_profile.json + with zipfile.ZipFile(jar_path) as jar: + with jar.open("version.json") as profile_zip_entry: + MojangVersion.parse_raw(version_data) + with jar.open("install_profile.json") as profile_zip_entry: + NeoForgeInstallerProfileV2.parse_raw(install_profile_data) + + # Cache installer info + installer_info = InstallerInfo() + installer_info.sha1hash = file_hash(jar_path, hashlib.sha1) + installer_info.sha256hash = file_hash(jar_path, hashlib.sha256) + installer_info.size = os.path.getsize(jar_path) + installer_info.write(installer_info_path) +``` + +--- + +## Phase 2: Generate — `generate_neoforge.py` + +### Single Generation Path + +Unlike Forge (which has three paths), NeoForge only uses the build-system installer path: + +```python +def version_from_build_system_installer( + installer: MojangVersion, + profile: NeoForgeInstallerProfileV2, + version: NeoForgeVersion, +) -> MetaVersion: + v = MetaVersion(name="NeoForge", version=version.rawVersion, uid=NEOFORGE_COMPONENT) + v.main_class = "io.github.zekerzhayard.forgewrapper.installer.Main" +``` + +### Library Handling + +Profile libraries go into `maven_files` (install-time downloads), and installer libraries plus ForgeWrapper go into `libraries` (runtime classpath): + +```python + v.maven_files = [] + + # Installer JAR as Maven file + installer_lib = Library( + name=GradleSpecifier("net.neoforged", version.artifact, version.long_version, "installer") + ) + installer_lib.downloads = MojangLibraryDownloads() + installer_lib.downloads.artifact = MojangArtifact( + url="https://maven.neoforged.net/releases/%s" % installer_lib.name.path(), + sha1=info.sha1hash, size=info.size, + ) + v.maven_files.append(installer_lib) + + # Profile libraries (processor dependencies) + for forge_lib in profile.libraries: + if forge_lib.name.is_log4j(): + continue + update_library_info(forge_lib) + v.maven_files.append(forge_lib) + + # Runtime libraries + v.libraries = [FORGEWRAPPER_LIBRARY] + for forge_lib in installer.libraries: + if forge_lib.name.is_log4j(): + continue + v.libraries.append(forge_lib) +``` + +### Library Info Fetching + +Same approach as Forge — fills in missing SHA-1 and size from Maven: + +```python +def update_library_info(lib: Library): + if not lib.downloads: + lib.downloads = MojangLibraryDownloads() + if not lib.downloads.artifact: + url = lib.url or f"https://maven.neoforged.net/releases/{lib.name.path()}" + lib.downloads.artifact = MojangArtifact(url=url, sha1=None, size=None) + + art = lib.downloads.artifact + if art and art.url: + if not art.sha1: + r = sess.get(art.url + ".sha1") + if r.status_code == 200: + art.sha1 = r.text.strip() + if not art.size: + r = sess.head(art.url) + if r.status_code == 200 and 'Content-Length' in r.headers: + art.size = int(r.headers['Content-Length']) +``` + +### Minecraft Version Dependency + +NeoForge extracts the Minecraft version from the installer profile's `minecraft` field: + +```python +v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=profile.minecraft)] + +# Skip if we don't have the corresponding Minecraft version +if not os.path.isfile( + os.path.join(LAUNCHER_DIR, MINECRAFT_COMPONENT, f"{profile.minecraft}.json") +): + eprint("Skipping %s with no corresponding Minecraft version %s" % (key, profile.minecraft)) + continue +``` + +### Argument Construction + +```python +mc_args = ( + "--username ${auth_player_name} --version ${version_name} --gameDir ${game_directory} " + "--assetsDir ${assets_root} --assetIndex ${assets_index_name} --uuid ${auth_uuid} " + "--accessToken ${auth_access_token} --userType ${user_type} --versionType ${version_type}" +) +for arg in installer.arguments.game: + mc_args += f" {arg}" +v.minecraft_arguments = mc_args +``` + +--- + +## Data Models + +### `NeoForgeEntry` + +```python +class NeoForgeEntry(MetaBase): + artifact: str # "forge" or "neoforge" + long_version: str # "1.20.1-47.1.100" or "20.4.237" + version: str # Short version: "47.1.100" or "237" + latest: Optional[bool] + recommended: Optional[bool] + files: Optional[Dict[str, NeoForgeFile]] +``` + +### `DerivedNeoForgeIndex` + +```python +class DerivedNeoForgeIndex(MetaBase): + versions: Dict[str, NeoForgeEntry] +``` + +Note: Unlike Forge's `DerivedForgeIndex`, this does not have a `by_mc_version` mapping. + +### `NeoForgeVersion` + +Post-processed version with resolved download URLs: + +```python +class NeoForgeVersion: + def __init__(self, entry: NeoForgeEntry): + self.artifact = entry.artifact + self.rawVersion = entry.version + if self.artifact == "neoforge": + self.rawVersion = entry.long_version + + self.long_version = entry.long_version + for classifier, file in entry.files.items(): + if classifier == "installer" and extension == "jar": + self.installer_filename = filename + self.installer_url = url +``` + +### `NeoForgeInstallerProfileV2` + +Same structure as Forge's v2 profile: + +```python +class NeoForgeInstallerProfileV2(MetaBase): + spec: Optional[int] + profile: Optional[str] + version: Optional[str] + path: Optional[GradleSpecifier] + minecraft: Optional[str] + data: Optional[Dict[str, DataSpec]] + processors: Optional[List[ProcessorSpec]] + libraries: Optional[List[Library]] +``` + +--- + +## Key Differences from Forge + +| Aspect | Forge | NeoForge | +|---|---|---| +| Maven URL | `maven.minecraftforge.net` | `maven.neoforged.net/releases` | +| File manifest API | `meta.json` per version | Maven details API | +| Artifacts | Always `forge` | `forge` (1.20.1) or `neoforge` (1.20.2+) | +| Version format | `<mc>-<forge_ver>` | `<mc>-<forge_ver>` or `<mcminor>.<mcpatch>.<build>` | +| Legacy support | Yes (MC 1.1–1.12.2) | No (MC 1.20.1+ only) | +| Component UID | `net.minecraftforge` | `net.neoforged` | +| Bad versions list | Yes | No | +| ForgeWrapper | Yes | Yes (same library) | +| Promotions/recommended | Yes | Not currently (`is_recommended = False`) | + +--- + +## Output Structure + +``` +launcher/net.neoforged/ +├── package.json +├── 21.4.38.json # New NeoForge format +├── 20.4.237.json # New NeoForge format +├── 47.1.100.json # Legacy Forge-style format (1.20.1) +└── ... +``` + +--- + +## Constants + +| Constant | Value | Location | +|---|---|---| +| `NEOFORGE_COMPONENT` | `"net.neoforged"` | `common/neoforge.py` | +| `BASE_DIR` | `"neoforge"` | `common/neoforge.py` | +| `FORGEWRAPPER_LIBRARY` | (shared with Forge) | `common/forge.py` | |
