mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 01:34:06 -04:00
2089 lines
63 KiB
Lua
2089 lines
63 KiB
Lua
local Rng = require("src.core.game3.rng")
|
|
|
|
local LB = {}
|
|
|
|
-- pokefirered/include/constants/battle.h:47
|
|
LB.BATTLE_TYPE = {
|
|
DOUBLE = 0x1,
|
|
LINK = 0x2,
|
|
IS_MASTER = 0x4,
|
|
TRAINER = 0x8,
|
|
MULTI = 0x40,
|
|
}
|
|
|
|
-- pokefirered/include/constants/battle.h:83
|
|
LB.B_OUTCOME = {
|
|
WON = 1,
|
|
LOST = 2,
|
|
DREW = 3,
|
|
LINK_BATTLE_RAN = 128,
|
|
}
|
|
|
|
-- pokefirered/include/global.h:238
|
|
LB.RECORDS_COUNT = 5
|
|
LB.RECORD_MAX = 9999
|
|
|
|
-- pokefirered/include/link.h:88
|
|
LB.LINKTYPE = {
|
|
BATTLE = 0x2211,
|
|
SINGLE_BATTLE = 0x2233,
|
|
DOUBLE_BATTLE = 0x2244,
|
|
MULTI_BATTLE = 0x2255,
|
|
RECORD_MIX_BEFORE = 0x3311,
|
|
}
|
|
|
|
LB.MSG = {
|
|
LINKUP = "game3_battle_linkup",
|
|
SETUP = "game3_battle_setup",
|
|
ACTION = "game3_battle_action",
|
|
SWITCH = "game3_battle_switch",
|
|
SEAT = "game3_battle_seat",
|
|
OUTCOME = "game3_battle_outcome",
|
|
HASH = "game3_battle_hash",
|
|
FORFEIT = "forfeit",
|
|
}
|
|
|
|
LB.HASH_PARTS = { "actives", "volatile", "bench", "field", "rng" }
|
|
|
|
LB.MODE_OF = { single = 1, double = 2, multi = 5 }
|
|
|
|
-- pokefirered/src/cable_club.c:493 TryBattleLinkup
|
|
LB.PLAYERS = {
|
|
[1] = { min = 2, max = 2, linkType = LB.LINKTYPE.SINGLE_BATTLE },
|
|
[2] = { min = 2, max = 2, linkType = LB.LINKTYPE.DOUBLE_BATTLE },
|
|
[5] = { min = 4, max = 4, linkType = LB.LINKTYPE.MULTI_BATTLE },
|
|
}
|
|
|
|
-- pokefirered/src/cable_club.c:532 TryRecordMixLinkup
|
|
LB.RECORD_MIX = { min = 2, max = 4, linkType = LB.LINKTYPE.RECORD_MIX_BEFORE }
|
|
|
|
-- pokefirered/src/cable_club.c:482 TryLinkTimeout
|
|
LB.LINKUP_TICKS = 600
|
|
LB.VAR_0x8005 = 0x8005
|
|
|
|
LB.state = "off"
|
|
LB.mode = nil
|
|
LB.seed = nil
|
|
LB.seat = nil
|
|
LB.linkup = nil
|
|
LB.peer = nil
|
|
LB.unionRoom = false
|
|
LB.outcome = nil
|
|
LB.headless = nil
|
|
LB.fade = nil
|
|
LB._actions = {}
|
|
LB._switches = {}
|
|
LB._sent = {}
|
|
LB._started = false
|
|
LB._draws = { n = 0 }
|
|
LB._myHashes = {}
|
|
LB._peerHashes = {}
|
|
LB._myParts = {}
|
|
LB._peerOutcomes = {}
|
|
LB._reported = false
|
|
LB.endReason = nil
|
|
LB.log = {}
|
|
|
|
local function link()
|
|
return require("src.core.game3.link")
|
|
end
|
|
|
|
local function union()
|
|
return package.loaded["src.core.game3.link.union_room"]
|
|
end
|
|
|
|
local function natives()
|
|
return require("src.core.game3.scripting.natives")
|
|
end
|
|
|
|
local function session()
|
|
return link().session()
|
|
end
|
|
|
|
local function copyTable(value, depth)
|
|
if type(value) ~= "table" or (depth or 0) > 8 then return value end
|
|
local out = {}
|
|
for k, v in pairs(value) do out[k] = copyTable(v, (depth or 0) + 1) end
|
|
return out
|
|
end
|
|
|
|
LB.copy = copyTable
|
|
|
|
-- pokefirered/src/random.c:15 ISO_RANDOMIZE1
|
|
function LB.makeRng(seed, counter)
|
|
local value = math.floor(tonumber(seed) or 0) % 4294967296
|
|
local function word()
|
|
if counter then counter.n = counter.n + 1 end
|
|
value = (Rng.mulU32(value, 1103515245) + 24691) % 4294967296
|
|
return math.floor(value / 65536) % 65536
|
|
end
|
|
return function(lo, hi)
|
|
if lo == nil and hi == nil then return word() / 65536 end
|
|
if hi == nil then
|
|
lo = math.floor(tonumber(lo) or 1)
|
|
if lo <= 0 then return 0 end
|
|
return 1 + (word() % lo)
|
|
end
|
|
lo = math.floor(tonumber(lo) or 0)
|
|
hi = math.floor(tonumber(hi) or lo)
|
|
if hi < lo then lo, hi = hi, lo end
|
|
local span = hi - lo + 1
|
|
if span <= 0 then return lo end
|
|
return lo + (word() % span)
|
|
end
|
|
end
|
|
|
|
function LB.dealSeed()
|
|
return Rng.Random32() % 4294967296
|
|
end
|
|
|
|
local function note(fmt, ...)
|
|
local line = string.format(fmt, ...)
|
|
LB.log[#LB.log + 1] = line
|
|
if #LB.log > 64 then table.remove(LB.log, 1) end
|
|
print("[link3] " .. line)
|
|
end
|
|
|
|
LB.note = note
|
|
|
|
function LB.transport()
|
|
if LB._spec then return LB._spec.t end
|
|
local lk = link().link
|
|
return lk and lk._transport or nil
|
|
end
|
|
|
|
function LB.onRelay()
|
|
local t = LB.transport()
|
|
return (t and t.relay == true) and true or false
|
|
end
|
|
|
|
function LB.seedFromRelay()
|
|
local t = LB.transport()
|
|
if t and t.relay == true and type(t.seed) == "function" then
|
|
local ok, seed = pcall(t.seed, t)
|
|
seed = ok and tonumber(seed) or nil
|
|
if seed then return math.floor(seed) % 4294967296 end
|
|
end
|
|
local spec = LB._arena or (LB._spec and LB._spec.spec)
|
|
local seed = spec and tonumber(spec.seed)
|
|
if seed then return math.floor(seed) % 4294967296 end
|
|
return nil
|
|
end
|
|
|
|
function LB.relaySeat()
|
|
local t = LB.transport()
|
|
if t and type(t.seat) == "function" then
|
|
local ok, seat = pcall(t.seat, t)
|
|
if ok and tonumber(seat) then return math.floor(tonumber(seat)) end
|
|
end
|
|
local spec = LB._arena
|
|
if spec and tonumber(spec.seat) then return math.floor(tonumber(spec.seat)) end
|
|
return nil
|
|
end
|
|
|
|
local function protocol()
|
|
return require("src.link.Protocol")
|
|
end
|
|
|
|
LB.LINK_LEVEL = 50
|
|
|
|
function LB.unpackOpts()
|
|
local spec = LB._arena or (LB._spec and LB._spec.spec)
|
|
local rule = spec and spec.profile and spec.profile.rule
|
|
if not spec then return { strict = true, forceLevel = LB.LINK_LEVEL } end
|
|
return { strict = true, forceLevel = rule and tonumber(rule.forceLevel) or nil }
|
|
end
|
|
|
|
function LB.packParty(party)
|
|
local P = protocol()
|
|
assert(type(P.packParty3) == "function", "Protocol.packParty3 is required for a Gen 3 link battle")
|
|
return P.packParty3(party)
|
|
end
|
|
|
|
-- pokefirered/include/constants/species.h:419
|
|
LB.SPECIES_DEOXYS = 410
|
|
|
|
-- pokefirered/src/pokemon.c:6163
|
|
function LB.applyLinkStats(mon)
|
|
if tonumber(mon and (mon.species or mon.speciesId)) ~= LB.SPECIES_DEOXYS then return mon end
|
|
local Pokemon = require("src.core.game3.pokemon")
|
|
local meta = Pokemon.speciesMeta(LB.SPECIES_DEOXYS)
|
|
local row = meta and meta.linkStats
|
|
if type(row) ~= "table" or #row < 6 or not Pokemon.stats(LB.SPECIES_DEOXYS) then
|
|
return nil, "no_link_stats"
|
|
end
|
|
local saved = Pokemon._stats[LB.SPECIES_DEOXYS]
|
|
Pokemon._stats[LB.SPECIES_DEOXYS] = {
|
|
hp = row[1], atk = row[2], def = row[3], spe = row[4], spa = row[5], spd = row[6],
|
|
}
|
|
local ok, st = pcall(Pokemon.calcStats, LB.SPECIES_DEOXYS, mon.level, mon.ivs, mon.evs, mon.personality)
|
|
Pokemon._stats[LB.SPECIES_DEOXYS] = saved
|
|
if not ok or type(st) ~= "table" then return nil, "no_link_stats" end
|
|
mon.attack, mon.atk = st.attack, st.attack
|
|
mon.defense, mon.def = st.defense, st.defense
|
|
mon.speed, mon.spe = st.speed, st.speed
|
|
mon.spAtk, mon.spa = st.spAtk, st.spAtk
|
|
mon.spDef, mon.spd = st.spDef, st.spDef
|
|
return mon
|
|
end
|
|
|
|
function LB.unpackParty(packed, opts)
|
|
if type(packed) ~= "table" or #packed == 0 or #packed > 6 then return nil, "bad party" end
|
|
local P = protocol()
|
|
assert(type(P.unpackMon3) == "function", "Protocol.unpackMon3 is required for a Gen 3 link battle")
|
|
opts = opts or LB.unpackOpts()
|
|
local out = {}
|
|
for i = 1, #packed do
|
|
local mon, why = P.unpackMon3(nil, packed[i], { strict = true, forceLevel = opts.forceLevel })
|
|
if not mon then return nil, why or "bad mon" end
|
|
local item = tonumber(mon.item or mon.heldItem) or 0
|
|
mon.item, mon.heldItem = item, item
|
|
local linked, linkWhy = LB.applyLinkStats(mon)
|
|
if not linked then return nil, linkWhy end
|
|
out[i] = linked
|
|
end
|
|
return out
|
|
end
|
|
|
|
local function partyOf(s)
|
|
return (s and s.party) or {}
|
|
end
|
|
|
|
local function speciesOf(mon)
|
|
return tonumber(mon and (mon.species or mon.speciesId)) or 0
|
|
end
|
|
|
|
-- pokefirered/src/party_menu.c:5674 GetMonForBattleEntry
|
|
function LB.eligible(mon)
|
|
if type(mon) ~= "table" then return false end
|
|
local sp = speciesOf(mon)
|
|
if sp == 0 or sp == 412 then return false end
|
|
if mon.isEgg or mon.egg then return false end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/data/scripts/cable_club.inc:847
|
|
local function hasBadEgg(s)
|
|
for _, mon in ipairs(partyOf(s)) do
|
|
if mon.isBadEgg == true then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
-- pokefirered/src/script_pokemon_util.c:119 DoesPartyHaveEnigmaBerry
|
|
local ITEM_ENIGMA_BERRY = 175
|
|
local function hasEnigmaBerry(s)
|
|
for _, mon in ipairs(partyOf(s)) do
|
|
if tonumber(mon.heldItem or mon.item) == ITEM_ENIGMA_BERRY then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function LB.validateParty(s, mode, opts)
|
|
opts = opts or {}
|
|
s = s or session()
|
|
if hasBadEgg(s) then return false, "bad_egg" end
|
|
local usable = 0
|
|
for _, mon in ipairs(partyOf(s)) do
|
|
if LB.eligible(mon) then usable = usable + 1 end
|
|
end
|
|
if usable < 1 then return false, "no_mons" end
|
|
if opts.unionRoom then
|
|
-- pokefirered/data/scripts/cable_club.inc:806
|
|
if hasEnigmaBerry(s) then return false, "enigma_berry" end
|
|
return true, nil
|
|
end
|
|
local L = link()
|
|
-- pokefirered/data/scripts/cable_club.inc:252 HasEnoughMonsForDoubleBattle
|
|
if mode == L.USING.DOUBLE_BATTLE or mode == L.USING.MULTI_BATTLE then
|
|
local Party = require("src.core.game3.party")
|
|
if Party.monsStateToDoubles(partyOf(s)) ~= Party.PLAYER_HAS_TWO_USABLE_MONS then
|
|
return false, "need_two_mons"
|
|
end
|
|
end
|
|
return true, nil
|
|
end
|
|
|
|
function LB.isActive()
|
|
return LB.state == "battle" or LB.state == "setup"
|
|
end
|
|
|
|
function LB.localPlayer()
|
|
local s = session()
|
|
return {
|
|
name = (s and s.name) or "PLAYER",
|
|
trainerId = tonumber(s and (s.trainerId or s.id)) or 0,
|
|
gender = (s and (s.gender == "female" or s.gender == 1)) and 1 or 0,
|
|
}
|
|
end
|
|
|
|
-- pokefirered/src/script_pokemon_util.c:197 ReducePlayerPartyToThree
|
|
function LB.battleParty(s)
|
|
s = s or session()
|
|
local party = partyOf(s)
|
|
local okT, Tower = pcall(require, "src.core.game3.trainer_tower")
|
|
local order = okT and Tower and Tower.selectedOrder and Tower.selectedOrder(s) or nil
|
|
if type(order) == "table" and (tonumber(order[1]) or 0) ~= 0 then
|
|
local out = {}
|
|
for i = 1, #order do
|
|
local slot = tonumber(order[i]) or 0
|
|
if slot > 0 and party[slot] then out[#out + 1] = copyTable(party[slot]) end
|
|
end
|
|
if #out > 0 then return out end
|
|
end
|
|
local out = {}
|
|
for i = 1, 6 do
|
|
if party[i] then out[#out + 1] = copyTable(party[i]) end
|
|
end
|
|
return out
|
|
end
|
|
|
|
function LB.isMulti()
|
|
return LB.mode == LB.MODE_OF.multi
|
|
end
|
|
|
|
-- pokefirered/src/script_pokemon_util.c:197
|
|
LB.MULTI_PARTY_SIZE = 3
|
|
|
|
local function firstThree(list)
|
|
if type(list) ~= "table" or #list <= LB.MULTI_PARTY_SIZE then return list end
|
|
local out = {}
|
|
for i = 1, LB.MULTI_PARTY_SIZE do out[i] = list[i] end
|
|
return out
|
|
end
|
|
|
|
function LB.myPacked()
|
|
if LB._myPacked then
|
|
if LB.isMulti() then LB._myPacked = firstThree(LB._myPacked) end
|
|
return LB._myPacked
|
|
end
|
|
local party = LB.battleParty()
|
|
if LB.isMulti() then party = firstThree(party) end
|
|
LB._myPacked = LB.packParty(party)
|
|
return LB._myPacked
|
|
end
|
|
|
|
function LB.myParty()
|
|
if LB._myParty then return LB._myParty end
|
|
local party, why = LB.unpackParty(LB.myPacked())
|
|
if not party then return nil, why end
|
|
LB._myParty = party
|
|
return party
|
|
end
|
|
|
|
local function sendSetup()
|
|
local lk = link().link
|
|
if not (lk and lk:isOpen()) then return false end
|
|
local me = LB.localPlayer()
|
|
lk:send({
|
|
type = LB.MSG.SETUP,
|
|
seed = (not LB.onRelay()) and LB.seed or nil,
|
|
mode = LB.mode,
|
|
unionRoom = LB.unionRoom and true or false,
|
|
name = me.name,
|
|
trainerId = me.trainerId,
|
|
gender = me.gender,
|
|
seat = LB.relaySeat() or LB.seat,
|
|
party = LB.myPacked(),
|
|
})
|
|
return true
|
|
end
|
|
|
|
LB.sendSetup = sendSetup
|
|
|
|
function LB.foeFrom(setup)
|
|
local party = {}
|
|
for _, mon in ipairs((setup and setup.party) or {}) do
|
|
local copy = copyTable(mon)
|
|
copy.item = tonumber(copy.item or copy.heldItem) or 0
|
|
copy.heldItem = copy.item
|
|
party[#party + 1] = copy
|
|
end
|
|
if #party == 0 then return nil end
|
|
local foe = copyTable(party[1])
|
|
foe.party = party
|
|
-- pokefirered/src/cable_club.c:672 TRAINER_LINK_OPPONENT is no gTrainers row
|
|
foe.trainerId = nil
|
|
foe.trainerClass = nil
|
|
foe.link = true
|
|
foe.name = setup and setup.name or nil
|
|
return foe
|
|
end
|
|
|
|
-- pokefirered/include/constants/trainers.h:156 TRAINER_PIC_RED / TRAINER_PIC_LEAF
|
|
LB.TRAINER_PIC_RED = 135
|
|
LB.TRAINER_PIC_LEAF = 136
|
|
|
|
-- pokefirered/include/constants/union_room.h:19
|
|
LB.NUM_UNION_ROOM_CLASSES = 8
|
|
|
|
function LB.unionRoomClasses()
|
|
if not LB._unionRoomClasses then
|
|
local rel = "data/generated/gba/trainers/union_room_classes.lua"
|
|
local src = assert(require("src.core.game3.dataset").cache():read(rel), "no " .. rel .. " in the cache")
|
|
LB._unionRoomClasses = assert(load(src, "@" .. rel, "t", {}))()
|
|
end
|
|
return LB._unionRoomClasses
|
|
end
|
|
|
|
-- pokefirered/src/pokemon.c:6197
|
|
local function unionRoomIndex(peer)
|
|
local i = (tonumber(peer and peer.trainerId) or 0) % LB.NUM_UNION_ROOM_CLASSES
|
|
if tonumber(peer and peer.gender) == 1 then i = i + LB.NUM_UNION_ROOM_CLASSES end
|
|
return i
|
|
end
|
|
|
|
-- pokefirered/src/pokemon.c:6206 GetUnionRoomTrainerClass
|
|
function LB.unionRoomTrainerClass(peer)
|
|
return LB.unionRoomClasses().trainerClass[unionRoomIndex(peer or LB.peer)]
|
|
end
|
|
|
|
-- pokefirered/src/pokemon.c:6197 GetUnionRoomTrainerPic
|
|
function LB.unionRoomTrainerPic(peer)
|
|
return LB.unionRoomClasses().trainerPic[unionRoomIndex(peer or LB.peer)]
|
|
end
|
|
|
|
-- pokefirered/src/battle_controller_link_opponent.c:1172
|
|
function LB.peerPicId(setup)
|
|
if LB.unionRoom then return LB.unionRoomTrainerPic(setup) end
|
|
if tonumber(setup and setup.gender) == 1 then return LB.TRAINER_PIC_LEAF end
|
|
return LB.TRAINER_PIC_RED
|
|
end
|
|
|
|
-- pokefirered/src/link.c:965 GetMultiplayerId
|
|
function LB.multiplayerId()
|
|
if LB._spec then return 0 end
|
|
local seat = LB.onRelay() and LB.relaySeat() or nil
|
|
if seat then return seat end
|
|
local lk = link().link
|
|
return (lk and lk.role == "guest") and 1 or 0
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:909 BATTLE_TYPE_IS_MASTER
|
|
function LB.isMaster()
|
|
return LB.multiplayerId() == 0
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:628 Task_StartWiredCableClubBattle
|
|
function LB.battleFlags(mode)
|
|
local L = link()
|
|
local flags = LB.BATTLE_TYPE.TRAINER + LB.BATTLE_TYPE.LINK
|
|
if LB.isMaster() then flags = flags + LB.BATTLE_TYPE.IS_MASTER end
|
|
if mode == L.USING.DOUBLE_BATTLE then
|
|
flags = flags + LB.BATTLE_TYPE.DOUBLE
|
|
elseif mode == L.USING.MULTI_BATTLE then
|
|
flags = flags + LB.BATTLE_TYPE.DOUBLE + LB.BATTLE_TYPE.MULTI
|
|
end
|
|
return flags
|
|
end
|
|
|
|
-- pokefirered/include/constants/songs.h:272
|
|
LB.MUS_RS_VS_GYM_LEADER = 265
|
|
LB.MUS_RS_VS_TRAINER = 266
|
|
|
|
-- pokefirered/src/cable_club.c:656 Task_StartWiredCableClubBattle
|
|
function LB.battleSong(setup, mine)
|
|
local leader
|
|
if LB.isMaster() then
|
|
leader = tonumber(mine and mine.trainerId) or LB.localPlayer().trainerId
|
|
else
|
|
leader = tonumber(setup and setup.trainerId) or 0
|
|
end
|
|
if leader % 2 == 1 then return LB.MUS_RS_VS_GYM_LEADER end
|
|
return LB.MUS_RS_VS_TRAINER
|
|
end
|
|
|
|
local function resetTurnState()
|
|
LB._actions = {}
|
|
LB._switches = {}
|
|
LB._seatActions = {}
|
|
LB._seatSwitches = {}
|
|
LB._sent = {}
|
|
LB._draws = { n = 0 }
|
|
LB._myHashes = {}
|
|
LB._peerHashes = {}
|
|
LB._myParts = {}
|
|
LB._peerOutcomes = {}
|
|
LB._checked = {}
|
|
LB._raw = nil
|
|
end
|
|
|
|
function LB.refuse(why, onDone)
|
|
why = tostring(why or "refused")
|
|
note("battle refused: %s", why)
|
|
LB.endReason = why
|
|
local lk = link().link
|
|
if lk and lk:isOpen() then lk:send({ type = LB.MSG.FORFEIT, reason = why }) end
|
|
LB.state = "off"
|
|
LB._started = false
|
|
LB.report("error")
|
|
if onDone then onDone("error") end
|
|
return false, why
|
|
end
|
|
|
|
function LB.beginBattle(setup, onDone)
|
|
local L = link()
|
|
local peerParty, why = LB.unpackParty(setup and setup.party)
|
|
if not peerParty then return LB.refuse(why, onDone) end
|
|
local mine, mineWhy = LB.myParty()
|
|
if not mine then return LB.refuse(mineWhy, onDone) end
|
|
local foe = LB.foeFrom({ name = setup.name, party = peerParty })
|
|
if not foe then return LB.refuse("peer_has_no_party", onDone) end
|
|
LB.peer = {
|
|
name = setup.name,
|
|
trainerId = tonumber(setup.trainerId) or 0,
|
|
gender = tonumber(setup.gender) or 0,
|
|
}
|
|
LB.state = "battle"
|
|
resetTurnState()
|
|
LB._relay = LB.onRelay()
|
|
LB._started = true
|
|
local flags = LB.battleFlags(LB.mode)
|
|
local double = (flags % (LB.BATTLE_TYPE.DOUBLE * 2)) >= LB.BATTLE_TYPE.DOUBLE
|
|
-- pokefirered/src/cable_club.c:669 ReducePlayerPartyToThree
|
|
if LB.mode == L.USING.MULTI_BATTLE then
|
|
local okT, Tower = pcall(require, "src.core.game3.trainer_tower")
|
|
if okT and Tower and Tower.reducePartyToThree then Tower.reducePartyToThree(session()) end
|
|
end
|
|
local BattleBridge = require("src.core.game3.battle_bridge")
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local ok, started, err = pcall(BattleBridge.start, rt and rt._mod, L.game(), foe, {
|
|
link = true,
|
|
linkParty = mine,
|
|
linkFlags = flags,
|
|
-- pokefirered/src/battle_controllers.c:148 the master's own mon is battler 0 on both machines
|
|
linkMaster = LB.isMaster(),
|
|
double = double,
|
|
unionRoom = LB.unionRoom,
|
|
trainerId = nil,
|
|
rng = LB.makeRng(LB.seed, LB._draws),
|
|
peerName = setup.name,
|
|
trainerName = setup.name,
|
|
trainerPicId = LB.peerPicId(setup),
|
|
song = LB.battleSong(setup),
|
|
headless = LB.headless,
|
|
autoFight = LB.autoFight,
|
|
fade = LB.fade,
|
|
done = function(result)
|
|
LB.finish(result)
|
|
if onDone then onDone(LB.resultWord(result)) end
|
|
end,
|
|
})
|
|
if not ok or not started then
|
|
LB._started = false
|
|
return LB.refuse(ok and (err or "battle_start_failed") or started, onDone)
|
|
end
|
|
return started
|
|
end
|
|
|
|
local function hasLiving(party)
|
|
for _, mon in ipairs(party or {}) do
|
|
if LB.eligible(mon) and (tonumber(mon.hp) or 0) > 0 then return true end
|
|
end
|
|
return false
|
|
end
|
|
|
|
function LB.unpackMultiParty(packed)
|
|
if type(packed) == "table" and #packed > LB.MULTI_PARTY_SIZE then return nil, "bad party" end
|
|
local party, why = LB.unpackParty(packed)
|
|
if not party then return nil, why end
|
|
if not hasLiving(party) then return nil, "no_mons" end
|
|
return party
|
|
end
|
|
|
|
-- pokefirered/src/battle_controllers.c:229
|
|
local MULTI_POSITION = {
|
|
[0] = { [0] = 0, [1] = 3, [2] = 2, [3] = 1 },
|
|
[1] = { [0] = 1, [1] = 2, [2] = 3, [3] = 0 },
|
|
}
|
|
|
|
function LB.localBattler(mySeat, seat)
|
|
return MULTI_POSITION[(tonumber(mySeat) or 0) % 2][seat]
|
|
end
|
|
|
|
-- pokefirered/src/battle_message.c:2092
|
|
function LB.oppositeSeat(seat)
|
|
return (seat % 2 == 0) and (seat + 1) or (seat - 1)
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:1295
|
|
local SIDE_SEATS = { [0] = { 0, 2 }, [1] = { 3, 1 } }
|
|
|
|
function LB.multiLayout(mySeat, parties, names, genders)
|
|
local mySide = (mySeat ~= nil and mySeat % 2 == 1) and 1 or 0
|
|
local function side(n)
|
|
local party, owners = {}, {}
|
|
for _, seat in ipairs(SIDE_SEATS[n]) do
|
|
for _, mon in ipairs(parties[seat] or {}) do
|
|
party[#party + 1] = mon
|
|
owners[#party] = LB.localBattler(mySeat, seat)
|
|
end
|
|
end
|
|
return party, owners
|
|
end
|
|
local playerParty, playerOwners = side(mySide)
|
|
local foeParty, foeOwners = side(1 - mySide)
|
|
local byLocal, seatOf, localOf, genderOf, order = {}, {}, {}, {}, {}
|
|
for seat = 0, 3 do
|
|
local id = LB.localBattler(mySeat, seat)
|
|
byLocal[id] = names and names[seat] or nil
|
|
genderOf[id] = (genders and tonumber(genders[seat]) == 1) and 1 or 0
|
|
seatOf[id] = seat
|
|
localOf[seat] = id
|
|
order[seat + 1] = id
|
|
end
|
|
return {
|
|
own = mySeat ~= nil and LB.localBattler(mySeat, mySeat) or nil,
|
|
names = byLocal,
|
|
genders = genderOf,
|
|
seatOf = seatOf,
|
|
localOf = localOf,
|
|
order = order,
|
|
owners = { player = playerOwners, enemy = foeOwners },
|
|
}, playerParty, foeParty
|
|
end
|
|
|
|
local function setupInfo(setup)
|
|
return {
|
|
name = setup and setup.name or nil,
|
|
trainerId = tonumber(setup and setup.trainerId) or 0,
|
|
gender = (tonumber(setup and setup.gender) == 1) and 1 or 0,
|
|
}
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:1196
|
|
function LB.beginMulti(setups, onDone)
|
|
local L = link()
|
|
local mySeat = LB.relaySeat() or tonumber(LB.seat) or 0
|
|
local mine, mineWhy = LB.myParty()
|
|
if not mine then return LB.refuse(mineWhy, onDone) end
|
|
if #mine > LB.MULTI_PARTY_SIZE or not hasLiving(mine) then return LB.refuse("no_mons", onDone) end
|
|
local me = LB.localPlayer()
|
|
local parties, names, info = { [mySeat] = mine }, { [mySeat] = me.name }, { [mySeat] = me }
|
|
for seat = 0, 3 do
|
|
if seat ~= mySeat then
|
|
local setup = setups and setups[seat]
|
|
if not setup then return LB.refuse("missing_seat", onDone) end
|
|
local party, why = LB.unpackMultiParty(setup.party)
|
|
if not party then return LB.refuse(why, onDone) end
|
|
parties[seat] = party
|
|
info[seat] = setupInfo(setup)
|
|
names[seat] = info[seat].name
|
|
end
|
|
end
|
|
local genders = {}
|
|
for seat = 0, 3 do genders[seat] = info[seat].gender end
|
|
local layout, playerParty, foeParty = LB.multiLayout(mySeat, parties, names, genders)
|
|
local oppSeat = LB.oppositeSeat(mySeat)
|
|
local foe = LB.foeFrom({ name = names[oppSeat], party = foeParty })
|
|
if not foe then return LB.refuse("peer_has_no_party", onDone) end
|
|
LB.peer = info[oppSeat]
|
|
LB.multiNames = names
|
|
LB.state = "battle"
|
|
resetTurnState()
|
|
LB._relay = LB.onRelay()
|
|
LB._started = true
|
|
if not LB._arena then
|
|
-- pokefirered/src/cable_club.c:669
|
|
local okT, Tower = pcall(require, "src.core.game3.trainer_tower")
|
|
if okT and Tower and Tower.reducePartyToThree then Tower.reducePartyToThree(session()) end
|
|
end
|
|
local BattleBridge = require("src.core.game3.battle_bridge")
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local ok, started, err = pcall(BattleBridge.start, rt and rt._mod, L.game(), foe, {
|
|
link = true,
|
|
linkParty = playerParty,
|
|
linkFlags = LB.battleFlags(L.USING.MULTI_BATTLE),
|
|
-- pokefirered/src/battle_controllers.c:248
|
|
linkMaster = mySeat % 2 == 0,
|
|
double = true,
|
|
multi = layout,
|
|
unionRoom = false,
|
|
trainerId = nil,
|
|
rng = LB.makeRng(LB.seed, LB._draws),
|
|
peerName = names[oppSeat],
|
|
trainerName = names[oppSeat],
|
|
trainerPicId = LB.peerPicId(info[oppSeat]),
|
|
song = LB.battleSong(info[0], info[mySeat]),
|
|
headless = LB.headless,
|
|
autoFight = LB.autoFight,
|
|
fade = LB.fade,
|
|
done = function(result)
|
|
LB.finish(result)
|
|
if onDone then onDone(LB.resultWord(result)) end
|
|
end,
|
|
})
|
|
if not ok or not started then
|
|
LB._started = false
|
|
return LB.refuse(ok and (err or "battle_start_failed") or started, onDone)
|
|
end
|
|
return started
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3226 the action block the other machine reads
|
|
local function wireAction(action)
|
|
return {
|
|
kind = action and action.kind or "move",
|
|
slot = action and tonumber(action.slot) or nil,
|
|
move = action and action.move or nil,
|
|
target = action and tonumber(action.target) or nil,
|
|
itemId = action and tonumber(action.itemId) or nil,
|
|
partySlot = action and tonumber(action.partySlot) or nil,
|
|
}
|
|
end
|
|
|
|
local function battleState()
|
|
local Battle = package.loaded["src.core.game3.battle"]
|
|
return Battle and Battle.getState and Battle.getState() or nil, Battle
|
|
end
|
|
|
|
local bit = require("bit")
|
|
|
|
local function fnv(text)
|
|
local h = 0x811C9DC5
|
|
for i = 1, #text do
|
|
h = bit.bxor(h, text:byte(i)) % 4294967296
|
|
h = ((bit.lshift(h, 24) % 4294967296) + h * 403) % 4294967296
|
|
end
|
|
return string.format("%08x", h)
|
|
end
|
|
|
|
LB.fnv = fnv
|
|
|
|
local function scalar(v)
|
|
local t = type(v)
|
|
if t == "number" then
|
|
if v == math.floor(v) then return string.format("%d", v) end
|
|
return string.format("%.6f", v)
|
|
end
|
|
if t == "boolean" then return v and "T" or "F" end
|
|
if t == "string" then return v end
|
|
if v == nil then return "-" end
|
|
return "t"
|
|
end
|
|
|
|
local STAGES = { "attack", "defense", "speed", "spAtk", "spDef", "accuracy", "evasion" }
|
|
|
|
local VOLATILE_KEYS = {
|
|
"confusion", "substitute", "toxicCounter", "focusEnergy", "perishSong", "seeded", "trapped",
|
|
"attracted", "disabled", "encore", "taunt", "bide", "rage", "endure", "protect", "destinyBond",
|
|
"transformed", "isFirstTurn",
|
|
"expCharged", "expCursed", "expDisableTurns", "expDisabledMove", "expEncoreMove", "expEncoreSlot",
|
|
"expEncoreTurns", "expFocusEnergy", "expFuryCutter", "expInfatuated", "expIngrain", "expLockedMove",
|
|
"expLockedSlot", "expMustRecharge", "expNightmare", "expPerishTurns", "expRampageTurns",
|
|
"expRechargeTurns", "expRolloutTimer", "expSeeded", "expTauntedTurns", "expTormented",
|
|
"expTransform", "expTrapTurns", "expTrapped", "expTruantCounter", "expUproarTurns", "expYawnTurns",
|
|
"expCastformForm",
|
|
}
|
|
|
|
local function perspective(st)
|
|
if st.linkMaster == false then
|
|
return { 1, 0, 3, 2 }, { "enemy", "player" }
|
|
end
|
|
return { 0, 1, 2, 3 }, { "player", "enemy" }
|
|
end
|
|
|
|
local function battlerOf(st, id)
|
|
local State = require("src.core.game3.battle.state")
|
|
if State.isAbsent(st, id) then return nil end
|
|
return State.battler(st, id)
|
|
end
|
|
|
|
local function sortedScalars(t, skip, st)
|
|
if type(t) ~= "table" then return scalar(t) end
|
|
local flip = st and st.linkMaster == false
|
|
local keys = {}
|
|
for k, v in pairs(t) do
|
|
if (type(k) == "string" or type(k) == "number") and not (skip and skip[k])
|
|
and type(v) ~= "table" and type(v) ~= "function" then
|
|
keys[#keys + 1] = tostring(k)
|
|
end
|
|
end
|
|
table.sort(keys)
|
|
local out = {}
|
|
for _, k in ipairs(keys) do
|
|
local v = t[k]
|
|
if v == nil then v = t[tonumber(k)] end
|
|
if flip and type(v) == "number" and k:sub(-2) == "Id" and v >= 0 and v <= 3 then
|
|
v = (v % 2 == 0) and (v + 1) or (v - 1)
|
|
end
|
|
out[#out + 1] = k .. "=" .. scalar(v)
|
|
end
|
|
return table.concat(out, ",")
|
|
end
|
|
|
|
local function monPp(mon)
|
|
local pp = {}
|
|
for i = 1, 4 do pp[i] = scalar(mon and mon.pp and mon.pp[i]) end
|
|
return table.concat(pp, "/")
|
|
end
|
|
|
|
local SIDE_SKIP = { id = true }
|
|
|
|
function LB.hashParts(st)
|
|
local order, sides = perspective(st)
|
|
local actives, volatile = {}, {}
|
|
for _, id in ipairs(order) do
|
|
local b = battlerOf(st, id)
|
|
if not b then
|
|
actives[#actives + 1] = "-"
|
|
volatile[#volatile + 1] = "-"
|
|
else
|
|
local mon = b.mon or {}
|
|
local stages = {}
|
|
for i, key in ipairs(STAGES) do stages[i] = scalar(b.stages and b.stages[key] or 0) end
|
|
actives[#actives + 1] = table.concat({
|
|
scalar(tonumber(b.species or mon.species)), scalar(tonumber(mon.hp)), scalar(tonumber(mon.maxHp)),
|
|
scalar(b.status or mon.status), scalar(mon.sleep or b.sleepTurns), table.concat(stages, "/"),
|
|
scalar(b.ability), scalar(tonumber(b.item) or 0), monPp(mon),
|
|
}, ":")
|
|
local vol = {}
|
|
for _, key in ipairs(VOLATILE_KEYS) do vol[#vol + 1] = scalar(b[key]) end
|
|
vol[#vol + 1] = sortedScalars(b.volatiles)
|
|
volatile[#volatile + 1] = table.concat(vol, ":")
|
|
end
|
|
end
|
|
local bench, field = {}, {}
|
|
for _, side in ipairs(sides) do
|
|
local party = (side == "player") and st.playerParty or st.foeParty
|
|
local active = {}
|
|
for _, id in ipairs((side == "player") and { 0, 2 } or { 1, 3 }) do
|
|
local b = battlerOf(st, id)
|
|
if b and b.partyIndex then active[b.partyIndex] = true end
|
|
end
|
|
local rows = {}
|
|
for i, mon in ipairs(party or {}) do
|
|
if active[i] then
|
|
rows[#rows + 1] = "*"
|
|
else
|
|
rows[#rows + 1] = table.concat({
|
|
scalar(tonumber(mon.species or mon.speciesId)), scalar(tonumber(mon.hp)), scalar(mon.status),
|
|
scalar(tonumber(mon.item or mon.heldItem) or 0), monPp(mon),
|
|
}, ":")
|
|
end
|
|
end
|
|
bench[#bench + 1] = table.concat(rows, ";")
|
|
local sideState = (side == "player") and st.playerSide or st.enemySide
|
|
field[#field + 1] = sortedScalars(sideState, SIDE_SKIP, st) .. "|" .. sortedScalars(sideState and sideState.hazards, nil, st)
|
|
end
|
|
table.insert(field, 1, scalar(st.weather) .. ":" .. scalar(st.weatherTurns))
|
|
local raw = {
|
|
actives = table.concat(actives, "#"),
|
|
volatile = table.concat(volatile, "#"),
|
|
bench = table.concat(bench, "#"),
|
|
field = table.concat(field, "#"),
|
|
}
|
|
return {
|
|
actives = fnv(raw.actives),
|
|
volatile = fnv(raw.volatile),
|
|
bench = fnv(raw.bench),
|
|
field = fnv(raw.field),
|
|
rng = string.format("%d", LB._draws and LB._draws.n or 0),
|
|
}, raw
|
|
end
|
|
|
|
function LB.hashValue(parts)
|
|
local list = {}
|
|
for i, key in ipairs(LB.HASH_PARTS) do list[i] = tostring(parts[key] or "") end
|
|
return fnv(table.concat(list, "|"))
|
|
end
|
|
|
|
local function firstDiff(mine, theirs)
|
|
if type(mine) ~= "table" or type(theirs) ~= "table" then return "state" end
|
|
for _, key in ipairs(LB.HASH_PARTS) do
|
|
if tostring(mine[key]) ~= tostring(theirs[key]) then return key end
|
|
end
|
|
return "state"
|
|
end
|
|
|
|
function LB.checkHashes()
|
|
if LB.state ~= "battle" or not LB._started then return end
|
|
for turn, mine in pairs(LB._myHashes) do
|
|
for seat, rows in pairs(LB._peerHashes) do
|
|
local theirs = rows[turn]
|
|
local key = tostring(seat) .. ":" .. tostring(turn)
|
|
if theirs and not LB._checked[key] then
|
|
LB._checked[key] = true
|
|
if theirs.value ~= mine then
|
|
local component = firstDiff(LB._myParts[turn], theirs.parts)
|
|
return LB.desync(turn, component, seat)
|
|
end
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
function LB.checkpoint(turn)
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
if turn < 1 or LB._myHashes[turn] then return nil end
|
|
local st = battleState()
|
|
if not st then return nil end
|
|
local parts, raw = LB.hashParts(st)
|
|
local value = LB.hashValue(parts)
|
|
LB._myHashes[turn] = value
|
|
LB._myParts[turn] = parts
|
|
if LB.keepRaw then
|
|
LB._raw = LB._raw or {}
|
|
LB._raw[turn] = raw
|
|
end
|
|
local lk = link().link
|
|
if lk and lk:isOpen() then
|
|
lk:send({ type = LB.MSG.HASH, turn = turn, value = value, parts = parts })
|
|
end
|
|
LB.checkHashes()
|
|
return value
|
|
end
|
|
|
|
function LB.desync(turn, component, seat)
|
|
if LB.endReason == "desync" then return false end
|
|
LB.endReason = "desync"
|
|
note("desync turn %s component=%s seat=%s", tostring(turn), tostring(component), tostring(seat))
|
|
local _, Battle = battleState()
|
|
local Strings = require("src.core.Strings")
|
|
local text = Strings("Link desync! %s differs. Are both games the same version and mods?", tostring(component))
|
|
if Battle and Battle.isActive and Battle.isActive() and Battle.linkEnd then
|
|
Battle.linkEnd("draw", text, "desync")
|
|
elseif LB._started then
|
|
LB.finish("draw")
|
|
end
|
|
return true
|
|
end
|
|
|
|
function LB.protocolError(component)
|
|
return LB.desync(LB._turn or 0, component or "protocol", nil)
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3226
|
|
function LB.sendAction(turn, action)
|
|
local lk = link().link
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
if LB._sent[turn] then return true end
|
|
if not (lk and lk:isOpen()) then return false end
|
|
LB.checkpoint(turn - 1)
|
|
LB._turn = turn
|
|
LB._sent[turn] = true
|
|
local msg = wireAction(action)
|
|
msg.type = LB.MSG.ACTION
|
|
msg.turn = turn
|
|
lk:send(msg)
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3226 HandleTurnActionSelectionState
|
|
function LB.sendActionList(turn, list)
|
|
local lk = link().link
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
if LB._sent[turn] then return true end
|
|
if not (lk and lk:isOpen()) then return false end
|
|
LB.checkpoint(turn - 1)
|
|
LB._turn = turn
|
|
LB._sent[turn] = true
|
|
local actions = {}
|
|
for i = 1, 2 do actions[i] = wireAction(list and list[i]) end
|
|
lk:send({ type = LB.MSG.ACTION, turn = turn, kind = "list", actions = actions })
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3097
|
|
function LB.sendMultiAction(turn, action, target)
|
|
local lk = link().link
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
if LB._sent[turn] then return true end
|
|
if not (lk and lk:isOpen()) then return false end
|
|
LB.checkpoint(turn - 1)
|
|
LB._turn = turn
|
|
LB._sent[turn] = true
|
|
if action == nil then return true end
|
|
local msg = wireAction(action)
|
|
msg.target = tonumber(target)
|
|
msg.type = LB.MSG.ACTION
|
|
msg.turn = turn
|
|
lk:send(msg)
|
|
return true
|
|
end
|
|
|
|
function LB.seatAction(seat, turn)
|
|
LB.pumpActions()
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
local rows = LB._spec and LB._spec.actions[seat] or LB._seatActions[seat]
|
|
return rows and rows[turn] or nil
|
|
end
|
|
|
|
function LB.forgetSeatAction(seat, turn)
|
|
local rows = LB._spec and LB._spec.actions[seat] or LB._seatActions[seat]
|
|
if rows then rows[math.floor(tonumber(turn) or 0)] = nil end
|
|
end
|
|
|
|
function LB.seatSwitch(seat)
|
|
LB.pumpActions()
|
|
local q = LB._spec and LB._spec.switches[seat] or LB._seatSwitches[seat]
|
|
if not q or #q == 0 then return nil end
|
|
return table.remove(q, 1)
|
|
end
|
|
|
|
function LB.peerAhead(seat, turn)
|
|
LB.pumpActions()
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
local outcomes = LB._spec and LB._spec.outcomes or LB._peerOutcomes
|
|
local function past(s)
|
|
local rows = LB._peerHashes[s]
|
|
return (rows and rows[turn] ~= nil) or outcomes[s] ~= nil
|
|
end
|
|
if seat ~= nil then return past(seat) end
|
|
for s in pairs(LB._peerHashes) do if past(s) then return true end end
|
|
for _ in pairs(outcomes) do return true end
|
|
return false
|
|
end
|
|
|
|
function LB.beginSpectatedTurn(turn)
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
LB.checkpoint(turn - 1)
|
|
LB._turn = turn
|
|
end
|
|
|
|
-- pokefirered/data/battle_scripts_1.s:2837
|
|
function LB.sendSwitch(slot)
|
|
local lk = link().link
|
|
if not (lk and lk:isOpen()) then return false end
|
|
lk:send({ type = LB.MSG.SWITCH, slot = math.floor(tonumber(slot) or 1) })
|
|
return true
|
|
end
|
|
|
|
function LB.peerSwitch()
|
|
LB.pumpActions()
|
|
if LB._spec then
|
|
local q = LB._spec.switches[1]
|
|
if #q == 0 then return nil end
|
|
return table.remove(q, 1)
|
|
end
|
|
if #LB._switches == 0 then return nil end
|
|
return table.remove(LB._switches, 1)
|
|
end
|
|
|
|
function LB.ownSwitch()
|
|
if not LB._spec then return nil end
|
|
LB.pumpActions()
|
|
local q = LB._spec.switches[0]
|
|
if #q == 0 then return nil end
|
|
return table.remove(q, 1)
|
|
end
|
|
|
|
function LB.linkOpen()
|
|
if LB._spec then
|
|
local t = LB._spec.t
|
|
return not (LB._spec.gone or (t and (t.closed or t.error))) and true or false
|
|
end
|
|
local lk = link().link
|
|
return (lk and lk:isOpen()) and true or false
|
|
end
|
|
|
|
local function isOwn(msg)
|
|
if type(msg) ~= "table" then return false end
|
|
local seat = tonumber(msg.seat)
|
|
if seat == nil or not LB.onRelay() then return false end
|
|
local mine = LB.relaySeat()
|
|
return mine ~= nil and seat == mine
|
|
end
|
|
|
|
local function noteOutcome(msg, seat)
|
|
LB._peerOutcomes[seat or 1] = { outcome = tonumber(msg.outcome), turn = tonumber(msg.turn) }
|
|
LB.compareOutcome()
|
|
end
|
|
|
|
local function noteHash(msg, seat)
|
|
local turn = math.floor(tonumber(msg.turn) or 0)
|
|
if turn < 1 then return end
|
|
seat = seat or 1
|
|
LB._peerHashes[seat] = LB._peerHashes[seat] or {}
|
|
LB._peerHashes[seat][turn] = { value = tostring(msg.value or ""), parts = msg.parts }
|
|
end
|
|
|
|
function LB.pumpActions()
|
|
if LB._spec then return LB.pumpSpectator() end
|
|
local lk = link().link
|
|
if lk then LB._lk = lk else lk = LB._lk end
|
|
if not lk then return end
|
|
if lk:isOpen() then lk:update(0) end
|
|
local multi = LB.isMulti()
|
|
local msg = lk:take(LB.MSG.ACTION)
|
|
while msg do
|
|
if not isOwn(msg) then
|
|
local turn = math.floor(tonumber(msg.turn) or 0)
|
|
local seat = tonumber(msg.seat)
|
|
if multi then
|
|
if seat then
|
|
LB._seatActions[seat] = LB._seatActions[seat] or {}
|
|
LB._seatActions[seat][turn] = msg
|
|
end
|
|
else
|
|
LB._actions[turn] = msg
|
|
end
|
|
end
|
|
msg = lk:take(LB.MSG.ACTION)
|
|
end
|
|
local sw = lk:take(LB.MSG.SWITCH)
|
|
while sw do
|
|
if not isOwn(sw) then
|
|
local slot = math.floor(tonumber(sw.slot) or 1)
|
|
local seat = tonumber(sw.seat)
|
|
if multi then
|
|
if seat then
|
|
local q = LB._seatSwitches[seat] or {}
|
|
LB._seatSwitches[seat] = q
|
|
q[#q + 1] = slot
|
|
end
|
|
else
|
|
LB._switches[#LB._switches + 1] = slot
|
|
end
|
|
end
|
|
sw = lk:take(LB.MSG.SWITCH)
|
|
end
|
|
local h = lk:take(LB.MSG.HASH)
|
|
while h do
|
|
if not isOwn(h) then noteHash(h, tonumber(h.seat) or 1) end
|
|
h = lk:take(LB.MSG.HASH)
|
|
end
|
|
local o = lk:take(LB.MSG.OUTCOME)
|
|
while o do
|
|
if not isOwn(o) then noteOutcome(o, tonumber(o.seat) or 1) end
|
|
o = lk:take(LB.MSG.OUTCOME)
|
|
end
|
|
local f = lk:take(LB.MSG.FORFEIT)
|
|
if f and not isOwn(f) then
|
|
note("peer forfeited: %s", tostring(f.reason))
|
|
local seat, mine = tonumber(f.seat), LB.relaySeat()
|
|
LB.peerForfeit = (multi and seat and mine and seat % 2 == mine % 2) and "lose" or "win"
|
|
end
|
|
LB.checkHashes()
|
|
end
|
|
|
|
function LB.peerAction(turn)
|
|
LB.pumpActions()
|
|
if LB._spec then return LB._spec.actions[1][math.floor(tonumber(turn) or 0)] end
|
|
return LB._actions[math.floor(tonumber(turn) or 0)]
|
|
end
|
|
|
|
function LB.ownAction(turn)
|
|
if not LB._spec then return nil end
|
|
LB.pumpActions()
|
|
turn = math.floor(tonumber(turn) or 0)
|
|
local msg = LB._spec.actions[0][turn]
|
|
if msg then
|
|
LB._spec.actions[0][turn] = nil
|
|
LB.checkpoint(turn - 1)
|
|
LB._turn = turn
|
|
end
|
|
return msg
|
|
end
|
|
|
|
function LB.forgetAction(turn)
|
|
LB._actions[math.floor(tonumber(turn) or 0)] = nil
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:295 GetLinkBattleRecordTotalBattles
|
|
local function totalBattles(entry)
|
|
return (tonumber(entry.wins) or 0) + (tonumber(entry.losses) or 0)
|
|
+ (tonumber(entry.draws) or 0)
|
|
end
|
|
|
|
local function clamp(n)
|
|
n = math.floor(tonumber(n) or 0)
|
|
if n < 0 then return 0 end
|
|
if n > LB.RECORD_MAX then return LB.RECORD_MAX end
|
|
return n
|
|
end
|
|
|
|
function LB.records(s)
|
|
s = s or session()
|
|
if type(s) ~= "table" then return {} end
|
|
if type(s.linkBattleRecords) ~= "table" then s.linkBattleRecords = {} end
|
|
return s.linkBattleRecords
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:313 SortLinkBattleRecords
|
|
local function sortRecords(records)
|
|
table.sort(records, function(a, b)
|
|
return totalBattles(a) > totalBattles(b)
|
|
end)
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:333 UpdateLinkBattleRecord
|
|
local function bumpRecord(entry, outcome)
|
|
if outcome == LB.B_OUTCOME.WON then
|
|
entry.wins = clamp((tonumber(entry.wins) or 0) + 1)
|
|
elseif outcome == LB.B_OUTCOME.LOST then
|
|
entry.losses = clamp((tonumber(entry.losses) or 0) + 1)
|
|
elseif outcome == LB.B_OUTCOME.DREW then
|
|
entry.draws = clamp((tonumber(entry.draws) or 0) + 1)
|
|
end
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:355 UpdateLinkBattleGameStats
|
|
local GAME_STAT = { [1] = "linkBattleWins", [2] = "linkBattleLosses", [3] = "linkBattleDraws" }
|
|
local function bumpGameStat(s, outcome)
|
|
local key = GAME_STAT[outcome]
|
|
if not (key and type(s) == "table") then return end
|
|
if type(s.gameStats) ~= "table" then s.gameStats = {} end
|
|
local n = tonumber(s.gameStats[key]) or 0
|
|
if n < LB.RECORD_MAX then s.gameStats[key] = n + 1 end
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:376 AddOpponentLinkBattleRecord
|
|
function LB.addOpponentRecord(s, name, trainerId, outcome)
|
|
s = s or session()
|
|
local records = LB.records(s)
|
|
bumpGameStat(s, outcome)
|
|
sortRecords(records)
|
|
name = tostring(name or "")
|
|
trainerId = math.floor(tonumber(trainerId) or 0)
|
|
local found
|
|
for _, entry in ipairs(records) do
|
|
if entry.name == name and (tonumber(entry.trainerId) or 0) == trainerId then
|
|
found = entry
|
|
break
|
|
end
|
|
end
|
|
if not found then
|
|
if #records >= LB.RECORDS_COUNT then
|
|
found = records[LB.RECORDS_COUNT]
|
|
else
|
|
found = {}
|
|
records[#records + 1] = found
|
|
end
|
|
found.name = name
|
|
found.trainerId = trainerId
|
|
found.wins, found.losses, found.draws = 0, 0, 0
|
|
end
|
|
bumpRecord(found, outcome)
|
|
sortRecords(records)
|
|
return found
|
|
end
|
|
|
|
-- pokefirered/src/battle_records.c:411 IncTrainerCardWinCount
|
|
local function bumpTrainerCard(s, outcome)
|
|
if type(s) ~= "table" then return end
|
|
if type(s.trainerCard) ~= "table" then s.trainerCard = {} end
|
|
local card = s.trainerCard
|
|
if outcome == LB.B_OUTCOME.WON then
|
|
card.linkBattleWins = clamp((tonumber(card.linkBattleWins) or 0) + 1)
|
|
elseif outcome == LB.B_OUTCOME.LOST then
|
|
card.linkBattleLosses = clamp((tonumber(card.linkBattleLosses) or 0) + 1)
|
|
end
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:4303
|
|
function LB.outcomeCode(result)
|
|
if result == "win" then return LB.B_OUTCOME.WON end
|
|
if result == "lose" or result == "whiteout" or result == "blackout" then
|
|
return LB.B_OUTCOME.LOST
|
|
end
|
|
if result == "draw" then return LB.B_OUTCOME.DREW end
|
|
if result == "run" then return LB.B_OUTCOME.LINK_BATTLE_RAN end
|
|
return LB.B_OUTCOME.DREW
|
|
end
|
|
|
|
-- pokefirered/src/battle_main.c:3777
|
|
function LB.recordOutcome(outcome)
|
|
if outcome == LB.B_OUTCOME.LINK_BATTLE_RAN then return LB.B_OUTCOME.LOST end
|
|
return outcome
|
|
end
|
|
|
|
local REPORT_WORD = { [1] = "win", [2] = "lose", [3] = "draw", [128] = "lose" }
|
|
|
|
function LB.resultWord(result)
|
|
return REPORT_WORD[LB.outcomeCode(result)] or "draw"
|
|
end
|
|
|
|
function LB.report(word)
|
|
if LB._reported or LB._spec then return false end
|
|
if not (LB._relay or LB.onRelay() or LB._arena) then return false end
|
|
LB._reported = true
|
|
local okC, Client = pcall(require, "src.online.Client")
|
|
if okC and type(Client) == "table" and type(Client.report) == "function" then
|
|
local ok, err = pcall(Client.report, word)
|
|
if not ok then note("report failed: %s", tostring(err)) end
|
|
end
|
|
note("report %s", tostring(word))
|
|
return true
|
|
end
|
|
|
|
local MIRROR = { [1] = { [2] = true, [128] = true }, [2] = { [1] = true }, [3] = { [3] = true },
|
|
[128] = { [1] = true } }
|
|
|
|
function LB.compareOutcome()
|
|
local mine = LB.outcome
|
|
if not mine then return end
|
|
for seat, row in pairs(LB._peerOutcomes) do
|
|
if not row.compared then
|
|
row.compared = true
|
|
local ok = MIRROR[mine] and MIRROR[mine][row.outcome]
|
|
local me = LB.relaySeat()
|
|
if LB.isMulti() and me and tonumber(seat) and tonumber(seat) % 2 == me % 2 then
|
|
ok = LB.recordOutcome(row.outcome) == LB.recordOutcome(mine)
|
|
end
|
|
if not ok then
|
|
note("outcome mismatch seat=%s mine=%s theirs=%s", tostring(seat), tostring(mine), tostring(row.outcome))
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:776 CB2_ReturnFromCableClubBattle
|
|
function LB.finish(result)
|
|
if not LB._started then return false end
|
|
LB._started = false
|
|
local L = link()
|
|
local s = session()
|
|
local outcome = LB.outcomeCode(result)
|
|
local recorded = LB.recordOutcome(outcome)
|
|
LB.outcome = outcome
|
|
local offField = LB._arena ~= nil or LB._spec ~= nil
|
|
if not offField then
|
|
local ctx, adapters = L.vmCtx()
|
|
-- pokefirered/src/load_save.c:170
|
|
L.callSpecial(ctx, adapters, 0x28)
|
|
-- pokefirered/src/load_save.c:239
|
|
L.savePlayerBag()
|
|
if s then s.battleOutcome = recorded end
|
|
-- pokefirered/src/battle_records.c:443
|
|
if LB.mode ~= L.USING.MULTI_BATTLE and not LB.unionRoom then
|
|
bumpTrainerCard(s, recorded)
|
|
LB.addOpponentRecord(s, LB.peer and LB.peer.name, LB.peer and LB.peer.trainerId, recorded)
|
|
-- pokefirered/src/cable_club.c:782
|
|
local okF, TFC = pcall(require, "src.core.game3.trainer_fan_club")
|
|
if okF and TFC and TFC.updateTrainerFansAfterLinkBattle then
|
|
TFC.updateTrainerFansAfterLinkBattle(s, ctx, recorded)
|
|
end
|
|
end
|
|
end
|
|
local lk = L.link
|
|
local live = lk and lk:isOpen() and not LB._spec
|
|
if live then
|
|
lk:send({ type = LB.MSG.OUTCOME, outcome = outcome, turn = LB._turn or 0 })
|
|
end
|
|
if (live or (LB._drained and LB.endReason == nil)) and not offField then
|
|
-- pokefirered/src/cable_club.c:806
|
|
local game = L.game()
|
|
if game and type(game.saveGame) == "function" then
|
|
local okSave, written = pcall(game.saveGame, game)
|
|
if not okSave or written == false then
|
|
print("[link] post-battle save failed: " .. tostring(written))
|
|
end
|
|
end
|
|
end
|
|
LB.report(REPORT_WORD[outcome] or "draw")
|
|
LB.pumpActions()
|
|
LB.compareOutcome()
|
|
LB.state = "done"
|
|
if LB.unionRoom then
|
|
-- pokefirered/src/cable_club.c:761 CB2_ReturnFromUnionRoomBattle
|
|
local U = union()
|
|
if U then U.state = "main" end
|
|
end
|
|
return true
|
|
end
|
|
|
|
function LB.peerDone()
|
|
if LB._peerOutcomes and next(LB._peerOutcomes) ~= nil then return true end
|
|
local st = battleState()
|
|
return (st and st.over) and true or false
|
|
end
|
|
|
|
function LB.streamComplete()
|
|
local sp = LB._spec
|
|
if not sp then return false end
|
|
for seat = 0, LB.isMulti() and 3 or 1 do
|
|
if sp.outcomes[seat] == nil then return false end
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:1002 Task_WaitForLinkPlayerConnection
|
|
function LB.peerDropped()
|
|
if not LB._started then return false end
|
|
if LB.streamComplete() then return LB.protocolError("stream") end
|
|
LB.endReason = LB.endReason or "peer_dropped"
|
|
local Battle = package.loaded["src.core.game3.battle"]
|
|
if Battle and Battle.isActive and Battle.isActive() then
|
|
Battle.abort("draw")
|
|
else
|
|
LB.finish("draw")
|
|
end
|
|
if not LB._spec then link().closeLink("peer_dropped") end
|
|
return true
|
|
end
|
|
|
|
function LB.update(dt)
|
|
if LB.state == "off" then return false end
|
|
local Guard = package.loaded["src.core.game3.battle.link_guard"]
|
|
if LB.state == "battle" and Guard and Guard.tripped then
|
|
local where = Guard.tripped
|
|
Guard.tripped = nil
|
|
LB.desync(LB._turn or 0, "rng:" .. tostring(where))
|
|
end
|
|
if LB.state == "arena_setup" then
|
|
LB.arenaStep()
|
|
return true
|
|
end
|
|
if LB.state == "spectate_setup" then
|
|
LB.spectatorStep()
|
|
return true
|
|
end
|
|
if LB._spec then
|
|
if LB.state == "battle" then LB.pumpActions() end
|
|
return LB.state ~= "off" and LB.state ~= "done"
|
|
end
|
|
local L = link()
|
|
local lk = L.link
|
|
if LB.state == "battle" then
|
|
LB.pumpActions()
|
|
if LB.peerForfeit then
|
|
local word = (LB.peerForfeit == "lose") and "lose" or "win"
|
|
LB.peerForfeit = nil
|
|
local Battle = package.loaded["src.core.game3.battle"]
|
|
if Battle and Battle.isActive and Battle.isActive() and Battle.linkEnd then
|
|
LB.endReason = "peer_forfeit"
|
|
Battle.linkEnd(word, nil, "peer_forfeit")
|
|
end
|
|
end
|
|
if not (lk and lk:isOpen()) then
|
|
if not LB.peerDone() then
|
|
LB.peerDropped()
|
|
return LB.state ~= "off"
|
|
end
|
|
LB._drained = true
|
|
end
|
|
end
|
|
if LB.state == "linkup" and lk and lk:isReady() then
|
|
LB.state = "seat"
|
|
end
|
|
return LB.state ~= "off" and LB.state ~= "done"
|
|
end
|
|
|
|
-- pokefirered/src/link.c:1069 GetLinkPlayerCount_2
|
|
function LB.playerCount()
|
|
local lk = link().link
|
|
if not (lk and lk:isOpen()) then return 1 end
|
|
if type(lk.players) == "function" then
|
|
local ok, list = pcall(lk.players, lk)
|
|
if ok and type(list) == "table" and #list > 0 then return #list end
|
|
end
|
|
return 2
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:222 CreateLinkupTask
|
|
function LB.createLinkupTask(ctx, adapters, spec)
|
|
local L = link()
|
|
local function report(code)
|
|
LB.linkup = code
|
|
L.setResult(ctx, code)
|
|
return code
|
|
end
|
|
report(L.LINKUP.ONGOING)
|
|
LB.state = "linkup"
|
|
LB.seed = nil
|
|
local announced = false
|
|
local lk = L.link
|
|
if lk then
|
|
lk.linkType = spec.linkType
|
|
-- pokefirered/src/cable_club.c:318 Task_LinkupExchangeDataWithLeader
|
|
lk:send({ type = LB.MSG.LINKUP, linkType = spec.linkType, players = spec.min })
|
|
announced = true
|
|
else
|
|
-- pokefirered/src/cable_club.c:222 CreateLinkupTask waits for the other machine
|
|
L.beginConnect({ linkType = spec.linkType })
|
|
end
|
|
local ticks = 0
|
|
local Natives = natives()
|
|
local yielded = Natives.yieldHost(ctx, adapters, function() end)
|
|
if not yielded then
|
|
LB.state = "off"
|
|
report(L.LINKUP.FAILED)
|
|
return false
|
|
end
|
|
ctx.nativePoll = function()
|
|
ticks = ticks + 1
|
|
local live = L.link
|
|
if live and not announced then
|
|
live.linkType = spec.linkType
|
|
live:send({ type = LB.MSG.LINKUP, linkType = spec.linkType, players = spec.min })
|
|
announced = true
|
|
end
|
|
if not announced then
|
|
-- pokefirered/src/cable_club.c:482 TryLinkTimeout
|
|
if ticks > LB.LINKUP_TICKS then
|
|
report(L.LINKUP.CONNECTION_ERROR)
|
|
LB.state = "off"
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
local peer = live and live:isReady() and live:take(LB.MSG.LINKUP) or nil
|
|
if peer then
|
|
local players = LB.playerCount()
|
|
if tonumber(peer.linkType) ~= spec.linkType then
|
|
-- pokefirered/src/cable_club.c:122 EXCHANGE_DIFF_SELECTIONS
|
|
report(L.LINKUP.DIFF_SELECTIONS)
|
|
LB.state = "off"
|
|
elseif players < spec.min or players > spec.max then
|
|
-- pokefirered/src/cable_club.c:127 EXCHANGE_WRONG_NUM_PLAYERS
|
|
report(L.LINKUP.WRONG_NUM_PLAYERS)
|
|
LB.state = "off"
|
|
else
|
|
report(L.LINKUP.SUCCESS)
|
|
LB.state = "seat"
|
|
end
|
|
return true
|
|
end
|
|
if not (live and live:isOpen()) then
|
|
-- pokefirered/src/cable_club.c:473 Task_LinkupConnectionError
|
|
report(L.LINKUP.CONNECTION_ERROR)
|
|
LB.state = "off"
|
|
return true
|
|
end
|
|
-- pokefirered/src/cable_club.c:482 TryLinkTimeout
|
|
if ticks > LB.LINKUP_TICKS then
|
|
report(L.LINKUP.CONNECTION_ERROR)
|
|
LB.state = "off"
|
|
return true
|
|
end
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:493 TryBattleLinkup
|
|
function LB.tryBattleLinkup(ctx, adapters)
|
|
local L = link()
|
|
local mode = L.getVar(ctx, L.VAR_0x8004)
|
|
LB.mode = mode
|
|
LB.unionRoom = false
|
|
return LB.createLinkupTask(ctx, adapters, LB.PLAYERS[mode] or LB.PLAYERS[1])
|
|
end
|
|
|
|
-- pokefirered/src/script_pokemon_util.c:90 HasEnoughMonsForDoubleBattle
|
|
function LB.hasEnoughMonsForDoubleBattle(ctx)
|
|
local Party = require("src.core.game3.party")
|
|
local state = Party.monsStateToDoubles(partyOf(session()))
|
|
link().setResult(ctx, state)
|
|
return false, state
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:964 EnterColosseumPlayerSpot
|
|
function LB.enterColosseumPlayerSpot(ctx, adapters)
|
|
local L = link()
|
|
LB.seat = L.getVar(ctx, LB.VAR_0x8005)
|
|
LB.mode = L.getVar(ctx, L.VAR_0x8004)
|
|
LB.unionRoom = false
|
|
local lk = L.link
|
|
if not (lk and lk:isOpen()) then
|
|
LB.state = "off"
|
|
return false
|
|
end
|
|
lk.linkType = LB.LINKTYPE.BATTLE
|
|
-- pokefirered/src/cable_club.c:838 SetInCableClubSeat
|
|
lk:send({ type = LB.MSG.SEAT, seat = LB.seat })
|
|
LB.state = "setup"
|
|
LB.freshBattle()
|
|
if LB.onRelay() then
|
|
LB.seed = LB.seedFromRelay()
|
|
else
|
|
LB.seed = (lk.role == "host") and LB.dealSeed() or nil
|
|
end
|
|
local mySetupSent = false
|
|
local peerSetup = nil
|
|
local seated = false
|
|
local multiSeats, multiSetups = {}, {}
|
|
local Natives = natives()
|
|
local yielded = Natives.yieldHost(ctx, adapters, function() end)
|
|
if not yielded then
|
|
LB.state = "off"
|
|
return false
|
|
end
|
|
ctx.nativePoll = function()
|
|
local live = L.link
|
|
if not (live and live:isOpen()) then
|
|
-- pokefirered/src/cable_club.c:859 CABLE_SEAT_FAILED
|
|
LB.state = "off"
|
|
return true
|
|
end
|
|
if LB.isMulti() then
|
|
local sm = live:take(LB.MSG.SEAT)
|
|
while sm do
|
|
multiSeats[tonumber(sm.seat) or 1] = true
|
|
sm = live:take(LB.MSG.SEAT)
|
|
end
|
|
local su = live:take(LB.MSG.SETUP)
|
|
while su do
|
|
local from = tonumber(su.seat) or 1
|
|
multiSetups[from] = multiSetups[from] or su
|
|
su = live:take(LB.MSG.SETUP)
|
|
end
|
|
if LB.seed and not mySetupSent then mySetupSent = sendSetup() end
|
|
local nSeats, nSetups = 0, 0
|
|
for _ in pairs(multiSeats) do nSeats = nSeats + 1 end
|
|
for _ in pairs(multiSetups) do nSetups = nSetups + 1 end
|
|
-- pokefirered/src/battle_main.c:1240
|
|
if not (mySetupSent and nSeats >= 3 and nSetups >= 3) then return false end
|
|
LB.beginMulti(multiSetups)
|
|
return true
|
|
end
|
|
if not seated and live:take(LB.MSG.SEAT) then seated = true end
|
|
peerSetup = peerSetup or live:take(LB.MSG.SETUP)
|
|
if peerSetup and LB.seed == nil and not LB.onRelay() then LB.seed = tonumber(peerSetup.seed) end
|
|
if LB.seed and not mySetupSent then mySetupSent = sendSetup() end
|
|
if not (seated and peerSetup and mySetupSent) then return false end
|
|
LB.beginBattle(peerSetup)
|
|
return true
|
|
end
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/union_room.c:1811 StartUnionRoomBattle
|
|
function LB.startUnionRoomBattle(onDone)
|
|
local L = link()
|
|
local lk = L.link
|
|
if not (lk and lk:isOpen()) then return false, "no_link" end
|
|
local ok, reason = LB.validateParty(nil, L.USING.SINGLE_BATTLE, { unionRoom = true })
|
|
if not ok then return false, reason end
|
|
LB.mode = L.USING.SINGLE_BATTLE
|
|
LB.unionRoom = true
|
|
LB.state = "setup"
|
|
LB.freshBattle()
|
|
if LB.onRelay() then
|
|
LB.seed = LB.seedFromRelay()
|
|
elseif lk.role == "host" then
|
|
LB.seed = LB.dealSeed()
|
|
end
|
|
local ctx, adapters = L.vmCtx()
|
|
-- pokefirered/src/union_room.c:1812 HealPlayerParty / SavePlayerParty / LoadPlayerBag
|
|
L.callSpecial(ctx, adapters, 0x00)
|
|
L.callSpecial(ctx, adapters, 0x27)
|
|
L.loadPlayerBag()
|
|
LB._onSetup = onDone or function() end
|
|
sendSetup()
|
|
return true
|
|
end
|
|
|
|
function LB.pumpUnionSetup()
|
|
if LB.state ~= "setup" or not LB.unionRoom then return false end
|
|
local lk = link().link
|
|
if not (lk and lk:isOpen()) then
|
|
LB.state = "off"
|
|
return false
|
|
end
|
|
local setup = lk:take(LB.MSG.SETUP)
|
|
if not setup then return false end
|
|
if LB.seed == nil and not LB.onRelay() then LB.seed = tonumber(setup.seed) end
|
|
if LB.seed == nil then return false end
|
|
local cb = LB._onSetup
|
|
LB._onSetup = nil
|
|
LB.beginBattle(setup, cb)
|
|
return true
|
|
end
|
|
|
|
-- pokefirered/src/cable_club.c:532 TryRecordMixLinkup
|
|
function LB.tryRecordMixLinkup(ctx, adapters)
|
|
LB.mode = link().USING.RECORD_CORNER
|
|
LB.unionRoom = false
|
|
return LB.createLinkupTask(ctx, adapters, LB.RECORD_MIX)
|
|
end
|
|
|
|
local STALE = { LB.MSG.ACTION, LB.MSG.SWITCH, LB.MSG.HASH, LB.MSG.OUTCOME, LB.MSG.FORFEIT }
|
|
|
|
function LB.freshBattle()
|
|
resetTurnState()
|
|
local lk = link().link
|
|
if lk and type(lk.take) == "function" then
|
|
for _, kind in ipairs(STALE) do
|
|
while lk:take(kind) do end
|
|
end
|
|
end
|
|
LB._lk = nil
|
|
LB._drained = nil
|
|
LB._myPacked = nil
|
|
LB._myParty = nil
|
|
LB._reported = false
|
|
LB._relay = nil
|
|
LB._turn = nil
|
|
LB.endReason = nil
|
|
LB.outcome = nil
|
|
LB.peerForfeit = nil
|
|
LB.multiNames = nil
|
|
end
|
|
|
|
local function arenaLinkType(mode)
|
|
if mode == LB.MODE_OF.double then return LB.LINKTYPE.DOUBLE_BATTLE end
|
|
if mode == LB.MODE_OF.multi then return LB.LINKTYPE.MULTI_BATTLE end
|
|
return LB.LINKTYPE.SINGLE_BATTLE
|
|
end
|
|
|
|
LB.arenaLinkType = arenaLinkType
|
|
|
|
-- pokefirered/src/cable_club.c:964
|
|
function LB.startArena(spec, onDone)
|
|
LB.reset()
|
|
LB.freshBattle()
|
|
LB._arena = spec
|
|
LB._arenaDone = onDone
|
|
LB.mode = LB.MODE_OF[spec and spec.mode or "single"] or LB.MODE_OF.single
|
|
LB.unionRoom = false
|
|
LB.seat = tonumber(spec and spec.seat) or 0
|
|
LB.seed = LB.seedFromRelay()
|
|
LB.headless = spec and spec.headless or nil
|
|
LB.autoFight = spec and spec.autoFight
|
|
LB.fade = false
|
|
if spec and spec.myParty then LB._myPacked = spec.myParty end
|
|
LB._arenaSetup = { sent = false, seats = {}, setups = {}, linkup = nil }
|
|
if LB.seed == nil then
|
|
LB._arena = nil
|
|
return LB.refuse("no_seed", onDone)
|
|
end
|
|
LB.state = "arena_setup"
|
|
LB.arenaStep()
|
|
return true
|
|
end
|
|
|
|
local function arenaFail(why)
|
|
local done = LB._arenaDone
|
|
LB._arenaDone = nil
|
|
LB.refuse(why, done)
|
|
end
|
|
|
|
function LB.arenaStep()
|
|
if LB.state ~= "arena_setup" then return false end
|
|
local s = LB._arenaSetup
|
|
local lk = link().link
|
|
if not (lk and lk:isOpen()) then
|
|
arenaFail((lk and lk.reason) or "link_closed")
|
|
return false
|
|
end
|
|
lk:update(0)
|
|
if not lk:isOpen() then
|
|
arenaFail(lk.reason or "link_closed")
|
|
return false
|
|
end
|
|
if not lk:isReady() then return false end
|
|
if not s.sent then
|
|
s.sent = true
|
|
local linkType = arenaLinkType(LB.mode)
|
|
lk.linkType = linkType
|
|
local seats = tonumber(LB._arena.seats) or 2
|
|
-- pokefirered/src/cable_club.c:318
|
|
lk:send({ type = LB.MSG.LINKUP, linkType = linkType, players = seats })
|
|
-- pokefirered/src/cable_club.c:838
|
|
lk:send({ type = LB.MSG.SEAT, seat = LB.seat })
|
|
local ok, sent = pcall(sendSetup)
|
|
if not ok or not sent then
|
|
arenaFail(ok and "setup_not_sent" or tostring(sent))
|
|
return false
|
|
end
|
|
end
|
|
local up = lk:take(LB.MSG.LINKUP)
|
|
while up do
|
|
if tonumber(up.linkType) ~= lk.linkType then
|
|
-- pokefirered/src/cable_club.c:122
|
|
arenaFail("diff_selections")
|
|
return false
|
|
end
|
|
s.linkup = true
|
|
up = lk:take(LB.MSG.LINKUP)
|
|
end
|
|
local seat = lk:take(LB.MSG.SEAT)
|
|
while seat do
|
|
s.seats[tonumber(seat.seat) or 1] = true
|
|
s.seated = true
|
|
seat = lk:take(LB.MSG.SEAT)
|
|
end
|
|
local setup = lk:take(LB.MSG.SETUP)
|
|
while setup do
|
|
s.peerSetup = s.peerSetup or setup
|
|
local from = tonumber(setup.seat) or 1
|
|
s.setups[from] = s.setups[from] or setup
|
|
setup = lk:take(LB.MSG.SETUP)
|
|
end
|
|
if LB.isMulti() then
|
|
local need = (tonumber(LB._arena.seats) or 4) - 1
|
|
local nSeats, nSetups = 0, 0
|
|
for _ in pairs(s.seats) do nSeats = nSeats + 1 end
|
|
for _ in pairs(s.setups) do nSetups = nSetups + 1 end
|
|
if nSeats < need or nSetups < need then return false end
|
|
local done = LB._arenaDone
|
|
LB._arenaDone = nil
|
|
LB.state = "setup"
|
|
LB.beginMulti(s.setups, function(result)
|
|
if done then done(result) end
|
|
end)
|
|
return true
|
|
end
|
|
if not (s.seated and s.peerSetup) then return false end
|
|
local done = LB._arenaDone
|
|
LB._arenaDone = nil
|
|
LB.state = "setup"
|
|
LB.beginBattle(s.peerSetup, function(result)
|
|
if done then done(result) end
|
|
end)
|
|
return true
|
|
end
|
|
|
|
function LB.startSpectator(spec, onDone)
|
|
LB.reset()
|
|
LB.freshBattle()
|
|
local okT, t = pcall(function()
|
|
if spec.transport then return spec.transport end
|
|
return require("src.core.game3.link.relay_transport").new(spec.session, { client = spec.client })
|
|
end)
|
|
if not okT or not t then
|
|
if onDone then onDone("error") end
|
|
return false, tostring(t)
|
|
end
|
|
LB._spec = {
|
|
spec = spec, t = t, done = onDone,
|
|
setups = {}, actions = { [0] = {}, [1] = {}, [2] = {}, [3] = {} },
|
|
switches = { [0] = {}, [1] = {}, [2] = {}, [3] = {} },
|
|
outcomes = {}, gone = false,
|
|
}
|
|
LB.mode = LB.MODE_OF[spec.mode or "single"] or LB.MODE_OF.single
|
|
LB.unionRoom = false
|
|
LB.seat = nil
|
|
LB.seed = LB.seedFromRelay()
|
|
LB.headless = spec.headless or nil
|
|
LB.fade = false
|
|
LB.state = "spectate_setup"
|
|
LB.spectatorStep()
|
|
return true
|
|
end
|
|
|
|
local function spectatorFinish(word)
|
|
local sp = LB._spec
|
|
if not sp or sp.finished then return end
|
|
sp.finished = true
|
|
local done = sp.done
|
|
sp.done = nil
|
|
if done then done(word) end
|
|
end
|
|
|
|
LB.spectatorFinish = spectatorFinish
|
|
|
|
function LB.pumpSpectator()
|
|
local sp = LB._spec
|
|
if not sp then return end
|
|
local t = sp.t
|
|
if t.update then pcall(t.update, t) end
|
|
local ok, msgs = pcall(t.poll, t)
|
|
if not ok or type(msgs) ~= "table" then
|
|
sp.gone = true
|
|
return
|
|
end
|
|
for _, msg in ipairs(msgs) do
|
|
local seat = tonumber(msg.seat)
|
|
local kind = msg.type
|
|
if seat and seat >= 0 and seat <= ((LB.isMulti() and 3) or 1) then
|
|
if kind == LB.MSG.SETUP then
|
|
sp.setups[seat] = sp.setups[seat] or msg
|
|
elseif kind == LB.MSG.ACTION then
|
|
local turn = math.floor(tonumber(msg.turn) or 0)
|
|
if turn > 0 then
|
|
sp.actions[seat][turn] = msg
|
|
sp.lastTurn = math.max(sp.lastTurn or 0, turn)
|
|
end
|
|
elseif kind == LB.MSG.SWITCH then
|
|
local q = sp.switches[seat]
|
|
q[#q + 1] = math.floor(tonumber(msg.slot) or 1)
|
|
elseif kind == LB.MSG.HASH then
|
|
noteHash(msg, seat)
|
|
elseif kind == LB.MSG.OUTCOME then
|
|
sp.outcomes[seat] = tonumber(msg.outcome)
|
|
elseif kind == "game3_bye" or kind == "bye" or kind == LB.MSG.FORFEIT then
|
|
sp.left = sp.left or {}
|
|
sp.left[seat] = true
|
|
end
|
|
end
|
|
end
|
|
if t.closed or t.error then sp.gone = true end
|
|
LB.checkHashes()
|
|
end
|
|
|
|
function LB.spectatorBehind()
|
|
local sp = LB._spec
|
|
if not sp or LB.state ~= "battle" then return false end
|
|
local st = battleState()
|
|
local turn = st and st.turn or 0
|
|
return (sp.lastTurn or 0) > turn + 1
|
|
end
|
|
|
|
function LB.spectatorStep()
|
|
if LB.state ~= "spectate_setup" then return false end
|
|
local sp = LB._spec
|
|
LB.pumpSpectator()
|
|
if LB.isMulti() then return LB.spectatorStepMulti() end
|
|
if sp.gone and not (sp.setups[0] and sp.setups[1]) then
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
if not (sp.setups[0] and sp.setups[1]) then return false end
|
|
if LB.seed == nil then
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
local mine, whyA = LB.unpackParty(sp.setups[0].party)
|
|
local theirs, whyB = LB.unpackParty(sp.setups[1].party)
|
|
if not (mine and theirs) then
|
|
note("spectator refused a party: %s", tostring(whyA or whyB))
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
local host, guest = sp.setups[0], sp.setups[1]
|
|
local foe = LB.foeFrom({ name = guest.name, party = theirs })
|
|
LB.peer = { name = guest.name, trainerId = tonumber(guest.trainerId) or 0, gender = tonumber(guest.gender) or 0 }
|
|
sp.watched = {
|
|
name = host.name, trainerId = tonumber(host.trainerId) or 0,
|
|
gender = (tonumber(host.gender) == 1) and 1 or 0, party = mine,
|
|
bag = {}, store = { flags = {}, vars = {} },
|
|
}
|
|
LB.state = "battle"
|
|
LB._started = true
|
|
local flags = LB.BATTLE_TYPE.TRAINER + LB.BATTLE_TYPE.LINK + LB.BATTLE_TYPE.IS_MASTER
|
|
if LB.mode == LB.MODE_OF.double then flags = flags + LB.BATTLE_TYPE.DOUBLE end
|
|
local BattleBridge = require("src.core.game3.battle_bridge")
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local ok, started, err = pcall(BattleBridge.start, rt and rt._mod, link().game(), foe, {
|
|
link = true,
|
|
spectate = true,
|
|
autoFight = false,
|
|
session = sp.watched,
|
|
linkParty = mine,
|
|
linkFlags = flags,
|
|
linkMaster = true,
|
|
double = LB.mode == LB.MODE_OF.double,
|
|
unionRoom = false,
|
|
rng = LB.makeRng(LB.seed, LB._draws),
|
|
peerName = guest.name,
|
|
trainerName = guest.name,
|
|
trainerPicId = LB.peerPicId(guest),
|
|
song = LB.battleSong(guest, host),
|
|
playerGender = sp.watched.gender,
|
|
headless = LB.headless,
|
|
fade = false,
|
|
done = function(result)
|
|
LB._started = false
|
|
LB.outcome = LB.outcomeCode(result)
|
|
LB.state = "done"
|
|
spectatorFinish(LB.endReason == "desync" and "error" or "ended")
|
|
end,
|
|
})
|
|
if not ok or not started then
|
|
note("spectator battle did not start: %s", tostring(ok and err or started))
|
|
LB.state = "off"
|
|
LB._started = false
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function LB.spectatorStepMulti()
|
|
local sp = LB._spec
|
|
local all = sp.setups[0] and sp.setups[1] and sp.setups[2] and sp.setups[3]
|
|
if sp.gone and not all then
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
if not all then return false end
|
|
if LB.seed == nil then
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
local parties, names, info = {}, {}, {}
|
|
for seat = 0, 3 do
|
|
local party, why = LB.unpackMultiParty(sp.setups[seat].party)
|
|
if not party then
|
|
note("spectator refused a party: %s", tostring(why))
|
|
LB.state = "off"
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
parties[seat] = party
|
|
info[seat] = setupInfo(sp.setups[seat])
|
|
names[seat] = info[seat].name
|
|
end
|
|
local genders = {}
|
|
for seat = 0, 3 do genders[seat] = info[seat].gender end
|
|
local layout, playerParty, foeParty = LB.multiLayout(nil, parties, names, genders)
|
|
local foe = LB.foeFrom({ name = names[1], party = foeParty })
|
|
LB.peer = info[1]
|
|
LB.multiNames = names
|
|
sp.watched = {
|
|
name = names[0], trainerId = info[0].trainerId, gender = info[0].gender, party = playerParty,
|
|
bag = {}, store = { flags = {}, vars = {} },
|
|
}
|
|
LB.state = "battle"
|
|
LB._started = true
|
|
local flags = LB.BATTLE_TYPE.TRAINER + LB.BATTLE_TYPE.LINK + LB.BATTLE_TYPE.IS_MASTER
|
|
+ LB.BATTLE_TYPE.DOUBLE + LB.BATTLE_TYPE.MULTI
|
|
local BattleBridge = require("src.core.game3.battle_bridge")
|
|
local rt = package.loaded["src.core.game3.runtime"]
|
|
local ok, started, err = pcall(BattleBridge.start, rt and rt._mod, link().game(), foe, {
|
|
link = true,
|
|
spectate = true,
|
|
autoFight = false,
|
|
session = sp.watched,
|
|
linkParty = playerParty,
|
|
linkFlags = flags,
|
|
linkMaster = true,
|
|
double = true,
|
|
multi = layout,
|
|
unionRoom = false,
|
|
rng = LB.makeRng(LB.seed, LB._draws),
|
|
peerName = names[1],
|
|
trainerName = names[1],
|
|
trainerPicId = LB.peerPicId(info[1]),
|
|
song = LB.battleSong(info[0], info[0]),
|
|
playerGender = sp.watched.gender,
|
|
headless = LB.headless,
|
|
fade = false,
|
|
done = function(result)
|
|
LB._started = false
|
|
LB.outcome = LB.outcomeCode(result)
|
|
LB.state = "done"
|
|
spectatorFinish(LB.endReason == "desync" and "error" or "ended")
|
|
end,
|
|
})
|
|
if not ok or not started then
|
|
note("spectator battle did not start: %s", tostring(ok and err or started))
|
|
LB.state = "off"
|
|
LB._started = false
|
|
spectatorFinish("error")
|
|
return false
|
|
end
|
|
return true
|
|
end
|
|
|
|
function LB.reset()
|
|
LB.state = "off"
|
|
LB.mode = nil
|
|
LB.seed = nil
|
|
LB.seat = nil
|
|
LB.linkup = nil
|
|
LB.peer = nil
|
|
LB.unionRoom = false
|
|
LB.outcome = nil
|
|
LB.headless = nil
|
|
LB.autoFight = nil
|
|
LB.fade = nil
|
|
LB._started = false
|
|
LB._onSetup = nil
|
|
LB._arena = nil
|
|
LB._arenaDone = nil
|
|
LB._arenaSetup = nil
|
|
LB._spec = nil
|
|
LB.freshBattle()
|
|
end
|
|
|
|
return LB
|