Mod relations: dependencies and incompatibilities between mods - #117
Mod relations: dependencies and incompatibilities between mods#117Pixnop wants to merge 2 commits into
Conversation
SaculRennorb
left a comment
There was a problem hiding this comment.
My focus is currently elsewhere, so be warned that this will take a while for me to review, and most importantly think about, before any of it will make it to the live site.
Please rebase this onto the current staging, recent rewrites seem to have somewhat messed up the pr.
Some initial feedback:
I don't think it makes sense to associate dependencies with a mods page directly, especially the edit interface for them.
Dependencies really are per-release, and should be treated that way. "Mod" pages can manage multiple different mod identifiers, which might have wildly different dependencies (e.g. mymod, and mymodlinux).
I think it would be best to have the edit interface on the release edit page, which also resolves some of the issue with the late binding of information.
To alleviate the annoyance of having to re-input the information for every release, the system can autofill dependencies from the previous release of the same mod identifier, as a sort of template, much like auto-filling mod version and identifier works.
This system directly provides contradictory information to what the game uses for dependency resolution at the moment.
The game uses only pinned or completely open dependencies, but this system allows to extend or restrict these.
Since dependency resolution also needs to work in offline environments there are quite a few scenarios to consider here, which this does not address. This pr is really only concerned wit the moddb - which is reasonable ofcourse - but extending this system also needs integration in the game to be meaningful.
That already is a reason this is going to take a while to work though.
From a cursory glance at least the concepts seem reasonable.
|
Thanks for the review @SaculRennorb! Pushed a revised version (force-push on master, since it's the PR head). I've also updated the PR description above to reflect the new model and motivation; this comment is just the changelog vs
On the in-game integration: I've added a section in the updated description framing what stays moddb-only here (version ranges and the non-required kinds) so the current pinned-or-open contract in the game isn't broken. Game-side is your call; let me know if anything in the moddb-side design would make your future work there harder than necessary. Note for testers re-applyingThe schema change is essentially |
|
Small follow-up in 1. 2. |
|
I'm building a VS launcher and this is the piece I'd rather consume than reinvent, so a big +1 from the client side. Deriving required relations from modinfo.json and exposing transitive resolution over Two things that would help a launcher consume it cleanly:
Either way, glad to be an early test consumer as this lands. Really nice work, and thanks for picking this up. |
- deps.rs: parse modinfo.json (JSON5, case-insensitive) from mod zips; report required deps missing from an install; skip base game (game/survival/creative) - check_deps command - App.tsx: after install, transitively install missing deps from ModDB by modid - upgrades to install-information?resolve-deps=1 later (anegostudios/vsmoddb#117)
ef6f567 to
489dcc9
Compare
Adds a first-class mod-relations system between mods, scoped per release. Four relation kinds: required (auto-detected from modinfo.json, also overridable), optional, incompatible, tested_with (manual only). Relations are pinned to a specific release so mods hosting multiple identifiers can declare different dependencies per release. On a new release upload, manual relations carry forward from the previous release of the same identifier as a template; auto-detected required relations are re-derived from the new release's own rawDependencies. Surfaces relations: - public mod page: latest release's merged auto+manual sections - edit-release page: split auto/manual editor - install-information API: optional resolve-deps=1 returns the transitive tree and warnings (cycle, incompatible, missing_dep, version_conflict, optional_unmet, tested_with_unmet, depth_limit). Without the flag, the response shape is byte-identical to before. Includes db/143_migrate.sql (schema), db/144_migrate.php (backfill from modPeekResults.rawDependencies), 32 pure unit tests, 25 DB integration tests, and 2 cURL E2E tests.
Address launcher-consumer feedback on the resolve-deps payload: - Declare the warning `kind` vocabulary as constants (WARN_CYCLE, WARN_DEPTH_LIMIT, WARN_MISSING_DEP, WARN_INCOMPATIBLE, WARN_VERSION_CONFLICT, WARN_OPTIONAL_UNMET, WARN_TESTED_WITH_UNMET) and document it as a stable contract clients can branch on. - Emit `resolved` in install order, dependencies before dependents, via Kahn's algorithm over the requiredBy edges, with BFS discovery order as deterministic tie-break. Also expose the same sequence as a plain `installOrder` array for JSON clients that do not preserve object key order. Consumers can apply the set in one forward pass without needing their own topological sort. Covered by 6 new pure resolver tests plus integration and E2E assertions on the endpoint payload.
|
Rebased onto current staging (28d1753), the PR merges cleanly again. Two commits this time, so what changed since your last read is easy to isolate. Commit 1 is the feature as reviewed, adapted to the new staging:
Commit 2 picks up @Lueken's two points. Warnings already carried a machine-readable Full suite on the docker dev env: 124 tests, everything green except |
Why
The motivating use case is simpler modpack creation. Today, building a modpack means manually tracking which mods need which other mods, which versions are compatible, which mods conflict, and rebuilding that whole web by hand whenever a member mod is updated. With a first-class relations layer, a modpack mod can declare its members through
requiredrelations and the moddb knows the install graph - launchers can then consume it through the install-information API instead of each tool reinventing dependency tracking.The system is general (any mod can declare any kind of relation), but modpacks are the headline use case it unlocks. Refs #54.
Summary
Adds a first-class mod-relations system, scoped per release, covering four relation kinds:
modinfo.json, also overridable manually)Surfaces relations on the public mod page (4 sections in the infobox), in a dedicated editor section on the edit-release page, and via a backward-compatible extension of the install-information API that exposes transitive resolution + warnings.
Closes #55 (Dependency graph - provides the underlying data that issue noted was missing).
Refs #13 (the original Mod dependencies suggestion, closed) and #54 (Modpacks).
Why per-release
Relations are pinned to a specific
releaseId(not to a mod), so a mod page that hosts multiple identifiers (e.g.mymodandmymodlinux) declares relations independently per release. On a new release upload, manual relations carry forward from the previous release of the same identifier as a template (much like the existing modid/version autofill), and auto-detectedrequiredrelations are independently re-derived from the new release's ownrawDependencies.Note on the existing show-dependencies branch
The upstream
show-dependenciesbranch (last touched Nov 2025, 69 commits behind master, no PR) prototypes a separateshow-dependencies.phppage with a tree-view solver. This PR takes a different shape:install-information?resolve-deps=1so launchers / modpack tooling can consume it directly.The
show-dependenciesbranch's solver concepts (cycle detection, tree resolution) match what's implemented here inbfsResolve; we add diamond dedup, version-range conflict detection, depth-limit cutoff, and incompatibility post-pass. Happy to fold in any specific feature from that prototype (mod-card overview, dedicated tree page, etc.) as follow-ups if useful.Highlights
modRelationswith one schema covering all 4 relation kinds + version ranges + auto/manual origin, strictly per-release (releaseId NOT NULL, unique on(releaseId, targetIdentifier, relationType)).requiredrelations are re-synced frommodPeekResults.rawDependencieson every release upload (no modder action needed - their existingmodinfo.jsondeps surface automatically).GET /api/v2/mods/install-information?ids=...&resolve-deps=1returnsresolved(transitive tree, emitted in install order, dependencies before dependents),installOrder(the same sequence as a plain array for clients that do not preserve JSON object key order) +warningswith stable machine-readable kinds. Without the flag, response shape is byte-identical to before.db/144_migrate.php) populatesmodRelationsfrom existingmodPeekResultsrows.On the game-side integration
The game side is your call, not something I can contribute to. A few notes on how this PR tries not to constrain your future choices there:
modPeekResults.rawDependenciesis still the source of truth for what the game sees today; this PR only mirrors it intomodRelationsasorigin = 'auto'. Nothing in this PR exposes the manual / version-range layer to the game resolver.optional,incompatible,tested_with) are moddb-only at this point. They're surfaced throughinstall-information?resolve-deps=1for tools that opt in (launchers, dependency-graph viewers), and as warnings rather than hard constraints, so the in-game resolver can keep its current semantics without contradiction.So if it stays purely moddb-side forever, the value is still there for modpack authors and for launchers consuming the new API field.
Screenshots
Public mod page - infobox with all 4 relation sections (auto-resolved mods linked, unresolved targets in italic, incompatible marked):
Edit-release page - relations editor in context:
Edit-release editor - close-up of the auto/manual split (auto section is read-only, manual section lets you add/edit/X-remove rows):
API example
Backward-compat (no
resolve-deps):{ "data": { "rel-test-A": { "fileName": "...", "fileUrl": "..." } } }With
resolve-deps=1:{ "data": { "rel-test-A": { "fileName": "...", "fileUrl": "..." } }, "resolved": { "rel-test-A": { "identifier": "rel-test-A", "version": 281483566907391, "fileName": "...", "fileUrl": "...", "requiredBy": ["<root>"], "depth": 0 } }, "installOrder": ["rel-test-A"], "warnings": [ { "kind": "tested_with_unmet", "from": "rel-test-A", "identifier": "rel-test-C" } ] }Warning kinds:
cycle(withpath),incompatible(withbetween+declaredBy),missing_dep(withidentifier+requiredBy),version_conflict(with raw ranges),optional_unmet/tested_with_unmet,depth_limit. The vocabulary is declared asWARN_*constants inlib/relations.phpand is a stable contract; clients can branch onkind.Tests
tests/relations-pure.php- 38 pure unit tests (parser, range merge, BFS resolver, cycle detection, cycle guard, install order).tests/relations-integration.php- 25 DB integration tests (CRUD per-release, sync, manual-wins merge, edit-view split, FK cascade on release delete, clone-from-previous-release, dangling target resolution, transitive resolution, form persistence, latest-release surfacing).tests/api-install-information.php- 2 cURL-based E2E tests (backward-compat + resolve-deps shape incl.installOrder).ApiV1Test::modIdentifier, caused by brokendb/999_sampledata.sqloncomments.textShort- out of scope of this PR; happy to file a follow-up).Test plan
docker compose -f docker/docker-compose.yml exec php php tests/phpunit.phar --test-suffix=.php tests-> 124 tests, 1 baseline failure onlydb/143_migrate.sqlthen runphp db/144_migrate.phpand verifymodRelationsis populated from existingmodPeekResultsrowscurlthe install-information endpoint with and withoutresolve-deps=1and confirm shapesNotes for reviewers
resolveDanglingTargetsis called fromcreateNewRelease()(notcreateNewMod()) - this is intentional: a mod'sidentifieris set on its release, not the mod itself, so the retro-link can only happen once the first release lands._hydrateResolvedModruns once per BFS node during transitive resolution. Flagged as a v2 optimization (dedicated loader that skips hydration for resolver use); not a blocker at current scale.(releaseId, targetIdentifier, relationType)- no NULL-distinct semantics involved.cc @SaculRennorb (issue #55 author, started the
show-dependenciesprototype)