Files
gen1recomp/tests/game3_hidden_items_test.lua
T

241 lines
9.8 KiB
Lua

-- Unit tests for Issue #2314: FireRed Hidden Items & Itemfinder
local GameVersion = require("src.core.GameVersion")
GameVersion.set("firered")
local Game3Cache = require("tests.game3_cache")
if not Game3Cache.bundle() then print("[skip] hidden_items: " .. tostring(Game3Cache.reason)) return end
local ExtractMapEvents = require("src.import.gba.extract_map_events")
local Field = require("src.core.game3.field")
local ItemUse = require("src.core.game3.item_use")
local ItemsData = require("src.core.game3.items_data")
local Bag = require("src.core.game3.bag")
local Flags = require("src.core.game3.scripting.flags")
local Message = require("src.ui.game3.message")
print("=== [Test 1: BgEvent parsing for Hidden Items] ===")
-- Mock ROM buffer reader helper
local function create_mock_rom(bytes)
local rom = {}
function rom:get(addr)
return bytes[addr + 1] or 0
end
function rom:u16(addr)
local b0 = self:get(addr)
local b1 = self:get(addr + 1)
return b0 + b1 * 256
end
function rom:u32(addr)
local b0 = self:get(addr)
local b1 = self:get(addr + 1)
local b2 = self:get(addr + 2)
local b3 = self:get(addr + 3)
return b0 + b1 * 256 + b2 * 65536 + b3 * 16777216
end
function rom:ptrOffset(ptr)
if ptr >= 0x08000000 and ptr <= 0x09FFFFFF then
return ptr - 0x08000000
end
return nil
end
return rom
end
-- Create mock BgEvents table in ROM:
-- BgEvent 0: Sign (kind 0) at x=5, y=10, elevation=3, scriptPtr=0x08123456
-- BgEvent 1: Hidden Item (kind 7) at x=12, y=20, elevation=0, item=14 (ANTIDOTE), hiddenItemId=1, quantity=1, underfoot=0
-- BgEvent 2: Hidden Item (kind 7) at x=3, y=4, elevation=0, item=13 (POTION), hiddenItemId=0, quantity=1, underfoot=1
local mock_bytes = {}
for i = 1, 100 do mock_bytes[i] = 0 end
-- Event 0: Sign at offset 0
mock_bytes[1] = 5; mock_bytes[2] = 0 -- x = 5
mock_bytes[3] = 10; mock_bytes[4] = 0 -- y = 10
mock_bytes[5] = 3 -- elevation = 3
mock_bytes[6] = 0 -- kind = 0 (sign)
-- scriptPtr at offset 8 = 0x08000020
mock_bytes[9] = 0x20; mock_bytes[10] = 0x00; mock_bytes[11] = 0x00; mock_bytes[12] = 0x08
-- Event 1: Hidden Antidote at offset 12
mock_bytes[13] = 12; mock_bytes[14] = 0 -- x = 12
mock_bytes[15] = 20; mock_bytes[16] = 0 -- y = 20
mock_bytes[17] = 0 -- elevation = 0
mock_bytes[18] = 7 -- kind = 7 (BG_EVENT_HIDDEN_ITEM)
mock_bytes[21] = 14; mock_bytes[22] = 0 -- item = 14 (ANTIDOTE)
mock_bytes[23] = 0x01; mock_bytes[24] = 0x01
-- Event 2: Hidden Potion at offset 24 (underfoot bit 15 set)
mock_bytes[25] = 3; mock_bytes[26] = 0 -- x = 3
mock_bytes[27] = 4; mock_bytes[28] = 0 -- y = 4
mock_bytes[29] = 0 -- elevation = 0
mock_bytes[30] = 7 -- kind = 7 (BG_EVENT_HIDDEN_ITEM)
mock_bytes[33] = 13; mock_bytes[34] = 0 -- item = 13 (POTION)
mock_bytes[35] = 0x00; mock_bytes[36] = 0x81
local rom = create_mock_rom(mock_bytes)
local mapEventsOff = 0x08000000
-- In GBA MapEvents: bgEvents count is at offset 2 (u8), pointer is at offset 8 (u32)
local eventsHeader = {}
for i = 1, 32 do eventsHeader[i] = 0 end
eventsHeader[1] = 0 -- objCount
eventsHeader[2] = 0 -- warpCount
eventsHeader[3] = 3 -- bgCount = 3
eventsHeader[4] = 0 -- coordCount
-- bgEvents pointer = 0x08000000 (at offset 8 in MapEvents struct: objectsPtr(4), warpsPtr(4), bgEventsPtr(4))
-- actually MapEvents struct in GBA:
-- u8 objectEventCount (0)
-- u8 warpCount (1)
-- u8 coordEventCount (2)
-- u8 bgEventCount (3)
-- u32 objectEvents (4)
-- u32 warps (8)
-- u32 coordEvents (12)
-- u32 bgEvents (16)
eventsHeader[4] = 3 -- bgCount = 3
eventsHeader[17] = 0x00; eventsHeader[18] = 0x00; eventsHeader[19] = 0x00; eventsHeader[20] = 0x08
local full_bytes = {}
for i, b in ipairs(eventsHeader) do full_bytes[i] = b end
for i, b in ipairs(mock_bytes) do full_bytes[#eventsHeader + i] = b end
-- bgEvents pointer at offset 16 points to offset 32 (0x08000020)
full_bytes[17] = 32; full_bytes[18] = 0; full_bytes[19] = 0; full_bytes[20] = 0x08
local romFull = create_mock_rom(full_bytes)
local parsed = ExtractMapEvents.parseMapEvents(romFull, 0x08000000)
assert(parsed ~= nil, "parseMapEvents should succeed")
assert(#parsed.bgEvents == 3, "should parse 3 bgEvents")
local sign = parsed.bgEvents[1]
assert(sign.type == "sign", "bgEvent 1 should be sign")
assert(sign.x == 5 and sign.y == 10, "sign coords match")
local antidote = parsed.bgEvents[2]
assert(antidote.type == "hidden_item", "bgEvent 2 should be hidden_item")
assert(antidote.x == 12 and antidote.y == 20, "antidote coords match")
assert(antidote.item == 14, "antidote item ID is 14")
assert(antidote.hiddenItemId == 1, "hiddenItemId is 1")
assert(antidote.quantity == 1, "quantity is 1")
assert(antidote.underfoot == false, "underfoot is false")
assert(antidote.flag == 0x3E8 + 1, "flag is 0x3E9 (1001)")
local potion = parsed.bgEvents[3]
assert(potion.type == "hidden_item", "bgEvent 3 should be hidden_item")
assert(potion.x == 3 and potion.y == 4, "potion coords match")
assert(potion.item == 13, "potion item ID is 13")
assert(potion.hiddenItemId == 0, "hiddenItemId is 0")
assert(potion.quantity == 1, "quantity is 1")
assert(potion.underfoot == true, "underfoot is true")
assert(potion.flag == 0x3E8 + 0, "flag is 0x3E8 (1000)")
print("[OK] BgEvent parsing correctly extracted hidden items and unpacked union fields.")
for _, hiddenId in ipairs({ 0, 1, 190, 255 }) do
for _, quantity in ipairs({ 1, 2, 3, 63, 64, 127 }) do
for _, underfoot in ipairs({ false, true }) do
full_bytes[32 + 23] = hiddenId
full_bytes[32 + 24] = quantity + (underfoot and 128 or 0)
local event = ExtractMapEvents.parseMapEvents(romFull, 0x08000000).bgEvents[2]
assert(event.hiddenItemId == hiddenId, "quantity must not leak into hidden item flag")
assert(event.flag == 1000 + hiddenId and event.flag < 1256, "hidden flags stay in their eight-bit range")
assert(event.quantity == quantity, "all seven quantity bits must survive extraction")
assert(event.underfoot == underfoot, "underfoot must not change quantity or flag")
end
end
end
full_bytes[32 + 23], full_bytes[32 + 24] = 1, 1
print("PASS hidden_item_flag_quantity_bit_boundaries")
print("=== [Test 2: Hidden Item Field Pickup & Bag Integration] ===")
local game = {
data = {
maps = {
["MAP_VIRIDIAN_FOREST"] = {
bgEvents = parsed.bgEvents
}
}
}
}
local session = {
name = "RED",
map = "MAP_VIRIDIAN_FOREST",
flags = {},
bag = Bag.new(),
playerX = 12,
playerY = 19,
}
Field._session = session
Field._game = game
-- Test hidden item lookup
local hiddenFound = Field.hiddenItemAt(game, 12, 20, 0)
assert(hiddenFound ~= nil, "Should find hidden Antidote at 12, 20")
assert(hiddenFound.item == 14, "Found item is Antidote")
-- Test picking up Antidote
local initialAntidoteCount = Bag.get(session.bag, 14)
assert(initialAntidoteCount == 0, "Bag starts with 0 Antidote")
assert(Flags.getFlag(session, nil, 0x3E9) == false, "Flag 0x3E9 starts false")
local pickedUp = Field.pickUpHiddenItem(game, hiddenFound)
assert(pickedUp == true, "pickUpHiddenItem should return true")
assert(Bag.get(session.bag, 14) == 1, "Bag now has 1 Antidote")
assert(Flags.getFlag(session, nil, 0x3E9) == true, "Flag 0x3E9 is now true")
-- Trying to find or pick up again should fail
local hiddenAgain = Field.hiddenItemAt(game, 12, 20, 0)
assert(hiddenAgain == nil, "Hidden item should no longer be found once flag is set")
local pickedUpAgain = Field.pickUpHiddenItem(game, hiddenFound)
assert(pickedUpAgain == false, "Cannot pick up already collected hidden item")
print("[OK] Hidden item pickup successfully added to bag and marked flag.")
print("=== [Test 3: Itemfinder Scanning Logic] ===")
-- Session at (12, 18), hidden Antidote (12, 20) is already collected (flag set),
-- hidden Potion is at (3, 4).
-- Distance from (12, 18) to Potion (3, 4) is dx = -9, dy = -14 (out of range > 7).
session.playerX = 12
session.playerY = 18
local ok, kind, text = ItemUse.useField(session, session.bag, ItemsData.ITEM_ITEMFINDER, nil)
assert(ok == false, "Itemfinder should not respond when out of range")
assert(text == "… … … …Nope!\nThere's no response.", "Response text matches pret nope string")
-- pokefirered/src/itemfinder.c:216
Flags.setFlag(session, nil, 0x3E9, false)
local okNear, kindNear, textNear = ItemUse.useField(session, session.bag, ItemsData.ITEM_ITEMFINDER, nil)
assert(okNear == true, "Itemfinder should detect nearby hidden item")
assert(textNear == "Huh?\nThe ITEMFINDER's responding!\fThere's an item buried around here!", "Nearby text matches pret")
Flags.setFlag(session, nil, 0x3E9, true)
-- pokefirered/src/itemfinder.c:213
session.playerX = 5
session.playerY = 6
local okAway = ItemUse.useField(session, session.bag, ItemsData.ITEM_ITEMFINDER, nil)
assert(okAway == false, "An underfoot item only answers when the player stands on it")
session.playerX = 3
session.playerY = 4
local okFeet, kindFeet, textFeet = ItemUse.useField(session, session.bag, ItemsData.ITEM_ITEMFINDER, nil)
assert(okFeet == true, "Itemfinder should detect underfoot hidden item")
assert(textFeet == "Oh!\nThe ITEMFINDER's shaking wildly!\fThere's an item buried underfoot!\f… … … … … …", "Underfoot text matches pret")
local hiddenPotion = Field.hiddenItemAt(game, 3, 4, 0)
assert(hiddenPotion ~= nil, "Should find hidden Potion at 3, 4")
assert(Field.digUpUnderfootItem(game, hiddenPotion), "the Itemfinder digs the underfoot item up")
assert(Bag.get(session.bag, 13) == 1, "Bag now has 1 Potion")
assert(Flags.getFlag(session, nil, 0x3E8) == true, "Flag 0x3E8 is now true")
Message.close()
local okDone, kindDone, textDone = ItemUse.useField(session, session.bag, ItemsData.ITEM_ITEMFINDER, nil)
assert(okDone == false, "Itemfinder has no response after items collected")
assert(textDone == "… … … …Nope!\nThere's no response.")
print("[OK] Itemfinder detects underfoot, nearby, and no-response cases with accurate FireRed text.")
print("=== All Issue #2314 Hidden Item & Itemfinder tests passed! ===")