CLOSES #455, CLOSES #487, CLOSES #501, CLOSES #540, CLOSES #585, CLOSES #591, CLOSES #593, CLOSES #595, CLOSES #597, CLOSES #599, CLOSES #600, CLOSES #606, CLOSES #607, CLOSES #610, CLOSES #613, CLOSES #616, CLOSES #620, CLOSES #626, CLOSES #632, CLOSES #633, CLOSES #647

This commit is contained in:
bryanthaboi
2026-08-02 08:17:16 -04:00
parent ebbc55c4d0
commit 35b3fa6d9c
87 changed files with 6349 additions and 285 deletions
+74 -6
View File
@@ -40,7 +40,8 @@ function Handshake.mods(game)
if not ok or not status then return mods end
for _, manifest in ipairs(status.loaded or {}) do
mods[#mods + 1] = { id = manifest.id, version = manifest.version,
affectsLink = manifest.affects_link ~= false }
affectsLink = manifest.affects_link ~= false,
language = manifest.language == true }
end
table.sort(mods, function(a, b) return tostring(a.id) < tostring(b.id) end)
return mods
@@ -67,15 +68,82 @@ function Handshake.linkModified(game)
return false
end
-- #501: the registries a declared translation may write, and nothing else.
-- `text` is the ROM's dialogue, `strings` the engine's own authored text
-- (src/mods/Schemas.lua R.text / R.strings) and `font` the glyphs a
-- language with accents needs. None of the three is hashed into the
-- fingerprint (src/link/Fingerprint.lua header), so two peers reading the
-- same game in different languages stay in lockstep -- which is also what
-- the cable did: TradeCenter_PrintPartyListNames (pokered
-- engine/link/cable_club.asm) names the peer's party out of the local
-- ROM's table, only the trainer name and the party bytes travel.
-- text_pointers is deliberately out: its rows carry mart inventories and
-- nurse/pc flags, which are gameplay, not language.
local LANGUAGE_REGISTRIES = { text = true, strings = true, font = true }
Handshake.LANGUAGE_REGISTRIES = LANGUAGE_REGISTRIES
-- The manifest's `language = true` is the author's claim; this is the
-- check. A mod counts as a translation only if every op it appended
-- landed in a language registry, it subscribed no code (a hook or listener
-- runs inside the battle the two peers are lockstepping) and it asked for
-- no permission. Online play meets strangers, so nothing here may rest on
-- the manifest alone. A patched client can still lie about its own mods --
-- the fingerprint, not this, is what keeps the shared simulation honest;
-- this gate is what keeps an honest install from being told to turn its
-- language off.
local function translationOnly(mod, id)
local record = mod.mods and mod.mods[id]
local manifest = record and record.manifest
if manifest and #(manifest.permissions or {}) > 0 then return false end
for name, registry in pairs(mod.content or {}) do
if not LANGUAGE_REGISTRIES[name] then
for _, list in pairs(registry.ops or {}) do
for _, entry in ipairs(list) do
if entry.owner == id then return false end
end
end
end
end
for _, chain in pairs((mod.hooks and mod.hooks.chains) or {}) do
for _, entry in ipairs(chain) do
if entry.owner == id then return false end
end
end
for _, list in pairs((mod.events and mod.events.listeners) or {}) do
for _, entry in ipairs(list) do
if entry.owner == id then return false end
end
end
return true
end
-- the enabled mods that keep this install out of online play, in the
-- id order Handshake.mods sorts: everything except verified translations.
-- LinkState:offerVanillaRestart names these and switches off exactly these.
function Handshake.onlineBlockers(game)
local mod = loader(game)
local blockers = {}
for _, entry in ipairs(Handshake.mods(game)) do
local allowed = entry.language and not entry.affectsLink
and mod ~= nil and translationOnly(mod, entry.id)
if not allowed then blockers[#blockers + 1] = entry end
end
return blockers
end
-- online play (the relay-based online match / tournament flows in
-- LinkState/Tournament) meets strangers, not a coordinating friend, so it
-- skips the LAN path's per-peer compatibility negotiation entirely and
-- just requires vanilla on both ends: no mod-added Pokemon, no surprises.
-- Mods only ever get baked in at boot (Loader:load), so this is a gate on
-- attempting to go online, not a live mod toggle -- the player disables
-- mods via the mod manager and relaunches.
-- just requires a vanilla simulation on both ends: no mod-added Pokemon, no
-- surprises. #501 carves out translations, because a language is not a
-- simulation: a mod that only rewrites text is invisible to the wire and
-- may stay on, so an English player and a Spanish one can meet the way two
-- regional carts always could. Mods only ever get baked in at boot
-- (Loader:load), so this is a gate on attempting to go online, not a live
-- mod toggle -- the player disables mods via the mod manager and relaunches.
function Handshake.onlineAllowed(game)
return #Handshake.mods(game) == 0
return #Handshake.onlineBlockers(game) == 0
end
-- mode is nil on the guest: it pairs and announces itself before the host
+9 -7
View File
@@ -110,17 +110,19 @@ function LinkState:exitWith(message, reason)
end
end
-- Online play meets strangers, so it requires vanilla on both ends
-- (Handshake.onlineAllowed). Mods merge into the shared Data registries at
-- boot and there is no unmerge, so switching them off has to go through a
-- relaunch -- but the player should not have to go find the mod manager and
-- work out which mods count. This turns every enabled mod off, records
-- them so the mod manager can put them back, and relaunches. The restart
-- Online play meets strangers, so it requires a vanilla simulation on both
-- ends (Handshake.onlineAllowed). Mods merge into the shared Data
-- registries at boot and there is no unmerge, so switching them off has to
-- go through a relaunch -- but the player should not have to go find the
-- mod manager and work out which mods count. This turns the blocking mods
-- off, records them so the mod manager can put them back, and relaunches.
-- Verified translations are not blockers (#501), so a player keeps their
-- language across the restart and only the gameplay mods go. The restart
-- is confirmed rather than silent: it drops unsaved progress.
function LinkState:offerVanillaRestart()
local game = self.game
local loader = game.mods
local mods = Handshake.mods(game)
local mods = Handshake.onlineBlockers(game)
local names = {}
for i, mod in ipairs(mods) do
if i > 2 then break end