mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-15 07:41:21 +02:00
- adding ability to specify gen specific deps in manifest system.
- fixed issue with wayland users drag and drop causing CTD
This commit is contained in:
+2
-1
@@ -3,8 +3,9 @@
|
|||||||
data/generated/
|
data/generated/
|
||||||
assets/generated/
|
assets/generated/
|
||||||
|
|
||||||
# LÖVE packages
|
# LÖVE packages & archives
|
||||||
*.love
|
*.love
|
||||||
|
*.zip
|
||||||
|
|
||||||
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
# Local saves (LÖVE writes to its save dir, but keep the repo clean anyway)
|
||||||
save/
|
save/
|
||||||
|
|||||||
@@ -245,7 +245,8 @@ real Gold boot.
|
|||||||
### 5. `mod.card`
|
### 5. `mod.card`
|
||||||
|
|
||||||
The manifest is the *engine's* contract: identity, load order, dependencies,
|
The manifest is the *engine's* contract: identity, load order, dependencies,
|
||||||
permissions, profile. The card is the *human-facing* one: who made this,
|
permissions, profile (see [Manifest specification](docs/modding.md#manifest-specification-manifestjson)).
|
||||||
|
The card is the *human-facing* one: who made this,
|
||||||
what it changes, what it does not do yet. It is never read by the loader's
|
what it changes, what it does not do yet. It is never read by the loader's
|
||||||
merge — only by tooling and the manager's detail pane — so an absent or
|
merge — only by tooling and the manager's detail pane — so an absent or
|
||||||
malformed card can never break a load.
|
malformed card can never break a load.
|
||||||
|
|||||||
@@ -118,10 +118,11 @@ Gen 2 games no longer loads on Red, Blue or Yellow. Say `["all"]` or list both
|
|||||||
generations if you want both.
|
generations if you want both.
|
||||||
|
|
||||||
Two riders. **A hard dependency that does not run here takes the dependent down
|
Two riders. **A hard dependency that does not run here takes the dependent down
|
||||||
with it**, as a skip rather than a failure and carrying the dependency's own
|
with it** (unless scoped to specific games, e.g.
|
||||||
wording (`depends on X, which does not run here (For Blue, not Red)`), so the
|
`dependencies: [{ id = "x", games = ["gen2"] }]`), as a skip rather than a
|
||||||
whole chain has to cover the same games. And **the claim is yours, not the last
|
failure and carrying the dependency's own wording (`depends on X, which does not
|
||||||
word**: it is the manager's `TRY HERE ANYWAY` row that lets a player run a mod
|
run here (For Blue, not Red)`), so the whole chain has to cover the same games.
|
||||||
|
And **the claim is yours, not the last word**: it is the manager's `TRY HERE ANYWAY` row that lets a player run a mod
|
||||||
whose author never opted in, which is the only route for a mod written before
|
whose author never opted in, which is the only route for a mod written before
|
||||||
the field existed. The override is per game -- `options.modsGen2[id]` is a
|
the field existed. The override is per game -- `options.modsGen2[id]` is a
|
||||||
`{ [version] = true }` table, so forcing a mod onto Red does not force it onto
|
`{ [version] = true }` table, so forcing a mod onto Red does not force it onto
|
||||||
|
|||||||
@@ -21,6 +21,76 @@ luajit tools/gen_registry_docs.lua
|
|||||||
luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
|
luajit tools/gen_registry_docs.lua ../gen1recomp.wiki
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Manifest specification (`manifest.json`)
|
||||||
|
|
||||||
|
Every mod contains a root `manifest.json` defining its metadata, supported games, and dependencies for the engine loader.
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "my_mod",
|
||||||
|
"name": "My Cool Mod",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"api": 2,
|
||||||
|
"entry": "main.lua",
|
||||||
|
"profile": "content",
|
||||||
|
"category": "GAMEPLAY",
|
||||||
|
"games": ["gen1", "gen2"],
|
||||||
|
"game_version": ">=0.0.0-dev <2.0.0",
|
||||||
|
"priority": 100,
|
||||||
|
"dependencies": [
|
||||||
|
"helper_lib@^1.0.0",
|
||||||
|
{ "id": "pokegear_cards", "games": ["gen2"], "range": "^1.0.0", "github": "1jamie/pokegear_cards" }
|
||||||
|
],
|
||||||
|
"optional_dependencies": [
|
||||||
|
"gen1_modern_ui"
|
||||||
|
],
|
||||||
|
"conflicts": [],
|
||||||
|
"permissions": ["engine_internals"],
|
||||||
|
"description": "A brief description of the mod.",
|
||||||
|
"github": "author/my_mod"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Manifest Fields
|
||||||
|
|
||||||
|
| Field | Type | Description |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| `id` | `string` | Unique identifier (lowercase alphanumeric, underscores, hyphens). |
|
||||||
|
| `name` | `string` | Human-readable title shown in launcher and manager. |
|
||||||
|
| `version` | `string` | Semantic version string (e.g. `"1.0.0"`). |
|
||||||
|
| `api` | `integer` | Mod API level (`2` for current standard, `1` for legacy). |
|
||||||
|
| `entry` | `string` | Entry Lua file path relative to mod root (usually `"main.lua"`). |
|
||||||
|
| `profile` | `string` | Mod profile: `"content"`, `"overhaul"`, or `"total_conversion"`. |
|
||||||
|
| `category` | `string` | Categorization chip (e.g. `"GAMEPLAY"`, `"CONTENT"`, `"UI"`, `"AUDIO"`). |
|
||||||
|
| `games` | `array` | Supported game versions: `["gen1"]`, `["gen2"]`, `["red"]`, `["blue"]`, `["yellow"]`, `["gold"]`, or `["all"]`. |
|
||||||
|
| `game_version`| `string` | Semver range of required engine version (e.g. `">=0.0.0-dev <2.0.0"`). |
|
||||||
|
| `priority` | `integer` | Load priority order (lower numbers load earlier; dependencies always precede dependents regardless of priority). |
|
||||||
|
| `dependencies` | `array` | Hard required dependencies. A mod will not load if a required dependency is missing or disabled for the active game. |
|
||||||
|
| `optional_dependencies` | `array` | Soft dependencies. Guarantees that if the target mod is present and active, it loads *before* this mod without blocking load if absent. |
|
||||||
|
| `conflicts` / `incompatible` | `array` | List of mod IDs that cannot run concurrently with this mod. |
|
||||||
|
| `permissions` | `array` | Requested privileges (e.g. `["engine_internals"]`, `["network"]`, `["filesystem"]`). |
|
||||||
|
| `github` | `string` | GitHub repository (`"owner/repo"`) used for update checks and dependency download links. |
|
||||||
|
|
||||||
|
### Declaring Dependencies & Scoping
|
||||||
|
|
||||||
|
Dependencies in `dependencies` and `optional_dependencies` can be declared in several formats:
|
||||||
|
|
||||||
|
1. **Simple string**: `"mod_id"`
|
||||||
|
2. **Version-pinned string**: `"mod_id@^1.2.0"`
|
||||||
|
3. **Repository-hinted string**: `"mod_id#owner/repo"` or `"mod_id@^1.2.0#owner/repo"`
|
||||||
|
4. **Structured object**:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"id": "mod_id",
|
||||||
|
"range": "^1.2.0",
|
||||||
|
"games": ["gen2"],
|
||||||
|
"github": "owner/repo"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Version-Scoped Dependencies
|
||||||
|
When a mod supports multiple games (`"games": ["gen1", "gen2"]`), a dependency can specify `"games": ["gen2"]` to indicate it is only required when booting Gen 2. When booting Gen 1, the engine will ignore the dependency, preventing unnecessary boot blocks on games that do not need it.
|
||||||
|
|
||||||
## Mods and Gold (Gen 2)
|
## Mods and Gold (Gen 2)
|
||||||
|
|
||||||
The mod API is one API across both generations, but Gold runs its own battle
|
The mod API is one API across both generations, but Gold runs its own battle
|
||||||
|
|||||||
@@ -258,6 +258,25 @@ row on the detail screen. The launcher's dependency verdict asks the same
|
|||||||
question of your dependencies: a mod whose hard dependency does not run on the
|
question of your dependencies: a mod whose hard dependency does not run on the
|
||||||
selected game reads `Needs <id> (not for Gold)` rather than `Ready`.
|
selected game reads `Needs <id> (not for Gold)` rather than `Ready`.
|
||||||
|
|
||||||
|
### Scoping dependencies per game / generation
|
||||||
|
|
||||||
|
For mods targeting multiple generations (`"games": ["gen1", "gen2"]`), a hard
|
||||||
|
dependency can be scoped to specific games so that it is only enforced when
|
||||||
|
booting those games:
|
||||||
|
|
||||||
|
```json
|
||||||
|
"dependencies": [
|
||||||
|
{ "id": "pokegear_cards", "games": ["gen2"], "range": "^1.0.0", "github": "1jamie/pokegear_cards" }
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
|
When booting a Gen 1 game (Red, Blue, Yellow), the engine loader sees that
|
||||||
|
`pokegear_cards` is scoped to `"gen2"` and will not skip or block the parent mod
|
||||||
|
on Gen 1. When booting Gen 2 (Gold), `pokegear_cards` is strictly required.
|
||||||
|
|
||||||
|
For conditional integrations where the dependency is optional across the board,
|
||||||
|
`optional_dependencies` remains the standard pattern.
|
||||||
|
|
||||||
### One limit worth knowing
|
### One limit worth knowing
|
||||||
|
|
||||||
**Enablement is per game.** The overlay
|
**Enablement is per game.** The overlay
|
||||||
|
|||||||
@@ -400,6 +400,9 @@ EOF
|
|||||||
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
grep -q '^FUSE_PATH="\$APPDIR/game.love"$' "$appdir/AppRun" \
|
||||||
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
|| fail "failed to enable FUSE_PATH in AppRun (upstream AppRun changed?)"
|
||||||
|
|
||||||
|
sed -i '' 's|^exec "\$APPDIR/bin/love"|if [ -n "$WAYLAND_DISPLAY" ] \&\& [ -z "$SDL_VIDEODRIVER" ]; then export SDL_VIDEODRIVER=x11; fi\
|
||||||
|
exec "$APPDIR/bin/love"|' "$appdir/AppRun"
|
||||||
|
|
||||||
# Match the upstream image's compression (gzip, 128K blocks) so the
|
# Match the upstream image's compression (gzip, 128K blocks) so the
|
||||||
# bundled runtime can read it.
|
# bundled runtime can read it.
|
||||||
local sfs_out="$WORK/game.squashfs"
|
local sfs_out="$WORK/game.squashfs"
|
||||||
|
|||||||
@@ -405,6 +405,12 @@ if [ -z "\$LUA_CPATH" ]; then
|
|||||||
fi
|
fi
|
||||||
export LUA_CPATH="\$APPDIR/lib/lua/5.1/?.so;\$LUA_CPATH"
|
export LUA_CPATH="\$APPDIR/lib/lua/5.1/?.so;\$LUA_CPATH"
|
||||||
|
|
||||||
|
# SDL2 on Wayland crashes during desktop drag-and-drop in certain compositors;
|
||||||
|
# default to X11/XWayland when available to ensure rock-solid drag-drop stability.
|
||||||
|
if [ -n "\$WAYLAND_DISPLAY" ] && [ -z "\$SDL_VIDEODRIVER" ]; then
|
||||||
|
export SDL_VIDEODRIVER=x11
|
||||||
|
fi
|
||||||
|
|
||||||
exec "\$APPDIR/bin/love" --fused "\$APPDIR/game.love" "\$@"
|
exec "\$APPDIR/bin/love" --fused "\$APPDIR/game.love" "\$@"
|
||||||
EOF
|
EOF
|
||||||
chmod +x "$APPDIR/AppRun"
|
chmod +x "$APPDIR/AppRun"
|
||||||
|
|||||||
@@ -35,4 +35,10 @@ find_love() {
|
|||||||
LOVE_BIN="$(find_love)" \
|
LOVE_BIN="$(find_love)" \
|
||||||
|| fail "LÖVE not found, run scripts/setup.sh (or install from https://love2d.org)"
|
|| fail "LÖVE not found, run scripts/setup.sh (or install from https://love2d.org)"
|
||||||
|
|
||||||
|
# SDL2 on Wayland crashes during desktop drag-and-drop in certain compositors;
|
||||||
|
# default to X11/XWayland when available to ensure rock-solid drag-drop stability.
|
||||||
|
if [ -n "${WAYLAND_DISPLAY:-}" ] && [ -z "${SDL_VIDEODRIVER:-}" ]; then
|
||||||
|
export SDL_VIDEODRIVER=x11
|
||||||
|
fi
|
||||||
|
|
||||||
exec "$LOVE_BIN" "$ROOT" "$@"
|
exec "$LOVE_BIN" "$ROOT" "$@"
|
||||||
|
|||||||
+44
-40
@@ -87,21 +87,23 @@ local function statusFor(mods, id, enabledSet, enabled, version, forcedFor)
|
|||||||
-- resolveToggle would cascade-enable a merely-disabled dep rather than flag
|
-- resolveToggle would cascade-enable a merely-disabled dep rather than flag
|
||||||
-- it, so the disabled case is judged straight off the manifest here.
|
-- it, so the disabled case is judged straight off the manifest here.
|
||||||
for _, spec in ipairs(m.dependencySpecs or {}) do
|
for _, spec in ipairs(m.dependencySpecs or {}) do
|
||||||
local dep = mods[spec.id]
|
if ModTargets.specApplies(spec, version) then
|
||||||
if not dep then
|
local dep = mods[spec.id]
|
||||||
return "warn", "Needs " .. spec.id .. " (not installed)"
|
if not dep then
|
||||||
elseif not enabledSet[spec.id] then
|
return "warn", "Needs " .. spec.id .. " (not installed)"
|
||||||
return "warn", "Needs " .. spec.id .. " (disabled)"
|
elseif not enabledSet[spec.id] then
|
||||||
-- installed and on, but not for THIS game: the loader skips the
|
return "warn", "Needs " .. spec.id .. " (disabled)"
|
||||||
-- dependency and the skip is contagious (Loader:_enforceDependencies),
|
-- installed and on, but not for THIS game: the loader skips the
|
||||||
-- so a mod that runs everywhere still does not run here
|
-- dependency and the skip is contagious (Loader:_enforceDependencies),
|
||||||
elseif version
|
-- so a mod that runs everywhere still does not run here
|
||||||
and not ModTargets.runsHere(dep, version, nil, forcedFor(spec.id)) then
|
elseif version
|
||||||
return "warn", "Needs " .. spec.id .. " (not for "
|
and not ModTargets.runsHere(dep, version, nil, forcedFor(spec.id)) then
|
||||||
.. ModTargets.gameLabel(version) .. ")"
|
return "warn", "Needs " .. spec.id .. " (not for "
|
||||||
elseif spec.range
|
.. ModTargets.gameLabel(version) .. ")"
|
||||||
and not Semver.satisfies(dep.version, spec.range) then
|
elseif spec.range
|
||||||
return "warn", "Needs " .. spec.id .. " " .. spec.range
|
and not Semver.satisfies(dep.version, spec.range) then
|
||||||
|
return "warn", "Needs " .. spec.id .. " " .. spec.range
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
return "ok", "Ready"
|
return "ok", "Ready"
|
||||||
@@ -168,33 +170,35 @@ function LauncherMods.checkDependencies(manifest, options, version, installedMan
|
|||||||
-- 1. Hard Dependencies (dependencySpecs)
|
-- 1. Hard Dependencies (dependencySpecs)
|
||||||
if type(manifest.dependencySpecs) == "table" then
|
if type(manifest.dependencySpecs) == "table" then
|
||||||
for _, spec in ipairs(manifest.dependencySpecs) do
|
for _, spec in ipairs(manifest.dependencySpecs) do
|
||||||
local depId = spec.id
|
if not version or ModTargets.specApplies(spec, version) then
|
||||||
local range = spec.range
|
local depId = spec.id
|
||||||
local installedDep = installedMap[depId]
|
local range = spec.range
|
||||||
local status = "satisfied"
|
local installedDep = installedMap[depId]
|
||||||
local installedVersion = installedDep and installedDep.version or nil
|
local status = "satisfied"
|
||||||
|
local installedVersion = installedDep and installedDep.version or nil
|
||||||
|
|
||||||
if not installedDep then
|
if not installedDep then
|
||||||
status = "missing"
|
status = "missing"
|
||||||
hasIssues = true
|
hasIssues = true
|
||||||
elseif range and not Semver.satisfies(installedDep.version, range) then
|
elseif range and not Semver.satisfies(installedDep.version, range) then
|
||||||
status = "incompatible"
|
status = "incompatible"
|
||||||
hasIssues = true
|
hasIssues = true
|
||||||
|
end
|
||||||
|
|
||||||
|
local ghRepo = LauncherMods.resolveDependencyRepo(depId, manifest, installedDep)
|
||||||
|
local safeUrl = ghRepo and ("https://github.com/" .. ghRepo) or nil
|
||||||
|
|
||||||
|
depsResult[#depsResult + 1] = {
|
||||||
|
id = depId,
|
||||||
|
name = (installedDep and installedDep.name) or depId,
|
||||||
|
range = range,
|
||||||
|
status = status,
|
||||||
|
kind = "dependency",
|
||||||
|
installedVersion = installedVersion,
|
||||||
|
github = ghRepo,
|
||||||
|
safeUrl = safeUrl,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
|
|
||||||
local ghRepo = LauncherMods.resolveDependencyRepo(depId, manifest, installedDep)
|
|
||||||
local safeUrl = ghRepo and ("https://github.com/" .. ghRepo) or nil
|
|
||||||
|
|
||||||
depsResult[#depsResult + 1] = {
|
|
||||||
id = depId,
|
|
||||||
name = (installedDep and installedDep.name) or depId,
|
|
||||||
range = range,
|
|
||||||
status = status,
|
|
||||||
kind = "dependency",
|
|
||||||
installedVersion = installedVersion,
|
|
||||||
github = ghRepo,
|
|
||||||
safeUrl = safeUrl,
|
|
||||||
}
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
|||||||
+57
-39
@@ -561,42 +561,46 @@ end
|
|||||||
-- hard dependencies must exist, be enabled, have survived, and satisfy their
|
-- hard dependencies must exist, be enabled, have survived, and satisfy their
|
||||||
-- range; run to a fixpoint so failures propagate to dependents transitively
|
-- range; run to a fixpoint so failures propagate to dependents transitively
|
||||||
function Loader:_enforceDependencies()
|
function Loader:_enforceDependencies()
|
||||||
|
local targetVersion = self:_targetVersion()
|
||||||
|
local generation = self.generation
|
||||||
local changed = true
|
local changed = true
|
||||||
while changed do
|
while changed do
|
||||||
changed = false
|
changed = false
|
||||||
for _, id in ipairs(orderedIds(self.mods, isActive)) do
|
for _, id in ipairs(orderedIds(self.mods, isActive)) do
|
||||||
local mod = self.mods[id]
|
local mod = self.mods[id]
|
||||||
for _, spec in ipairs(mod.manifest.dependencySpecs) do
|
for _, spec in ipairs(mod.manifest.dependencySpecs) do
|
||||||
local dep = self.mods[spec.id]
|
if ModTargets.specApplies(spec, targetVersion, generation) then
|
||||||
local reason, skip
|
local dep = self.mods[spec.id]
|
||||||
if not dep then
|
local reason, skip
|
||||||
reason = "missing dependency: " .. spec.id
|
if not dep then
|
||||||
elseif not dep.enabled then
|
reason = "missing dependency: " .. spec.id
|
||||||
reason = ("dependency %s is disabled"):format(spec.id)
|
elseif not dep.enabled then
|
||||||
elseif dep.state == "wrong_generation" then
|
reason = ("dependency %s is disabled"):format(spec.id)
|
||||||
-- the gate's skip is contagious as a SKIP, not as a failure: the
|
elseif dep.state == "wrong_generation" then
|
||||||
-- dependency has no bug to report and neither does this mod, so
|
-- the gate's skip is contagious as a SKIP, not as a failure: the
|
||||||
-- nothing here lands on the boot error list
|
-- dependency has no bug to report and neither does this mod, so
|
||||||
skip = true
|
-- nothing here lands on the boot error list
|
||||||
-- carry the dependency's own reason: it names the game or the
|
skip = true
|
||||||
-- missing gen2compat, and a guess here would name the wrong one
|
-- carry the dependency's own reason: it names the game or the
|
||||||
reason = ("depends on %s, which does not run here (%s)")
|
-- missing gen2compat, and a guess here would name the wrong one
|
||||||
:format(spec.id, dep.skipReason or "not made for this game")
|
reason = ("depends on %s, which does not run here (%s)")
|
||||||
elseif dep.failed then
|
:format(spec.id, dep.skipReason or "not made for this game")
|
||||||
reason = ("dependency %s failed to load"):format(spec.id)
|
elseif dep.failed then
|
||||||
elseif spec.range
|
reason = ("dependency %s failed to load"):format(spec.id)
|
||||||
and not Semver.satisfies(dep.manifest.version, spec.range) then
|
elseif spec.range
|
||||||
reason = ("needs %s@%s, found %s")
|
and not Semver.satisfies(dep.manifest.version, spec.range) then
|
||||||
:format(spec.id, spec.range, dep.manifest.version)
|
reason = ("needs %s@%s, found %s")
|
||||||
end
|
:format(spec.id, spec.range, dep.manifest.version)
|
||||||
if reason then
|
end
|
||||||
if skip then
|
if reason then
|
||||||
self:_skip(mod, "wrong_generation", reason)
|
if skip then
|
||||||
else
|
self:_skip(mod, "wrong_generation", reason)
|
||||||
self:_fail(mod, "blocked_dependency", reason)
|
else
|
||||||
|
self:_fail(mod, "blocked_dependency", reason)
|
||||||
|
end
|
||||||
|
changed = true
|
||||||
|
break
|
||||||
end
|
end
|
||||||
changed = true
|
|
||||||
break
|
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -606,6 +610,8 @@ end
|
|||||||
-- Tarjan SCC over the hard-dependency graph: only a cycle's own members
|
-- Tarjan SCC over the hard-dependency graph: only a cycle's own members
|
||||||
-- fail, so an unrelated mod beside a cycle still loads
|
-- fail, so an unrelated mod beside a cycle still loads
|
||||||
function Loader:_failCycles()
|
function Loader:_failCycles()
|
||||||
|
local targetVersion = self:_targetVersion()
|
||||||
|
local generation = self.generation
|
||||||
local mods = self.mods
|
local mods = self.mods
|
||||||
local counter, stack, onStack, index, low = 0, {}, {}, {}, {}
|
local counter, stack, onStack, index, low = 0, {}, {}, {}, {}
|
||||||
local cycles = {}
|
local cycles = {}
|
||||||
@@ -616,14 +622,16 @@ function Loader:_failCycles()
|
|||||||
onStack[id] = true
|
onStack[id] = true
|
||||||
local selfEdge = false
|
local selfEdge = false
|
||||||
for _, spec in ipairs(mods[id].manifest.dependencySpecs) do
|
for _, spec in ipairs(mods[id].manifest.dependencySpecs) do
|
||||||
local dep = mods[spec.id]
|
if ModTargets.specApplies(spec, targetVersion, generation) then
|
||||||
if spec.id == id then selfEdge = true end
|
local dep = mods[spec.id]
|
||||||
if dep and isActive(dep) and spec.id ~= id then
|
if spec.id == id then selfEdge = true end
|
||||||
if not index[spec.id] then
|
if dep and isActive(dep) and spec.id ~= id then
|
||||||
connect(spec.id)
|
if not index[spec.id] then
|
||||||
if low[spec.id] < low[id] then low[id] = low[spec.id] end
|
connect(spec.id)
|
||||||
elseif onStack[spec.id] and index[spec.id] < low[id] then
|
if low[spec.id] < low[id] then low[id] = low[spec.id] end
|
||||||
low[id] = index[spec.id]
|
elseif onStack[spec.id] and index[spec.id] < low[id] then
|
||||||
|
low[id] = index[spec.id]
|
||||||
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
@@ -674,6 +682,8 @@ end
|
|||||||
-- Kahn over the surviving graph with the ready set kept in (priority, id)
|
-- Kahn over the surviving graph with the ready set kept in (priority, id)
|
||||||
-- order, so dependencies come first and the rest matches the v1 contract
|
-- order, so dependencies come first and the rest matches the v1 contract
|
||||||
function Loader:_order()
|
function Loader:_order()
|
||||||
|
local targetVersion = self:_targetVersion()
|
||||||
|
local generation = self.generation
|
||||||
local pending, indegree, dependents = {}, {}, {}
|
local pending, indegree, dependents = {}, {}, {}
|
||||||
for _, id in ipairs(orderedIds(self.mods, isActive)) do
|
for _, id in ipairs(orderedIds(self.mods, isActive)) do
|
||||||
pending[id], indegree[id] = true, 0
|
pending[id], indegree[id] = true, 0
|
||||||
@@ -686,9 +696,17 @@ function Loader:_order()
|
|||||||
dependents[depId][#dependents[depId] + 1] = id
|
dependents[depId][#dependents[depId] + 1] = id
|
||||||
indegree[id] = indegree[id] + 1
|
indegree[id] = indegree[id] + 1
|
||||||
end
|
end
|
||||||
for _, spec in ipairs(manifest.dependencySpecs) do edge(spec.id) end
|
for _, spec in ipairs(manifest.dependencySpecs) do
|
||||||
|
if ModTargets.specApplies(spec, targetVersion, generation) then
|
||||||
|
edge(spec.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
-- optional dependencies order without requiring anything
|
-- optional dependencies order without requiring anything
|
||||||
for _, spec in ipairs(manifest.optionalSpecs) do edge(spec.id) end
|
for _, spec in ipairs(manifest.optionalSpecs) do
|
||||||
|
if ModTargets.specApplies(spec, targetVersion, generation) then
|
||||||
|
edge(spec.id)
|
||||||
|
end
|
||||||
|
end
|
||||||
end
|
end
|
||||||
local ordered = {}
|
local ordered = {}
|
||||||
local function nextId()
|
local function nextId()
|
||||||
|
|||||||
+24
-2
@@ -58,11 +58,13 @@ local function parseSpecs(list, field, sources)
|
|||||||
local specs = {}
|
local specs = {}
|
||||||
sources = type(sources) == "table" and sources or {}
|
sources = type(sources) == "table" and sources or {}
|
||||||
for _, entry in ipairs(list) do
|
for _, entry in ipairs(list) do
|
||||||
local id, range, ghHint
|
local id, range, ghHint, gamesRaw, gameVersion
|
||||||
if type(entry) == "table" then
|
if type(entry) == "table" then
|
||||||
id = entry.id
|
id = entry.id
|
||||||
range = entry.range or entry.version
|
range = entry.range or entry.version
|
||||||
ghHint = entry.github or entry.repo
|
ghHint = entry.github or entry.repo
|
||||||
|
gamesRaw = entry.games or entry.game
|
||||||
|
gameVersion = entry.game_version
|
||||||
elseif type(entry) == "string" and entry ~= "" then
|
elseif type(entry) == "string" and entry ~= "" then
|
||||||
local main, hashRepo = entry:match("^([^#]+)#(.*)$")
|
local main, hashRepo = entry:match("^([^#]+)#(.*)$")
|
||||||
if main then
|
if main then
|
||||||
@@ -82,6 +84,20 @@ local function parseSpecs(list, field, sources)
|
|||||||
local ok, err = Semver.validRange(range)
|
local ok, err = Semver.validRange(range)
|
||||||
assert(ok, ("malformed %s range in %q: %s"):format(field, tostring(entry), tostring(err)))
|
assert(ok, ("malformed %s range in %q: %s"):format(field, tostring(entry), tostring(err)))
|
||||||
|
|
||||||
|
if gameVersion then
|
||||||
|
local okV, errV = Semver.validRange(gameVersion)
|
||||||
|
assert(okV, ("malformed %s game_version range in %q: %s"):format(field, tostring(entry), tostring(errV)))
|
||||||
|
end
|
||||||
|
|
||||||
|
local parsedGames = nil
|
||||||
|
if gamesRaw ~= nil then
|
||||||
|
if type(gamesRaw) == "string" then gamesRaw = { gamesRaw } end
|
||||||
|
assert(type(gamesRaw) == "table", "dependency games must be a string or table")
|
||||||
|
local normalized, unknown = ModTargets.normalize(gamesRaw)
|
||||||
|
assert(#unknown == 0, ("unknown game in %s: %s"):format(field, table.concat(unknown, ", ")))
|
||||||
|
parsedGames = normalized
|
||||||
|
end
|
||||||
|
|
||||||
local parsedGh = nil
|
local parsedGh = nil
|
||||||
local rawGh = ghHint or sources[id]
|
local rawGh = ghHint or sources[id]
|
||||||
if rawGh then
|
if rawGh then
|
||||||
@@ -89,7 +105,13 @@ local function parseSpecs(list, field, sources)
|
|||||||
if okGh and cleanGh then parsedGh = cleanGh end
|
if okGh and cleanGh then parsedGh = cleanGh end
|
||||||
end
|
end
|
||||||
|
|
||||||
specs[#specs + 1] = { id = id, range = range, github = parsedGh }
|
specs[#specs + 1] = {
|
||||||
|
id = id,
|
||||||
|
range = range,
|
||||||
|
github = parsedGh,
|
||||||
|
games = parsedGames,
|
||||||
|
game_version = gameVersion,
|
||||||
|
}
|
||||||
end
|
end
|
||||||
return specs
|
return specs
|
||||||
end
|
end
|
||||||
|
|||||||
@@ -168,4 +168,39 @@ function ModTargets.detail(manifest, version)
|
|||||||
ModTargets.gameLabel(version))
|
ModTargets.gameLabel(version))
|
||||||
end
|
end
|
||||||
|
|
||||||
|
-- Does a dependency spec apply to this game / version?
|
||||||
|
-- If spec.games is provided, it must match version / generation.
|
||||||
|
-- If spec.game_version is provided, the engine version must satisfy it.
|
||||||
|
function ModTargets.specApplies(spec, version, generation)
|
||||||
|
if not spec then return false end
|
||||||
|
if type(spec.games) == "table" and #spec.games > 0 then
|
||||||
|
if version or generation then
|
||||||
|
local match = false
|
||||||
|
for _, id in ipairs(spec.games) do
|
||||||
|
if version and id == version then
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
if generation and GameVersion.generation(id) == generation then
|
||||||
|
match = true
|
||||||
|
break
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if not match then return false end
|
||||||
|
end
|
||||||
|
end
|
||||||
|
if spec.game_version then
|
||||||
|
local ok = pcall(function()
|
||||||
|
local Semver = require("src.mods.Semver")
|
||||||
|
local Version = require("src.core.Version")
|
||||||
|
if Version and Version.engine and Version.engine:match("^0%.0%.0%-") == nil then
|
||||||
|
return Semver.satisfies(Version.engine, spec.game_version)
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end)
|
||||||
|
if not ok then return false end
|
||||||
|
end
|
||||||
|
return true
|
||||||
|
end
|
||||||
|
|
||||||
return ModTargets
|
return ModTargets
|
||||||
|
|||||||
@@ -552,11 +552,46 @@ local installedColorlib = Manifest.validate({
|
|||||||
version = "1.0.0",
|
version = "1.0.0",
|
||||||
entry = "main.lua",
|
entry = "main.lua",
|
||||||
}, "mods/colorlib")
|
}, "mods/colorlib")
|
||||||
local depConflictCheck = LauncherMods.checkDependencies(testTargetManifest, nil, nil, { installedColorlib })
|
-- ------- scoped dependency tests
|
||||||
check(depConflictCheck.hasIssues == true, "conflict with colorlib triggers hasIssues")
|
local Json = require("src.link.Json")
|
||||||
check(#depConflictCheck.deps == 1 and depConflictCheck.deps[1].status == "conflict",
|
local scopedDepManifest = Manifest.validate({
|
||||||
"incompatible mod is flagged as status conflict")
|
id = "dual_gen_mod",
|
||||||
check(depConflictCheck.deps[1].kind == "conflict", "conflict item carries kind conflict")
|
name = "Dual Gen Mod",
|
||||||
|
version = "1.0.0",
|
||||||
|
entry = "main.lua",
|
||||||
|
games = { "gen1", "gen2" },
|
||||||
|
dependencies = {
|
||||||
|
{ id = "gen2_only_dep", games = { "gen2" }, version = "^1.0.0" }
|
||||||
|
},
|
||||||
|
}, "mods/dual_gen_mod")
|
||||||
|
check(#scopedDepManifest.dependencySpecs == 1, "scoped dependency parsed")
|
||||||
|
check(scopedDepManifest.dependencySpecs[1].games ~= nil, "dependency carries games list")
|
||||||
|
|
||||||
|
local dualGenFiles = {
|
||||||
|
["mods/dual_gen_mod/manifest.json"] = Json.encode({
|
||||||
|
id = "dual_gen_mod",
|
||||||
|
name = "Dual Gen Mod",
|
||||||
|
version = "1.0.0",
|
||||||
|
entry = "main.lua",
|
||||||
|
games = { "gen1", "gen2" },
|
||||||
|
dependencies = {
|
||||||
|
{ id = "gen2_only_dep", games = { "gen2" } }
|
||||||
|
},
|
||||||
|
}),
|
||||||
|
["mods/dual_gen_mod/main.lua"] = [[
|
||||||
|
return function(mod)
|
||||||
|
mod.content.pokemon:register("DUAL_MON", { hp = 100 })
|
||||||
|
end
|
||||||
|
]],
|
||||||
|
}
|
||||||
|
local gen1Loader = Loader.new({ fs = memfs(dualGenFiles), generation = 1 })
|
||||||
|
check(gen1Loader:load({}) == true, "dual gen mod loads on Gen 1 when Gen 2 dep is absent")
|
||||||
|
check(gen1Loader.content.pokemon:get("DUAL_MON") ~= nil, "dual gen mod executed on Gen 1")
|
||||||
|
|
||||||
|
local gen2Loader = Loader.new({ fs = memfs(dualGenFiles), generation = 2 })
|
||||||
|
check(gen2Loader:load({}) == false, "loader returns false on Gen 2 when missing required Gen 2 dep")
|
||||||
|
check(gen2Loader.content.pokemon:get("DUAL_MON") == nil, "dual gen mod is blocked on Gen 2 when missing required Gen 2 dep")
|
||||||
|
check(#gen2Loader:status().errors > 0, "missing dependency error logged on Gen 2")
|
||||||
|
|
||||||
Runtime.install(savedEvents, savedHooks)
|
Runtime.install(savedEvents, savedHooks)
|
||||||
|
|
||||||
|
|||||||
+11
-4
@@ -2989,16 +2989,23 @@ def check_gen2_manifest(repo, mod_dir, manifest, named):
|
|||||||
"manifest.json"))
|
"manifest.json"))
|
||||||
deps = manifest.get("dependencies") or []
|
deps = manifest.get("dependencies") or []
|
||||||
for dep in deps if isinstance(deps, list) else []:
|
for dep in deps if isinstance(deps, list) else []:
|
||||||
if not isinstance(dep, str):
|
dep_id = dep if isinstance(dep, str) else dep.get("id") if isinstance(dep, dict) else None
|
||||||
|
if not dep_id:
|
||||||
continue
|
continue
|
||||||
found = named.get(dep) or find_mod_by_id(repo, mod_dir, dep)
|
if isinstance(dep, dict) and "games" in dep:
|
||||||
|
g_list = dep.get("games")
|
||||||
|
if isinstance(g_list, str):
|
||||||
|
g_list = [g_list]
|
||||||
|
if isinstance(g_list, list) and not any(g in ["gen2", "gold", "silver", "crystal", "all"] for g in g_list):
|
||||||
|
continue
|
||||||
|
found = named.get(dep_id) or find_mod_by_id(repo, mod_dir, dep_id)
|
||||||
if found is None:
|
if found is None:
|
||||||
notes.append("unresolved: dependency %s is not installed beside "
|
notes.append("unresolved: dependency %s is not installed beside "
|
||||||
"this mod, so its games list could not be read" % dep)
|
"this mod, so its games list could not be read" % dep_id)
|
||||||
elif not declares_gen2(repo, found):
|
elif not declares_gen2(repo, found):
|
||||||
findings.append(Finding(
|
findings.append(Finding(
|
||||||
"MK401", "error",
|
"MK401", "error",
|
||||||
f"depends on {dep}, which claims no Gen 2 game; the "
|
f"depends on {dep_id}, which claims no Gen 2 game; the "
|
||||||
f"loader disables a mod whose dependency a Gen 2 boot skipped",
|
f"loader disables a mod whose dependency a Gen 2 boot skipped",
|
||||||
"manifest.json"))
|
"manifest.json"))
|
||||||
return findings, notes
|
return findings, notes
|
||||||
|
|||||||
Reference in New Issue
Block a user