summaryrefslogtreecommitdiff
path: root/docs/handbook/meta/forge-metadata.md
diff options
context:
space:
mode:
Diffstat (limited to 'docs/handbook/meta/forge-metadata.md')
-rw-r--r--docs/handbook/meta/forge-metadata.md492
1 files changed, 492 insertions, 0 deletions
diff --git a/docs/handbook/meta/forge-metadata.md b/docs/handbook/meta/forge-metadata.md
new file mode 100644
index 0000000000..93d9c11efa
--- /dev/null
+++ b/docs/handbook/meta/forge-metadata.md
@@ -0,0 +1,492 @@
+# Meta — Forge Metadata
+
+## Overview
+
+Forge is the oldest and most complex mod loader supported by Meta. The processing pipeline handles:
+
+- Multiple Forge distribution formats across different Minecraft versions (legacy JARs, installer profiles v1, installer profiles v2 with build system)
+- ForgeWrapper integration for modern Forge versions
+- FML library injection for legacy versions (1.3.2–1.5.2)
+- Installer JAR downloading, extraction, and caching
+- Library deduplication against Minecraft's own libraries
+
+---
+
+## Phase 1: Update — `update_forge.py`
+
+### Fetching the Version Index
+
+Forge publishes two key metadata files:
+
+```python
+# Maven metadata — maps MC versions to Forge version lists
+r = sess.get(
+ "https://files.minecraftforge.net/net/minecraftforge/forge/maven-metadata.json"
+)
+main_json = r.json() # dict: {"1.20.4": ["1.20.4-49.0.31", ...], ...}
+
+# Promotions — marks latest/recommended versions
+r = sess.get(
+ "https://files.minecraftforge.net/net/minecraftforge/forge/promotions_slim.json"
+)
+promotions_json = r.json() # {"promos": {"1.20.4-latest": "49.0.31", ...}}
+```
+
+### Building the Derived Index
+
+The updater reconstructs a comprehensive index from these fragments:
+
+```python
+new_index = DerivedForgeIndex()
+
+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\.]+))?$"
+)
+
+for mc_version, value in main_json.items():
+ for long_version in value:
+ match = version_expression.match(long_version)
+ files = get_single_forge_files_manifest(long_version)
+
+ entry = ForgeEntry(
+ long_version=long_version,
+ mc_version=mc_version,
+ version=match.group("ver"),
+ build=int(match.group("build")),
+ branch=match.group("branch"),
+ latest=False,
+ recommended=version in recommended_set,
+ files=files,
+ )
+ new_index.versions[long_version] = entry
+```
+
+### File Manifest Fetching
+
+For each version, the updater fetches a file manifest from Forge's Maven:
+
+```python
+def get_single_forge_files_manifest(longversion):
+ file_url = (
+ "https://files.minecraftforge.net/net/minecraftforge/forge/%s/meta.json"
+ % longversion
+ )
+ r = sess.get(file_url)
+ files_json = r.json()
+
+ for classifier, extensionObj in files_json.get("classifiers").items():
+ # Parse each file: installer, universal, changelog, etc.
+ file_obj = ForgeFile(
+ classifier=classifier, hash=processed_hash, extension=extension
+ )
+ ret_dict[classifier] = file_obj
+
+ return ret_dict
+```
+
+Each `ForgeFile` represents a downloadable artifact:
+
+```python
+class ForgeFile(MetaBase):
+ classifier: str # "installer", "universal", "client", "changelog"
+ hash: str # MD5 hash
+ extension: str # "jar", "zip", "txt"
+
+ def filename(self, long_version):
+ return "%s-%s-%s.%s" % ("forge", long_version, self.classifier, self.extension)
+
+ def url(self, long_version):
+ return "https://maven.minecraftforge.net/net/minecraftforge/forge/%s/%s" % (
+ long_version, self.filename(long_version),
+ )
+```
+
+### Installer JAR Processing
+
+For versions that use installers, the updater downloads the JAR and extracts two files:
+
+```python
+def process_forge_version(version, jar_path):
+ # Download if not cached
+ if not os.path.isfile(jar_path):
+ download_binary_file(sess, jar_path, version.url())
+
+ # Extract from ZIP
+ with zipfile.ZipFile(jar_path) as jar:
+ # Extract version.json (Minecraft version overlay)
+ with jar.open("version.json") as profile_zip_entry:
+ version_data = profile_zip_entry.read()
+ MojangVersion.parse_raw(version_data) # Validate
+ with open(version_file_path, "wb") as f:
+ f.write(version_data)
+
+ # Extract install_profile.json
+ with jar.open("install_profile.json") as profile_zip_entry:
+ install_profile_data = profile_zip_entry.read()
+ # Try both v1 and v2 formats
+ try:
+ ForgeInstallerProfile.parse_raw(install_profile_data)
+ except ValidationError:
+ ForgeInstallerProfileV2.parse_raw(install_profile_data)
+```
+
+### Installer Info Caching
+
+SHA-1, SHA-256, and size of each installer JAR are cached:
+
+```python
+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)
+```
+
+### SHA-1 Verification
+
+Before processing, the updater checks if the local JAR matches the remote checksum:
+
+```python
+fileSha1 = get_file_sha1_from_file(jar_path, sha1_file)
+rfile = sess.get(version.url() + ".sha1")
+new_sha1 = rfile.text.strip()
+if fileSha1 != new_sha1:
+ remove_files([jar_path, profile_path, installer_info_path, sha1_file])
+```
+
+If the SHA-1 mismatch is detected, all cached artifacts are deleted and re-downloaded.
+
+### Legacy Version Info
+
+For pre-installer Forge versions (MC 1.1–1.5.2), the updater extracts release times from JAR file timestamps:
+
+```python
+tstamp = datetime.fromtimestamp(0)
+with zipfile.ZipFile(jar_path) as jar:
+ for info in jar.infolist():
+ tstamp_new = datetime(*info.date_time)
+ if tstamp_new > tstamp:
+ tstamp = tstamp_new
+legacy_info = ForgeLegacyInfo()
+legacy_info.release_time = tstamp
+legacy_info.sha1 = file_hash(jar_path, hashlib.sha1)
+```
+
+### Bad Versions
+
+Certain versions are blacklisted:
+
+```python
+BAD_VERSIONS = ["1.12.2-14.23.5.2851"]
+```
+
+---
+
+## Phase 2: Generate — `generate_forge.py`
+
+### ForgeVersion Post-Processing
+
+The raw `ForgeEntry` is converted into a `ForgeVersion` object that resolves download URLs:
+
+```python
+class ForgeVersion:
+ def __init__(self, entry: ForgeEntry):
+ self.build = entry.build
+ self.rawVersion = entry.version
+ self.mc_version = entry.mc_version
+ self.mc_version_sane = self.mc_version.replace("_pre", "-pre", 1)
+ self.long_version = "%s-%s" % (self.mc_version, self.rawVersion)
+ if self.branch is not None:
+ self.long_version += "-%s" % self.branch
+
+ for classifier, file in entry.files.items():
+ if classifier == "installer" and extension == "jar":
+ self.installer_filename = filename
+ self.installer_url = url
+ if (classifier == "universal" or classifier == "client") and ...:
+ self.universal_filename = filename
+ self.universal_url = url
+
+ def uses_installer(self):
+ if self.installer_url is None:
+ return False
+ if self.mc_version == "1.5.2":
+ return False
+ return True
+```
+
+### Library Deduplication
+
+Libraries already provided by Minecraft are filtered out:
+
+```python
+def should_ignore_artifact(libs: Collection[GradleSpecifier], match: GradleSpecifier):
+ for ver in libs:
+ if ver.group == match.group and ver.artifact == match.artifact and ver.classifier == match.classifier:
+ if ver.version == match.version:
+ return True # Exact match, ignore
+ elif pversion.parse(ver.version) > pversion.parse(match.version):
+ return True # Minecraft has newer version
+ else:
+ return False # Forge has newer, keep it
+ return False # Not in Minecraft, keep it
+```
+
+### Three Generation Paths
+
+The generator handles three distinct Forge eras:
+
+#### Path 1: Legacy (MC 1.1–1.5.2) — `version_from_legacy()`
+
+Pre-installer versions that inject a JAR mod:
+
+```python
+def version_from_legacy(info: ForgeLegacyInfo, version: ForgeVersion) -> MetaVersion:
+ v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
+ v.requires = [Dependency(uid=MINECRAFT_COMPONENT, equals=mc_version)]
+ v.release_time = info.release_time
+ v.order = 5
+
+ if fml_libs_for_version(mc_version):
+ v.additional_traits = ["legacyFML"]
+
+ main_mod = Library(
+ name=GradleSpecifier("net.minecraftforge", "forge", version.long_version, classifier)
+ )
+ main_mod.downloads = MojangLibraryDownloads()
+ main_mod.downloads.artifact = MojangArtifact(url=version.url(), sha1=info.sha1, size=info.size)
+ v.jar_mods = [main_mod]
+ return v
+```
+
+#### Path 2: Old Installer (MC 1.6–1.12.2) — `version_from_profile()` / `version_from_modernized_installer()`
+
+These versions have installer profiles with embedded version JSONs:
+
+```python
+def version_from_profile(profile: ForgeInstallerProfile, version: ForgeVersion) -> MetaVersion:
+ v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
+ v.main_class = profile.version_info.main_class
+ v.release_time = profile.version_info.time
+
+ # Extract tweaker classes from arguments
+ args = profile.version_info.minecraft_arguments
+ tweakers = []
+ expression = re.compile(r"--tweakClass ([a-zA-Z0-9.]+)")
+ match = expression.search(args)
+ while match is not None:
+ tweakers.append(match.group(1))
+ # ...
+ v.additional_tweakers = tweakers
+
+ # Filter libraries against Minecraft's library set
+ mc_filter = load_mc_version_filter(mc_version)
+ for forge_lib in profile.version_info.libraries:
+ if forge_lib.name.is_lwjgl() or should_ignore_artifact(mc_filter, forge_lib.name):
+ continue
+ # Rename minecraftforge → forge with universal classifier
+ # ...
+ v.libraries.append(overridden_lib)
+```
+
+#### Path 3: Modern Build System (MC 1.13+) — `version_from_build_system_installer()`
+
+Modern Forge uses a two-file installer system (`install_profile.json` + `version.json`). The launcher cannot run the installer's processors directly, so ForgeWrapper is injected:
+
+```python
+def version_from_build_system_installer(
+ installer: MojangVersion, profile: ForgeInstallerProfileV2, version: ForgeVersion
+) -> MetaVersion:
+ v = MetaVersion(name="Forge", version=version.rawVersion, uid=FORGE_COMPONENT)
+ v.main_class = "io.github.zekerzhayard.forgewrapper.installer.Main"
+
+ v.maven_files = []
+
+ # Add installer JAR as a Maven file
+ info = InstallerInfo.parse_file(...)
+ installer_lib = Library(
+ name=GradleSpecifier("net.minecraftforge", "forge", version.long_version, "installer")
+ )
+ installer_lib.downloads = MojangLibraryDownloads()
+ installer_lib.downloads.artifact = MojangArtifact(
+ url="https://maven.minecraftforge.net/%s" % installer_lib.name.path(),
+ sha1=info.sha1hash, size=info.size,
+ )
+ v.maven_files.append(installer_lib)
+
+ # Add profile libraries as Maven files
+ for forge_lib in profile.libraries:
+ if forge_lib.name.is_log4j():
+ continue
+ update_library_info(forge_lib)
+ v.maven_files.append(forge_lib)
+
+ # Add ForgeWrapper as runtime library
+ v.libraries = [FORGEWRAPPER_LIBRARY]
+
+ # Add installer's runtime libraries
+ for forge_lib in installer.libraries:
+ if forge_lib.name.is_log4j():
+ continue
+ v.libraries.append(forge_lib)
+
+ # Build Minecraft arguments
+ mc_args = "--username ${auth_player_name} --version ${version_name} ..."
+ for arg in installer.arguments.game:
+ mc_args += f" {arg}"
+ if "--fml.forgeGroup" not in installer.arguments.game:
+ mc_args += f" --fml.forgeGroup net.minecraftforge"
+ if "--fml.forgeVersion" not in installer.arguments.game:
+ mc_args += f" --fml.forgeVersion {version.rawVersion}"
+ if "--fml.mcVersion" not in installer.arguments.game:
+ mc_args += f" --fml.mcVersion {version.mc_version}"
+ v.minecraft_arguments = mc_args
+ return v
+```
+
+### Library Info Population
+
+For libraries that lack complete download metadata, the generator fetches it:
+
+```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.minecraftforge.net/{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'])
+```
+
+### FML Libraries
+
+Legacy Forge versions (MC 1.3.2–1.5.2) require FML libraries. The `fml_libs_for_version()` function returns the exact set for each MC version:
+
+```python
+def fml_libs_for_version(mc_version: str) -> List[FMLLib]:
+ if mc_version == "1.3.2":
+ return [argo_2_25, guava_12_0_1, asm_all_4_0]
+ elif mc_version in ["1.4", "1.4.1", ..., "1.4.7"]:
+ return [argo_2_25, guava_12_0_1, asm_all_4_0, bcprov_jdk15on_147]
+ elif mc_version == "1.5":
+ return [argo_small_3_2, guava_14_0_rc3, asm_all_4_1,
+ bcprov_jdk15on_148, deobfuscation_data_1_5, scala_library]
+ # ...
+```
+
+### Recommended Versions
+
+Versions marked as `recommended` by Forge promotions are tracked:
+
+```python
+recommended_versions = []
+for key, entry in remote_versions.versions.items():
+ if entry.recommended:
+ recommended_versions.append(version.rawVersion)
+
+package = MetaPackage(uid=FORGE_COMPONENT, name="Forge",
+ project_url="https://www.minecraftforge.net/forum/")
+package.recommended = recommended_versions
+```
+
+---
+
+## Data Models
+
+### `ForgeEntry`
+
+```python
+class ForgeEntry(MetaBase):
+ long_version: str # "1.20.4-49.0.31"
+ mc_version: str # "1.20.4"
+ version: str # "49.0.31"
+ build: int # 31
+ branch: Optional[str]
+ latest: Optional[bool]
+ recommended: Optional[bool]
+ files: Optional[Dict[str, ForgeFile]]
+```
+
+### `DerivedForgeIndex`
+
+```python
+class DerivedForgeIndex(MetaBase):
+ versions: Dict[str, ForgeEntry]
+ by_mc_version: Dict[str, ForgeMCVersionInfo]
+```
+
+### `ForgeInstallerProfile` (v1)
+
+```python
+class ForgeInstallerProfile(MetaBase):
+ install: ForgeInstallerProfileInstallSection
+ version_info: ForgeVersionFile
+ optionals: Optional[List[ForgeOptional]]
+```
+
+### `ForgeInstallerProfileV2`
+
+```python
+class ForgeInstallerProfileV2(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]]
+```
+
+### `InstallerInfo`
+
+```python
+class InstallerInfo(MetaBase):
+ sha1hash: Optional[str]
+ sha256hash: Optional[str]
+ size: Optional[int]
+```
+
+---
+
+## ForgeWrapper
+
+The `FORGEWRAPPER_LIBRARY` is defined in `common/forge.py`:
+
+```python
+FORGEWRAPPER_LIBRARY = make_launcher_library(
+ GradleSpecifier("io.github.zekerzhayard", "ForgeWrapper", "projt-2026-04-04"),
+ "4c4653d80409e7e968d3e3209196ffae778b7b4e",
+ 29731,
+)
+```
+
+This creates a `Library` object with:
+- Download URL: `https://files.projecttick.org/maven/io/github/zekerzhayard/ForgeWrapper/projt-2026-04-04/ForgeWrapper-projt-2026-04-04.jar`
+- SHA-1: `4c4653d80409e7e968d3e3209196ffae778b7b4e`
+- Size: 29731 bytes
+
+---
+
+## Output Structure
+
+```
+launcher/net.minecraftforge/
+├── package.json # name, recommended versions, project URL
+├── 49.0.31.json # Modern (build system installer)
+├── 36.2.39.json # Modern (build system installer)
+├── 14.23.5.2860.json # Old installer (profile v1)
+├── 10.13.4.1614.json # Modernized installer for legacy MC
+├── 7.8.1.740.json # Legacy JAR mod
+└── ...
+```