FIELD GOAL

This commit is contained in:
bryanthaboi
2026-09-27 16:58:27 -04:00
parent 690f0cd9a1
commit 3620e787d2
20 changed files with 1823 additions and 48 deletions
+35 -12
View File
@@ -133,7 +133,10 @@ local function seq_push(text, wait, id)
-- pokefirered/src/battle_controller_pokedude.c:209 HandlePokedudeVoiceoverEtc
Pokedude.event(st, "printstring", st.pdActor, id)
end
Ui.pushTimed(tostring(text or ""), tonumber(wait) or (AnimSeq.isMoveUsedId(id) and 0 or 64))
text = tostring(text or "")
-- pokeemerald/src/battle_message.c:3005
if text:sub(-2) == "\\p" and not (st and (st.link or st.pokedude or (st.kinds and st.kinds.recordedLink))) then return Ui.push(text) end
Ui.pushTimed(text, tonumber(wait) or (AnimSeq.isMoveUsedId(id) and 0 or 64))
end
local function push_msgs(list)
@@ -1653,21 +1656,39 @@ local function handle_player_faint(opts)
})
end
local function wild_victory_song(st, awards)
if not st or not st.wild or st.pokedude or st.link or st._wildVictorySong then return end
if not (awards and #awards > 0) then return end
local lead = State.battler and State.battler(st, 0) or st.player
if not (lead and lead.mon and (tonumber(lead.mon.hp) or 0) > 0) then return end
st._wildVictorySong = true
stop_low_hp_song()
local Audio = require("src.core.game3.audio")
local bp = BattleProfile.of(st)
if bp.music then
-- pokeemerald/src/battle_script_commands.c:3363
Audio.playSong(BattleProfile.victorySong(bp, Battle.songInfo(st)))
else
-- pokefirered/src/battle_script_commands.c:3219
local id = Audio.role("victoryWild")
if id then Audio.playSong(id) end
end
end
local function begin_trainer_win(st)
Battle._pendingEnd = "win"
local Audio = require("src.core.game3.audio")
local role, fallback
if st and st.wild then
role, fallback = "victoryWild", 311
else
role, fallback = Trainers.getVictoryMusicRole(st and st.trainerId)
end
local bp = BattleProfile.of(st)
if bp.music then
-- pokeemerald/src/battle_main.c:4983
Audio.playSong(BattleProfile.victorySong(bp, Battle.songInfo(st)))
-- pokefirered/src/battle_script_commands.c:3216
elseif not st.pokedude then Audio.playSong(Audio.role(role) or fallback) end
-- pokeemerald/src/battle_main.c:5011
if st and not st.wild then
if bp.music then
-- pokeemerald/src/battle_main.c:4983
Audio.playSong(BattleProfile.victorySong(bp, Battle.songInfo(st)))
elseif not st.pokedude then
local role, fallback = Trainers.getVictoryMusicRole(st.trainerId)
Audio.playSong(Audio.role(role) or fallback)
end
end
local pname = st.playerName
local twoTrainers = st.trainerB ~= nil
@@ -1872,6 +1893,7 @@ local function handle_enemy_faint(opts)
end
end
wild_victory_song(st, awards)
local hooks = choice_hooks()
if st.pokedude then
for _, entry in ipairs(awards) do
@@ -3104,6 +3126,7 @@ function D.faintStep()
and Experience.awardFoe(st, foe, { trainer = not st.wild }) or {}
-- pokefirered/src/battle_util.c:1181
State.opponentSwitchInResetSentPokes(st, foe)
wild_victory_song(st, awards)
if D.presentAwards(awards) then return end
end
end
+48 -20
View File
@@ -26,6 +26,11 @@ local MODULES = {
oldMan = "src.core.game3.rse.old_man",
dewfordTrend = "src.core.game3.rse.dewford_trend",
lilycoveLady = "src.core.game3.rse.lilycove_lady",
daycareMail = "src.core.game3.rse.daycare_mail_mix",
tower = "src.core.game3.rse.frontier.tower",
gift = "src.core.game3.rse.record_mixing_gift",
apprentice = "src.core.game3.rse.frontier.apprentice",
rankingHall = "src.core.game3.rse.frontier.ranking_hall",
}
local function system(name)
@@ -74,29 +79,37 @@ function RecordMix.fromWire(v)
end
-- pokeemerald/src/record_mixing.c:220 PrepareExchangePacket
function RecordMix.packet(session)
function RecordMix.packet(session, multiplayerId)
session = sessionOf(session)
local sess = type(session) == "table" and session or {}
local MixUtil = require("src.core.game3.rse.record_mix_util")
local out = {
version = type(session) == "table" and session.version or Family.activeVersion(),
trainerId = tonumber(type(session) == "table" and (session.trainerId or session.id)) or 0,
name = type(session) == "table" and (session.name or session.playerName) or nil,
version = sess.version or Family.activeVersion(),
trainerId = tonumber(sess.trainerId or sess.id) or 0,
linkTrainerId = MixUtil.sessionLinkTrainerId(sess),
language = MixUtil.GAME_LANGUAGE,
name = sess.name or sess.playerName,
}
local tvData = call(system("tv"), "mixExport", session)
out.secretBases = call(system("secretBase"), "mixExport", session)
local Tv = system("tv")
pcall(function() Tv.deactivateAllNormalShows(session) end)
local tvData = call(Tv, "mixExport", session)
if type(tvData) == "table" then
out.tvShows, out.pokeNews = tvData.tvShows, tvData.pokeNews
end
out.secretBases = call(system("secretBase"), "mixExport", session)
out.oldMan = call(system("oldMan"), "mixExport", session)
out.dewfordTrends = call(system("dewfordTrend"), "mixExport", session)
local lady = system("lilycoveLady")
out.lilycoveLady = lady and deep(call(lady, "state", session)) or nil
out.lilycoveLady = call(lady, "mixExport", session)
if out.lilycoveLady == nil then out.lilycoveLady = deep(call(lady, "state", session)) end
out.dewfordTrends = call(system("dewfordTrend"), "mixExport", session)
out.daycareMail = call(system("daycareMail"), "mixExport", session)
out.battleTowerRecord = call(system("tower"), "mixExport", session)
out.giftItem = call(system("gift"), "mixExport", session, tonumber(multiplayerId) or 0) or 0
out.apprentices = call(system("apprentice"), "mixExport", session)
out.hallRecords = call(system("rankingHall"), "mixExport", session)
return deep(out)
end
local function tvModule()
return system("tv")
end
-- pokeemerald/src/record_mixing.c:253 ReceiveExchangePacket
function RecordMix.receive(session, packets, myIndex, logger)
session = sessionOf(session)
@@ -104,12 +117,12 @@ function RecordMix.receive(session, packets, myIndex, logger)
for i, p in ipairs(packets or {}) do players[i] = type(p) == "table" and deep(p) or {} end
myIndex = tonumber(myIndex) or 1
local applied = {}
local sb = system("secretBase")
local randSum = call(system("daycareMail"), "randSum", players[1] and players[1].tvShows) or 0
local bases = {}
for i, p in ipairs(players) do bases[i] = p.secretBases or {} end
local _, okS = call(sb, "mixImport", session, bases)
local _, okS = call(system("secretBase"), "mixImport", session, bases, myIndex)
applied.secretBases = okS
local Tv = tvModule()
local Tv = system("tv")
local tvPlayers = {}
for i, p in ipairs(players) do
tvPlayers[i] = { tvShows = p.tvShows or {}, pokeNews = p.pokeNews or {}, trainerId = p.trainerId }
@@ -118,14 +131,20 @@ function RecordMix.receive(session, packets, myIndex, logger)
applied.tvShows = okTv
local _, okNews = call(Tv, "receivePokeNews", session, tvPlayers, myIndex)
applied.pokeNews = okNews
local oldMan = system("oldMan")
local _, okOld = call(oldMan, "mixImport", players, session, myIndex)
local _, okOld = call(system("oldMan"), "mixImport", players, session, myIndex)
applied.oldMan = okOld
if not okOld then rse().missing("oldMan", "ReceiveOldManData", logger) end
local trends = {}
for i, p in ipairs(players) do trends[i] = p.dewfordTrends or {} end
local _, okDew = call(system("dewfordTrend"), "mixImport", trends, session)
applied.dewfordTrends = okDew
local _, okMail = call(system("daycareMail"), "mixImport", players, session, myIndex, randSum)
applied.daycareMail = okMail
local _, okTower = call(system("tower"), "mixImport", players, session, myIndex)
applied.battleTower = okTower
local gift, okGift = call(system("gift"), "mixImport", players, session, myIndex)
applied.giftItem = okGift
applied.gift = gift
local lady = system("lilycoveLady")
local _, okLady = call(lady, "mixImport", players, session, myIndex)
if not okLady then
@@ -134,7 +153,10 @@ function RecordMix.receive(session, packets, myIndex, logger)
rse().missing("lilycoveLady", "ReceiveLilycoveLadyData", logger)
end
applied.lilycoveLady = okLady
rse().missing("recordMixing", "ReceiveDaycareMailData/ReceiveBattleTowerData/ReceiveGiftItem/ReceiveApprenticeData/ReceiveRankingHallRecords", logger)
local _, okApp = call(system("apprentice"), "mixImport", players, session, myIndex)
applied.apprentices = okApp
local _, okHall = call(system("rankingHall"), "mixImport", players, session, myIndex)
applied.hallRecords = okHall
RecordMix.last = { players = #players, myIndex = myIndex, applied = applied }
return applied
end
@@ -178,7 +200,7 @@ function RecordMix.playerSpotTriggered(ctx, adapters)
local spot = L.getVar(ctx, LB.VAR_0x8005)
-- pokeemerald/src/record_mixing.c:321
varSet(ctx, session, "VAR_TEMP_MIXED_RECORDS", 1)
local mine = RecordMix.packet(session)
local mine = RecordMix.packet(session, spot)
mine.spot = spot
lk:send({ type = RecordMix.MSG.PACKET, spot = spot, packet = RecordMix.toWire(mine) })
RecordMix.state = "mixing"
@@ -217,7 +239,13 @@ function RecordMix.playerSpotTriggered(ctx, adapters)
packets[i] = row.packet
if row.mine then myIndex = i end
end
RecordMix.receive(session, packets, myIndex, adapters and adapters.log)
local applied = RecordMix.receive(session, packets, myIndex, adapters and adapters.log)
local gift = applied and applied.gift
if type(gift) == "table" and (tonumber(gift.item) or 0) ~= 0 then
-- pokeemerald/src/record_mixing.c:976
if adapters and adapters.setStringVar then adapters.setStringVar(1, tostring(gift.from or "")) end
if ctx.stringVars then ctx.stringVars[1] = tostring(gift.from or "") end
end
-- pokeemerald/src/record_mixing.c:333
flagSet(session, "FLAG_SYS_MIX_RECORD")
message("gText_RecordMixingComplete")
+1
View File
@@ -34,5 +34,6 @@ return {
{ name = "frontier", module = "src.core.game3.rse.frontier.util" },
{ name = "trainerHill", module = "src.core.game3.rse.trainer_hill" },
{ name = "apprentice", module = "src.core.game3.rse.frontier.apprentice" },
{ name = "recordMixingGift", module = "src.core.game3.rse.record_mixing_gift" },
},
}
+162
View File
@@ -0,0 +1,162 @@
local Rse = require("src.core.game3.rse.init")
local MixUtil = require("src.core.game3.rse.record_mix_util")
local DaycareMail = {}
-- pokeemerald/include/constants/global.h:34
DaycareMail.DAYCARE_MON_COUNT = 2
-- pokeemerald/src/record_mixing.c:41
DaycareMail.NUM_SWAP_COMBOS = 3
-- pokeemerald/src/record_mixing.c:152
local SWAP_3P = { { 0, 1 }, { 1, 2 }, { 2, 0 } }
-- pokeemerald/src/record_mixing.c:159
local SWAP_4P = { { 0, 1, 2, 3 }, { 0, 2, 1, 3 }, { 0, 3, 2, 1 } }
local function sessionOf(sess)
return sess or Rse.session()
end
local function daycare()
return require("src.core.game3.daycare")
end
local function blankMail()
return { message = { itemId = 0 }, otName = "", monName = "", gameLanguage = 0, monLanguage = 0 }
end
local function itemOf(mail)
return tonumber(type(mail) == "table" and type(mail.message) == "table" and mail.message.itemId) or 0
end
local function heldItem(mon)
return tonumber(mon and (mon.heldItem or mon.item)) or 0
end
-- pokeemerald/src/record_mixing.c:1366
function DaycareMail.mixExport(sess)
sess = sessionOf(sess)
local Daycare = daycare()
local dc = Daycare.stateOf(sess)
local out = { mail = {}, numDaycareMons = 0, cantHoldItem = {} }
local stored = dc and dc.mail or {}
-- pokeemerald/src/daycare.c:122
for i = 1, DaycareMail.DAYCARE_MON_COUNT do
local m = stored[i]
out.mail[i] = type(m) == "table" and MixUtil.deep(m) or blankMail()
if type(out.mail[i].message) ~= "table" then out.mail[i].message = { itemId = 0 } end
local mon = dc and Daycare.mon(dc, i)
if mon and Daycare.speciesOf(mon) ~= 0 then
out.numDaycareMons = out.numDaycareMons + 1
out.cantHoldItem[i] = heldItem(mon) ~= 0
else
out.cantHoldItem[i] = true
end
end
return out
end
-- pokeemerald/src/record_mixing.c:743
function DaycareMail.randSum(tvShows)
local sum = 0
if type(tvShows) == "table" then
for i = 0, 7 do
local show = tvShows[i]
if type(show) == "table" then
sum = sum + MixUtil.num(show.kind) + ((show.active == true or show.active == 1) and 1 or 0)
end
end
end
return sum % 256
end
local function recordOf(players, i)
local p = players[i + 1]
local r = type(p) == "table" and p.daycareMail or nil
if type(r) ~= "table" then
r = { mail = { blankMail(), blankMail() }, numDaycareMons = 0, cantHoldItem = { true, true } }
if type(p) == "table" then p.daycareMail = r end
end
r.mail = r.mail or {}
for j = 1, DaycareMail.DAYCARE_MON_COUNT do
if type(r.mail[j]) ~= "table" then r.mail[j] = blankMail() end
end
r.cantHoldItem = r.cantHoldItem or {}
return r
end
-- pokeemerald/src/record_mixing.c:724
local function swap(players, idxs, s1, s2)
local a, b = idxs[s1 + 1], idxs[s2 + 1]
local ra, rb = recordOf(players, a[1]), recordOf(players, b[1])
local tmp = ra.mail[a[2] + 1]
ra.mail[a[2] + 1] = rb.mail[b[2] + 1]
rb.mail[b[2] + 1] = tmp
end
-- pokeemerald/src/record_mixing.c:760
function DaycareMail.mixImport(players, sess, myIndex, randSum)
sess = sessionOf(sess)
players = players or {}
local Rng = require("src.core.game3.rng")
local oldSeed = Rng.Random2()
Rng.SeedRng2(MixUtil.linkTrainerId(players[1]) % 0x10000)
local count = #players
local canHold = {}
for i = 0, count - 1 do
canHold[i] = { false, false }
local r = recordOf(players, i)
local n = MixUtil.num(r.numDaycareMons)
for j = 1, math.min(n, DaycareMail.DAYCARE_MON_COUNT) do
local cant = r.cantHoldItem[j]
if not (cant == true or cant == 1) then canHold[i][j] = true end
end
end
local idxs, numCanHold = {}, 0
for i = 0, count - 1 do
local r = recordOf(players, i)
local c0, c1 = canHold[i][1], canHold[i][2]
if c0 or c1 then numCanHold = numCanHold + 1 end
if c0 and not c1 then
idxs[#idxs + 1] = { i, 0 }
elseif c1 and not c0 then
idxs[#idxs + 1] = { i, 1 }
elseif c0 and c1 then
local item1, item2 = itemOf(r.mail[1]), itemOf(r.mail[2])
local slot
if (item1 == 0 and item2 == 0) or (item1 ~= 0 and item2 ~= 0) then
slot = Rng.Random2() % 2
elseif item1 ~= 0 then
slot = 0
else
slot = 1
end
idxs[#idxs + 1] = { i, slot }
end
end
local tableId = (tonumber(randSum) or 0) % DaycareMail.NUM_SWAP_COMBOS
if numCanHold == 2 then
swap(players, idxs, 0, 1)
elseif numCanHold == 3 then
swap(players, idxs, SWAP_3P[tableId + 1][1], SWAP_3P[tableId + 1][2])
elseif numCanHold == 4 then
local row = SWAP_4P[tableId + 1]
swap(players, idxs, row[1], row[2])
swap(players, idxs, row[3], row[4])
end
local mine = recordOf(players, (tonumber(myIndex) or 1) - 1)
local dc = daycare().stateOf(sess)
if dc then
dc.mail = dc.mail or {}
for j = 1, DaycareMail.DAYCARE_MON_COUNT do
local m = mine.mail[j]
dc.mail[j] = itemOf(m) ~= 0 and MixUtil.deep(m) or nil
end
end
Rng.SeedRng(oldSeed)
return true
end
Rse.register("daycareMail", DaycareMail)
return DaycareMail
@@ -655,6 +655,84 @@ local function easyChatType()
end
easyChatType()
local function mixUtil()
return require("src.core.game3.rse.record_mix_util")
end
-- pokeemerald/src/record_mixing.c:1062
function Apprentice.mixExport(sess)
sess = sess or Rse.session()
local MixUtil = mixUtil()
local src = Apprentice.saved(sess)
local saveId = tonumber(Apprentice.player(sess).saveId) or 0
local dst = { Apprentice.copy(src[1]), Apprentice.newSaved() }
dst[2].playerName = ""
local mine = MixUtil.sessionLinkTrainerId(sess)
local slots = Apprentice.COUNT - 1
local oldId, numOld, mixId, numMix = 0, 0, 0, 0
for i = 0, 1 do
local id = (i + saveId) % slots + 1
if (src[id + 1].playerName or "") ~= "" then
local tid = MixUtil.getTrainerId(src[id + 1].playerId)
if tid ~= mine then numMix, mixId = numMix + 1, id end
if tid == mine then numOld, oldId = numOld + 1, id end
end
end
if numMix == 0 and numOld ~= 0 then numMix, mixId = numOld, oldId end
if numMix == 1 then
dst[2] = Apprentice.copy(src[mixId + 1])
elseif numMix == 2 then
if rng().Random2() > 0x3333 then
dst[2] = Apprentice.copy(src[saveId + 1 + 1])
else
dst[2] = Apprentice.copy(src[((saveId + 1) % slots + 1) + 1])
end
end
return dst
end
-- pokeemerald/src/record_mixing.c:1155
local function alreadySaved(a, saved)
local MixUtil = mixUtil()
for i = 1, Apprentice.COUNT do
if MixUtil.getTrainerId(a.playerId) == MixUtil.getTrainerId(saved[i].playerId)
and (tonumber(a.number) or 0) == (tonumber(saved[i].number) or 0) then
return true
end
end
return false
end
-- pokeemerald/src/record_mixing.c:1169
function Apprentice.mixImport(players, sess, myIndex)
sess = sess or Rse.session()
local partner = mixUtil().partner(players or {}, tonumber(myIndex) or 1)
local mix = type(partner) == "table" and partner.apprentices or nil
if type(mix) ~= "table" then return false end
local saved = Apprentice.saved(sess)
local p = Apprentice.player(sess)
local slots = Apprentice.COUNT - 1
local num, which = 0, 0
for i = 0, 1 do
local a = mix[i + 1]
if type(a) == "table" and (a.playerName or "") ~= "" and not alreadySaved(a, saved) then
num, which = num + 1, i
end
end
local saveId = tonumber(p.saveId) or 0
if num == 1 then
saved[saveId + 1 + 1] = Apprentice.copy(mix[which + 1])
p.saveId = (saveId + 1) % slots
elseif num == 2 then
for i = 0, 1 do
local idx = (bit.bxor(i, 1) + saveId) % slots + 1
saved[idx + 1] = Apprentice.copy(mix[i + 1])
end
p.saveId = (saveId + 2) % slots
end
return true
end
Rse.register("apprentice", Apprentice)
return Apprentice
@@ -0,0 +1,174 @@
local Rse = require("src.core.game3.rse.init")
local D = require("src.core.game3.rse.frontier.trainers")
local Util = require("src.core.game3.rse.frontier.util")
local MixUtil = require("src.core.game3.rse.record_mix_util")
local Hall = {}
local RH = Util.RANKING_HALL
local M = D.MODE
local COUNT = Util.HALL_RECORDS_COUNT
local function lvlModes()
return D.LVL_MODE_COUNT
end
local function new1P()
return { id = { 0, 0, 0, 0 }, name = "", language = 0, winStreak = 0 }
end
local function new2P()
return { id1 = { 0, 0, 0, 0 }, id2 = { 0, 0, 0, 0 }, name1 = "", name2 = "", language = 0, winStreak = 0 }
end
function Hall.saved(sess)
sess = sess or Rse.session()
if type(sess.hallRecords1P) ~= "table" or type(sess.hallRecords2P) ~= "table" then
Util.clearRankingHallRecords(sess)
end
for i = 1, Util.HALL_FACILITIES_COUNT do
sess.hallRecords1P[i] = sess.hallRecords1P[i] or {}
for j = 1, lvlModes() do
sess.hallRecords1P[i][j] = sess.hallRecords1P[i][j] or {}
for k = 1, COUNT do sess.hallRecords1P[i][j][k] = sess.hallRecords1P[i][j][k] or new1P() end
end
end
for j = 1, lvlModes() do
sess.hallRecords2P[j] = sess.hallRecords2P[j] or {}
for k = 1, COUNT do sess.hallRecords2P[j][k] = sess.hallRecords2P[j][k] or new2P() end
end
return sess.hallRecords1P, sess.hallRecords2P
end
local function streak(r)
return tonumber(type(r) == "table" and r.winStreak) or 0
end
-- pokeemerald/src/record_mixing.c:1116
function Hall.mixExport(sess)
sess = sess or Rse.session()
local f = Util.frontier(sess)
local id = MixUtil.trainerIdBytes(sess)
local name = tostring(sess.name or sess.playerName or "")
local out = { onePlayer = {}, twoPlayers = {} }
for i = 1, Util.HALL_FACILITIES_COUNT do
out.onePlayer[i] = {}
for j = 1, lvlModes() do
out.onePlayer[i][j] = { id = MixUtil.deep(id), language = MixUtil.GAME_LANGUAGE, name = name, winStreak = 0 }
end
end
for j = 1, lvlModes() do
out.twoPlayers[j] = {
language = MixUtil.GAME_LANGUAGE, id1 = MixUtil.deep(id),
id2 = MixUtil.deep(f.opponentTrainerIds and f.opponentTrainerIds[j] or { 0, 0, 0, 0 }),
name1 = name, name2 = tostring(f.opponentNames and f.opponentNames[j] or ""), winStreak = 0,
}
end
for lvl = 0, lvlModes() - 1 do
local one = function(hall) return out.onePlayer[hall + 1][lvl + 1] end
one(RH.TOWER_SINGLES).winStreak = Util.get2(f.towerRecordWinStreaks, M.SINGLES, lvl)
one(RH.TOWER_DOUBLES).winStreak = Util.get2(f.towerRecordWinStreaks, M.DOUBLES, lvl)
one(RH.TOWER_MULTIS).winStreak = Util.get2(f.towerRecordWinStreaks, M.MULTIS, lvl)
one(RH.DOME).winStreak = Util.get2(f.domeRecordWinStreaks, M.SINGLES, lvl)
one(RH.PALACE).winStreak = Util.get2(f.palaceRecordWinStreaks, M.SINGLES, lvl)
one(RH.ARENA).winStreak = Util.get1(f.arenaRecordStreaks, lvl)
one(RH.FACTORY).winStreak = Util.get2(f.factoryRecordWinStreaks, M.SINGLES, lvl)
one(RH.PIKE).winStreak = Util.get1(f.pikeRecordStreaks, lvl)
one(RH.PYRAMID).winStreak = Util.get1(f.pyramidRecordStreaks, lvl)
out.twoPlayers[lvl + 1].winStreak = Util.get2(f.towerRecordWinStreaks, M.LINK_MULTIS, lvl)
end
return out
end
local function tid(bytes)
return MixUtil.getTrainerId(bytes)
end
-- pokeemerald/src/record_mixing.c:1207
local function getNewHallRecords(players, myIndex, sess)
local saved1, saved2 = Hall.saved(sess)
local partners = {}
for i = 1, #players do
if i ~= myIndex then
local h = players[i] and players[i].hallRecords
partners[#partners + 1] = type(h) == "table" and h or {}
end
if #partners == COUNT then break end
end
local n = #players - 1
local mix1, mix2 = {}, {}
for i = 1, Util.HALL_FACILITIES_COUNT do
mix1[i] = {}
for j = 1, lvlModes() do
local dst = {}
for k = 1, COUNT * 2 do dst[k] = new1P() end
for k = 1, COUNT do dst[k] = MixUtil.deep(saved1[i][j][k]) end
for k = 1, n do
local rec = partners[k] and partners[k].onePlayer and partners[k].onePlayer[i] and partners[k].onePlayer[i][j]
rec = type(rec) == "table" and rec or new1P()
local repeats = 0
for l = 1, COUNT do
if tid(dst[l].id) == tid(rec.id) then
repeats = repeats + 1
if streak(dst[l]) < streak(rec) then dst[l] = MixUtil.deep(rec) end
end
end
if repeats == 0 then dst[k + COUNT] = MixUtil.deep(rec) end
end
mix1[i][j] = dst
end
end
for j = 1, lvlModes() do
local dst = {}
for k = 1, COUNT * 2 do dst[k] = new2P() end
for k = 1, COUNT do dst[k] = MixUtil.deep(saved2[j][k]) end
for k = 1, n do
local rec = partners[k] and partners[k].twoPlayers and partners[k].twoPlayers[j]
rec = type(rec) == "table" and rec or new2P()
local repeats = 0
for l = 1, COUNT do
if tid(dst[l].id1) == tid(rec.id1) and tid(dst[l].id2) == tid(rec.id2) then
repeats = repeats + 1
if streak(dst[l]) < streak(rec) then dst[l] = MixUtil.deep(rec) end
end
end
if repeats == 0 then dst[k + COUNT] = MixUtil.deep(rec) end
end
mix2[j] = dst
end
return mix1, mix2
end
-- pokeemerald/src/record_mixing.c:1286
local function fillWinStreakRecords(playerRecords, mixRecords)
for i = 1, COUNT do
local highest, highestId = 0, nil
for j = 1, COUNT * 2 do
if streak(mixRecords[j]) > highest then
highestId, highest = j, streak(mixRecords[j])
end
end
if highestId then
playerRecords[i] = MixUtil.deep(mixRecords[highestId])
mixRecords[highestId].winStreak = 0
end
end
end
-- pokeemerald/src/record_mixing.c:1355
function Hall.mixImport(players, sess, myIndex)
sess = sess or Rse.session()
players = players or {}
local mix1, mix2 = getNewHallRecords(players, tonumber(myIndex) or 1, sess)
-- pokeemerald/src/record_mixing.c:1342
local saved1, saved2 = Hall.saved(sess)
for i = 1, Util.HALL_FACILITIES_COUNT do
for j = 1, lvlModes() do fillWinStreakRecords(saved1[i][j], mix1[i][j]) end
end
for j = 1, lvlModes() do fillWinStreakRecords(saved2[j], mix2[j]) end
return true
end
Rse.register("rankingHall", Hall)
return Hall
+90
View File
@@ -643,6 +643,96 @@ function Tower.doTowerBattle(ctx, adapters, sess)
})
end
-- pokeemerald/include/constants/global.h:97
local PLAYER_NAME_LENGTH = 7
local function nameChar(s, i)
return tostring(s or ""):sub(i + 1, i + 1)
end
local function idByte(rec, i)
return tonumber(type(rec.trainerId) == "table" and rec.trainerId[i + 1]) or 0
end
-- pokeemerald/src/battle_tower.c:1311
function Tower.putNewRecord(sess, newRecord)
local f = Util.frontier(sess)
local records = f.towerRecords
local count = Util.BATTLE_TOWER_RECORD_COUNT
for i = 1, count do
if type(records[i]) ~= "table" then records[i] = {} end
end
local function streak(i) return tonumber(records[i + 1].winStreak) or 0 end
local i = 0
while i < count do
local rec, k, j = records[i + 1], 0, 0
while j < 4 and idByte(rec, j) == idByte(newRecord, j) do j = j + 1 end
if j == 4 then
while k < PLAYER_NAME_LENGTH do
if nameChar(rec.name, j) ~= nameChar(newRecord.name, j) then break end
if nameChar(newRecord.name, j) == "" then
k = PLAYER_NAME_LENGTH
break
end
k = k + 1
end
end
if k == PLAYER_NAME_LENGTH then break end
i = i + 1
end
if i < count then
records[i + 1] = Util.deepCopy(newRecord)
return i
end
for s = 0, count - 1 do
if streak(s) == 0 then
records[s + 1] = Util.deepCopy(newRecord)
return s
end
end
local slotValues, slotIds = { streak(0) }, { 0 }
for s = 1, count - 1 do
local j = 1
while j <= #slotValues do
if streak(s) < slotValues[j] then
slotValues, slotIds = { streak(s) }, { s }
j = 1
break
elseif streak(s) > slotValues[j] then
break
end
j = j + 1
end
if j == #slotValues + 1 then
slotValues[#slotValues + 1] = streak(s)
slotIds[#slotIds + 1] = s
end
end
local pick = slotIds[(D.rng().Random() % #slotIds) + 1]
records[pick + 1] = Util.deepCopy(newRecord)
return pick
end
-- pokeemerald/src/record_mixing.c:1396
function Tower.mixExport(sess)
local rec = Util.deepCopy(Util.frontier(sess).towerPlayer or {})
return rec
end
-- pokeemerald/src/record_mixing.c:650
function Tower.mixImport(players, sess, myIndex)
sess = sess or Rse.session()
local MixUtil = require("src.core.game3.rse.record_mix_util")
local partner = MixUtil.partner(players or {}, tonumber(myIndex) or 1)
local rec = type(partner) == "table" and partner.battleTowerRecord or nil
if type(rec) ~= "table" then return false end
rec = Util.deepCopy(rec)
local me = players[tonumber(myIndex) or 1]
if type(me) == "table" then me.battleTowerRecord = rec end
Tower.putNewRecord(sess, rec)
return true
end
Rse.register("tower", Tower)
return Tower
+36
View File
@@ -339,6 +339,42 @@ function Lady.contestLadyTvData(sess)
}
end
function Lady.mixExport(sess)
local MixUtil = require("src.core.game3.rse.record_mix_util")
return MixUtil.deep(Lady.state(sess))
end
-- pokeemerald/src/lilycove_lady.c:577
local function quizClearQuestionForRecordMix(prev, sess)
local l = Lady.state(sess)
local count = #data().quizQuestions
local prevId = tonumber(prev.quiz and prev.quiz.prevQuestionId) or count
if prevId < count and l.id == Lady.QUIZ then
local q = l.quiz
for _ = 1, 4 do
if prevId ~= q.questionId then break end
q.questionId = Town.random() % count
end
if prevId == q.questionId then q.questionId = (q.questionId + 1) % count end
q.prevQuestionId = prevId
end
end
-- pokeemerald/src/record_mixing.c:682
function Lady.mixImport(players, sess, myIndex)
sess = Town.session(sess)
local MixUtil = require("src.core.game3.rse.record_mix_util")
local partner = MixUtil.partner(players or {}, tonumber(myIndex) or 1)
local src = type(partner) == "table" and partner.lilycoveLady or nil
if type(src) ~= "table" or src.id == nil then return false end
local mine = Lady.state(sess)
local prev = mine.id == Lady.QUIZ and MixUtil.deep(mine) or nil
sess.lilycoveLady = MixUtil.deep(src)
Lady.resetForRecordMix(sess)
if prev then quizClearQuestionForRecordMix(prev, sess) end
return true
end
local SaveSections = require("src.core.game3.save_sections")
-- pokeemerald/src/new_game.c:199
SaveSections.register("lilycoveLady", SaveSections.fields({ "lilycoveLady" }, function(sess) Lady.init(sess) end))
+25
View File
@@ -361,6 +361,31 @@ function OldMan.mixExport(sess)
return deep(OldMan.state(sess))
end
-- pokeemerald/src/mauville_old_man.c:858
local function sanitizeReceived(m, language)
if m.id ~= OldMan.STORYTELLER or language ~= 1 then return end
for i = 1, OldMan.NUM_STORYTELLER_TALES do
if (tonumber(m.gameStatIDs and m.gameStatIDs[i]) or 0) ~= 0 then
m.language = m.language or {}
m.language[i] = Town.GAME_LANGUAGE
end
end
end
-- pokeemerald/src/record_mixing.c:629
function OldMan.mixImport(players, sess, myIndex)
sess = Town.session(sess)
local MixUtil = require("src.core.game3.rse.record_mix_util")
local partner = MixUtil.partner(players or {}, tonumber(myIndex) or 1)
local m = type(partner) == "table" and partner.oldMan or nil
if type(m) ~= "table" or m.id == nil then return false end
m = MixUtil.deep(m)
sanitizeReceived(m, tonumber(partner.language) or Town.GAME_LANGUAGE)
sess.oldMan = m
OldMan.resetFlag(sess)
return true
end
local SaveSections = require("src.core.game3.save_sections")
-- pokeemerald/src/new_game.c:191
SaveSections.register("oldMan", SaveSections.fields({ "oldMan" }, function(sess) OldMan.set(sess) end))
+77
View File
@@ -0,0 +1,77 @@
local MixUtil = {}
-- pokeemerald/src/record_mixing.c:130
local ORDERS = {
[2] = { { 1, 0 } },
[3] = { { 1, 2, 0 }, { 2, 0, 1 } },
[4] = {
{ 1, 0, 3, 2 }, { 3, 0, 1, 2 }, { 2, 0, 3, 1 },
{ 1, 3, 0, 2 }, { 2, 3, 0, 1 }, { 3, 2, 0, 1 },
{ 1, 2, 3, 0 }, { 2, 3, 1, 0 }, { 3, 2, 1, 0 },
},
}
MixUtil.ORDERS = ORDERS
-- pokeemerald/include/constants/global.h:20
MixUtil.LANGUAGE_JAPANESE = 1
MixUtil.GAME_LANGUAGE = 2
function MixUtil.deep(v)
if type(v) ~= "table" then return v end
local out = {}
for k, x in pairs(v) do out[k] = MixUtil.deep(x) end
return out
end
function MixUtil.num(v)
return tonumber(v) or 0
end
-- pokeemerald/src/link.c:326
function MixUtil.linkTrainerId(p)
if type(p) ~= "table" then return 0 end
local full = tonumber(p.linkTrainerId)
if full then return full end
return (tonumber(p.trainerId) or 0) % 0x10000
end
function MixUtil.sessionLinkTrainerId(sess)
if type(sess) ~= "table" then return 0 end
local tid = math.floor(tonumber(sess.trainerId or sess.playerId) or 0) % 0x10000
local sid = math.floor(tonumber(sess.secretId) or 0) % 0x10000
return tid + sid * 0x10000
end
function MixUtil.trainerIdBytes(sess)
local full = MixUtil.sessionLinkTrainerId(sess)
return { full % 256, math.floor(full / 256) % 256, math.floor(full / 65536) % 256, math.floor(full / 16777216) % 256 }
end
-- pokeemerald/src/new_game.c:72
function MixUtil.getTrainerId(bytes)
if type(bytes) ~= "table" then return tonumber(bytes) or 0 end
return MixUtil.num(bytes[1]) + MixUtil.num(bytes[2]) * 0x100 + MixUtil.num(bytes[3]) * 0x10000
+ MixUtil.num(bytes[4]) * 0x1000000
end
-- pokeemerald/src/record_mixing.c:604
function MixUtil.shuffle(players)
local count = #players
local out = {}
local rows = ORDERS[count]
if not rows then
for i = 1, count do out[i] = i end
return out
end
local row = rows[(MixUtil.linkTrainerId(players[1]) % #rows) + 1]
for i = 1, count do out[i] = row[i] + 1 end
return out
end
function MixUtil.partner(players, myIndex)
local idx = MixUtil.shuffle(players)
local from = idx[myIndex] or myIndex
return players[from], from
end
return MixUtil
+116
View File
@@ -0,0 +1,116 @@
local Rse = require("src.core.game3.rse.init")
local Gift = {}
local function sessionOf(sess)
return sess or Rse.session()
end
local function blank()
return { unk0 = 0, quantity = 0, itemId = 0, checksum = 0 }
end
function Gift.state(sess)
sess = sessionOf(sess)
if type(sess.recordMixingGift) ~= "table" then sess.recordMixingGift = blank() end
return sess.recordMixingGift
end
-- pokeemerald/src/mystery_event_script.c:103
function Gift.checksum(g)
local item = tonumber(g.itemId) or 0
return (tonumber(g.unk0) or 0) % 256 + (tonumber(g.quantity) or 0) % 256 + item % 256 + math.floor(item / 256) % 256
end
-- pokeemerald/src/mystery_event_script.c:115
local function isValid(g)
local sum = Gift.checksum(g)
return (tonumber(g.unk0) or 0) ~= 0 and (tonumber(g.quantity) or 0) ~= 0 and (tonumber(g.itemId) or 0) ~= 0
and sum ~= 0 and sum == (tonumber(g.checksum) or 0)
end
-- pokeemerald/src/mystery_event_script.c:130
function Gift.clear(sess)
sess = sessionOf(sess)
sess.recordMixingGift = blank()
end
-- pokeemerald/src/mystery_event_script.c:135
function Gift.set(unk, quantity, itemId, sess)
sess = sessionOf(sess)
if (tonumber(unk) or 0) == 0 or (tonumber(quantity) or 0) == 0 or (tonumber(itemId) or 0) == 0 then
Gift.clear(sess)
return
end
local g = Gift.state(sess)
g.unk0, g.quantity, g.itemId = tonumber(unk), tonumber(quantity), tonumber(itemId)
g.checksum = Gift.checksum(g)
end
-- pokeemerald/src/mystery_event_script.c:150
function Gift.take(sess)
sess = sessionOf(sess)
local g = Gift.state(sess)
if not isValid(g) then
Gift.clear(sess)
return 0
end
local itemId = tonumber(g.itemId) or 0
g.quantity = g.quantity - 1
if g.quantity == 0 then Gift.clear(sess) else g.checksum = Gift.checksum(g) end
return itemId
end
-- pokeemerald/src/record_mixing.c:246
function Gift.mixExport(sess, multiplayerId)
if (tonumber(multiplayerId) or 0) ~= 0 then return 0 end
return Gift.take(sess)
end
local function constant(sess, kind, name)
local Constants = require("src.core.game3.constants")
local ok, v = pcall(function() return Constants.of(Constants.versionOf(sess)):require(kind, name) end)
return ok and v or nil
end
local function pcHas(sess, item)
local okS, Storage = pcall(require, "src.core.game3.storage")
local storage = okS and Storage.ensure(sess) or nil
for _, entry in ipairs(storage and storage.items or {}) do
if (tostring(entry.id) == tostring(item) or tonumber(entry.id) == item) and (tonumber(entry.qty) or 0) >= 1 then
return true
end
end
return false
end
-- pokeemerald/src/record_mixing.c:969
function Gift.mixImport(players, sess, myIndex)
sess = sessionOf(sess)
players = players or {}
local multiplayerId = (tonumber(myIndex) or 1) - 1
local p0 = players[1] or {}
local item = tonumber(p0.giftItem) or 0
local ItemsData = require("src.core.game3.items_data")
if multiplayerId == 0 or item == 0 or ItemsData.pocketOf(item) ~= "KEY_ITEMS" then return nil end
local Bag = require("src.core.game3.bag")
sess.bag = sess.bag or Bag.new()
local got = not Bag.has(sess.bag, item, 1) and not pcHas(sess, item) and Bag.add(sess.bag, item, 1)
if got then
pcall(Rse.setVar, "VAR_TEMP_RECORD_MIX_GIFT_ITEM", item, sess)
if item == constant(sess, "items", "ITEM_EON_TICKET") then
pcall(Rse.setFlag, "FLAG_ENABLE_SHIP_SOUTHERN_ISLAND", true, sess)
end
return { item = item, from = p0.name or "" }
end
pcall(Rse.setVar, "VAR_TEMP_RECORD_MIX_GIFT_ITEM", 0, sess)
return { item = 0 }
end
local SaveSections = require("src.core.game3.save_sections")
-- pokeemerald/include/global.h:1070
SaveSections.register("recordMixingGift", SaveSections.fields({ "recordMixingGift" }, function(sess) Gift.clear(sess) end))
Rse.register("recordMixingGift", Gift)
return Gift
+238 -3
View File
@@ -1181,14 +1181,249 @@ function SB.setUpFieldMove(ctx)
return { ok = true, action = "secret_power", secretPowerScript = label }
end
function SB.mixExport(sess)
return {}
local function copyBase(b)
local out = {}
for k, v in pairs(b) do
if type(v) == "table" then
local t = {}
for k2, v2 in pairs(v) do
if type(v2) == "table" then
local u = {}
for k3, v3 in pairs(v2) do u[k3] = v3 end
t[k2] = u
else
t[k2] = v2
end
end
out[k] = t
else
out[k] = v
end
end
return out
end
function SB.mixImport(sess, records)
-- pokeemerald/src/record_mixing.c:222
function SB.mixExport(sess)
sess = session(sess)
pcall(SB.setPlayerParty, sess)
local out = {}
for i, b in ipairs(SB.bases(sess)) do out[i] = copyBase(b) end
return out
end
local function on(v)
return v == true or v == 1
end
local function nameChars(s)
return tostring(s or ""):sub(1, SB.PLAYER_NAME_LENGTH)
end
local function sameTrainerId(a, b)
for i = 1, SB.TRAINER_ID_LENGTH do
if (tonumber(a.trainerId[i]) or 0) ~= (tonumber(b.trainerId[i]) or 0) then return false end
end
return true
end
-- pokeemerald/src/secret_base.c:1383
local function belongToSamePlayer(a, b)
return (a.gender or 0) == (b.gender or 0) and sameTrainerId(a, b) and nameChars(a.trainerName) == nameChars(b.trainerName)
end
-- pokeemerald/src/secret_base.c:1519
local function belongsToPlayer(b, sess)
if (b.secretBaseId or 0) == 0 then return false end
if b.gender ~= playerGender(sess) then return false end
if not sameTrainerId(b, { trainerId = trainerIdBytes(sess) }) then return false end
return nameChars(b.trainerName) == nameChars(sess.name or sess.playerName)
end
-- pokeemerald/src/secret_base.c:1395
local function indexFromId(saved, id)
for i = 0, SB.COUNT - 1 do
if saved[i + 1].secretBaseId == id then return i end
end
return -1
end
-- pokeemerald/src/secret_base.c:1336
local function saveBase(saved, idx, b)
saved[idx + 1] = normalize(copyBase(b))
saved[idx + 1].registryStatus = SB.NEW
end
-- pokeemerald/src/secret_base.c:1432
local function trySaveFriendsBase(saved, b)
if (b.secretBaseId or 0) == 0 then return 0 end
local index = indexFromId(saved, b.secretBaseId)
if index == 0 then return 0 end
if index ~= -1 then
if on(saved[index + 1].toRegister) then return 0 end
if saved[index + 1].registryStatus ~= SB.NEW or on(b.toRegister) then
saveBase(saved, index, b)
return index
end
return 0
end
for i = 1, SB.COUNT - 1 do
if saved[i + 1].secretBaseId == 0 then
saveBase(saved, i, b)
return i
end
end
for i = 1, SB.COUNT - 1 do
if saved[i + 1].registryStatus == SB.UNREGISTERED and not on(saved[i + 1].toRegister) then
saveBase(saved, i, b)
return i
end
end
return 0
end
-- pokeemerald/src/secret_base.c:1486
local function sortByRegistryStatus(saved)
for i = 1, SB.COUNT - 2 do
for j = i + 1, SB.COUNT - 1 do
local a, b = saved[i + 1], saved[j + 1]
if (a.registryStatus == SB.UNREGISTERED and b.registryStatus == SB.REGISTERED)
or (a.registryStatus == SB.NEW and b.registryStatus ~= SB.NEW) then
saved[i + 1], saved[j + 1] = b, a
end
end
end
end
-- pokeemerald/src/secret_base.c:1509
local function trySaveFriendsBases(saved, mixer, status)
for i = 1, SB.COUNT - 1 do
if mixer[i + 1].registryStatus == status then trySaveFriendsBase(saved, mixer[i + 1]) end
end
end
-- pokeemerald/src/secret_base.c:1549
local function deleteFirstOldBaseFromPlayer(mixers, sess)
local done = { false, false, false }
for i = 1, SB.COUNT do
for m = 1, 3 do
if not done[m] and belongsToPlayer(mixers[m][i], sess) then
clearBase(mixers[m][i])
done[m] = true
end
end
if done[1] and done[2] and done[3] then break end
end
end
-- pokeemerald/src/secret_base.c:1595
local function clearDuplicateOwned(b, list, idx)
for i = 1, SB.COUNT do
local other = list[i]
if other.secretBaseId ~= 0 and belongToSamePlayer(b, other) then
if idx == 0 then
clearBase(other)
return false
end
if (b.numSecretBasesReceived or 0) > (other.numSecretBasesReceived or 0) then
clearBase(other)
return false
end
other.toRegister = b.toRegister
clearBase(b)
return true
end
end
return false
end
-- pokeemerald/src/secret_base.c:1627
local function clearDuplicateOwnedBases(saved, a, b, c)
for i = 1, SB.COUNT - 1 do
local mine = saved[i + 1]
if mine.secretBaseId ~= 0 then
if mine.registryStatus == SB.REGISTERED then mine.toRegister = 1 end
if not clearDuplicateOwned(mine, a, i) then
if not clearDuplicateOwned(mine, b, i) then clearDuplicateOwned(mine, c, i) end
end
end
end
for i = 0, SB.COUNT - 1 do
if a[i + 1].secretBaseId ~= 0 then
a[i + 1].battledOwnerToday = 0
if not clearDuplicateOwned(a[i + 1], b, i) then clearDuplicateOwned(a[i + 1], c, i) end
end
end
for i = 0, SB.COUNT - 1 do
if b[i + 1].secretBaseId ~= 0 then
b[i + 1].battledOwnerToday = 0
clearDuplicateOwned(b[i + 1], c, i)
end
if c[i + 1].secretBaseId ~= 0 then c[i + 1].battledOwnerToday = 0 end
end
end
-- pokeemerald/src/secret_base.c:1696
local function saveRecordMixBases(saved, mixers, sess)
deleteFirstOldBaseFromPlayer(mixers, sess)
clearDuplicateOwnedBases(saved, mixers[1], mixers[2], mixers[3])
-- pokeemerald/src/secret_base.c:1684
for i = 1, SB.COUNT do
for m = 1, 3 do
local b = mixers[m][i]
if on(b.toRegister) then
trySaveFriendsBase(saved, b)
clearBase(b)
end
end
end
for m = 1, 3 do trySaveFriendsBase(saved, mixers[m][1]) end
for m = 1, 3 do trySaveFriendsBases(saved, mixers[m], SB.REGISTERED) end
for m = 1, 3 do trySaveFriendsBases(saved, mixers[m], SB.UNREGISTERED) end
end
local function mixerList(list)
local out = {}
for i = 1, SB.COUNT do
local b = type(list) == "table" and list[i] or nil
out[i] = normalize(type(b) == "table" and copyBase(b) or nil)
out[i].secretBaseId = tonumber(out[i].secretBaseId) or 0
out[i].registryStatus = tonumber(out[i].registryStatus) or 0
end
return out
end
-- pokeemerald/src/secret_base.c:1731
function SB.mixImport(sess, records, myIndex)
sess = session(sess)
local okF, received = pcall(flag, "FLAG_RECEIVED_SECRET_POWER", sess)
if not (okF and received) then return true end
records = records or {}
local count = #records
local slots = {}
for i = 0, 3 do slots[i] = i < count and records[i + 1] or nil end
local linkIdx = (tonumber(myIndex) or 1) - 1
local mixers = {}
for k = 1, 3 do mixers[k] = mixerList(slots[(linkIdx + k) % 4]) end
local saved = SB.bases(sess)
saveRecordMixBases(saved, mixers, sess)
for i = 1, SB.COUNT - 1 do
if on(saved[i + 1].toRegister) then
saved[i + 1].registryStatus = SB.REGISTERED
saved[i + 1].toRegister = 0
end
end
sortByRegistryStatus(saved)
for i = 1, SB.COUNT - 1 do
if saved[i + 1].registryStatus == SB.NEW then saved[i + 1].registryStatus = SB.UNREGISTERED end
end
local mine = saved[1]
if mine.secretBaseId ~= 0 and (mine.numSecretBasesReceived or 0) ~= 0xFFFF then
mine.numSecretBasesReceived = (mine.numSecretBasesReceived or 0) + 1
end
return true
end
function SB.reset()
SB._curId = 0
SB._inFriendBase = false
+24 -10
View File
@@ -315,10 +315,28 @@ local function gendered_ph(name, ctx)
return by.male
end
-- pokeemerald/src/string_util.c:428
local function live(field, ctx)
local v = ctx and ctx[field]
if v ~= nil then return v end
local provider = TextIR._provider
if not provider then return nil end
return provider(field, TextIR.dialect(ctx and ctx.dialect), ctx)
end
local function player_name(ctx)
return live("playerName", ctx) or "PLAYER"
end
local function string_var(n, ctx)
local sv = live("stringVars", ctx)
return (type(sv) == "table" and sv[n]) or ""
end
local function rival_name(ctx)
local v = gendered_ph("RIVAL", ctx)
if v ~= nil then return v end
return (ctx and ctx.rivalName) or "RIVAL"
return live("rivalName", ctx) or "RIVAL"
end
local function expand_seg(seg, ctx)
@@ -329,12 +347,11 @@ local function expand_seg(seg, ctx)
local dyn = ctx and ctx.dynamic
return (dyn and dyn[seg.n]) or ""
elseif t == "player" then
return (ctx and ctx.playerName) or "PLAYER"
return player_name(ctx)
elseif t == "rival" then
return rival_name(ctx)
elseif t == "strvar" then
local sv = ctx and ctx.stringVars
return (sv and sv[seg.n]) or ""
return string_var(seg.n, ctx)
elseif t == "tag" then
return seg.tag
elseif t == "bph" then
@@ -350,19 +367,16 @@ local function expand_seg(seg, ctx)
local code = tonumber(seg.code)
local name = seg.name
if code == 0x01 or name == "PLAYER" then
return (ctx and ctx.playerName) or "PLAYER"
return player_name(ctx)
end
if code == 0x06 or name == "RIVAL" then
return rival_name(ctx)
end
if code and code >= 0x02 and code <= 0x04 then
local sv = ctx and ctx.stringVars
return (sv and sv[code - 1]) or ""
return string_var(code - 1, ctx)
end
if name == "STR_VAR_1" or name == "STR_VAR_2" or name == "STR_VAR_3" then
local n = tonumber(name:sub(-1)) or 1
local sv = ctx and ctx.stringVars
return (sv and sv[n]) or ""
return string_var(tonumber(name:sub(-1)) or 1, ctx)
end
if name and (name == "FONT_MALE" or name == "FONT_FEMALE" or name == "FONT_NORMAL"
or name:find("^COLOR") or name:find("^SHADOW") or name:find("^HIGHLIGHT") or name:find("^BG")) then
+24 -2
View File
@@ -222,6 +222,28 @@ function Trainers.foeFromId(trainerId)
}
end
local DIALOG_TEXT_KEYS = {
intro = "introTextKey", defeat = "defeatTextKey",
victory = "victoryTextKey", notEnough = "notEnoughTextKey",
}
local function live_dialogs(t)
local d = t.dialogs or {}
local Sp = package.loaded["src.core.game3.scripting.space"]
local vm = Sp and Sp.vm
if not (vm and vm.getText) then return d end
local out
for field, keyName in pairs(DIALOG_TEXT_KEYS) do
local key = t[keyName]
local ir = key and vm:getText(key)
if type(ir) == "table" then
out = out or setmetatable({}, { __index = d })
out[field] = ir
end
end
return out or d
end
--- ROM-derived trainer presentation info (class / name / pic / partySize / dialogs).
-- opts.rivalName replaces the placeholder "TERRY" of the rival and champion
-- classes when provided.
@@ -247,7 +269,7 @@ function Trainers.info(trainerId, opts)
ai = t.ai,
items = t.items,
party = t.party,
dialogs = t.dialogs,
dialogs = live_dialogs(t),
}
-- pokefirered/src/battle_message.c:2078 names these classes by the player's
@@ -263,7 +285,7 @@ end
--- Get dialog texts table: { intro, defeat, victory, notEnough }
function Trainers.dialogs(trainerId)
local t = Trainers.get(trainerId)
return t and t.dialogs or {}
return t and live_dialogs(t) or {}
end
--- FRLG intro string pieces for a trainer battle.
+20
View File
@@ -531,6 +531,26 @@ section({ name = "trainerHill", fields = {}, template = { "trainerHill", "traine
section({ name = "apprentice", fields = {}, template = { "playerApprentice", "apprentices" } })
section({ name = "lilycoveLady", fields = {}, template = { "lilycoveLady" } })
section({ name = "oldMan", fields = {}, template = { "oldMan" } })
-- pokeemerald/include/global.h:746
section({
name = "recordMixingGift",
fields = { "recordMixingGift" },
read = function(x)
local b = x.L.RSE.sb1.recordMixingGift
return { recordMixingGift = { checksum = u32(x.sb1, b), unk0 = u8(x.sb1, b + 4), quantity = u8(x.sb1, b + 5),
itemId = u16(x.sb1, b + 6) } }
end,
write = function(x, v)
local b = x.L.RSE.sb1.recordMixingGift
local g = type(v.recordMixingGift) == "table" and v.recordMixingGift or {}
x.w1:fill(b, 16, 0)
x.w1:w32(b, num(g.checksum) % U32)
x.w1:w8(b + 4, num(g.unk0) % 256)
x.w1:w8(b + 5, num(g.quantity) % 256)
x.w1:w16(b + 6, num(g.itemId) % 65536)
end,
})
section({ name = "lottery", fields = {}, vars = true })
local function context(codec, blocks, w1, w2)
+15
View File
@@ -41,6 +41,21 @@ TextIR.setContextProvider(function(kind, dialect, ctx)
local s = liveSession()
return s and s.gender or nil
end
if kind == "playerName" then
local s = liveSession()
local name = s and (s.name or s.playerName)
return (type(name) == "string" and name ~= "") and name or nil
end
if kind == "rivalName" or kind == "stringVars" then
local Sp = package.loaded["src.core.game3.scripting.space"]
local vm = Sp and Sp.vm
if not vm then return nil end
if kind == "stringVars" then return vm.ctx and vm.ctx.stringVars end
local a = vm.adapters
local r = a and a.rivalName
if type(r) == "function" then r = r() end
return r or (vm.ctx and vm.ctx.rivalName)
end
if kind ~= "placeholders" or not dialect.placeholders then return nil end
local GameVersion = require("src.core.GameVersion")
local s = liveSession()
+15 -1
View File
@@ -919,7 +919,18 @@ BEATS[#BEATS + 1] = { id = "champion", run = function(game)
local track = labelWatch(labels)
local rivalShot, birchShot = false, false
local ratingText
local settle = { limit = 120000, onBattleFrame = battleShot(game, "26_wallace_battle"),
local wallaceShot, kudos = battleShot(game, "26_wallace_battle"), nil
local function onBattleFrame(st, phase)
wallaceShot(st, phase)
local M = require("src.ui.game3.message")
if not kudos and M.isOpen() and messageText():find("Kudos to you", 1, true) then
if M.isTyping() then M.skipReveal() end
kudos = messageText()
d.note("wallace defeat: " .. kudos)
d.shot(game, "26b_wallace_kudos")
end
end
local settle = { limit = 120000, onBattleFrame = onBattleFrame,
watch = function()
mug()
track()
@@ -950,6 +961,9 @@ BEATS[#BEATS + 1] = { id = "champion", run = function(game)
d.check(d.ran("EverGrandeCity_ChampionsRoom_EventScript_EnterRoom"), "the player walks up to Wallace on entry")
d.check(seen.key == "champion", "Wallace's mugshot battle transition (" .. tostring(seen.key) .. ")")
d.check(lastBattleWon(tid("TRAINER_WALLACE")), "Champion Wallace battle won through the battle UI")
local pname = S.session().name or ""
d.check(kudos ~= nil and pname ~= "" and kudos:find("Kudos to you, " .. pname .. "!", 1, true) ~= nil,
"Wallace's defeat line names the player (" .. tostring(kudos) .. ")")
d.check(labels.EverGrandeCity_ChampionsRoom_EventScript_MayAdvice and rivalShot, "May runs in after the battle")
local dexCaught = 0
for _ in pairs(S.session().dex.caught or {}) do dexCaught = dexCaught + 1 end
+188
View File
@@ -0,0 +1,188 @@
local U = require("tests.drivers.util")
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/game3_wild_exp_flow"
return function(game)
io.stdout:setvbuf("line")
local fails = 0
local function result(ok, label)
print((ok and "PASS " or "FAIL ") .. label)
if not ok then fails = fails + 1 end
return ok
end
local function finish()
print(string.format("[driver] game3_wild_exp_flow: %d failure(s)", fails))
love.event.quit(fails == 0 and 0 or 1)
U.wait(10)
end
for _ = 1, 900 do
if game.phase == "boot" and game.boot then break end
U.wait(1)
end
game:_handleBootAction({ action = "new_game", name = "BRENDAN", gender = 0 })
U.wait(240)
local Runtime = require("src.core.game3.runtime")
local Party = require("src.core.game3.party")
local BattleBridge = require("src.core.game3.battle_bridge")
local Battle = require("src.core.game3.battle")
local Ui = require("src.core.game3.battle.ui")
local Message = require("src.ui.game3.message")
local StatGrowth = require("src.ui.game3.stat_growth")
local Anim = require("src.core.game3.battle.anim")
local Audio = require("src.core.game3.audio")
local Map = require("src.core.game3.map")
local Encounters = require("src.core.game3.encounters")
local Pokemon = require("src.core.game3.pokemon")
local Experience = require("src.core.game3.battle.experience")
local Constants = require("src.core.game3.constants")
local session = Runtime.getSession()
if not result(session ~= nil, "field session exists") then return finish() end
local C = Constants.of(Constants.versionOf(session))
print("[driver] version " .. tostring(C.game))
Encounters.onStep = function() return nil end
if C.game == "emerald" then
Map.load(nil, game, "EM_ROUTE101", { x = 7, y = 13, facing = "down" })
else
Map.load(nil, game, "FR_ROUTE1", { x = 10, y = 20, facing = "down" })
end
U.wait(40)
local MUDKIP = C.species.byName.SPECIES_MUDKIP
local learnLv, learnMove
for lv = 6, 40 do
local mv = Pokemon.movesLearnedAt(MUDKIP, lv)
if mv and #mv > 0 then learnLv, learnMove = lv, mv[1] break end
end
if not result(learnLv ~= nil, "Mudkip learnset has a move between LV6 and LV40 (" .. tostring(learnLv) .. ")") then
return finish()
end
session.party = {}
Party.giveMon(session, MUDKIP, learnLv - 1, "")
local mon = session.party[1]
local mv = function(n) return C.moves.byName["MOVE_" .. n] end
mon.moves = {}
for _, n in ipairs({ "TACKLE", "GROWL", "PROTECT", "WATER_GUN", "SCRATCH" }) do
if mv(n) ~= learnMove and #mon.moves < 4 then mon.moves[#mon.moves + 1] = mv(n) end
end
mon.pp = { 35, 40, 10, 10 }
mon.maxPp = { 35, 40, 10, 10 }
mon.exp = Experience.expForLevel(mon, learnLv) - 1
print(string.format("[driver] Mudkip LV%d exp %d, learns %s at LV%d", mon.level, mon.exp,
tostring(C:name("moves", learnMove, "MOVE_")), learnLv))
local VS_WILD = C.songs.byName.MUS_VS_WILD
local VICTORY_WILD = C.songs.byName.MUS_VICTORY_WILD
local outcome
local ok = BattleBridge.startWild(Runtime._mod, game,
{ species = C.species.byName.SPECIES_ZIGZAGOON, level = 5 }, { done = function(r) outcome = r end })
if not result(ok == true, "wild Zigzagoon battle started") then return finish() end
for _ = 1, 3000 do
if Battle.isActive() then break end
U.wait(1)
end
if not result(Battle.isActive(), "battle active") then return finish() end
local f = 0
local ev = {}
local function mark(name)
if not ev[name] then
ev[name] = f
print(string.format("[driver] f=%d %s song=%s", f, name, tostring(Audio._currentSong and Audio._currentSong.id)))
end
end
local songAt = {}
local waitF = 0
local statShot, expShot, learnShot = false, false, false
local gainedOpenFrames, gainedPrinted = 0, false
local endedWhileGained = false
local foeLowered = false
while (Battle.isActive() or outcome == nil) and f < 30000 do
f = f + 1
local song = Audio._currentSong and Audio._currentSong.id
local page = Message.isOpen() and Message.currentPage() or ""
local phase = Battle._phase
if song == VICTORY_WILD then mark("victory_song") end
if page:find("fainted") then
mark("fainted_open")
songAt.fainted = songAt.fainted or song
if Message.isWaiting() then mark("fainted_waiting") end
end
if page:find("EXP%. Points") then
mark("gained_open")
songAt.gained = songAt.gained or song
gainedOpenFrames = gainedOpenFrames + 1
if Message.isWaiting() then
gainedPrinted = true
mark("gained_printed")
if not expShot then
expShot = U.still(game, DIR .. "/01_gained_exp.png")
end
end
if phase == "ending" or phase == "fade_out" then endedWhileGained = true end
elseif ev.gained_open and not ev.gained_closed then
mark("gained_closed")
end
if page:find("grew to") then mark("grew_open") end
if page:find("trying to") or page:find("learn") then mark("learn_text") end
if phase == "ending" or phase == "fade_out" then mark("battle_ending") end
if StatGrowth.isOpen() then
mark("stat_box")
if not statShot then
U.wait(20)
statShot = U.still(game, DIR .. "/02_level_up_box.png")
end
U.wait(30)
U.tap(game, "a")
elseif Ui.choiceActive() then
mark("learn_prompt")
if not learnShot then
U.wait(10)
learnShot = U.still(game, DIR .. "/03_learn_prompt.png")
end
U.wait(20)
local lg = Ui.log()
local last = tostring(lg[#lg] or "")
U.tap(game, last:find("Stop") and "a" or "b")
elseif phase == "command" and Ui._mode == "menu" and not Anim.busy() then
local st = Battle.getState()
if st and st.enemy and st.enemy.mon and not foeLowered then
st.enemy.mon.hp = 1
foeLowered = true
end
U.wait(10) U.tap(game, "a") U.wait(10) U.tap(game, "a") U.wait(4)
elseif Message.isWaiting() and Ui.dialogPending() then
waitF = waitF + 1
if waitF >= 90 then
waitF = 0
U.tap(game, "a")
else
U.wait(1)
end
else
waitF = 0
U.wait(1)
end
end
result(outcome == "win", "battle outcome win (" .. tostring(outcome) .. ")")
result(ev.fainted_waiting ~= nil and ev.gained_open ~= nil and ev.gained_open - ev.fainted_waiting >= 90,
"fainted!\\p waits for a button press before getexp (" .. tostring(ev.fainted_waiting) .. " -> " .. tostring(ev.gained_open) .. ")")
result(songAt.fainted == VS_WILD, "MUS_VS_WILD still playing on the fainted message (" .. tostring(songAt.fainted) .. ")")
result(ev.victory_song ~= nil and ev.gained_open ~= nil and ev.victory_song <= ev.gained_open,
"MUS_VICTORY_WILD starts before the gained EXP text (" .. tostring(ev.victory_song) .. " <= " .. tostring(ev.gained_open) .. ")")
result(songAt.gained == VICTORY_WILD, "MUS_VICTORY_WILD playing under the gained EXP text")
result(gainedPrinted, "gained EXP text printed in full")
result(gainedOpenFrames >= 60, "gained EXP text on screen " .. gainedOpenFrames .. " frames")
result(not endedWhileGained, "battle did not start ending while the gained EXP text was up")
result(ev.grew_open ~= nil and ev.grew_open > (ev.gained_open or math.huge), "grew to LV text after the gained EXP text")
result(ev.stat_box ~= nil and ev.stat_box >= (ev.grew_open or math.huge), "level-up stat box shown after grew to LV")
result(ev.learn_prompt ~= nil and ev.learn_prompt > (ev.stat_box or math.huge), "move-learn prompt after the level-up box")
result(ev.battle_ending ~= nil and ev.battle_ending > (ev.learn_prompt or math.huge)
and ev.battle_ending > (ev.gained_closed or math.huge), "battle ends after the EXP / level / learn flow")
result(expShot and statShot and learnShot, "gained / level-up / learn prompt shots")
local m = session.party[1]
result(m and m.level == learnLv, "Mudkip reached LV" .. learnLv .. " (" .. tostring(m and m.level) .. ")")
return finish()
end
+120
View File
@@ -0,0 +1,120 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
local T = require("tests.harness")
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
Profile.reset()
GameVersion.set("emerald")
local Dataset = require("src.core.game3.dataset")
local cache = Dataset.cache()
local function loadRel(rel)
local src = cache and cache.read and cache:read(rel)
if type(src) ~= "string" then return nil end
local chunk = load(src, "@" .. rel, "t", { setmetatable = setmetatable, pairs = pairs, ipairs = ipairs })
local ok, v = pcall(chunk)
return ok and v or nil
end
local manifest = loadRel("data/generated/gba/pokemon/battle/manifest.lua")
local text = manifest and manifest.layout == "rse" and loadRel("data/generated/gba/scripts/text.lua")
if type(text) ~= "table" then
print("emerald_player_placeholder_test: skipped (no Emerald cache; set POKEPORT_IDENTITY)")
os.exit(0)
end
require("src.import.gba.versions").select("emerald")
local C = require("src.core.game3.constants").of("emerald")
local TextIR = require("src.core.game3.scripting.text_ir")
local Trainers = require("src.core.game3.scripting.trainers")
local Space = require("src.core.game3.scripting.space")
local Runtime = require("src.core.game3.runtime")
local Message = require("src.ui.game3.message")
local BattleText = require("src.core.game3.battle.battle_text")
local placeholders = loadRel("data/generated/gba/text/placeholders.lua")
local liveProvider = TextIR._provider
TextIR.setContextProvider(function(kind, dialect, ctx)
if kind == "placeholders" then return placeholders end
return liveProvider(kind, dialect, ctx)
end)
local sess = { version = "emerald", name = "QUINCY", gender = 0, trainerId = 1, money = 0, party = {} }
local prevSession, prevVm = Runtime.session, Space.vm
Runtime.session = sess
Space.vm = {
ctx = { stringVars = { "ZIGZAGOON", "SECOND", "" } },
adapters = {},
getText = function(_, key) return text[key] end,
}
local function shown(t, opts)
Message.show(t, opts or { frame = "battle", battle = true })
local s = table.concat(Message._pages or {}, " ")
Message.close()
return (s:gsub("[\n\f\v]", " "))
end
local function has(s, needle) return type(s) == "string" and s:find(needle, 1, true) ~= nil end
local WALLACE = C.trainers and C.trainers.TRAINER_WALLACE or 335
local baked = Trainers.get(WALLACE).dialogs.defeat
check(has(baked, "Kudos to you, PLAYER!"), "cache dialogs.defeat is the flattened import string")
local d = Trainers.dialogs(WALLACE)
eq(type(d.defeat), "table", "Wallace defeat resolves to IR through the live script text")
local wallace = shown(d.defeat)
check(has(wallace, "Kudos to you, QUINCY!"), "Wallace defeat names the player: " .. wallace)
check(not has(wallace, "PLAYER"), "Wallace defeat has no literal PLAYER")
check(has(shown(Trainers.info(WALLACE).dialogs.intro), "QUINCY"), "Wallace intro via Trainers.info names the player")
for _, tid in ipairs({ 269, 519, 520, 529, 664, 804 }) do
local dl = Trainers.dialogs(tid)
for _, field in ipairs({ "intro", "defeat" }) do
local ir = dl[field]
if type(ir) == "table" then
local s = shown(ir)
local wantsPlayer = false
for _, seg in ipairs(ir) do if seg.t == "player" then wantsPlayer = true end end
if wantsPlayer then
check(has(s, "QUINCY") and not has(s, "PLAYER"), ("trainer %d %s names the player: %s"):format(tid, field, s))
end
end
end
end
check(has(TextIR.toPlain(d.defeat), "QUINCY"), "toPlain without ctx falls back to the live player")
check(has(TextIR.toPlain(d.defeat, { playerName = "ZED" }), "ZED"), "explicit ctx playerName still wins")
local rivalIr, strIr
for _, ir in pairs(text) do
if type(ir) == "table" then
for _, seg in ipairs(ir) do
if not rivalIr and seg.t == "rival" then rivalIr = ir end
if not strIr and seg.t == "strvar" and seg.n == 1 then strIr = ir end
end
end
if rivalIr and strIr then break end
end
if rivalIr then
check(has(shown(rivalIr), "MAY"), "{RIVAL} is MAY for a male player")
sess.gender = 1
check(has(shown(rivalIr), "BRENDAN"), "{RIVAL} is BRENDAN for a female player")
sess.gender = 0
end
if strIr then
check(has(shown(strIr), "ZIGZAGOON"), "{STR_VAR_1} reads the live script string var")
end
local vs = shown(BattleText.get("STRINGID_PLAYERGOTMONEY", { playerName = "QUINCY", buff1 = "9900" }))
check(has(vs, "QUINCY") and has(vs, "9900"), "battle prize string names the player: " .. vs)
local money = shown(require("src.core.game3.battle.prize").moneyMessage(sess.name, 9900))
check(has(money, "QUINCY got"), "prize money message names the player: " .. money)
Space.vm = nil
eq(Trainers.dialogs(WALLACE).defeat, baked, "without a script vm the cache string is returned unchanged")
Runtime.session, Space.vm = prevSession, prevVm
TextIR.setContextProvider(liveProvider)
T.finish("emerald_player_placeholder_test")
+337
View File
@@ -0,0 +1,337 @@
package.path = "./?.lua;./?/init.lua;" .. package.path
love = love or require("tests.love_stub")
local T = require("tests.harness")
T.verbose = true
local check, eq = T.check, T.eq
local GameVersion = require("src.core.GameVersion")
local Profile = require("src.core.game3.profile")
Profile.reset()
GameVersion.set("emerald")
local Dataset = require("src.core.game3.dataset")
local emCache = Dataset.cache()
local meta = emCache and emCache.read and emCache:read("data/generated/gba/meta.json")
if not (type(meta) == "string" and meta:find('"version":"emerald"', 1, true)) then
print("emerald_record_mix_test: skipped (no Emerald cache; set POKEPORT_IDENTITY)")
os.exit(0)
end
require("src.import.gba.versions").select("emerald")
local Pokemon = require("src.core.game3.pokemon")
Pokemon.install(nil)
local Json = require("src.link.Json")
local Wire = require("src.link.Wire")
local SaveData = require("src.core.SaveData")
local Schema = require("src.core.game3.save_schema_firered")
local Flags = require("src.core.game3.scripting.flags")
local Space = require("src.core.game3.scripting.space")
local Rse = require("src.core.game3.rse.init")
local RecordMix = require("src.core.game3.link.record_mix")
local MixUtil = require("src.core.game3.rse.record_mix_util")
local OldMan = require("src.core.game3.rse.old_man")
local Lady = require("src.core.game3.rse.lilycove_lady")
local SB = require("src.core.game3.rse.secret_base")
local Tv = require("src.core.game3.rse.tv")
local Dex = require("src.core.game3.dex")
local Daycare = require("src.core.game3.daycare")
local DaycareMail = require("src.core.game3.rse.daycare_mail_mix")
local Gift = require("src.core.game3.rse.record_mixing_gift")
local FUtil = require("src.core.game3.rse.frontier.util")
local Apprentice = require("src.core.game3.rse.frontier.apprentice")
local Bag = require("src.core.game3.bag")
local EM = require("src.core.game3.constants").of("emerald")
local ORANGE_MAIL = EM:require("items", "ITEM_ORANGE_MAIL")
local EON_TICKET = EM:require("items", "ITEM_EON_TICKET")
local SEEDOT = EM:require("species", "SPECIES_SEEDOT")
local MUDKIP = EM:require("species", "SPECIES_MUDKIP")
local stores = {}
local function use(s)
Space.store = stores[s]
end
local function makePlayer(idx, name, tid, gender)
local s = Schema.newGame({ version = "emerald", name = name, rngSeed = tid })
s.trainerId, s.secretId, s.gender = tid, 0, gender
s.party = { { species = MUDKIP, level = 20, nickname = "", heldItem = 0 } }
s.dex = s.dex or Dex.new()
Dex.setSeen(s.dex, SEEDOT)
Dex.setSeen(s.dex, MUDKIP)
stores[s] = Flags.newStore()
use(s)
Rse.setFlag("FLAG_RECEIVED_SECRET_POWER", true, s)
local b = SB.base(s, 0)
b.secretBaseId, b.trainerName, b.gender, b.language = 10 * idx + 1, name, gender, 2
b.trainerId = MixUtil.trainerIdBytes(s)
local m = OldMan.set(s)
if m.id == OldMan.STORYTELLER then
m.gameStatIDs[1], m.trainerNames[1], m.statValues[1] = 1, name, 100 * idx
elseif m.id == OldMan.GIDDY then
m.randomWords[1] = 100 + idx
elseif m.id == OldMan.BARD then
m.playerName = name
end
local l = Lady.init(s)
local which = l.quiz or l.favor or l.contest
which.playerName = name
local dc = Daycare.stateOf(s)
Daycare.setMon(dc, 1, { species = SEEDOT, level = 5, heldItem = 0 })
dc.mail = {}
local f = FUtil.frontier(s)
f.towerPlayer = { trainerId = MixUtil.trainerIdBytes(s), name = name, winStreak = 20 * idx, lvlMode = 0,
facilityClass = 1, party = { { species = MUDKIP, level = 50 } }, language = 2 }
FUtil.set2(f.towerRecordWinStreaks, 0, 0, 20 * idx)
FUtil.set2(f.towerRecordWinStreaks, 3, 0, 5 * idx)
local a = Apprentice.saved(s)[1]
a.playerName, a.playerId, a.number, a.id, a.lvlMode = name, MixUtil.trainerIdBytes(s), idx, idx, 1
return s
end
local function giveShow(s)
local saved = Tv.random
Tv.random = function() return 0 end
Tv.onBattleEnd(s, { playerMon1Species = MUDKIP, caughtMonSpecies = SEEDOT, caughtMonNick = "ACORN",
catchAttempts = { [3] = 2 }, lastUsedItem = EM:require("items", "ITEM_POKE_BALL"), lastOpponentSpecies = SEEDOT },
Tv.B_OUTCOME_CAUGHT, {})
Tv.random = saved
end
local function wire(packet, spot)
local msg = Wire.sanitize(Json.decode(Json.encode({ type = RecordMix.MSG.PACKET, spot = spot,
packet = RecordMix.toWire(packet) })))
return msg and RecordMix.fromWire(msg.packet) or nil
end
local function mix(players)
local packets, wired = {}, {}
for i, s in ipairs(players) do
use(s)
packets[i] = RecordMix.packet(s, i - 1)
wired[i] = wire(packets[i], i - 1)
end
local applied = {}
for i, s in ipairs(players) do
local list = {}
for j = 1, #players do list[j] = j == i and packets[i] or wired[j] end
use(s)
applied[i] = RecordMix.receive(s, list, i)
end
return applied, packets, wired
end
local function hasBase(s, id)
for i = 2, SB.COUNT do
if SB.base(s, i - 1).secretBaseId == id then return SB.base(s, i - 1) end
end
return nil
end
local function towerRecordNamed(s, name)
for _, r in ipairs(FUtil.frontier(s).towerRecords) do
if r.name == name then return r end
end
return nil
end
local function apprenticeNamed(s, name)
for i, a in ipairs(s.apprentices or {}) do
if i > 1 and a.playerName == name then return a end
end
return nil
end
local function tvHasCaught(s)
for i = 0, Tv.TV_SHOWS_COUNT - 1 do
local show = s.tvShows[i]
if show and show.kind == Tv.TVSHOW_POKEMON_TODAY_CAUGHT and show.active == true then return true end
end
return false
end
local function reload(s)
local save = SaveData.decode(SaveData.encode(Schema.toSaveTable(s)))
return Schema.fromSaveTable(save)
end
local function giftVar(s)
use(s)
return Rse.var("VAR_TEMP_RECORD_MIX_GIFT_ITEM", s)
end
print("[test] 1. ShufflePlayerIndices (pokeemerald/src/record_mixing.c:604)")
do
local two = MixUtil.shuffle({ { trainerId = 5 }, {} })
eq(two[1] .. two[2], "21", "2 players swap")
local three0 = MixUtil.shuffle({ { linkTrainerId = 4 }, {}, {} })
eq(three0[1] .. three0[2] .. three0[3], "231", "3 players, even leader id: 0<-1, 1<-2, 2<-0")
local three1 = MixUtil.shuffle({ { linkTrainerId = 7 }, {}, {} })
eq(three1[1] .. three1[2] .. three1[3], "312", "3 players, odd leader id: 0<-2, 1<-0, 2<-1")
local four = MixUtil.shuffle({ { linkTrainerId = 9 * 3 + 4 }, {}, {}, {} })
eq(four[1] .. four[2] .. four[3] .. four[4], "3412", "4 players use sPlayerIdxOrders_4Player[id % 9]")
end
print("[test] 2. two players mix every record")
do
local A = makePlayer(1, "MAY", 1116, 1)
local B = makePlayer(2, "BRENDAN", 2228, 0)
giveShow(B)
local mail = { message = { itemId = ORANGE_MAIL, words = { 1, 2, 3, 4, 5, 6, 7, 8, 9 }, playerName = "MAY",
trainerId = 1116, species = SEEDOT }, otName = "MAY", monName = "SEEDOT" }
Daycare.stateOf(A).mail[1] = mail
Gift.set(1, 1, EON_TICKET, A)
eq(OldMan.current(A), OldMan.STORYTELLER, "MAY has the storyteller")
eq(OldMan.current(B), OldMan.GIDDY, "BRENDAN has Giddy")
eq(Lady.id(A), Lady.QUIZ, "MAY has the quiz lady")
eq(Lady.id(B), Lady.FAVOR, "BRENDAN has the favor lady")
local applied, packets, wired = mix({ A, B })
eq(packets[1].giftItem, EON_TICKET, "multiplayer id 0 sends its record mixing gift")
eq(packets[2].giftItem, 0, "other players send no gift")
check(wired[2] and wired[2].daycareMail and wired[2].apprentices and wired[2].hallRecords
and wired[2].battleTowerRecord and wired[2].lilycoveLady and wired[2].oldMan, "every record crosses the wire")
for _, key in ipairs({ "secretBases", "tvShows", "pokeNews", "oldMan", "dewfordTrends", "daycareMail",
"battleTower", "giftItem", "lilycoveLady", "apprentices", "hallRecords" }) do
check(applied[1][key] and applied[2][key], "both sides ran the " .. key .. " receiver")
end
eq(OldMan.current(A), OldMan.GIDDY, "MAY now has BRENDAN's Giddy")
eq(A.oldMan.randomWords[1], 102, "with BRENDAN's tale words")
eq(OldMan.current(B), OldMan.STORYTELLER, "BRENDAN now has MAY's storyteller")
eq(B.oldMan.trainerNames[1], "MAY", "with MAY's story")
eq(B.oldMan.alreadyRecorded, false, "ResetMauvilleOldManFlag ran")
eq(Lady.id(A), Lady.FAVOR, "MAY now has the favor lady")
eq(A.lilycoveLady.favor.playerName, "BRENDAN", "carrying BRENDAN's data")
eq(A.lilycoveLady.favor.state, Lady.STATE_READY, "ResetLilycoveLadyForRecordMix ran")
eq(Lady.id(B), Lady.QUIZ, "BRENDAN now has the quiz lady")
eq(B.lilycoveLady.quiz.playerName, "MAY", "carrying MAY's quiz")
local bb = hasBase(A, 21)
check(bb ~= nil, "MAY registered BRENDAN's secret base")
eq(bb and bb.trainerName, "BRENDAN", "owned by BRENDAN")
eq(bb and bb.registryStatus, SB.UNREGISTERED, "NEW is cleared back to UNREGISTERED")
check(hasBase(B, 11) ~= nil, "BRENDAN registered MAY's secret base")
eq(SB.base(A, 0).numSecretBasesReceived, 1, "numSecretBasesReceived counts the mix")
check(tvHasCaught(A), "BRENDAN's Pokemon Today show airs on MAY's TV")
eq(Daycare.stateOf(A).mail[1], nil, "MAY's daycare mail swapped away")
local bm = Daycare.stateOf(B).mail[1]
eq(bm and bm.message.itemId, ORANGE_MAIL, "BRENDAN's daycare SEEDOT now carries MAY's mail")
eq(bm and bm.otName, "MAY", "with MAY as the writer")
local tr = towerRecordNamed(A, "BRENDAN")
eq(tr and tr.winStreak, 40, "BRENDAN's tower record joined MAY's towerRecords")
eq(towerRecordNamed(B, "MAY") and towerRecordNamed(B, "MAY").winStreak, 20, "and MAY's joined BRENDAN's")
check(Bag.has(B.bag, EON_TICKET, 1), "BRENDAN received MAY's EON TICKET")
eq(giftVar(B), EON_TICKET, "VAR_TEMP_RECORD_MIX_GIFT_ITEM holds it")
use(B)
check(Rse.flag("FLAG_ENABLE_SHIP_SOUTHERN_ISLAND", B), "FLAG_ENABLE_SHIP_SOUTHERN_ISLAND is set")
check(not Bag.has(A.bag, EON_TICKET, 1), "the sender keeps no copy")
eq(Gift.state(A).quantity, 0, "the one-use gift is spent")
local ap = apprenticeNamed(A, "BRENDAN")
check(ap ~= nil, "MAY saved BRENDAN's apprentice")
eq(A.playerApprentice.saveId, 1, "playerApprentice.saveId advanced")
check(apprenticeNamed(B, "MAY") ~= nil, "BRENDAN saved MAY's apprentice")
local h = A.hallRecords1P[1][1][1]
eq(h.name, "BRENDAN", "MAY's Ranking Hall shows BRENDAN")
eq(h.winStreak, 40, "with his tower singles streak")
eq(B.hallRecords1P[1][1][1].name, "MAY", "BRENDAN's Ranking Hall shows MAY")
eq(A.hallRecords2P[1][1].name1 .. "/" .. A.hallRecords2P[1][1].winStreak, "BRENDAN/10", "link multi hall row filled")
print("[test] 3. the mixed records survive save and reload")
local A2, B2 = reload(A), reload(B)
eq(A2.oldMan and A2.oldMan.id, OldMan.GIDDY, "old man")
eq(A2.lilycoveLady and A2.lilycoveLady.favor and A2.lilycoveLady.favor.playerName, "BRENDAN", "Lilycove lady")
check(hasBase(A2, 21) ~= nil, "secret base registry")
check(tvHasCaught(A2), "TV show")
local bm2 = Daycare.stateOf(B2).mail[1]
eq(bm2 and bm2.message.itemId, ORANGE_MAIL, "daycare mail")
eq(towerRecordNamed(A2, "BRENDAN") and towerRecordNamed(A2, "BRENDAN").winStreak, 40, "tower record")
check(Bag.has(B2.bag, EON_TICKET, 1), "gift item")
check(apprenticeNamed(A2, "BRENDAN") ~= nil, "apprentice")
eq(A2.playerApprentice and A2.playerApprentice.saveId, 1, "apprentice save id")
eq(A2.hallRecords1P[1][1][1].name, "BRENDAN", "ranking hall")
local g2 = reload(A)
eq(g2.recordMixingGift and g2.recordMixingGift.itemId, 0, "record mixing gift block")
end
print("[test] 4. three players follow pret's partner table")
do
local A = makePlayer(1, "MAY", 1116, 1)
local B = makePlayer(2, "BRENDAN", 2228, 0)
local C = makePlayer(3, "CHRIS", 3340, 0)
giveShow(C)
local mail = { message = { itemId = ORANGE_MAIL, words = { 9, 9, 9, 9, 9, 9, 9, 9, 9 }, playerName = "MAY",
trainerId = 1116, species = SEEDOT }, otName = "MAY", monName = "SEEDOT" }
Daycare.stateOf(A).mail[1] = mail
Gift.set(1, 2, EON_TICKET, A)
eq(OldMan.current(C), OldMan.BARD, "CHRIS has the bard")
eq(Lady.id(C), Lady.CONTEST, "CHRIS has the contest lady")
local order = MixUtil.shuffle({ { linkTrainerId = MixUtil.sessionLinkTrainerId(A) }, {}, {} })
eq(order[1] .. order[2] .. order[3], "231", "MAY's even id picks {1, 2, 0}")
local _, packets = mix({ A, B, C })
local randSum = DaycareMail.randSum(packets[1].tvShows)
eq(OldMan.current(A), OldMan.GIDDY, "MAY <- BRENDAN's Giddy")
eq(OldMan.current(B), OldMan.BARD, "BRENDAN <- CHRIS's bard")
eq(B.oldMan.playerName, "CHRIS", "the bard keeps CHRIS's song credit")
eq(OldMan.current(C), OldMan.STORYTELLER, "CHRIS <- MAY's storyteller")
eq(Lady.id(A), Lady.FAVOR, "MAY <- favor lady")
eq(Lady.id(B), Lady.CONTEST, "BRENDAN <- contest lady")
eq(B.lilycoveLady.contest.playerName, "CHRIS", "with CHRIS's contest data")
eq(Lady.id(C), Lady.QUIZ, "CHRIS <- quiz lady")
eq(C.lilycoveLady.quiz.playerName, "MAY", "with MAY's quiz")
check(hasBase(A, 21) and hasBase(A, 31), "MAY registered both partners' bases")
check(hasBase(B, 11) and hasBase(B, 31), "BRENDAN registered both partners' bases")
check(hasBase(C, 11) and hasBase(C, 21), "CHRIS registered both partners' bases")
check((tvHasCaught(A) and 1 or 0) + (tvHasCaught(B) and 1 or 0) == 1, "CHRIS's show airs on exactly one partner's TV (tv.c:3449)")
local swap = ({ { 0, 1 }, { 1, 2 }, { 2, 0 } })[randSum % 3 + 1]
local holder = (swap[1] == 0 and swap[2]) or (swap[2] == 0 and swap[1]) or 0
local holders = {}
for i, s in ipairs({ A, B, C }) do
local m = Daycare.stateOf(s).mail[1]
if m and m.message.itemId == ORANGE_MAIL then holders[#holders + 1] = i - 1 end
end
eq(#holders, 1, "MAY's mail exists exactly once after the swap")
eq(holders[1], holder, "sDaycareMailSwapIds_3Player[" .. (randSum % 3) .. "] decides who holds it")
eq(towerRecordNamed(A, "BRENDAN") and towerRecordNamed(A, "BRENDAN").winStreak, 40, "MAY <- BRENDAN's tower record")
eq(towerRecordNamed(B, "CHRIS") and towerRecordNamed(B, "CHRIS").winStreak, 60, "BRENDAN <- CHRIS's tower record")
eq(towerRecordNamed(C, "MAY") and towerRecordNamed(C, "MAY").winStreak, 20, "CHRIS <- MAY's tower record")
check(towerRecordNamed(A, "CHRIS") == nil, "MAY does not take CHRIS's tower record")
check(Bag.has(B.bag, EON_TICKET, 1) and Bag.has(C.bag, EON_TICKET, 1), "both partners get the leader's gift")
check(apprenticeNamed(A, "BRENDAN") ~= nil, "MAY <- BRENDAN's apprentice")
check(apprenticeNamed(B, "CHRIS") ~= nil, "BRENDAN <- CHRIS's apprentice")
check(apprenticeNamed(C, "MAY") ~= nil, "CHRIS <- MAY's apprentice")
local hall = A.hallRecords1P[1][1]
eq(hall[1].name .. "/" .. hall[1].winStreak, "CHRIS/60", "MAY's hall ranks CHRIS first")
eq(hall[2].name .. "/" .. hall[2].winStreak, "BRENDAN/40", "then BRENDAN")
local hallC = C.hallRecords1P[1][1]
eq(hallC[1].name .. "/" .. hallC[2].name, "BRENDAN/MAY", "CHRIS ranks BRENDAN over MAY")
local C2 = reload(C)
eq(C2.oldMan.id, OldMan.STORYTELLER, "CHRIS's old man survives reload")
eq(C2.hallRecords1P[1][1][1].name, "BRENDAN", "and his hall records")
check(hasBase(C2, 11) and hasBase(C2, 21), "and both bases")
end
T.finish("emerald_record_mix")