diff --git a/src/battle/MoveEffects.lua b/src/battle/MoveEffects.lua index d04e843c..acdd2118 100644 --- a/src/battle/MoveEffects.lua +++ b/src/battle/MoveEffects.lua @@ -332,7 +332,7 @@ MoveEffects.primary = { battle.data.moves[id].name) } end, - SPLASH_EFFECT = function() + SPLASH_EFFECT = function(battle) return { romText(battle.data, "_NoEffectText", "No effect!") } end, } diff --git a/src/link/Handshake.lua b/src/link/Handshake.lua index aec8437e..710ad5c8 100644 --- a/src/link/Handshake.lua +++ b/src/link/Handshake.lua @@ -169,6 +169,8 @@ end -- full identical link surfaces: nothing to negotiate, lockstep is safe -- vanilla_peer an old build, and we are unmodified, so it is right about us +-- engine_skew both v2 on the same major, but different releases: trade +-- still negotiates, battle is refused (see below) -- subset both v2 but the surfaces differ: negotiated trade, no battle -- refused an old build we would silently corrupt, or a different engine function Handshake.checkCompat(localHello, remoteHello) @@ -182,6 +184,16 @@ function Handshake.checkCompat(localHello, remoteHello) if major(remoteHello.engineVersion) ~= major(localHello.engineVersion) then return "refused", "engine_mismatch" end + -- A lockstep battle needs the same engine RELEASE, not just the same + -- major: the fingerprint only covers the data/mod link surface, and + -- battle logic changes between minor releases (parity fixes, move + -- effect rework...), so two honest vanilla installs a release apart + -- pair as "full" and then diverge a few turns in -- the mid-battle + -- "same mods?" desync draw of #758. Trade doesn't lockstep a + -- simulation, so it stays negotiable across releases. + if tostring(remoteHello.engineVersion) ~= tostring(localHello.engineVersion) then + return "engine_skew", "engine_release_mismatch" + end if remoteHello.fingerprint == localHello.fingerprint then return "full", nil end @@ -191,7 +203,7 @@ end -- only two v2 peers that agreed on a verdict may reject a mon outright; a v1 -- peer keeps the old substitute-a-move behaviour it was built against function Handshake.strict(verdict) - return verdict == "full" or verdict == "subset" + return verdict == "full" or verdict == "subset" or verdict == "engine_skew" end function Handshake.battleAllowed(verdict) @@ -278,6 +290,24 @@ function Handshake.describe(localHello, remoteHello, verdict, mode) end return lines end + if verdict == "engine_skew" then + -- name both releases so two friends can tell WHO updates: this used + -- to surface three turns in as a desync draw blaming mods (#758) + wrap(lines, "Your game versions") + wrap(lines, "differ:") + wrap(lines, (" you: v%s"):format(tostring(localHello.engineVersion))) + wrap(lines, (" %s: v%s"):format(peer:sub(1, 8), + tostring(remoteHello.engineVersion))) + if mode == "battle" then + wrap(lines, "Battle needs the") + wrap(lines, "same version on") + wrap(lines, "both games.") + else + wrap(lines, "Trading is limited") + wrap(lines, "to shared POKéMON.") + end + return lines + end wrap(lines, "Your games differ.") local diff = Handshake.modDiff(localHello, remoteHello) listMods(lines, peer .. " has:", diff.onlyTheirs) diff --git a/src/link/LinkBattle.lua b/src/link/LinkBattle.lua index c51f57c5..99e36aef 100644 --- a/src/link/LinkBattle.lua +++ b/src/link/LinkBattle.lua @@ -202,7 +202,7 @@ function LinkBattle.new(game, net, opts) local theirName = opts.theirName or "FOE" if not Handshake.battleAllowed(opts.verdict) then - return nil, Strings("Link battle needs\nthe same mods on\nboth games.") + return nil, Strings("Link battle needs\nthe same version\nand mods.") end -- both parties pass through the same pack->unpack clamp on both @@ -342,7 +342,7 @@ function LinkBattle.new(game, net, opts) localHash = localH, remoteHash = remoteH, fatal = true }) endAsDraw(s, Strings( - "Link desync!\n%s differs.\fAre both games\nrunning the same\nmods?", + "Link desync!\n%s differs.\fAre both games\nthe same version\nand mods?", component)) end @@ -692,7 +692,7 @@ function LinkBattle.newSpectator(game, net, opts) local guestName = opts.guestName or "GUEST" if not Handshake.battleAllowed(opts.verdict) then - return nil, Strings("Link battle needs\nthe same mods on\nboth games.") + return nil, Strings("Link battle needs\nthe same version\nand mods.") end local unpackOpts = { strict = opts.strict or false, forceLevel = opts.forceLevel } diff --git a/src/link/LinkState.lua b/src/link/LinkState.lua index 59d15520..4e97133e 100644 --- a/src/link/LinkState.lua +++ b/src/link/LinkState.lua @@ -714,7 +714,9 @@ function LinkState:draw() end elseif self.stage == "notice" then - drawTitle("CHECK YOUR MODS") + -- a version-skew notice has nothing to do with mods (#758) + drawTitle(self.verdict == "engine_skew" and "UPDATE YOUR GAME" + or "CHECK YOUR MODS") for i, line in ipairs(self.noticeLines or {}) do if i > 8 then break end -- what fits above the prompt row Font.draw(line, 8, 24 + (i - 1) * 12) diff --git a/tests/mod_link_tests.lua b/tests/mod_link_tests.lua index bb55a825..eaa2839a 100644 --- a/tests/mod_link_tests.lua +++ b/tests/mod_link_tests.lua @@ -378,6 +378,25 @@ local nextEngine = Handshake.hello(fakeGame(vanilla, "BLUE"), nil) nextEngine.engineVersion = "2.0.0" eq(Handshake.checkCompat(helloA, nextEngine), "refused", "engine major mismatch refuses") +-- same major, different release: the fingerprint can't see engine code, +-- and battle logic changes between releases, so lockstep would desync a +-- few turns in (#758) -- battle is refused up front, trade still works +local skewed = Handshake.hello(fakeGame(vanilla, "BLUE"), nil) +skewed.engineVersion = (tostring(helloA.engineVersion):match("^(%d+)") or "0") .. ".999.0" +local skewVerdict, skewReason = Handshake.checkCompat(helloA, wire(skewed)) +eq(skewVerdict, "engine_skew", "same-major release skew is its own verdict") +eq(skewReason, "engine_release_mismatch", "and says why") +check(not Handshake.battleAllowed("engine_skew"), "release skew refuses lockstep") +check(Handshake.tradeAllowed("engine_skew"), "release skew still trades") +check(Handshake.strict("engine_skew"), "release skew negotiates strictly") +local skewLines = Handshake.describe(helloA, wire(skewed), "engine_skew", "battle") +local skewJoined = table.concat(skewLines, " ") +check(skewJoined:find("version", 1, true) ~= nil, "skew notice mentions versions") +check(skewJoined:find("999", 1, true) ~= nil, "skew notice names the peer release") +for _, line in ipairs(skewLines) do + check(#line <= 20, "skew line fits the screen: " .. line) +end + local lines = Handshake.describe(helloA, wire(helloMod), "subset", "battle") check(#lines > 0, "the incompatibility screen has something to say") local joined = table.concat(lines, " ")