mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-09-28 23:00:21 -04:00
516 lines
27 KiB
Lua
516 lines
27 KiB
Lua
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
local T = require("tests.harness")
|
|
local check, eq = T.check, T.eq
|
|
|
|
local ROM_PATH = os.getenv("POKEPORT_EMERALD_ROM") or "../pokeemerald/pokeemerald.gba"
|
|
local PRET = os.getenv("POKEPORT_PRET_EMERALD") or "../pokeemerald"
|
|
|
|
local GameVersion = require("src.core.GameVersion")
|
|
local Profile = require("src.core.game3.profile")
|
|
Profile.reset()
|
|
GameVersion.set("emerald")
|
|
|
|
local session
|
|
package.loaded["src.core.game3.runtime"] = { getSession = function() return session end }
|
|
|
|
local Dataset = require("src.core.game3.dataset")
|
|
local overlay = {}
|
|
local realCache = Dataset.cache
|
|
Dataset.cache = function()
|
|
local c = realCache()
|
|
local realRead = c.read
|
|
c.read = function(self, rel)
|
|
if overlay[rel] then return overlay[rel] end
|
|
return realRead(self, rel)
|
|
end
|
|
return c
|
|
end
|
|
local TV_REL = "data/generated/gba/tv/manifest.lua"
|
|
|
|
local romData
|
|
do
|
|
local f = io.open(ROM_PATH, "rb")
|
|
if f then
|
|
romData = f:read("*a")
|
|
f:close()
|
|
if romData:sub(0xAD, 0xB0) ~= "BPEE" then romData = nil end
|
|
end
|
|
end
|
|
|
|
local function readPret(rel)
|
|
local h = io.open(PRET .. "/" .. rel, "rb")
|
|
if not h then return nil end
|
|
local s = h:read("*a")
|
|
h:close()
|
|
return s
|
|
end
|
|
|
|
local C = require("src.core.game3.constants").of("emerald")
|
|
local function sp(name) return C:require("species", name) end
|
|
|
|
if romData then
|
|
print("[test] 1. tv_extract reads the ROM tables by symbol (pokeemerald/src/tv.c:192)")
|
|
local rom = { id = "emerald", size = #romData }
|
|
function rom.get(_, o) return romData:byte(o + 1) end
|
|
function rom.u16(_, o) local a, b = romData:byte(o + 1, o + 2); return a + b * 256 end
|
|
function rom.u32(_, o)
|
|
local a, b, c, d = romData:byte(o + 1, o + 4)
|
|
return a + b * 256 + c * 65536 + d * 16777216
|
|
end
|
|
function rom.readString(_, o, n) return romData:sub(o + 1, o + n) end
|
|
local files = {}
|
|
local fake = {
|
|
write = function(_, rel, bytes) files[rel] = bytes; return true end,
|
|
read = function(_, rel) return files[rel] end,
|
|
}
|
|
local X = require("src.import.gba.rse.tv_extract")
|
|
check(not X.ready(fake, "data/generated/gba"), "tv not ready before run")
|
|
eq(X.run(rom, fake, { cacheRoot = "data/generated/gba" }), true, "tv extractor ran")
|
|
check(X.ready(fake, "data/generated/gba"), "tv ready after run")
|
|
for _, rel in ipairs(X.REQUIRED) do check(files["data/generated/gba/" .. rel] ~= nil, rel .. " written") end
|
|
overlay[TV_REL] = files[TV_REL]
|
|
local m = assert(load(files[TV_REL], "@tv", "t", {}))()
|
|
eq(#m.outbreakSpecies, 5, "sPokeOutbreakSpeciesList has 5 rows")
|
|
local src = readPret("src/tv.c")
|
|
if src then
|
|
local body = src:match("sPokeOutbreakSpeciesList%[%]%s*=%s*(%b{})")
|
|
local i = 0
|
|
for species, moves, level, map in (body or ""):gmatch("%.species%s*=%s*(SPECIES_[%w_]+),%s*%.moves%s*=%s*(%b{}),%s*%.level%s*=%s*(%d+),%s*%.location%s*=%s*MAP_NUM%((MAP_[%w_]+)%)") do
|
|
i = i + 1
|
|
local row = m.outbreakSpecies[i]
|
|
eq(row.species, sp(species), "outbreak " .. i .. " species " .. species)
|
|
eq(row.level, tonumber(level), "outbreak " .. i .. " level")
|
|
eq(row.location, C.map_groups.byName[map].num, "outbreak " .. i .. " location " .. map)
|
|
local k = 0
|
|
for mv in moves:gmatch("MOVE_[%w_]+") do
|
|
k = k + 1
|
|
eq(row.moves[k], C:require("moves", mv), "outbreak " .. i .. " move " .. mv)
|
|
end
|
|
end
|
|
eq(i, 5, "pret list parsed")
|
|
local nb = src:match("sNumberOneVarsAndThresholds%[%]%[2%]%s*=%s*(%b{})")
|
|
local j = 0
|
|
for v, th in (nb or ""):gmatch("{(VAR_[%w_]+),%s*(%d+)}") do
|
|
j = j + 1
|
|
eq(m.numberOne[j].var, v, "number one var " .. v)
|
|
eq(m.numberOne[j].threshold, tonumber(th), "number one threshold " .. v)
|
|
end
|
|
eq(j, 7, "seven number one rows")
|
|
end
|
|
eq(#m.secretBaseSecretsActions, 32, "sTVSecretBaseSecretsActions has 32 rows")
|
|
eq(m.secretBaseSecretsActions[1], 10, "first secret base action is SBSECRETS_STATE_USED_CHAIR")
|
|
eq(m.secretBaseSecretsActions[32], 43, "last row is SBSECRETS_NUM_STATES")
|
|
eq(m.goldSymbolFlags[1], "FLAG_SYS_TOWER_GOLD", "gold symbol flags by name")
|
|
eq(m.silverSymbolFlags[7], "FLAG_SYS_PYRAMID_SILVER", "silver symbol flags by name")
|
|
else
|
|
print("emerald_tv_test: ROM section skipped (no ROM at " .. ROM_PATH .. ")")
|
|
end
|
|
|
|
local Space = require("src.core.game3.scripting.space")
|
|
Space.store = { flags = {}, vars = {} }
|
|
local Flags = require("src.core.game3.scripting.flags")
|
|
local EM = Flags.forVersion("emerald")
|
|
local function setFlag(name, on) Flags.setFlag(Space.store, nil, assert(EM.IDS[name], name), on ~= false) end
|
|
local function getVar(name) return tonumber(Flags.getVar(Space.store, nil, assert(EM.VAR_IDS[name], name))) or 0 end
|
|
local function setVar(name, v) Flags.setVar(Space.store, nil, assert(EM.VAR_IDS[name], name), v) end
|
|
|
|
local Tv = require("src.core.game3.rse.tv")
|
|
local Dex = require("src.core.game3.dex")
|
|
local haveData = Tv.data() ~= nil
|
|
|
|
local queue = {}
|
|
Tv.random = function()
|
|
local v = table.remove(queue, 1)
|
|
return v or 0
|
|
end
|
|
local function rolls(...) queue = { ... } end
|
|
|
|
Tv.names = {
|
|
species = function(id) return "SP" .. tostring(id) end,
|
|
move = function(id) return "MV" .. tostring(id) end,
|
|
item = function(id) return "IT" .. tostring(id) end,
|
|
itemPrice = function() return 200 end,
|
|
decoration = function(id) return "DC" .. tostring(id) end,
|
|
mapName = function(sec) return "MAP" .. tostring(sec) end,
|
|
word = function(id) if id == nil or id == 0xFFFF then return "" end return "W" .. tostring(id) end,
|
|
phrase = function(words) return "PHRASE:" .. table.concat(words, ",", 1, 4) end,
|
|
text = function(key) return "<" .. key .. ">" end,
|
|
}
|
|
|
|
local function newSession(extra)
|
|
local s = { version = "emerald", name = "QUINCY", trainerId = 47118, secretId = 27998, gender = 0,
|
|
party = {}, dex = Dex.new(), gameStats = {}, map = "EM_SLATEPORT_CITY", regionMapSectionId = 8 }
|
|
for k, v in pairs(extra or {}) do s[k] = v end
|
|
Tv.newGame(s)
|
|
return s
|
|
end
|
|
|
|
local function runShow(s, idx, sv, limit)
|
|
local seq, texts = {}, {}
|
|
sv = sv or {}
|
|
for _ = 1, limit or 60 do
|
|
local res, done = Tv.doTVShow(s, idx, sv)
|
|
if not res then break end
|
|
seq[#seq + 1] = res.group and res.index or "text"
|
|
texts[#texts + 1] = res
|
|
if done then return seq, true, texts, sv end
|
|
end
|
|
return seq, false, texts, sv
|
|
end
|
|
|
|
print("[test] 2. new game clears TV state (pokeemerald/src/new_game.c:168)")
|
|
session = newSession()
|
|
eq(#session.tvShows, Tv.TV_SHOWS_COUNT - 1, "25 show slots (0..24)")
|
|
eq(session.tvShows[0].kind, 0, "slot 0 off the air")
|
|
eq(session.pokeNews[15].kind, 0, "16 PokeNews slots")
|
|
eq(session.gabbyAndTyData.quote[0], 0xFFFF, "Gabby quote is EC_EMPTY_WORD")
|
|
eq(session.outbreakPokemonSpecies, 0, "no outbreak species")
|
|
eq(session.outbreak, nil, "no outbreak for the encounter rule")
|
|
eq(Tv.groupOf(12), Tv.TVGROUP.NORMAL, "kind 12 is a normal show")
|
|
eq(Tv.groupOf(21), Tv.TVGROUP.RECORD_MIX, "kind 21 is a record-mix show")
|
|
eq(Tv.groupOf(41), Tv.TVGROUP.OUTBREAK, "kind 41 is the outbreak group")
|
|
|
|
print("[test] 3. interviews (pokeemerald/src/tv.c:2894, 1077)")
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "", name = "MUDKIP", level = 20, friendship = 200 } }
|
|
check(not Tv.interviewBefore(session, Tv.TVSHOW_FAN_CLUB_LETTER), "fan club letter interview is available")
|
|
eq(Tv._curSlot, 0, "first normal slot reserved")
|
|
eq(Tv._var8006, 0, "VAR_0x8006 is the slot")
|
|
eq(Tv._stringVar1, "SP283", "gStringVar1 = lead species name")
|
|
eq(#session.tvShows[0].words, 6, "six EC_EMPTY words initialised")
|
|
local words, first, count = Tv.easyChatWords(session, Tv.EASY_CHAT_TYPE.INTERVIEW, 0, 0)
|
|
check(words == session.tvShows[0].words and first == 1 and count == 4, "easy chat INTERVIEW edits the show's first 4 words (2x2)")
|
|
words[1], words[2], words[3], words[4] = 101, 102, 103, 104
|
|
Tv.interviewAfter(session, Tv.TVSHOW_FAN_CLUB_LETTER, {})
|
|
eq(session.tvShows[0].kind, Tv.TVSHOW_FAN_CLUB_LETTER, "fan club letter goes on air")
|
|
check(session.tvShows[0].active == true, "normal shows are active immediately")
|
|
eq(session.tvShows[0].species, sp("SPECIES_MUDKIP"), "letter stores the lead species")
|
|
eq(session.tvShows[0].trainerIdLo, 47118 % 256, "StorePlayerIdInNormalShow low byte")
|
|
check(Tv.interviewBefore(session, Tv.TVSHOW_FAN_CLUB_LETTER), "a second letter waits while the first is on air")
|
|
rolls(0, 1, 0, 0, 5)
|
|
local seq, done = runShow(session, 0)
|
|
check(done, "fan club letter finishes")
|
|
eq(table.concat(seq, ","), "0,text,1,2,text,3,5,7", "letter pages: intro, phrase, roll 0 -> 2, phrase again, roll -> 5, sign-off")
|
|
check(session.tvShows[0].active == false, "TVShowDone takes the show off the air")
|
|
check(not Tv.interviewBefore(session, Tv.TVSHOW_FAN_CLUB_LETTER), "an aired letter can be replaced")
|
|
eq(session.tvShows[0].kind, 0, "the old inactive letter was deleted")
|
|
|
|
print("[test] 4. Name Rater show (pokeemerald/src/tv.c:3280, 4570)")
|
|
session = newSession()
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "ZAPPY", name = "MUDKIP", level = 20 } }
|
|
Dex.setSeen(session.dex, sp("SPECIES_ZIGZAGOON"))
|
|
check(not Tv.tryPutNameRaterShowOnTheAir(session, 0, "ZAPPY"), "unchanged nickname puts nothing on air")
|
|
rolls(0, 1, 5)
|
|
check(Tv.tryPutNameRaterShowOnTheAir(session, 0, "MUDKIP"), "renamed mon puts the Name Rater on air")
|
|
local nr = session.tvShows[0]
|
|
eq(nr.kind, Tv.TVSHOW_NAME_RATER_SHOW, "kind NAME_RATER_SHOW")
|
|
eq(nr.pokemonName, "ZAPPY", "stores the new nickname")
|
|
eq(nr.trainerName, "QUINCY", "stores the player")
|
|
eq(nr.random, 0, "random = Random() % 3")
|
|
eq(nr.random2, 1, "random2 = Random() % 2")
|
|
eq(nr.randomSpecies, sp("SPECIES_ZIGZAGOON"), "randomSpecies walks down to a seen species")
|
|
local sv
|
|
seq, done, _, sv = runShow(session, 0)
|
|
check(done, "Name Rater show finishes")
|
|
eq(table.concat(seq, ","), "0,7,9,18", "ZAPPY sums to state 7 (tv.c:3171), random 0 -> 9 -> 18")
|
|
eq(sv[1], "ZAPPY", "last page names the mon")
|
|
eq(sv[2], "Z", "first letter substring")
|
|
eq(sv[3], "A", "second letter substring")
|
|
check(nr.active == false, "aired Name Rater show goes inactive")
|
|
|
|
if haveData then
|
|
print("[test] 5. mass outbreak countdown (pokeemerald/src/tv.c:1646, 1707, 4898)")
|
|
session = newSession()
|
|
setFlag("FLAG_SYS_GAME_CLEAR", true)
|
|
rolls(400)
|
|
eq(Tv.tryStartRandomMassOutbreak(session), nil, "Random() > 327 fails the 1/200 roll")
|
|
rolls(327, 0)
|
|
eq(Tv.tryStartRandomMassOutbreak(session), 0, "Random() <= 327 starts an outbreak show in slot 0")
|
|
local ob = session.tvShows[0]
|
|
eq(ob.kind, Tv.TVSHOW_MASS_OUTBREAK, "kind MASS_OUTBREAK")
|
|
eq(ob.species, sp("SPECIES_SEEDOT"), "Seedot outbreak")
|
|
eq(ob.locationMapNum, C.map_groups.byName.MAP_ROUTE102.num, "on Route 102")
|
|
eq(ob.level, 3, "level 3")
|
|
eq(ob.probability, 50, "50% of encounters")
|
|
eq(ob.daysBeforeOutbreak, 1, "airs after one day")
|
|
eq(Tv.getRandomActiveShowIdx(session, function() return 0 end), 0xFF, "not on the air on day 0")
|
|
Tv.updatePerDay(session, 1)
|
|
eq(ob.daysBeforeOutbreak, 0, "countdown reaches 0")
|
|
eq(Tv.getRandomActiveShowIdx(session, function() return 0 end), 0, "on the air the next day")
|
|
sv = {}
|
|
local res, fin = Tv.doTVShow(session, 0, sv)
|
|
check(fin and res.group == "sTVMassOutbreakTextGroup" and res.index == 0, "outbreak news is one page")
|
|
eq(sv[1], "MAP17", "gStringVar1 = Route 102 map name")
|
|
eq(sv[2], "SP298", "gStringVar2 = Seedot")
|
|
eq(session.outbreakPokemonSpecies, sp("SPECIES_SEEDOT"), "StartMassOutbreak copies the species")
|
|
eq(session.outbreakDaysLeft, 2, "outbreak lasts two days")
|
|
check(session.outbreak and session.outbreak.map == "EM_ROUTE102", "encounter hook sees EM_ROUTE102")
|
|
eq(session.outbreak.probability, 50, "encounter hook probability")
|
|
eq(table.concat(session.outbreak.moves, ","), "117,106,73,0", "encounter hook moves (Bide/Harden/Leech Seed)")
|
|
eq(Tv.nextActiveIfMassOutbreak(session, 0), 0xFF, "no other show to fall back to")
|
|
Tv.updatePerDay(session, 1)
|
|
eq(session.outbreakDaysLeft, 1, "one day left")
|
|
check(session.outbreak ~= nil, "still running")
|
|
Tv.updatePerDay(session, 1)
|
|
eq(session.outbreakPokemonSpecies, 0, "EndMassOutbreak after two days")
|
|
eq(session.outbreak, nil, "encounter hook cleared")
|
|
setFlag("FLAG_SYS_GAME_CLEAR", false)
|
|
else
|
|
print("emerald_tv_test: outbreak section skipped (no tv manifest; needs ROM or POKEPORT_IDENTITY)")
|
|
end
|
|
|
|
print("[test] 6. PokeNews (pokeemerald/src/tv.c:2540, 2621, 2712)")
|
|
session = newSession()
|
|
setFlag("FLAG_SYS_GAME_CLEAR", true)
|
|
rolls(1000)
|
|
Tv.tryPutRandomPokeNewsOnAir(session)
|
|
eq(session.pokeNews[0].kind, 0, "Random() > 655 skips PokeNews")
|
|
rolls(655, 0)
|
|
Tv.tryPutRandomPokeNewsOnAir(session)
|
|
eq(session.pokeNews[0].kind, Tv.POKENEWS_SLATEPORT, "Slateport sale news")
|
|
eq(session.pokeNews[0].dayCountdown, 4, "four days out")
|
|
eq(Tv.findPokeNewsOnAir(session), 0xFF, "not announced on day 4")
|
|
Tv.updatePerDay(session, 2)
|
|
eq(Tv.findPokeNewsOnAir(session), 0, "announced once the countdown is below 3")
|
|
sv = {}
|
|
local news, shown = Tv.doPokeNews(session, 10, sv)
|
|
check(shown and news.group == "sPokeNewsTextGroup_Upcoming" and news.index == 1, "upcoming Slateport text")
|
|
eq(sv[1], "2", "gStringVar1 = days left")
|
|
eq(session.pokeNews[0].state, Tv.POKENEWS_STATE_INACTIVE, "countdown airing does not repeat")
|
|
eq(Tv.findPokeNewsOnAir(session), 0xFF, "not on the air again the same day")
|
|
Tv.updatePerDay(session, 2)
|
|
eq(session.pokeNews[0].state, Tv.POKENEWS_STATE_UPCOMING, "UpdatePokeNewsCountdown re-arms it")
|
|
news = Tv.doPokeNews(session, 21, sv)
|
|
check(news.group == "sPokeNewsTextGroup_Ending", "after 20:00 the ending text")
|
|
eq(session.pokeNews[0].state, Tv.POKENEWS_STATE_ACTIVE, "event is active")
|
|
check(Tv.isPokeNewsActive(session, Tv.POKENEWS_SLATEPORT, function() return true end), "IsPokeNewsActive")
|
|
session.map = "EM_SLATEPORT_CITY"
|
|
check(Tv.shouldApplyPokeNews(Tv.POKENEWS_SLATEPORT, session, Tv.LOCALID_SLATEPORT_ENERGY_GURU), "discount at the Energy Guru")
|
|
check(not Tv.shouldApplyPokeNews(Tv.POKENEWS_SLATEPORT, session, 3), "not for another NPC")
|
|
Tv.updatePerDay(session, 1)
|
|
eq(session.pokeNews[0].kind, 0, "elapsed news is cleared")
|
|
setFlag("FLAG_SYS_GAME_CLEAR", false)
|
|
|
|
print("[test] 7. battle results -> Pokemon Today / World of Masters (pokeemerald/src/battle_main.c:5126)")
|
|
session = newSession()
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "", name = "MUDKIP", level = 20 } }
|
|
local results = { playerMon1Species = sp("SPECIES_MUDKIP"), caughtMonSpecies = sp("SPECIES_SEEDOT"), caughtMonNick = "ACORN",
|
|
catchAttempts = { [3] = 2 }, lastUsedItem = C:require("items", "ITEM_POKE_BALL"), lastOpponentSpecies = sp("SPECIES_SEEDOT") }
|
|
Tv.onBattleEnd(session, results, Tv.B_OUTCOME_CAUGHT, { frontier = true })
|
|
eq(session.tvShows[5].kind, 0, "frontier battles never make a show")
|
|
rolls(0)
|
|
Tv.onBattleEnd(session, results, Tv.B_OUTCOME_CAUGHT, {})
|
|
local today = session.tvShows[5]
|
|
eq(today.kind, Tv.TVSHOW_POKEMON_TODAY_CAUGHT, "Pokemon Today in the first record-mix slot")
|
|
check(today.active == false, "record-mix show waits for a partner (tv.c:1139)")
|
|
eq(today.nickname, "ACORN", "stores the nickname")
|
|
eq(today.nBallsUsed, 2, "counts the balls")
|
|
eq(today.ball, C:require("items", "ITEM_POKE_BALL"), "last ball used")
|
|
eq(session.tvShows[24].kind, Tv.TVSHOW_WORLD_OF_MASTERS, "World of Masters tracker in the last slot")
|
|
eq(session.tvShows[24].numPokeCaught, 1, "one catch counted")
|
|
eq(Tv.getRandomActiveShowIdx(session, function() return 0 end), 0xFF, "nothing on this player's TV")
|
|
Tv.updatePerDay(session, 1)
|
|
eq(session.tvShows[24].kind, 0, "World of Masters tracker resolves at the day change")
|
|
|
|
print("[test] 8. record mixing airs a partner's show (pokeemerald/src/tv.c:3449)")
|
|
local partner = newSession({ name = "WALLY", trainerId = 1234, secretId = 1 })
|
|
Dex.setSeen(partner.dex, sp("SPECIES_SEEDOT"))
|
|
Dex.setSeen(partner.dex, sp("SPECIES_ZIGZAGOON"))
|
|
local export = Tv.mixExport(session)
|
|
session, partner = partner, session
|
|
Tv.receiveShows(session, { export, {} }, 2)
|
|
local mixed = session.tvShows[5]
|
|
eq(mixed.kind, Tv.TVSHOW_POKEMON_TODAY_CAUGHT, "partner receives Pokemon Today")
|
|
check(mixed.active == true, "and it is on the air there")
|
|
eq(mixed.playerName, "QUINCY", "still credits the catcher")
|
|
eq(export.tvShows[5].kind, 0, "the sender's copy is removed")
|
|
rolls(0, 0)
|
|
seq, done, _, sv = runShow(session, 5)
|
|
check(done, "Pokemon Today airs to the end")
|
|
eq(seq[1], 0, "intro page")
|
|
eq(sv[3], "SP288", "a random seen species is compared (tv.c:3079)")
|
|
session = partner
|
|
|
|
print("[test] 9. Gabby and Ty (pokeemerald/src/tv.c:935, 979, 5427)")
|
|
session = newSession()
|
|
Tv.gabbyAndTyBeforeInterview(session, { playerMon1Species = 1, playerMon2Species = 2, lastUsedMovePlayer = 33,
|
|
playerMonWasDamaged = true, catchAttempts = {} })
|
|
local g = session.gabbyAndTyData
|
|
eq(g.battleNum, 1, "first Gabby battle")
|
|
check(g.onAir == false, "interview takes the old show off the air")
|
|
g.quote[0] = 555
|
|
Tv.gabbyAndTyAfterInterview(session)
|
|
check(Tv.isGabbyAndTyOnAir(session), "In Search of Trainers is on the air")
|
|
eq(session.gameStats[Tv.GAME_STAT_GOT_INTERVIEWED], 1, "GAME_STAT_GOT_INTERVIEWED")
|
|
sv = {}
|
|
local steps = {}
|
|
for _ = 1, 10 do
|
|
local res, fin = Tv.doInSearchOfTrainers(session, sv)
|
|
steps[#steps + 1] = res.index
|
|
if fin then break end
|
|
end
|
|
eq(table.concat(steps, ","), "0,2,3,8", "damaged, no ball/item/faint -> the move trivia page")
|
|
eq(sv[1], "W555", "last page quotes the interview word")
|
|
check(not Tv.isGabbyAndTyOnAir(session), "show ends")
|
|
words = Tv.easyChatWords(session, Tv.EASY_CHAT_TYPE.GABBY_AND_TY, 0, 0)
|
|
check(words == g.quote and words[0] == 0xFFFF, "GABBY_AND_TY easy chat writes quote[0]")
|
|
|
|
print("[test] 10. smart shopper and daily Number One (pokeemerald/src/tv.c:1503, 2467)")
|
|
session = newSession()
|
|
rolls(30000)
|
|
Tv.tryPutSmartShopperOnAir(session, { { itemId = 13, quantity = 25 }, { itemId = 4, quantity = 30 }, { itemId = 0, quantity = 0 } })
|
|
eq(session.tvShows[5].kind, 0, "rbernoulli(1, 3) blocks the show")
|
|
rolls(100)
|
|
Tv.tryPutSmartShopperOnAir(session, { { itemId = 13, quantity = 25 }, { itemId = 4, quantity = 30 }, { itemId = 0, quantity = 0 } })
|
|
eq(session.tvShows[5].kind, Tv.TVSHOW_SMART_SHOPPER, "20+ of one item makes the show")
|
|
eq(session.tvShows[5].itemIds[1], 4, "purchases sorted by quantity")
|
|
if haveData then
|
|
setVar("VAR_DAILY_WILDS", 120)
|
|
setVar("VAR_DAILY_SLOTS", 5)
|
|
Tv.updatePerDay(session, 1)
|
|
local found
|
|
for i = 5, 23 do if session.tvShows[i].kind == Tv.TVSHOW_NUMBER_ONE then found = session.tvShows[i] end end
|
|
check(found and found.actionIdx == 2 and found.count == 120, "100+ wild battles -> What's No. 1 action 2")
|
|
eq(getVar("VAR_DAILY_WILDS"), 0, "daily vars reset")
|
|
end
|
|
|
|
print("[test] 11. every show airs to the end within its text table")
|
|
local SAMPLES = {
|
|
[Tv.TVSHOW_RECENT_HAPPENINGS] = { words = { 1, 2, 3, 4, 5, 6 } },
|
|
[Tv.TVSHOW_PKMN_FAN_CLUB_OPINIONS] = { species = 1, questionAsked = 2, words = { 7, 8 } },
|
|
[Tv.TVSHOW_BRAVO_TRAINER_POKEMON_PROFILE] = { species = 1, pokemonNickname = "BUDDY", contestCategory = 2, contestRank = 1,
|
|
contestResult = 0, move = 33, words = { 5, 6 } },
|
|
[Tv.TVSHOW_BRAVO_TRAINER_BATTLE_TOWER_PROFILE] = { species = 1, numFights = 7, wonTheChallenge = true, btLevel = 50,
|
|
interviewResponse = 1, words = { 9 } },
|
|
[Tv.TVSHOW_CONTEST_LIVE_UPDATES] = { round1Placing = 0, round2Placing = 0, winnerAppealFlag = 128, loserAppealFlag = 4,
|
|
category = 3, winningSpecies = 1, losingSpecies = 2, move = 33, winningTrainerName = "Q", losingTrainerName = "L" },
|
|
[Tv.TVSHOW_3_CHEERS_FOR_POKEBLOCKS] = { sheen = 23, flavor = 2 },
|
|
[Tv.TVSHOW_BATTLE_UPDATE] = { battleType = 2, speciesPlayer = 1, speciesOpponent = 2, move = 33 },
|
|
[Tv.TVSHOW_FAN_CLUB_SPECIAL] = { score = 80, words = { 3 } },
|
|
[Tv.TVSHOW_LILYCOVE_CONTEST_LADY] = { contestCategory = 1, pokeblockState = 0 },
|
|
[Tv.TVSHOW_POKEMON_TODAY_FAILED] = { species = 1, species2 = 2, nBallsUsed = 3, outcome = 1 },
|
|
[Tv.TVSHOW_SMART_SHOPPER] = { itemIds = { 13, 4, 1 }, itemAmounts = { 30, 20, 1 }, priceReduced = 1 },
|
|
[Tv.TVSHOW_FISHING_ADVICE] = { nBites = 5, nFails = 1, species = 129 },
|
|
[Tv.TVSHOW_WORLD_OF_MASTERS] = { steps = 400, numPokeCaught = 21, species = 1, caughtPoke = 2, location = 3 },
|
|
[Tv.TVSHOW_TODAYS_RIVAL_TRAINER] = { location = 3, badgeCount = 8, dexCount = 100, battlePoints = 0 },
|
|
[Tv.TVSHOW_TREND_WATCHER] = { gender = 1, words = { 1, 2 } },
|
|
[Tv.TVSHOW_TREASURE_INVESTIGATORS] = { item = 13, location = 4 },
|
|
[Tv.TVSHOW_FIND_THAT_GAMER] = { won = false, whichGame = 1, nCoins = 120 },
|
|
[Tv.TVSHOW_BREAKING_NEWS] = { outcome = 1, lastUsedMove = 33, lastOpponentSpecies = 2, poke1Species = 1, location = 3 },
|
|
[Tv.TVSHOW_SECRET_BASE_VISIT] = { numDecorations = 4, decorations = { 1, 2, 3, 4 }, avgLevel = 55, species = 1, move = 33 },
|
|
[Tv.TVSHOW_LOTTO_WINNER] = { whichPrize = 1, item = 13 },
|
|
[Tv.TVSHOW_BATTLE_SEMINAR] = { species = 1, foeSpecies = 2, move = 33, otherMoves = { 34, 35 }, nOtherMoves = 2, betterMove = 36 },
|
|
[Tv.TVSHOW_TRAINER_FAN_CLUB] = { words = { 1, 2 }, trainerIdLo = 7, trainerIdHi = 0 },
|
|
[Tv.TVSHOW_CUTIES] = { nRibbons = 12, selectedRibbon = 24 },
|
|
[Tv.TVSHOW_FRONTIER] = { winStreak = 50, facilityAndMode = 2, species1 = 1, species2 = 2, species3 = 3, species4 = 4 },
|
|
[Tv.TVSHOW_NUMBER_ONE] = { actionIdx = 3, count = 25 },
|
|
[Tv.TVSHOW_SECRET_BASE_SECRETS] = { flags = 0x401 + 0x800, stepsInBase = 60, item = 13, baseOwnersName = "MAY" },
|
|
[Tv.TVSHOW_SAFARI_FAN_CLUB] = { monsCaught = 5, pokeblocksUsed = 2 },
|
|
[Tv.TVSHOW_POKEMON_TODAY_CAUGHT] = { species = 1, nickname = "NICK", ball = 4, nBallsUsed = 5 },
|
|
}
|
|
local emitted = {}
|
|
if not haveData then SAMPLES[Tv.TVSHOW_SECRET_BASE_SECRETS] = nil end
|
|
for kind, fields in pairs(SAMPLES) do
|
|
session = newSession()
|
|
local show = session.tvShows[0]
|
|
for k, v in pairs(fields) do show[k] = v end
|
|
show.kind, show.active, show.playerName = kind, true, show.playerName or "QUINCY"
|
|
Tv.resetShowState()
|
|
rolls(1, 2, 0, 1, 2, 0, 1, 2)
|
|
local s2, fin, texts = runShow(session, 0)
|
|
check(fin, "show kind " .. kind .. " reaches TVShowDone (" .. table.concat(s2, ",") .. ")")
|
|
eq(Tv.showState(), 0, "show kind " .. kind .. " resets sTVShowState")
|
|
for _, r in ipairs(texts) do
|
|
if r.group then emitted[#emitted + 1] = { kind = kind, group = r.group, index = r.index } end
|
|
end
|
|
end
|
|
check(#emitted > 60, "sample shows emitted " .. #emitted .. " text pages")
|
|
for state, want in pairs({ [0] = "0,2", [1] = "0,1", [2] = "0,3" }) do
|
|
session = newSession()
|
|
local lady = session.tvShows[0]
|
|
lady.kind, lady.active, lady.pokeblockState, lady.contestCategory = Tv.TVSHOW_LILYCOVE_CONTEST_LADY, true, state, 0
|
|
eq(table.concat(runShow(session, 0), ","), want, "contest lady pokeblock state " .. state
|
|
.. " (pokeemerald/include/constants/lilycove_lady.h:26)")
|
|
end
|
|
eq(Tv.ribbonOf(71), Tv.RIBBON.effort, "MON_DATA_EFFORT_RIBBON (71) -> EFFORT_RIBBON")
|
|
eq(Tv.ribbonOf(67), Tv.RIBBON.champion, "MON_DATA_CHAMPION_RIBBON (67) -> CHAMPION_RIBBON")
|
|
eq(Tv.ribbonOf(52), Tv.RIBBON.cute, "MON_DATA_CUTE_RIBBON (52) -> CUTE_RIBBON_NORMAL")
|
|
eq(Tv.ribbonCount({ ribbons = 7 + 2 ^ 15 + 2 ^ 19 }), 9, "packed ribbon word: cool 7 + champion + effort")
|
|
|
|
print("[test] 12. time hook and FireRed gating")
|
|
local NativesTv = require("src.core.game3.scripting.natives_tv")
|
|
local TimeEvents = require("src.core.game3.time_events")
|
|
check(TimeEvents.handlers().UpdateTVShowsPerDay ~= nil, "UpdateTVShowsPerDay is registered (pokeemerald/src/clock.c:46)")
|
|
local Capabilities = require("src.core.game3.capabilities")
|
|
check(not Capabilities.nativeAllowed({ version = "firered" }, "natives_tv"), "FireRed never binds natives_tv")
|
|
check(NativesTv.BY_NAME.DoTVShow and NativesTv.BY_NAME.InterviewBefore and NativesTv.BY_NAME.InterviewAfter
|
|
and NativesTv.BY_NAME.TryPutNameRaterShowOnTheAir and NativesTv.BY_NAME.GabbyAndTyBeforeInterview, "tv.c specials bound")
|
|
local Rse = require("src.core.game3.rse.init")
|
|
check(Rse.system("tv") == Tv.api, "rse system tv is registered")
|
|
local okTypes, Types = pcall(require, "src.core.game3.rse.easy_chat_types")
|
|
if okTypes then
|
|
local def = Types.get(Tv.EASY_CHAT_TYPE.FAN_CLUB)
|
|
check(def ~= nil and Types.get(Tv.EASY_CHAT_TYPE.GABBY_AND_TY) ~= nil and Types.get(Tv.EASY_CHAT_TYPE.INTERVIEW) ~= nil,
|
|
"TV easy chat types registered")
|
|
session = newSession()
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "", name = "MUDKIP", level = 20, friendship = 200 } }
|
|
Tv.interviewBefore(session, Tv.TVSHOW_PKMN_FAN_CLUB_OPINIONS)
|
|
local ctx = { specialVars = { [0x8005] = Tv._curSlot, [0x8006] = 1 } }
|
|
local w = def.words(ctx, session)
|
|
eq(#w, 1, "FAN_CLUB edits one word")
|
|
def.commit(ctx, session, { 777 })
|
|
eq(session.tvShows[Tv._curSlot].words[2], 777, "FAN_CLUB answer 2 lands in fanclubOpinions.words[1]")
|
|
end
|
|
session = newSession()
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "", name = "MUDKIP", level = 20, ribbons = { effort = true, cool = 4,
|
|
beauty = 2 } } }
|
|
local _, handled = Rse.call("tv", "tryPutSpotTheCutiesOnAir", "test", nil, session.party[1], "effort")
|
|
check(handled, "Rse.call reaches Tv.api")
|
|
eq(session.tvShows[5].kind, Tv.TVSHOW_CUTIES, "Spot the Cuties recorded")
|
|
eq(session.tvShows[5].nRibbons, 7, "ribbon count sums contest ranks (tv.c:2277)")
|
|
eq(session.tvShows[5].selectedRibbon, 24, "EFFORT_RIBBON")
|
|
local SaveSections = require("src.core.game3.save_sections")
|
|
local out = {}
|
|
session.outbreakPokemonSpecies, session.outbreakLocationMapNum, session.outbreakLocationMapGroup = 298, 17, 0
|
|
SaveSections.export(session, out, "emerald")
|
|
eq(out.outbreakLocationMapNum, 17, "save section keeps the cart outbreak fields")
|
|
local restored = { version = "emerald" }
|
|
SaveSections.restore(out, restored, "emerald")
|
|
check(restored.outbreak and restored.outbreak.map == "EM_ROUTE102", "restore rebuilds session.outbreak")
|
|
|
|
local identityText = (function()
|
|
local RomText = require("src.core.game3.rom_text")
|
|
local ok, has = pcall(RomText.has, "sTVNameRaterTextGroup[0]")
|
|
return ok and has
|
|
end)()
|
|
if identityText then
|
|
print("[test] 13. every emitted page exists in the Emerald text cache and renders")
|
|
Tv.names = {}
|
|
local RomText = require("src.core.game3.rom_text")
|
|
local TextIR = require("src.core.game3.scripting.text_ir")
|
|
local missing = 0
|
|
for _, e in ipairs(emitted) do
|
|
local key = RomText.key(e.group, e.index)
|
|
if not RomText.has(key) then
|
|
missing = missing + 1
|
|
print(" missing " .. key .. " for kind " .. e.kind)
|
|
end
|
|
end
|
|
eq(missing, 0, "all emitted TV pages are ROM text")
|
|
local count = RomText.count("sTVNameRaterTextGroup")
|
|
eq(count, 19, "sTVNameRaterTextGroup has 19 pages")
|
|
session = newSession()
|
|
session.party = { { species = sp("SPECIES_MUDKIP"), nickname = "ZAPPY", name = "MUDKIP", level = 20 } }
|
|
rolls(0, 1, 5)
|
|
Tv.tryPutNameRaterShowOnTheAir(session, 0, "MUDKIP")
|
|
sv = { "", "", "" }
|
|
local res = Tv.doTVShow(session, 0, sv)
|
|
local page = TextIR.toPlain(RomText.ir(RomText.key(res.group, res.index)), { stringVars = sv, playerName = "QUINCY" })
|
|
check(page:find("ZAPPY", 1, true) ~= nil, "first Name Rater page names ZAPPY: " .. page:gsub("\n", " "))
|
|
check(page:find("QUINCY", 1, true) ~= nil, "and the trainer")
|
|
else
|
|
print("emerald_tv_test: text section skipped (no Emerald script cache; set POKEPORT_IDENTITY)")
|
|
end
|
|
|
|
T.finish()
|