File size: 26,121 Bytes
d984ed3 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 | """
Version resolver: turns "latest" (or a specific version string) into a
fully-resolved Fabric build configuration by consulting the Mojang manifest,
Fabric Meta, and Modrinth APIs.
The most subtle part of this module is the **snapshot fallback logic** in
`VersionResolver.resolve_version`. See its docstring for the rationale.
"""
from __future__ import annotations
import asyncio
import logging
import re
from datetime import datetime
from typing import Any, Optional
import httpx
from models import FabricConfig, MinecraftVersion
logger = logging.getLogger(__name__)
# ---------------------------------------------------------------------------
# Constants
# ---------------------------------------------------------------------------
MOJANG_MANIFEST_URL = "https://piston-meta.mojang.com/mc/game/version_manifest_v2.json"
FABRIC_YARN_URL = "https://meta.fabricmc.net/v2/versions/yarn/{mc_version}"
FABRIC_LOADER_URL = "https://meta.fabricmc.net/v2/versions/loader"
MODRINTH_FABRIC_API_URL = "https://api.modrinth.com/v2/project/fabric-api/version"
# These defaults are not currently fetched from a remote source. They reflect
# the toolchain versions that work across the MC 1.21 / 26.x range. Override
# them on the returned FabricConfig if your project requires different values.
DEFAULT_LOOM_VERSION = "1.7-SNAPSHOT"
DEFAULT_GRADLE_VERSION = "8.8"
# Lowest Minecraft version the oracle will consider for "latest" resolution.
MINIMUM_MC_VERSION_ID = "1.21"
# Cache TTLs
MANIFEST_CACHE_TTL_SECONDS = 600 # 10 minutes β manifest doesn't change often
# Safety caps so a runaway loop never hammers the upstream APIs.
MAX_SNAPSHOTS_TO_PROBE = 20
MAX_RELEASES_TO_PROBE = 10
# ---------------------------------------------------------------------------
# Version string parsing
# ---------------------------------------------------------------------------
# Match the space form "26.3 Snapshot 2" OR the dash form "26.3-snapshot-2"
# (the dash form is what Mojang's manifest actually publishes).
_SNAPSHOT_RE = re.compile(
r"^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*[-\s]+[Ss]napshot\s*[-\s]*(\d+)\s*$"
)
# Pre-release and release candidate forms: "26.2-pre-6", "26.2-rc-1".
# Treated as snapshots for sorting (below the corresponding release).
_PRE_RE = re.compile(
r"^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*[-\s]+(?:pre|rc)\s*[-\s]*(\d+)\s*$"
)
_RELEASE_RE = re.compile(r"^\s*(\d+)\.(\d+)(?:\.(\d+))?\s*$")
# Legacy weekly-snapshot format like "23w13a", "25w46a". Used by Mojang for
# older snapshots. We need to recognize these so we can correctly *exclude*
# pre-1.21 ones from the "latest" candidate pool.
_WEEKLY_SNAPSHOT_RE = re.compile(r"^\s*(\d{2})w(\d{1,2})[a-z]\s*$")
def parse_mc_version(version_id: str) -> tuple[int, ...]:
"""
Parse a Minecraft version string into a comparable tuple.
Minecraft recently switched from `1.21.x` style versioning to year-based
`26.x` versioning. A standard semver parser would treat `1.21.4` as
*newer* than `26.2` because semver would parse `1` and `26` as the major
version. We instead want `26.2` to be newer, because year 2026 follows
year 2024 (when `1.21` shipped).
The trick: keep the numeric components in tuple order. Python's tuple
comparison walks left-to-right, so `(26, 2, 0, ...)` > `(1, 21, 0, ...)`
because `26 > 1`. No special "year offset" is needed.
Snapshots are sorted *below* their corresponding release by appending a
type marker (`1` for snapshot, `2` for release) followed by the snapshot
number for snapshots. This means:
`26.3 Snapshot 2` < `26.3 Snapshot 10` < `26.3` (release)
Supports three input forms (all observed in Mojang's manifest):
- "26.3 Snapshot 2" (space form, user-facing)
- "26.3-snapshot-2" (dash form, manifest id)
- "26.2-pre-6" (dash form, pre-release)
- "26.2-rc-1" (dash form, release candidate)
Returns a tuple of ints. Unparseable strings return (0,) so they always
sort below any real version β this is intentional, because the resolver's
`is_minimum_version` filter then excludes them from the "latest" pool.
"""
snap = _SNAPSHOT_RE.match(version_id)
if snap:
major = int(snap.group(1))
minor = int(snap.group(2))
patch = int(snap.group(3)) if snap.group(3) else 0
snap_num = int(snap.group(4))
# type marker: 1 = snapshot (lower than release's 2)
return (major, minor, patch, 1, snap_num)
pre = _PRE_RE.match(version_id)
if pre:
major = int(pre.group(1))
minor = int(pre.group(2))
patch = int(pre.group(3)) if pre.group(3) else 0
pre_num = int(pre.group(4))
# Treat pre-releases as snapshots for ordering purposes.
return (major, minor, patch, 1, pre_num)
rel = _RELEASE_RE.match(version_id)
if rel:
major = int(rel.group(1))
minor = int(rel.group(2))
patch = int(rel.group(3)) if rel.group(3) else 0
# type marker: 2 = release
return (major, minor, patch, 2)
# Legacy weekly snapshots like "23w13a", "25w46a". Map them onto the
# (year, week) tuple. Year 23 == 2023, week 13. We use the same 4-tuple
# shape with a sentinel marker so they sort *above* 1.21 if year>=24 but
# below 1.21 otherwise. (24wXXa corresponds to MC 1.21 snapshots.)
weekly = _WEEKLY_SNAPSHOT_RE.match(version_id)
if weekly:
year_short = int(weekly.group(1))
week = int(weekly.group(2))
# Weekly snapshots sort *below* the corresponding release: use type
# marker 1. Major version = full year (e.g. 23 -> 2023) so 23w13a
# (2023) sorts below 1.21 (which is (1, 21, 0, 2)).
return (2000 + year_short, week, 0, 1)
# Truly unparseable: return (0,) so it sorts below everything.
return (0,)
def is_minimum_version(version_id: str, minimum: str = MINIMUM_MC_VERSION_ID) -> bool:
"""
True iff `version_id` >= `minimum` under our custom parser.
For minimum "1.21", this rejects:
- Legacy weekly snapshots from before 1.21's release (mid-2024).
Weekly snapshots live in a separate "epoch" (year + week) that
doesn't tuple-compare cleanly against `1.x` / `26.x` versions, so
we apply a separate cutoff: weekly snapshots at or after week 24
of 2024 (when 1.21 dropped) are considered "above 1.21".
- Truly unparseable strings (which return (0,)).
"""
# Special case: legacy weekly snapshots (YYwNNa format). 1.21 was
# released on 2024-06-13 (ISO week 24). Snapshots at or after that
# point are experimental snapshots for 1.21.x+ content.
weekly = _WEEKLY_SNAPSHOT_RE.match(version_id)
if weekly:
year_short = int(weekly.group(1))
week = int(weekly.group(2))
return (year_short, week) >= (24, 24)
try:
return parse_mc_version(version_id) >= parse_mc_version(minimum)
except Exception:
return False
# Minecraft 26.1 (released March 2026) is the first release where Mojang
# shipped the Java jar *unobfuscated*, with parameter names and (limited)
# Javadoc-style comments included. As a consequence, the Fabric Project
# officially retired Yarn mappings β Yarn builds simply don't exist for
# 26.1+. Mods targeting 26.1+ use Mojang's official mappings directly via
# Loom's `loom.officialMojangMappings()` call in build.gradle, and the
# `gradle.properties` file does NOT carry a `yarn_mappings` line.
#
# Source: https://docs.fabricmc.net/develop/porting/mappings
# "Minecraft 26.1 is unobfuscated and includes parameter names,
# so there is no need for any obfuscation mappings."
UNOBFUSCATED_MINIMUM = "26.1"
def is_unobfuscated(version_id: str) -> bool:
"""
True iff `version_id` is an unobfuscated Minecraft release (>= 26.1).
For these versions:
- Yarn mappings do NOT exist on Fabric Meta and never will.
- The Fabric ecosystem (Yarn, Loader, Fabric API) still works β only
the mappings layer is replaced by Mojang's official Mojmap.
- Loom reads Mojmap via `loom.officialMojangMappings()` in build.gradle
instead of `mappings "net.fabricmc:yarn:..."`.
The resolver uses this to decide whether Yarn is a hard requirement
when probing for "latest" support.
"""
# Weekly snapshots from 2025+ (25wXXa) are 1.21.x dev cycle, still
# obfuscated. Only year-based 26.1+ versions are unobfuscated.
if _WEEKLY_SNAPSHOT_RE.match(version_id):
return False
try:
return parse_mc_version(version_id) >= parse_mc_version(UNOBFUSCATED_MINIMUM)
except Exception:
return False
# ---------------------------------------------------------------------------
# Resolver
# ---------------------------------------------------------------------------
class VersionResolver:
"""
Resolves the optimal Fabric configuration for a Minecraft version.
## Snapshot fallback logic
When `target_version` is `"latest"` (or omitted), the resolver must be
*intelligent* about which version it picks. Minecraft snapshots are
released frequently by Mojang, but the Fabric ecosystem (Fabric API mod
for all versions; Yarn mappings only for *obfuscated* versions) often
lags by days. If we blindly returned the newest snapshot, the AI might
generate a `gradle.properties` that references Fabric API versions that
*do not exist yet*, and the user's mod would fail to build.
The resolver therefore:
1. Fetches the Mojang manifest and keeps only versions >= `1.21`.
2. Sorts them by `releaseTime` descending (newest first).
3. Iterates the newest snapshots, one by one. For each, it asks
`has_fabric_support(mc_version)`, which:
- For OBFUSCATED versions (1.21.x and earlier): requires BOTH
Yarn mappings AND a Fabric API build.
- For UNOBFUSCATED versions (26.1+, per Mojang's Oct 2025
announcement and Fabric's deprecation of Yarn): requires ONLY
a Fabric API build. Yarn doesn't exist for these versions
and is not needed β mods use Mojang's official Mojmap via
Loom's `loom.officialMojangMappings()`.
If support is confirmed, that snapshot is returned immediately.
4. If none of the probed snapshots have support, the resolver repeats
the same check against the newest stable releases.
5. If even releases have no support (extremely unlikely β would
indicate a Fabric ecosystem outage), it returns the newest release
as a last-resort so the tool can still produce a useful error.
Iteration is *sequential* not parallel. This is intentional: the typical
case is that the 1st or 2nd snapshot already has support, so we make at
most 2-3 API round-trips total. Parallel probing would always make N
round-trips and risk rate-limiting on Modrinth.
See: https://docs.fabricmc.net/develop/porting/mappings
"""
def __init__(self, http_client: Optional[httpx.AsyncClient] = None) -> None:
# A shared client gives us connection pooling and HTTP/2 multiplexing.
self._client: httpx.AsyncClient = http_client or httpx.AsyncClient(
timeout=httpx.Timeout(30.0),
headers={"User-Agent": "fabric-config-oracle/1.0"},
)
# In-process caches. They live for the lifetime of the resolver
# (i.e. for the lifetime of the MCP server process). The manifest
# cache additionally has a TTL; the per-version caches do not, but
# they're small (one entry per probed MC version) and never grow
# unboundedly because we cap probes via MAX_*_TO_PROBE.
self._manifest_cache: Optional[dict[str, Any]] = None
self._manifest_cache_time: Optional[datetime] = None
self._loader_cache: Optional[list[dict[str, Any]]] = None
self._yarn_cache: dict[str, Optional[dict[str, Any]]] = {}
self._fabric_api_cache: dict[str, Optional[dict[str, Any]]] = {}
# -- lifecycle --------------------------------------------------------
async def aclose(self) -> None:
"""Close the underlying HTTP client. Safe to call multiple times."""
await self._client.aclose()
async def __aenter__(self) -> "VersionResolver":
return self
async def __aexit__(self, *_: object) -> None:
await self.aclose()
# -- low-level HTTP ---------------------------------------------------
async def _fetch_json(
self,
url: str,
params: Optional[dict[str, Any]] = None,
) -> Any:
"""
Fetch JSON from `url` with graceful error handling.
Returns:
- The parsed JSON value on success.
- `None` on 404 (treated as "no data available").
- `None` on any other error after logging.
429 (rate limited) responses trigger a single backoff-and-retry.
"""
try:
response = await self._client.get(url, params=params)
response.raise_for_status()
return response.json()
except httpx.HTTPStatusError as exc:
status = exc.response.status_code
if status == 404:
return None
if status == 429:
logger.warning("Rate limited by %s; backing off 2s and retrying once.", url)
await asyncio.sleep(2.0)
try:
retry = await self._client.get(url, params=params)
retry.raise_for_status()
return retry.json()
except Exception as retry_exc: # noqa: BLE001
logger.error("Retry of %s failed: %s", url, retry_exc)
return None
logger.error("HTTP %d from %s: %s", status, url, exc)
return None
except (httpx.RequestError, ValueError) as exc:
logger.error("Error fetching %s: %s", url, exc)
return None
# -- API wrappers -----------------------------------------------------
async def fetch_mojang_manifest(self) -> list[MinecraftVersion]:
"""Fetch and parse the Mojang manifest, filtered to versions >= 1.21."""
now = datetime.now()
if (
self._manifest_cache is not None
and self._manifest_cache_time is not None
and (now - self._manifest_cache_time).total_seconds() < MANIFEST_CACHE_TTL_SECONDS
):
return self._parse_manifest(self._manifest_cache)
data = await self._fetch_json(MOJANG_MANIFEST_URL)
if data is None:
if self._manifest_cache is not None:
logger.warning("Mojang manifest fetch failed; using stale cache.")
return self._parse_manifest(self._manifest_cache)
logger.error("Mojang manifest unavailable and no cache to fall back on.")
return []
self._manifest_cache = data
self._manifest_cache_time = now
return self._parse_manifest(data)
def _parse_manifest(self, data: dict[str, Any]) -> list[MinecraftVersion]:
"""Parse raw manifest JSON into a list of MinecraftVersion objects."""
out: list[MinecraftVersion] = []
for v in data.get("versions", []):
vid = v.get("id", "")
vtype = v.get("type", "")
# Only release/snapshot types are relevant to us. Mojang also
# exposes 'old_beta' / 'old_alpha' which we deliberately skip.
if vtype not in ("release", "snapshot"):
continue
if not is_minimum_version(vid):
continue
try:
rt_raw = v["releaseTime"]
rt = datetime.fromisoformat(rt_raw.replace("Z", "+00:00"))
except (KeyError, ValueError):
continue
out.append(
MinecraftVersion(
id=vid,
type=vtype, # type: ignore[arg-type]
release_time=rt,
url=v.get("url"),
)
)
return out
async def fetch_yarn(self, mc_version: str) -> Optional[dict[str, Any]]:
"""Fetch the newest Yarn mappings build for `mc_version`, or None."""
if mc_version in self._yarn_cache:
return self._yarn_cache[mc_version]
data = await self._fetch_json(FABRIC_YARN_URL.format(mc_version=mc_version))
if not isinstance(data, list) or len(data) == 0:
self._yarn_cache[mc_version] = None
return None
# Fabric Meta returns Yarn builds newest-first, but sort defensively.
data.sort(key=lambda y: y.get("build", 0), reverse=True)
yarn = data[0]
self._yarn_cache[mc_version] = yarn
return yarn
async def fetch_loader(self) -> Optional[dict[str, Any]]:
"""Fetch the newest stable Fabric Loader version."""
if self._loader_cache is not None:
return self._loader_cache[0] if self._loader_cache else None
data = await self._fetch_json(FABRIC_LOADER_URL)
if not isinstance(data, list) or len(data) == 0:
return None
self._loader_cache = data
# First entry is the latest stable loader per Fabric Meta convention.
return data[0]
async def fetch_fabric_api(self, mc_version: str) -> Optional[dict[str, Any]]:
"""Fetch the newest Fabric API mod build for `mc_version` from Modrinth."""
if mc_version in self._fabric_api_cache:
return self._fabric_api_cache[mc_version]
# Modrinth expects JSON-encoded array query params, e.g.
# ?game_versions=["1.21.4"]&loaders=["fabric"]
params = {
"game_versions": f'["{mc_version}"]',
"loaders": '["fabric"]',
}
data = await self._fetch_json(MODRINTH_FABRIC_API_URL, params=params)
if not isinstance(data, list) or len(data) == 0:
self._fabric_api_cache[mc_version] = None
return None
data.sort(key=lambda v: v.get("date_published", ""), reverse=True)
fabric_api = data[0]
self._fabric_api_cache[mc_version] = fabric_api
return fabric_api
async def has_fabric_support(self, mc_version: str) -> bool:
"""
True iff `mc_version` has the Fabric dependencies required to build
a working mod.
## Unobfuscated versions (26.1+)
As of Minecraft 26.1 (March 2026), Mojang ships Java jars
unobfuscated, so Yarn mappings are deprecated and no longer
published. Mods targeting 26.1+ use Mojang's official Mojmap
directly via Loom's `loom.officialMojangMappings()`. For these
versions, the only Fabric dependency we need to verify is Fabric
API on Modrinth.
## Obfuscated versions (<= 1.21.11)
For these, BOTH Yarn AND Fabric API must exist, since a mod
referencing obfuscated class names cannot compile without Yarn
(or another mapping set) to translate them.
See: https://docs.fabricmc.net/develop/porting/mappings
"""
if is_unobfuscated(mc_version):
# Only Fabric API required; Yarn doesn't exist for 26.1+.
fabric_api = await self.fetch_fabric_api(mc_version)
return fabric_api is not None
# Obfuscated path: Yarn + Fabric API both required.
yarn, fabric_api = await asyncio.gather(
self.fetch_yarn(mc_version),
self.fetch_fabric_api(mc_version),
)
return yarn is not None and fabric_api is not None
# -- resolution -------------------------------------------------------
async def resolve_version(
self,
target_version: str = "latest",
) -> Optional[MinecraftVersion]:
"""
Resolve a target Minecraft version, applying fallback logic for "latest".
See the `VersionResolver` class docstring for the full snapshot
fallback rationale.
Args:
target_version: Either "latest" (default) or a specific version
string like "1.21.4", "26.2", or "26.3 Snapshot 2".
Returns:
A `MinecraftVersion` if resolution succeeded, else `None`.
For specific (non-"latest") requests, the version is looked up in the
manifest without a support check. If it isn't in the manifest, `None`
is returned and the caller may still attempt to fetch dependencies for
it β the per-API wrappers will return empty results gracefully.
"""
versions = await self.fetch_mojang_manifest()
if not versions:
return None
# Direct (specific-version) request β no fallback logic.
if target_version and target_version.lower() != "latest":
for v in versions:
if v.id == target_version:
return v
# Case-insensitive fallback (some users mistype casing).
for v in versions:
if v.id.lower() == target_version.lower():
return v
# Not in manifest. Return None so the caller can still try to
# fetch dependencies directly β Fabric Meta / Modrinth might
# know about it even if Mojang's manifest hasn't updated yet.
return None
# "latest" β apply snapshot-first, release-fallback resolution.
versions_sorted = sorted(versions, key=lambda v: v.release_time, reverse=True)
# Step 3: probe newest snapshots.
snapshots = [v for v in versions_sorted if v.type == "snapshot"]
for snap in snapshots[:MAX_SNAPSHOTS_TO_PROBE]:
if await self.has_fabric_support(snap.id):
return snap
# Step 4: fall back to newest stable releases.
releases = [v for v in versions_sorted if v.type == "release"]
for rel in releases[:MAX_RELEASES_TO_PROBE]:
if await self.has_fabric_support(rel.id):
return rel
# Step 5: last resort β return the newest release even without
# confirmed support, so we can surface a meaningful error message
# via FabricConfig.message.
return releases[0] if releases else (snapshots[0] if snapshots else None)
async def resolve_config(self, target_version: str = "latest") -> FabricConfig:
"""
Resolve the full Fabric configuration for a target MC version.
This is the high-level entry point used by the MCP tool. It first
resolves the version (applying fallback logic if "latest"), then
fetches Yarn, Loader, and Fabric API versions concurrently.
The returned `FabricConfig.message` field always describes the
outcome β success, partial success, or failure β in human-readable
form so the AI caller can react accordingly.
"""
mc_version = await self.resolve_version(target_version)
if mc_version is None:
return FabricConfig(
minecraft_version=target_version,
version_type="release",
yarn_mappings="",
loader_version="",
fabric_api_version="",
loom_version=DEFAULT_LOOM_VERSION,
gradle_wrapper_version=DEFAULT_GRADLE_VERSION,
message=(
f"Could not resolve Minecraft version '{target_version}'. "
"It may not exist in the Mojang manifest, or the manifest "
"could not be fetched."
),
)
# For unobfuscated versions (26.1+), Yarn does not exist and is not
# needed β mods use Mojang's official Mojmap directly via Loom. Skip
# the Yarn fetch entirely to avoid a wasted 404 round-trip.
unobf = is_unobfuscated(mc_version.id)
if unobf:
loader, fabric_api = await asyncio.gather(
self.fetch_loader(),
self.fetch_fabric_api(mc_version.id),
)
yarn = None
else:
yarn, loader, fabric_api = await asyncio.gather(
self.fetch_yarn(mc_version.id),
self.fetch_loader(),
self.fetch_fabric_api(mc_version.id),
)
yarn_version = (yarn or {}).get("version", "")
loader_version = (loader or {}).get("version", "")
# Modrinth's `version_number` is the human-facing string
# (e.g. "0.100.0+1.21.4"). `id` is the URL-safe UUID.
fabric_api_version = (fabric_api or {}).get("version_number", "")
# Build the missing-deps list. For unobfuscated versions, Yarn is
# intentionally absent β it's not "missing", it's deprecated.
missing: list[str] = []
if not unobf and not yarn_version:
missing.append("Yarn mappings")
if not loader_version:
missing.append("Fabric Loader")
if not fabric_api_version:
missing.append("Fabric API")
if missing:
message = (
f"Configuration partially resolved for MC {mc_version.id}. "
f"Missing: {', '.join(missing)}."
)
elif unobf:
message = (
f"Configuration resolved successfully for MC {mc_version.id}. "
"Yarn mappings are not required (Minecraft 26.1+ ships "
"unobfuscated; mods use Mojang official Mojmap via Loom's "
"`loom.officialMojangMappings()` in build.gradle)."
)
else:
message = "Configuration resolved successfully."
return FabricConfig(
minecraft_version=mc_version.id,
version_type=mc_version.type,
yarn_mappings=yarn_version,
loader_version=loader_version,
fabric_api_version=fabric_api_version,
loom_version=DEFAULT_LOOM_VERSION,
gradle_wrapper_version=DEFAULT_GRADLE_VERSION,
message=message,
)
|