mirror of
https://github.com/bryanthaboi/gen1recomp.git
synced 2026-08-12 08:21:02 +02:00
435 lines
18 KiB
Lua
435 lines
18 KiB
Lua
-- Text no script pointer reaches: the Day-Care and breeding block, the whole
|
|
-- POKeMART conversation, and the Hall of Fame's three flavour strings.
|
|
--
|
|
-- The extractor's text walker only decodes what some `writetext` names, and
|
|
-- all three of these blocks are printed by engine asm instead, so they used to
|
|
-- exist in the port only as hand transcriptions at their call sites.
|
|
-- RomExtractorGen2's NAMED_TEXT seeds the walker at them by symbol and writes
|
|
-- text.labels[label] -> the "bank:addr" key the string landed on; the screens
|
|
-- read that and keep their transcriptions as the fallback.
|
|
--
|
|
-- This pins both sides against each other, the way tests/gen2_phone_test.lua
|
|
-- pins the phone tables:
|
|
--
|
|
-- PINNED is the cart's own decoded characters, one entry per label.
|
|
-- Every screen is then driven with a text.lua built out of PINNED, and what
|
|
-- it says has to equal what its transcription says -- so a transcription
|
|
-- that drifts from the ROM fails here rather than on screen.
|
|
-- The cache section at the bottom asserts the real import produced exactly
|
|
-- these strings, and SKIPs when there is no Gold cache or when the cache
|
|
-- predates the seed.
|
|
--
|
|
-- ROM-free: `luajit tests/gen2_common_text_test.lua`.
|
|
package.path = "./?.lua;./?/init.lua;" .. package.path
|
|
|
|
-- The UI modules require love-side helpers at load time. Stub the pieces they
|
|
-- touch during construction; nothing here draws.
|
|
love = love or {}
|
|
love.graphics = love.graphics or {
|
|
getColor = function() return 1, 1, 1, 1 end,
|
|
setColor = function() end,
|
|
rectangle = function() end,
|
|
print = function() end,
|
|
printf = function() end,
|
|
draw = function() end,
|
|
newQuad = function() return {} end,
|
|
newImage = function() return nil end,
|
|
getShader = function() return nil end,
|
|
setShader = function() end,
|
|
newShader = function() error("no shaders in this harness") end,
|
|
getDimensions = function() return 160, 144 end,
|
|
push = function() end, pop = function() end,
|
|
translate = function() end, scale = function() end,
|
|
circle = function() end, clear = function() end,
|
|
}
|
|
love.math = love.math or { random = function(a, b) return b and a or 1 end }
|
|
love.image = love.image or {}
|
|
love.filesystem = love.filesystem or {
|
|
load = function() return nil end,
|
|
getInfo = function() return nil end,
|
|
read = function() return nil end,
|
|
write = function() return true end,
|
|
remove = function() return true end,
|
|
}
|
|
love.timer = love.timer or { getTime = function() return 0 end }
|
|
|
|
local S = require("tests.harness").suite("gen2 common text")
|
|
local check, eq = S.check, S.eq
|
|
|
|
require("src.core.Logger").warn = function() end
|
|
|
|
local CommonText = require("src.core.gen2.CommonText")
|
|
local DayCareMenu = require("src.ui.gen2.DayCareMenu")
|
|
local HallOfFame = require("src.ui.gen2.HallOfFame")
|
|
local MartMenu = require("src.ui.gen2.MartMenu")
|
|
|
|
-- The cart's own characters, decoded out of the ROM by the extractor's
|
|
-- decodeGen2Text: \n is `line` / `next`, \f is `para`, \v is `cont` (the box
|
|
-- scrolls one row), {STRBUF} is a TX_RAM name and {NUM} a TX_DECIMAL field.
|
|
-- data/text/common_1.asm and common_2.asm, plus the three `db` strings inside
|
|
-- engine/events/halloffame.asm.
|
|
local PINNED = {
|
|
["_DayCareManIntroText"] =
|
|
"I'm the DAY-CARE\nMAN. Want me to\vraise a POKéMON?",
|
|
["_DayCareManIntroEggText"] =
|
|
"I'm the DAY-CARE\nMAN. Do you know\vabout EGGS?\fI was raising\nPOKéMON with my\vwife, you see.\fWe were shocked to\nfind an EGG!\fHow incredible is\nthat?\fSo, want me to\nraise a POKéMON?",
|
|
["_DayCareLadyIntroText"] =
|
|
"I'm the DAY-CARE\nLADY.\fShould I raise a\nPOKéMON for you?",
|
|
["_DayCareLadyIntroEggText"] =
|
|
"I'm the DAY-CARE\nLADY. Do you know\vabout EGGS?\fMy husband and I\nwere raising some\vPOKéMON, you see.\fWe were shocked to\nfind an EGG!\fHow incredible\ncould that be?\fShould I raise a\nPOKéMON for you?",
|
|
["_WhatShouldIRaiseText"] =
|
|
"What should I\nraise for you?",
|
|
["_OnlyOneMonText"] =
|
|
"Oh? But you have\njust one POKéMON.",
|
|
["_CantAcceptEggText"] =
|
|
"Sorry, but I can't\naccept an EGG.",
|
|
["_RemoveMailText"] =
|
|
"Remove MAIL before\nyou come see me.",
|
|
["_LastHealthyMonText"] =
|
|
"If you give me\nthat, what will\vyou battle with?",
|
|
["_IllRaiseYourMonText"] =
|
|
"OK. I'll raise\nyour {STRBUF}.",
|
|
["_ComeBackLaterText"] =
|
|
"Come back for it\nlater.",
|
|
["_AreWeGeniusesText"] =
|
|
"Are we geniuses or\nwhat? Want to see\vyour {STRBUF}?",
|
|
["_YourMonHasGrownText"] =
|
|
"Your {STRBUF}\nhas grown a lot.\fBy level, it's\ngrown by {NUM}.\fIf you want your\nPOKéMON back, it\vwill cost ¥{NUM}.",
|
|
["_PerfectHeresYourMonText"] =
|
|
"Perfect! Here's\nyour POKéMON.",
|
|
["_GotBackMonText"] =
|
|
"{PLAYER} got back\n{STRBUF}.",
|
|
["_BackAlreadyText"] =
|
|
"Huh? Back already?\nYour {STRBUF}\fneeds a little\nmore time with us.\fIf you want your\nPOKéMON back, it\vwill cost ¥100.",
|
|
["_HaveNoRoomText"] =
|
|
"You have no room\nfor it.",
|
|
["_NotEnoughMoneyText"] =
|
|
"You don't have\nenough money.",
|
|
["_OhFineThenText"] =
|
|
"Oh, fine then.",
|
|
["_ComeAgainText"] =
|
|
"Come again.",
|
|
["_NotYetText"] =
|
|
"Not yet…",
|
|
["_FoundAnEggText"] =
|
|
"Ah, it's you!\fWe were raising\nyour POKéMON, and\fmy goodness, were\nwe surprised!\fYour POKéMON had\nan EGG!\fWe don't know how\nit got there, but\fyour POKéMON had\nit. You want it?",
|
|
["_ReceivedEggText"] =
|
|
"{PLAYER} received\nthe EGG!",
|
|
["_TakeGoodCareOfEggText"] =
|
|
"Take good care of\nit.",
|
|
["_IllKeepItThanksText"] =
|
|
"Well then, I'll\nkeep it. Thanks!",
|
|
["_NoRoomForEggText"] =
|
|
"You have no room\nin your party.\vCome back later.",
|
|
["_BreedEggHatchText"] =
|
|
"{STRBUF} came\nout of its EGG!",
|
|
["_BreedAskNicknameText"] =
|
|
"Give a nickname to\n{STRBUF}?",
|
|
["_LeftWithDayCareManText"] =
|
|
"It's {STRBUF}\nthat was left with\vthe DAY-CARE MAN.",
|
|
["_LeftWithDayCareLadyText"] =
|
|
"It's {STRBUF}\nthat was left with\vthe DAY-CARE LADY.",
|
|
["_BreedBrimmingWithEnergyText"] =
|
|
"It's brimming with\nenergy.",
|
|
["_BreedNoInterestText"] =
|
|
"It has no interest\nin {STRBUF}.",
|
|
["_BreedAppearsToCareForText"] =
|
|
"It appears to care\nfor {STRBUF}.",
|
|
["_BreedFriendlyText"] =
|
|
"It's friendly with\n{STRBUF}.",
|
|
["_BreedShowsInterestText"] =
|
|
"It shows interest\nin {STRBUF}.",
|
|
["_MartWelcomeText"] =
|
|
"Welcome! How may I\nhelp you?",
|
|
["_MartAskMoreText"] =
|
|
"Can I do anything\nelse for you?",
|
|
["_MartComeAgainText"] =
|
|
"Please come again!",
|
|
["_MartHowManyText"] =
|
|
"How many?",
|
|
["_MartFinalPriceText"] =
|
|
"{NUM} {STRBUF}(S)\nwill be ¥{NUM}.",
|
|
["_MartThanksText"] =
|
|
"Here you are.\nThank you!",
|
|
["_MartNoMoneyText"] =
|
|
"You don't have\nenough money.",
|
|
["_MartPackFullText"] =
|
|
"You can't carry\nany more items.",
|
|
["_HerbShopLadyIntroText"] =
|
|
"Hello, dear.\fI sell inexpensive\nherbal medicine.\fThey're good, but\na trifle bitter.\fYour POKéMON may\nnot like them.\fHehehehe…",
|
|
["_HerbalLadyHowManyText"] =
|
|
"How many?",
|
|
["_HerbalLadyFinalPriceText"] =
|
|
"{NUM} {STRBUF}(S)\nwill be ¥{NUM}.",
|
|
["_HerbalLadyThanksText"] =
|
|
"Thank you, dear.\nHehehehe…",
|
|
["_HerbalLadyPackFullText"] =
|
|
"Oh? Your PACK is\nfull, dear.",
|
|
["_HerbalLadyNoMoneyText"] =
|
|
"Hehehe… You don't\nhave the money.",
|
|
["_HerbalLadyComeAgainText"] =
|
|
"Come again, dear.\nHehehehe…",
|
|
["_BargainShopIntroText"] =
|
|
"Hiya! Care to see\nsome bargains?\fI sell rare items\nthat nobody else\fcarries--but only\none of each item.",
|
|
["_BargainShopFinalPriceText"] =
|
|
"{STRBUF} costs\n¥{NUM}. Want it?",
|
|
["_BargainShopThanksText"] =
|
|
"Thanks.",
|
|
["_BargainShopPackFullText"] =
|
|
"Uh-oh, your PACK\nis chock-full.",
|
|
["_BargainShopSoldOutText"] =
|
|
"You bought that\nalready. I'm all\vsold out of it.",
|
|
["_BargainShopNoFundsText"] =
|
|
"Uh-oh, you're\nshort on funds.",
|
|
["_BargainShopComeAgainText"] =
|
|
"Come by again\nsometime.",
|
|
["_PharmacyIntroText"] =
|
|
"What's up? Need\nsome medicine?",
|
|
["_PharmacyHowManyText"] =
|
|
"How many?",
|
|
["_PharmacyFinalPriceText"] =
|
|
"{NUM} {STRBUF}(S)\nwill cost ¥{NUM}.",
|
|
["_PharmacyThanksText"] =
|
|
"Thanks much!",
|
|
["_PharmacyPackFullText"] =
|
|
"You don't have any\nmore space.",
|
|
["_PharmacyNoMoneyText"] =
|
|
"Huh? That's not\nenough money.",
|
|
["_PharmacyComeAgainText"] =
|
|
"All right.\nSee you around.",
|
|
["_NothingToSellText"] =
|
|
"You don't have\nanything to sell.",
|
|
["_MartSellHowManyText"] =
|
|
"How many?",
|
|
["_MartSellPriceText"] =
|
|
"I can pay you\n¥{NUM}.\fIs that OK?",
|
|
["_MartCantBuyText"] =
|
|
"Sorry, I can't buy\nthat from you.",
|
|
["_MartBoughtText"] =
|
|
"Got ¥{NUM} for\n{STRBUF}(S).",
|
|
["AnimateHallOfFame.String_NewHallOfFamer"] =
|
|
"New Hall of Famer!",
|
|
["_HallOfFamePC.TimeFamer"] =
|
|
" -Time Famer",
|
|
["_HallOfFamePC.HOFMaster"] =
|
|
" HOF Master!",
|
|
}
|
|
|
|
-- Text_BreedHuh is the "Huh?" the hatch opens on, and the two empty strings
|
|
-- are real: `text_start / done` with nothing between them. They are seeded
|
|
-- too, so a label with no body is still a label the cache has.
|
|
PINNED["Text_BreedHuh"] = "Huh?\f"
|
|
local EMPTY_LABELS = { "_DaycareDummyText", "_BreedClearboxText" }
|
|
|
|
-- The transcriptions spell the four-tile POKé compression byte as `#`, which
|
|
-- is the same sixteen columns the extractor's expansion draws.
|
|
local function norm(s) return (tostring(s):gsub("#", "POKé")) end
|
|
|
|
-- One eq per page: concatenating the lines compares the line count too.
|
|
local function samePages(got, want, what)
|
|
eq(type(got) == "table" and #got or -1, #want, what .. ": page count")
|
|
for i = 1, #want do
|
|
eq(table.concat((got or {})[i] or {}, "|"),
|
|
norm(table.concat(want[i] or {}, "|")), ("%s: page %d"):format(what, i))
|
|
end
|
|
end
|
|
|
|
-- A text.lua shaped like the extractor's output, built out of PINNED so the
|
|
-- screens can be driven with it. The keys are made up (nothing reads an
|
|
-- address, only text.labels), which is the whole point of the labels table.
|
|
local TEXT = { generation = 2, labels = {} }
|
|
do
|
|
local n = 0
|
|
for label, body in pairs(PINNED) do
|
|
n = n + 1
|
|
local key = ("64:%04x"):format(0x4000 + n)
|
|
TEXT.labels[label] = key
|
|
TEXT[key] = body
|
|
end
|
|
end
|
|
|
|
-- ---- CommonText itself ----------------------------------------------------
|
|
do
|
|
eq(CommonText.get(TEXT, "_MartHowManyText"), "How many?",
|
|
"a label resolves through text.labels to its string")
|
|
check(CommonText.get(TEXT, "_NoSuchText") == nil, "an unknown label is nil")
|
|
check(CommonText.get({}, "_MartHowManyText") == nil,
|
|
"and so is a cache with no labels table at all")
|
|
check(CommonText.of(nil, "_MartHowManyText") == nil, "as is no text at all")
|
|
|
|
-- `line` fills the box's second row, `para` clears it, `cont` scrolls it --
|
|
-- so a cont page opens on the previous page's second line.
|
|
local pages = CommonText.pages("one\ntwo\vthree\ffour")
|
|
eq(#pages, 3, "para and cont each end a page")
|
|
eq(table.concat(pages[1], "|"), "one|two", "line is the second row")
|
|
eq(table.concat(pages[2], "|"), "two|three", "cont scrolls the row up")
|
|
eq(table.concat(pages[3], "|"), "four", "para starts an empty box")
|
|
eq(#CommonText.pages("just one line"), 1, "a bare string is one page")
|
|
|
|
-- The markers are consumed in the order they appear, not by kind: the mart
|
|
-- clerk leads with the quantity and the bargain shop with the item name.
|
|
local price = CommonText.of(TEXT, "_MartFinalPriceText", { 2, "POTION", 600 })
|
|
eq(table.concat(price[1], "|"), "2 POTION(S)|will be ¥600.",
|
|
"{NUM} then {STRBUF} then {NUM}")
|
|
local bargain = CommonText.of(TEXT, "_BargainShopFinalPriceText",
|
|
{ "POTION", 600 })
|
|
eq(table.concat(bargain[1], "|"), "POTION costs|¥600. Want it?",
|
|
"{STRBUF} then {NUM}")
|
|
local egg = CommonText.of(TEXT, "_ReceivedEggText", { player = "GOLD" })
|
|
eq(table.concat(egg[1], "|"), "GOLD received|the EGG!", "{PLAYER} by name")
|
|
-- A marker with no value left prints nothing, the way the cart's freshly
|
|
-- `@`-filled buffer does.
|
|
local unset = CommonText.of(TEXT, "_IllRaiseYourMonText", {})
|
|
eq(table.concat(unset[1], "|"), "OK. I'll raise|your .",
|
|
"an unfilled marker leaves an empty span")
|
|
end
|
|
|
|
-- ---- the Day-Care ---------------------------------------------------------
|
|
--
|
|
-- The six entries that take arguments, with the values their markers name.
|
|
local DAY_CARE_ARGS = {
|
|
deposit = { "NICK" },
|
|
geniuses = { "NICK" },
|
|
hasGrown = { "NICK", 2, 300 },
|
|
backAlready = { "NICK" },
|
|
gotBack = { "GOLD", "NICK" },
|
|
receivedEgg = { "GOLD" },
|
|
}
|
|
do
|
|
local screen = DayCareMenu.new(nil,
|
|
{ save = { daycare = {} }, side = "man", text = TEXT })
|
|
local seen = 0
|
|
for key, label in pairs(DayCareMenu.LABELS) do
|
|
seen = seen + 1
|
|
check(PINNED[label] ~= nil, label .. " is pinned")
|
|
check(rawget(screen.TEXT, key) ~= nil,
|
|
key .. " comes from the cache, not from the transcription")
|
|
local got, want = screen.TEXT[key], DayCareMenu.TEXT[key]
|
|
if type(want) == "function" then
|
|
local args = DAY_CARE_ARGS[key]
|
|
check(args ~= nil, key .. " has arguments pinned")
|
|
got, want = got(unpack(args)), want(unpack(args))
|
|
end
|
|
samePages(got, want, label)
|
|
end
|
|
eq(seen, 26, "every Day-Care string the screen says is extracted")
|
|
|
|
-- With no cache the screen still talks: the transcription is the fallback.
|
|
local bare = DayCareMenu.new(nil, { save = { daycare = {} }, side = "man" })
|
|
eq(bare.TEXT.comeAgain, DayCareMenu.TEXT.comeAgain,
|
|
"and a cache with no labels falls back to the transcription")
|
|
end
|
|
|
|
-- ---- the POKeMART ---------------------------------------------------------
|
|
--
|
|
-- welcome / askMore / howMany are ONE screenful of lines rather than a list of
|
|
-- pages, because a menu sits over them instead of paging them.
|
|
local MART_SINGLE = { welcome = true, askMore = true, howMany = true }
|
|
local MART_ARGS = { finalPrice = { 2, "POTION", 600 } }
|
|
local SELL_ARGS = { price = { 600 }, bought = { "POTION", 600 } }
|
|
do
|
|
local seen = 0
|
|
for kind, labels in pairs(MartMenu.LABELS) do
|
|
if kind ~= "SELL" then
|
|
local screen = MartMenu.new(nil,
|
|
{ martType = kind, text = TEXT, save = { money = 1000 } })
|
|
for key, label in pairs(labels) do
|
|
seen = seen + 1
|
|
check(PINNED[label] ~= nil, label .. " is pinned")
|
|
check(rawget(screen.text, key) ~= nil,
|
|
("%s.%s comes from the cache"):format(kind, key))
|
|
local got, want = screen.text[key], MartMenu.TEXTS[kind][key]
|
|
if type(want) == "function" then
|
|
got, want = got(unpack(MART_ARGS[key])), want(unpack(MART_ARGS[key]))
|
|
elseif MART_SINGLE[key] then
|
|
got, want = { got }, { want }
|
|
end
|
|
samePages(got, want, label)
|
|
end
|
|
end
|
|
end
|
|
|
|
-- SellMenu's own four, which only MARTTYPE_STANDARD ever reaches.
|
|
local standard = MartMenu.new(nil,
|
|
{ martType = "STANDARD", text = TEXT, save = { money = 1000 } })
|
|
local sell = MartMenu.new(nil, { martType = "STANDARD", save = {} })
|
|
for key, label in pairs(MartMenu.LABELS.SELL) do
|
|
seen = seen + 1
|
|
check(PINNED[label] ~= nil, label .. " is pinned")
|
|
check(rawget(standard.sellText, key) ~= nil,
|
|
("SELL.%s comes from the cache"):format(key))
|
|
local got, want = standard.sellText[key], sell.sellText[key]
|
|
if type(want) == "function" then
|
|
got, want = got(unpack(SELL_ARGS[key])), want(unpack(SELL_ARGS[key]))
|
|
elseif MART_SINGLE[key] then
|
|
got, want = { got }, { want }
|
|
end
|
|
samePages(got, want, label)
|
|
end
|
|
eq(seen, 33, "every clerk line the screen says is extracted")
|
|
end
|
|
|
|
-- ---- the Hall of Fame -----------------------------------------------------
|
|
do
|
|
local induct = HallOfFame.headerPlacements("induct", 1, TEXT)
|
|
eq(HallOfFame.at(induct, 1, 2),
|
|
PINNED["AnimateHallOfFame.String_NewHallOfFamer"],
|
|
"the induction header is the cart's own string")
|
|
eq(HallOfFame.at(induct, 1, 2), HallOfFame.NEW_FAMER,
|
|
"and the transcription says the same thing")
|
|
local viewed = HallOfFame.headerPlacements("view", 12, TEXT)
|
|
eq(HallOfFame.at(viewed, 1, 2), PINNED["_HallOfFamePC.TimeFamer"],
|
|
"the PC's header keeps its four leading spaces")
|
|
eq(HallOfFame.at(viewed, 1, 2), HallOfFame.TIME_FAMER, "same transcription")
|
|
-- The count is written OVER those spaces at (2,2), which is why they exist.
|
|
eq(HallOfFame.at(viewed, 2, 2), " 12", "the win count lands on top of them")
|
|
local master = HallOfFame.headerPlacements("view", 100000, TEXT)
|
|
eq(HallOfFame.at(master, 1, 2), PINNED["_HallOfFamePC.HOFMaster"],
|
|
"and the unreachable HOF Master! title is extracted too")
|
|
eq(HallOfFame.at(master, 1, 2), HallOfFame.HOF_MASTER, "same transcription")
|
|
eq(HallOfFame.at(HallOfFame.headerPlacements("induct", 1), 1, 2),
|
|
HallOfFame.NEW_FAMER, "with no cache the constant is the fallback")
|
|
end
|
|
|
|
-- ---- against the cache ----------------------------------------------------
|
|
--
|
|
-- The strings above were transcribed from the decomp; this is what says the
|
|
-- import agrees. A cache built before the NAMED_TEXT seed has no labels
|
|
-- table at all, and needs a re-import rather than a fix here.
|
|
do
|
|
local cacheDir = os.getenv("GOLD_CACHE")
|
|
if not cacheDir then
|
|
cacheDir = (os.getenv("HOME") or "") ..
|
|
"/Library/Application Support/LOVE/gold-dev/gold"
|
|
end
|
|
local textFile = loadfile(cacheDir .. "/data/generated/text.lua")
|
|
if not textFile then
|
|
check(true, "no Gold cache (SKIP)")
|
|
else
|
|
local text = textFile()
|
|
if type(text.labels) ~= "table" then
|
|
check(true, "cache predates the NAMED_TEXT seed; re-import (SKIP)")
|
|
else
|
|
local missing, wrong = {}, {}
|
|
for label, want in pairs(PINNED) do
|
|
local key = text.labels[label]
|
|
if not key then
|
|
missing[#missing + 1] = label
|
|
elseif text[key] ~= want then
|
|
wrong[#wrong + 1] = label
|
|
end
|
|
end
|
|
eq(table.concat(missing, ", "), "", "every seeded label is in the cache")
|
|
eq(table.concat(wrong, ", "), "",
|
|
"and every string matches the transcription exactly")
|
|
for _, label in ipairs(EMPTY_LABELS) do
|
|
check(text.labels[label] ~= nil,
|
|
label .. " is seeded even though it is empty")
|
|
eq(text[text.labels[label]], "", label .. " really is empty")
|
|
end
|
|
end
|
|
end
|
|
end
|
|
|
|
S.finish()
|