mirror of
https://github.com/bryanthaboi/gen1recomp
synced 2026-10-08 09:42:55 -04:00
Merge pull request #2722 from bryanthaboi/dev
Gen 3 online: all versions on the relay, event catalog, link room pre…
This commit is contained in:
@@ -16,6 +16,10 @@ Features intentionally added to the games themselves, beyond what the original c
|
||||
* **Pokédex diploma and printer image exports**
|
||||
* **Fast-forward locks to 1X in link play**, every link or online battle, link session and the FireRed/LeafGreen Union Room on every game
|
||||
* **40-player Union Room** on FireRed and LeafGreen online, a larger square room where every trainer keeps a fixed spot and anyone can be talked to
|
||||
* **Name badges over trainers** in Gen 3 link rooms (Union Room, Trade Center, Colosseum, Record Corner, Berry Blender), your own included
|
||||
* **Chat, trade and battle icons** on those badges, so a busy trainer is visible at a glance
|
||||
* **Idle Union Room trainers wander**, a few steps around their spot and back
|
||||
* **Translation mods stay online** on Gen 3: mods that only change text, fonts, names or visuals can still link, trade and battle
|
||||
* **Button remapping** on every game: OPTION -> CONTROLS (keys, pads, L/R on FireRed/LeafGreen/Emerald, fast-forward buttons)
|
||||
|
||||
## Gen 2 Specifics
|
||||
@@ -24,4 +28,6 @@ Features intentionally added to the games themselves, beyond what the original c
|
||||
* **Battle screen options** on Gold, Silver and Crystal: BATTLE SIZE (fixed or window-filling) and BATTLE BG (white, black or the dimmed map as the surround)
|
||||
* **Widescreen battle layout** on Gold, Silver and Crystal: BATTLE LAYOUT -> WIDE spreads the HUDs and bottom windows across a 304-wide screen
|
||||
* **Extended battle HUD** on Gold, Silver and Crystal: BATTLE HUD -> EXTENDED docks the WIDE layout's foe panel to the top of the window and the text area and player panel to the bottom
|
||||
* **Event tickets** on Emerald: OPTION -> EXTRAS -> EVENT TICKETS lets the Lilycove Mystery Gift man hand out the Eon, Aurora and Mystic tickets and the Old Sea Map
|
||||
* **Event tickets** on Emerald: OPTION -> EXTRAS -> EVENT TICKETS lets the Lilycove Mystery Gift man hand out whichever of the Eon, Aurora and Mystic tickets and the Old Sea Map the relay is currently distributing
|
||||
* **Relay Mystery Events** on Ruby/Sapphire: MYSTERY EVENTS loads signed e-Reader events (Eon Ticket, gift ribbon) from the relay instead of a link partner
|
||||
* **Wonder News on Ruby/Sapphire**: relay Wonder News is listed and saved under MYSTERY EVENTS, so every Gen 3 game reads the same news
|
||||
|
||||
+42
-5
@@ -14,6 +14,44 @@ Events.ROWS = {
|
||||
{ id="oldSeaMap",name="Old Sea Map",item="ITEM_OLD_SEA_MAP",versions={emerald={[1]=true}},
|
||||
card="rse_old_sea_map",route="Faraway Island",method="Wonder Card" },
|
||||
}
|
||||
Events.FAMILY={firered="frlg",leafgreen="frlg",emerald="rse",ruby="rs",sapphire="rs"}
|
||||
Events._feeds,Events._jobs={},{}
|
||||
function Events.setFeed(version,list) Events._feeds[version]=list end
|
||||
function Events.poll(version,opts)
|
||||
local family=Events.FAMILY[version]
|
||||
if not family then return "error","no_family" end
|
||||
if Events._feeds[version] then return "ok",Events._feeds[version] end
|
||||
local Gift=require("src.core.game3.mystery_gift")
|
||||
local job=Events._jobs[version]
|
||||
if not job then
|
||||
opts=opts or {}
|
||||
job=Gift.fetchOnline({family=family,version=version,transport=opts.transport,client=opts.client})
|
||||
Events._jobs[version]=job
|
||||
end
|
||||
local status,result=Gift.pollOnline(job)
|
||||
if status=="pending" then return status end
|
||||
Events._jobs[version]=nil
|
||||
if status=="ok" then Events._feeds[version]=result end
|
||||
return status,result
|
||||
end
|
||||
local function relayOffer(save,row)
|
||||
local status,list=Events.poll(save.version)
|
||||
if status=="pending" then return nil,"Checking the relay for tickets. Try again in a moment." end
|
||||
if status~="ok" then return nil,"The relay could not be reached, so no tickets can be received." end
|
||||
local Gift=require("src.core.game3.mystery_gift")
|
||||
local item=require("src.core.game3.constants").of(save.version):require("items",row.item)
|
||||
if Events.FAMILY[save.version]=="rs" then
|
||||
for _,ev in ipairs(list.events or {}) do
|
||||
if Gift.eventRecordMixingItem(ev.bytes)==item then return ev end
|
||||
end
|
||||
else
|
||||
for _,entry in ipairs(list.cards or {}) do
|
||||
if entry.card.gift and entry.card.gift.kind=="item" and entry.card.gift.item==item then return entry end
|
||||
end
|
||||
end
|
||||
return nil,"The relay is not offering this ticket right now."
|
||||
end
|
||||
Events.relayOffer=relayOffer
|
||||
function Events.allowed(id,version,language)
|
||||
for _,row in ipairs(Events.ROWS) do
|
||||
if row.id==id then return row.versions[version] and row.versions[version][language] and row or nil end
|
||||
@@ -22,6 +60,7 @@ end
|
||||
function Events.list(version,language)
|
||||
local out={}
|
||||
for _,row in ipairs(Events.ROWS) do if Events.allowed(row.id,version,language) then out[#out+1]=row end end
|
||||
if #out>0 and Events.autoFetch~=false then pcall(Events.poll,version) end
|
||||
return out
|
||||
end
|
||||
local function session(save)
|
||||
@@ -67,6 +106,8 @@ function Events.receive(save,id,language)
|
||||
if not status then return nil,why end
|
||||
if status~="Available" then return nil,"This event has already been received." end
|
||||
return Context.withItems(save.version,function()
|
||||
local offer,offerWhy=relayOffer(save,row)
|
||||
if not offer then return nil,offerWhy end
|
||||
local out=Store.copy(save)
|
||||
local s=session(out)
|
||||
local C=require("src.core.game3.constants").of(save.version)
|
||||
@@ -85,11 +126,7 @@ function Events.receive(save,id,language)
|
||||
local flag=Gift.receivedGiftFlag(existing.flagId,s)
|
||||
if flag and not Gift.getFlag(s,flag) then return nil,"Collect your existing Wonder Card gift before receiving another card." end
|
||||
end
|
||||
local family=save.version=="emerald" and "rse" or "frlg"
|
||||
local key=family=="rse" and (row.rseCard or row.card) or row.card
|
||||
local card
|
||||
for _,candidate in ipairs(Gift.builtins(family)) do if candidate.key==key then card=candidate.card end end
|
||||
if not card or not Gift.receiveCard(s,card) then return nil,"The game's ticket delivery card could not be prepared." end
|
||||
if not Gift.receiveCard(s,offer.card) then return nil,"The game's ticket delivery card could not be prepared." end
|
||||
end
|
||||
apply(s,out);return out
|
||||
end)
|
||||
|
||||
@@ -1812,6 +1812,14 @@ function FieldView.draw(game, canvasW, canvasH, opts)
|
||||
end
|
||||
end
|
||||
|
||||
if underActors or overActors then
|
||||
local LinkTags = package.loaded["src.ui.game3.link_tags"]
|
||||
if LinkTags and LinkTags.draw then
|
||||
LinkTags.draw(underActors, overActors, camX, camY, canvasW, canvasH,
|
||||
FieldView._billboard and pushBillboard or nil)
|
||||
end
|
||||
end
|
||||
|
||||
-- pokefirered/src/field_effect.c:1024
|
||||
if not opts.actorsOnly then
|
||||
local Heal = modHeal()
|
||||
|
||||
@@ -29,6 +29,9 @@ LB.LINKTYPE = {
|
||||
SINGLE_BATTLE = 0x2233,
|
||||
DOUBLE_BATTLE = 0x2244,
|
||||
MULTI_BATTLE = 0x2255,
|
||||
-- pokeemerald/include/link.h:95
|
||||
BATTLE_TOWER_50 = 0x2266,
|
||||
BATTLE_TOWER_OPEN = 0x2277,
|
||||
RECORD_MIX_BEFORE = 0x3311,
|
||||
BERRY_BLENDER_SETUP = 0x4411,
|
||||
}
|
||||
@@ -1619,9 +1622,59 @@ function LB.playerCount()
|
||||
return 2
|
||||
end
|
||||
|
||||
-- pokefirered/src/cable_club.c:222 CreateLinkupTask
|
||||
local function relayPeers(live)
|
||||
local out = {}
|
||||
for seat, hello in pairs(type(live.peerHellos) == "table" and live.peerHellos or {}) do
|
||||
local g3 = type(hello) == "table" and type(hello.game3) == "table" and hello.game3 or {}
|
||||
out[#out + 1] = { seat = seat, linkType = tonumber(g3.linkType), version = tonumber(g3.gameVersion),
|
||||
progressFlags = tonumber(g3.progressFlags) or 0 }
|
||||
end
|
||||
table.sort(out, function(a, b) return a.seat < b.seat end)
|
||||
return out
|
||||
end
|
||||
|
||||
-- pokeemerald/src/link.c:817 GetLinkPlayerDataExchangeStatusTimed
|
||||
function LB.linkupStatus(ctx, spec, peers, relay)
|
||||
local L = link()
|
||||
local Family = require("src.core.game3.link.family")
|
||||
local CableEntry = require("src.core.game3.link.cable_entry")
|
||||
local players = LB.playerCount()
|
||||
-- pokeemerald/src/link.c:830
|
||||
if players < spec.min or players > spec.max then return L.LINKUP.WRONG_NUM_PLAYERS end
|
||||
local want = spec.linkType
|
||||
if relay then want = CableEntry.classOf(spec.linkType) end
|
||||
for _, peer in ipairs(peers) do
|
||||
local got = tonumber(peer.linkType)
|
||||
if relay then got = CableEntry.classOf(peer.linkType) end
|
||||
if (relay and want ~= nil and got ~= nil and got ~= want) or (not relay and got ~= want) then
|
||||
-- pokeemerald/src/link.c:876
|
||||
local towers = { battle_tower = true, battle_tower_open = true }
|
||||
if towers[CableEntry.classOf(spec.linkType)] and towers[CableEntry.classOf(peer.linkType)] then
|
||||
L.setVar(ctx, LB.VAR_0x8005, 3)
|
||||
end
|
||||
return L.LINKUP.DIFF_SELECTIONS
|
||||
end
|
||||
end
|
||||
local peer = peers[1]
|
||||
if spec.linkType == require("src.link.Game3Link").LINKTYPE.TRADE_SETUP and peer and peer.version ~= nil then
|
||||
local mine = Family.localLinkPlayer(L.session())
|
||||
-- pokeemerald/src/link.c:853
|
||||
local code = Family.gameProgressForLinkTrade(mine.family, mine, peer)
|
||||
if code == Family.TRADE.PLAYER_NOT_READY then return L.LINKUP.PLAYER_NOT_READY end
|
||||
if code == Family.TRADE.PARTNER_NOT_READY then return L.LINKUP.PARTNER_NOT_READY end
|
||||
end
|
||||
return L.LINKUP.SUCCESS
|
||||
end
|
||||
|
||||
-- pokefirered/src/cable_club.c:76
|
||||
function LB.createLinkupTask(ctx, adapters, spec)
|
||||
local L = link()
|
||||
local lk = L.link
|
||||
if not (lk and lk.isOpen and lk:isOpen()) then
|
||||
return require("src.core.game3.link.cable_entry").run(ctx, adapters, spec, function(c, a)
|
||||
return LB.createLinkupTask(c, a, spec)
|
||||
end)
|
||||
end
|
||||
local function report(code)
|
||||
LB.linkup = code
|
||||
L.setResult(ctx, code)
|
||||
@@ -1630,22 +1683,13 @@ function LB.createLinkupTask(ctx, adapters, spec)
|
||||
report(L.LINKUP.ONGOING)
|
||||
LB.state = "linkup"
|
||||
LB.seed = nil
|
||||
local announced = false
|
||||
local lk = L.link
|
||||
local Family = require("src.core.game3.link.family")
|
||||
local mine = Family.localLinkPlayer(L.session())
|
||||
local function linkupMsg()
|
||||
return { type = LB.MSG.LINKUP, linkType = spec.linkType, players = spec.min,
|
||||
version = mine.version, progressFlags = mine.progressFlags }
|
||||
end
|
||||
if lk then
|
||||
lk.linkType = spec.linkType
|
||||
-- pokefirered/src/cable_club.c:318 Task_LinkupExchangeDataWithLeader
|
||||
lk:send(linkupMsg())
|
||||
announced = true
|
||||
else
|
||||
-- pokefirered/src/cable_club.c:222 CreateLinkupTask waits for the other machine
|
||||
L.beginConnect({ linkType = spec.linkType })
|
||||
local relay = LB.onRelay()
|
||||
lk.linkType = spec.linkType
|
||||
if not relay then
|
||||
local mine = require("src.core.game3.link.family").localLinkPlayer(L.session())
|
||||
-- pokefirered/src/cable_club.c:318
|
||||
lk:send({ type = LB.MSG.LINKUP, linkType = spec.linkType, players = spec.min,
|
||||
version = mine.version, progressFlags = mine.progressFlags })
|
||||
end
|
||||
local ticks = 0
|
||||
local Natives = natives()
|
||||
@@ -1658,41 +1702,20 @@ function LB.createLinkupTask(ctx, adapters, spec)
|
||||
ctx.nativePoll = function()
|
||||
ticks = ticks + 1
|
||||
local live = L.link
|
||||
if live and not announced then
|
||||
live.linkType = spec.linkType
|
||||
live:send(linkupMsg())
|
||||
announced = true
|
||||
end
|
||||
if not announced then
|
||||
-- pokefirered/src/cable_club.c:482 TryLinkTimeout
|
||||
if ticks > LB.LINKUP_TICKS then
|
||||
report(L.LINKUP.CONNECTION_ERROR)
|
||||
LB.state = "off"
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
local peer = live and live:isReady() and live:take(LB.MSG.LINKUP) or nil
|
||||
if peer then
|
||||
local players = LB.playerCount()
|
||||
if tonumber(peer.linkType) ~= spec.linkType then
|
||||
-- pokefirered/src/cable_club.c:122 EXCHANGE_DIFF_SELECTIONS
|
||||
report(L.LINKUP.DIFF_SELECTIONS)
|
||||
LB.state = "off"
|
||||
elseif players < spec.min or players > spec.max then
|
||||
-- pokefirered/src/cable_club.c:127 EXCHANGE_WRONG_NUM_PLAYERS
|
||||
report(L.LINKUP.WRONG_NUM_PLAYERS)
|
||||
LB.state = "off"
|
||||
elseif spec.linkType == require("src.link.Game3Link").LINKTYPE.TRADE_SETUP and peer.version ~= nil
|
||||
and Family.gameProgressForLinkTrade(mine.family, mine, peer) ~= Family.TRADE.BOTH_PLAYERS_READY then
|
||||
-- pokeemerald/src/link.c:853
|
||||
local code = Family.gameProgressForLinkTrade(mine.family, mine, peer)
|
||||
report(code == Family.TRADE.PLAYER_NOT_READY and L.LINKUP.PLAYER_NOT_READY or L.LINKUP.PARTNER_NOT_READY)
|
||||
LB.state = "off"
|
||||
local peers
|
||||
if live and live:isOpen() and live:isReady() then
|
||||
if relay then
|
||||
peers = relayPeers(live)
|
||||
else
|
||||
report(L.LINKUP.SUCCESS)
|
||||
LB.state = "seat"
|
||||
local peer = live:take(LB.MSG.LINKUP)
|
||||
if peer then
|
||||
peers = { { linkType = tonumber(peer.linkType), version = peer.version, progressFlags = peer.progressFlags } }
|
||||
end
|
||||
end
|
||||
end
|
||||
if peers then
|
||||
local code = report(LB.linkupStatus(ctx, spec, peers, relay))
|
||||
LB.state = code == L.LINKUP.SUCCESS and "seat" or "off"
|
||||
return true
|
||||
end
|
||||
if not (live and live:isOpen()) then
|
||||
@@ -1718,9 +1741,19 @@ function LB.tryBattleLinkup(ctx, adapters)
|
||||
local mode = L.getVar(ctx, L.VAR_0x8004)
|
||||
LB.mode = mode
|
||||
LB.unionRoom = false
|
||||
if mode == L.USING.BATTLE_TOWER then return LB.createLinkupTask(ctx, adapters, LB.towerSpec()) end
|
||||
return LB.createLinkupTask(ctx, adapters, LB.PLAYERS[mode] or LB.PLAYERS[1])
|
||||
end
|
||||
|
||||
-- pokeemerald/src/cable_club.c:591
|
||||
function LB.towerSpec()
|
||||
local okU, Util = pcall(require, "src.core.game3.rse.frontier.util")
|
||||
local okD, D = pcall(require, "src.core.game3.rse.frontier.trainers")
|
||||
local f = okU and Util.frontier and Util.frontier(session()) or nil
|
||||
local open = okD and type(f) == "table" and D.LVL and f.lvlMode == D.LVL.OPEN
|
||||
return { min = 2, max = 2, linkType = open and LB.LINKTYPE.BATTLE_TOWER_OPEN or LB.LINKTYPE.BATTLE_TOWER_50 }
|
||||
end
|
||||
|
||||
-- pokefirered/src/script_pokemon_util.c:90 HasEnoughMonsForDoubleBattle
|
||||
function LB.hasEnoughMonsForDoubleBattle(ctx)
|
||||
local Party = require("src.core.game3.party")
|
||||
|
||||
@@ -0,0 +1,330 @@
|
||||
local Link = require("src.core.game3.link.init")
|
||||
local Strings = require("src.core.Strings")
|
||||
|
||||
local CableEntry = {}
|
||||
|
||||
-- pokeemerald/include/link.h:86
|
||||
CableEntry.WIRE = {
|
||||
[0x1133] = "trade",
|
||||
[0x2233] = "battle_single",
|
||||
[0x2244] = "battle_double",
|
||||
[0x2255] = "battle_multi",
|
||||
[0x2266] = "battle_tower",
|
||||
[0x2277] = "battle_tower_open",
|
||||
[0x3311] = "record_corner",
|
||||
[0x4411] = "berry_blender",
|
||||
[0x5501] = "mystery_event",
|
||||
}
|
||||
-- pokeemerald/include/link.h:105
|
||||
CableEntry.CONTEST = { [0x6601] = true, [0x6602] = true }
|
||||
-- pokeemerald/include/constants/union_room.h:73
|
||||
CableEntry.CONTEST_WIRES = { [0] = "contest_cool", "contest_beauty", "contest_cute", "contest_smart", "contest_tough" }
|
||||
CableEntry.DIRECT = {
|
||||
battle_single = true, battle_double = true, battle_multi = true, trade = true,
|
||||
record_corner = true, berry_blender = true, contest_cool = true, contest_beauty = true,
|
||||
contest_cute = true, contest_smart = true, contest_tough = true,
|
||||
}
|
||||
CableEntry.CONNECT_TICKS = 1200
|
||||
-- pokefirered/src/cable_club.c:483
|
||||
CableEntry.READY_TICKS = 600
|
||||
CableEntry.LOST_TICKS = 120
|
||||
CableEntry.AWAY_TICKS = 600
|
||||
|
||||
local function contestCategory(ctx)
|
||||
local ok, id = pcall(function()
|
||||
return require("src.core.game3.constants").active(Link.session()):var("VAR_CONTEST_CATEGORY")
|
||||
end)
|
||||
return Link.getVar(ctx, ok and id or 0x8011)
|
||||
end
|
||||
|
||||
function CableEntry.classOf(linkType)
|
||||
linkType = tonumber(linkType)
|
||||
if CableEntry.CONTEST[linkType] then return "contest" end
|
||||
return CableEntry.WIRE[linkType]
|
||||
end
|
||||
|
||||
function CableEntry.wireFor(spec, ctx)
|
||||
if type(spec) ~= "table" then return nil end
|
||||
if spec.wire then return spec.wire end
|
||||
local linkType = tonumber(spec.linkType)
|
||||
if CableEntry.CONTEST[linkType] then
|
||||
return CableEntry.CONTEST_WIRES[contestCategory(ctx)]
|
||||
end
|
||||
return CableEntry.WIRE[linkType]
|
||||
end
|
||||
|
||||
function CableEntry.matched(room, stale)
|
||||
return type(room) == "table" and room.stage == "battling" and room.match ~= nil
|
||||
and (stale == nil or room.match ~= stale)
|
||||
end
|
||||
|
||||
local function protocol()
|
||||
return require("src.online.Protocol2")
|
||||
end
|
||||
|
||||
-- pokefirered/src/cable_club.c:76
|
||||
function CableEntry.run(ctx, adapters, spec, handshake)
|
||||
assert(type(spec) == "table", "cable service missing")
|
||||
local live = Link.link
|
||||
if live and live.isOpen and live:isOpen() then return handshake(ctx, adapters) end
|
||||
local wire = CableEntry.wireFor(spec, ctx)
|
||||
if not wire then
|
||||
Link.setResult(ctx, Link.LINKUP.FAILED)
|
||||
return false, Link.LINKUP.FAILED
|
||||
end
|
||||
local N = require("src.core.game3.scripting.natives")
|
||||
local UI = require("src.ui.game3.rs.cable_lobby")
|
||||
local P = protocol()
|
||||
local watchDirect = CableEntry.DIRECT[wire] == true
|
||||
local ruleset = P.ACTIVITY_RULESET[wire]
|
||||
local min, max = tonumber(spec.min) or 2, tonumber(spec.max) or 2
|
||||
local autoLead = min == max
|
||||
local phase, ticks, done, role, pending, answered = "connect", 0, false, nil, nil, {}
|
||||
local seenGroup, goneTicks, awayTicks, joining = false, 0, 0, nil
|
||||
local avatar = spec.avatar or Link.avatar()
|
||||
local profile = spec.profile
|
||||
local room0 = Link.clientCall("room")
|
||||
local stale = type(room0) == "table" and room0.match or nil
|
||||
local function unwatch()
|
||||
Link.clientCall("groupList", nil)
|
||||
if watchDirect then Link.clientCall("directList", nil) end
|
||||
end
|
||||
local function finish(code)
|
||||
if joining then Link.clientCall("leaveRoom") end
|
||||
Link.clientCall("leaveGroup")
|
||||
unwatch()
|
||||
Link.closeLink("cable_entry_failed")
|
||||
UI.close()
|
||||
Link.setStatus("busy")
|
||||
Link.setResult(ctx, code)
|
||||
done = true
|
||||
end
|
||||
local function offline(member)
|
||||
if type(member) ~= "table" then return false end
|
||||
if member.online == false then return true end
|
||||
for _, entry in ipairs(Link.clientCall("lobby") or {}) do
|
||||
if type(entry) == "table" and entry.id ~= nil and entry.id == member.id then return entry.online == false end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function liveMembers(group)
|
||||
local members = type(group) == "table" and type(group.members) == "table" and group.members or {}
|
||||
local out = {}
|
||||
for _, member in ipairs(members) do
|
||||
if not offline(member) then out[#out + 1] = member end
|
||||
end
|
||||
return out, #out == #members
|
||||
end
|
||||
local function joinRows()
|
||||
local out = {}
|
||||
for _, g in ipairs(Link.clientCall("groups", wire) or {}) do
|
||||
if type(g) == "table" and g.leader ~= nil then
|
||||
local av = g.avatar or {}
|
||||
out[#out + 1] = { kind = "group", id = g.leader, label = av.name or g.name or "",
|
||||
disabled = (tonumber(g.joined) or 0) >= (tonumber(g.max) or max) }
|
||||
end
|
||||
end
|
||||
if watchDirect then
|
||||
for _, e in ipairs(Link.clientCall("directEntries", wire) or {}) do
|
||||
if type(e) == "table" and e.kind == "room" and e.room ~= nil then
|
||||
local av = type(e.avatar) == "table" and e.avatar or {}
|
||||
out[#out + 1] = { kind = "room", id = e.room, label = av.name or e.name or "",
|
||||
disabled = e.locked == true }
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local function rows()
|
||||
if phase == "mode" then
|
||||
return { { id = "leader", label = Strings("CREATE GROUP") }, { id = "join", label = Strings("JOIN GROUP") } }
|
||||
end
|
||||
if phase == "ask" then return { { id = "yes", label = Strings("YES") }, { id = "no", label = Strings("NO") } } end
|
||||
if role == "leader" then
|
||||
local out = {}
|
||||
local members, all = liveMembers(Link.clientCall("group"))
|
||||
for _, member in ipairs(members) do
|
||||
local av = member.avatar or {}
|
||||
out[#out + 1] = { label = av.name or member.name or "", disabled = true }
|
||||
end
|
||||
out[#out + 1] = { id = "start", label = Strings("START"), disabled = not all or #members < min or #members > max }
|
||||
return out
|
||||
end
|
||||
if phase == "list" then return joinRows() end
|
||||
return {}
|
||||
end
|
||||
local function select(row)
|
||||
if phase == "mode" then
|
||||
role, phase = row.id, "list"
|
||||
if role == "leader" then
|
||||
Link.clientCall("openGroup", wire, profile, avatar)
|
||||
else
|
||||
Link.clientCall("groupList", wire, profile)
|
||||
if watchDirect then Link.clientCall("directList", wire, profile, avatar) end
|
||||
end
|
||||
elseif phase == "ask" then
|
||||
answered[pending.id] = true
|
||||
Link.clientCall("acceptGroup", pending.id, row.id == "yes")
|
||||
pending, phase = nil, "list"
|
||||
elseif role == "leader" and row.id == "start" then
|
||||
Link.clientCall("startGroup")
|
||||
phase, ticks = "starting", 0
|
||||
elseif role == "join" and row.kind == "room" then
|
||||
joining = Link.clientCall("joinRoom", row.id, "player", profile, nil)
|
||||
phase, ticks = "joining", 0
|
||||
elseif role == "join" then
|
||||
Link.clientCall("joinGroup", row.id, profile, avatar)
|
||||
phase, ticks, seenGroup, goneTicks = "waiting", 0, false, 0
|
||||
end
|
||||
end
|
||||
local function enter()
|
||||
profile = profile or Link.liveProfile(ruleset)
|
||||
Link.setStatus("idle")
|
||||
phase, ticks = "mode", 0
|
||||
UI.cursor = 1
|
||||
end
|
||||
local function leaderTick(g)
|
||||
local requests = type(g) == "table" and type(g.pending) == "table" and g.pending or {}
|
||||
if phase == "ask" then
|
||||
local still = false
|
||||
for _, request in ipairs(requests) do
|
||||
if pending and request.id == pending.id and not offline(request) then still = true end
|
||||
end
|
||||
if not still then
|
||||
if pending then answered[pending.id] = true end
|
||||
pending, phase = nil, "list"
|
||||
UI.cursor = 1
|
||||
end
|
||||
elseif phase == "list" then
|
||||
local members, all = liveMembers(g)
|
||||
for _, request in ipairs(requests) do
|
||||
if not answered[request.id] and not offline(request) then
|
||||
if autoLead then
|
||||
-- pokeemerald/src/cable_club.c:225
|
||||
answered[request.id] = true
|
||||
Link.clientCall("acceptGroup", request.id, #members < max)
|
||||
else
|
||||
pending, phase = request, "ask"
|
||||
end
|
||||
break
|
||||
end
|
||||
end
|
||||
if phase == "list" and autoLead and all and #members >= max then
|
||||
Link.clientCall("startGroup")
|
||||
phase, ticks = "starting", 0
|
||||
end
|
||||
elseif phase == "starting" and type(g) == "table" then
|
||||
local members, all = liveMembers(g)
|
||||
if not all or #members < min then phase, ticks = "list", 0 end
|
||||
end
|
||||
end
|
||||
local function joinTick(g)
|
||||
if phase == "joining" then
|
||||
local p = joining
|
||||
if type(p) ~= "table" or (p.done and p.reason ~= nil) then
|
||||
joining, phase = nil, "list"
|
||||
UI.cursor = 1
|
||||
end
|
||||
return
|
||||
end
|
||||
if phase ~= "waiting" then return end
|
||||
if type(g) == "table" then
|
||||
seenGroup, goneTicks = true, 0
|
||||
local leader
|
||||
for _, member in ipairs(type(g.members) == "table" and g.members or {}) do
|
||||
if member.id == g.leader then leader = member end
|
||||
end
|
||||
awayTicks = offline(leader) and awayTicks + 1 or 0
|
||||
if awayTicks > CableEntry.AWAY_TICKS then return finish(Link.LINKUP.CONNECTION_ERROR) end
|
||||
ticks = 0
|
||||
elseif seenGroup then
|
||||
goneTicks = goneTicks + 1
|
||||
if goneTicks > CableEntry.LOST_TICKS then
|
||||
phase, seenGroup, goneTicks = "list", false, 0
|
||||
UI.cursor = 1
|
||||
end
|
||||
end
|
||||
end
|
||||
if not Link.online() then
|
||||
local ok = (spec.connect or Link.connect)()
|
||||
if not ok then
|
||||
finish(Link.LINKUP.CONNECTION_ERROR)
|
||||
return false, Link.LINKUP.CONNECTION_ERROR
|
||||
end
|
||||
else
|
||||
enter()
|
||||
end
|
||||
N.yieldHost(ctx, adapters, function() end)
|
||||
Link.setResult(ctx, Link.LINKUP.ONGOING)
|
||||
local poll
|
||||
poll = function()
|
||||
if done then return true end
|
||||
ticks = ticks + 1
|
||||
if phase == "connect" then
|
||||
if Link.online() then
|
||||
enter()
|
||||
elseif ticks > CableEntry.CONNECT_TICKS or Link.connectState() == "error" then
|
||||
finish(Link.LINKUP.CONNECTION_ERROR)
|
||||
end
|
||||
return done
|
||||
end
|
||||
if not Link.online() then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
if phase == "ready" then
|
||||
local lk = Link.link
|
||||
if not (lk and lk:isOpen()) then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
if lk:isReady() then
|
||||
UI.close()
|
||||
Link.setStatus("busy")
|
||||
local yielded, value = handshake(ctx, adapters)
|
||||
if not yielded then
|
||||
Link.setResult(ctx, value or Link.getVar(ctx, Link.VAR_RESULT))
|
||||
return true
|
||||
end
|
||||
return false
|
||||
elseif ticks > CableEntry.READY_TICKS then
|
||||
finish(Link.LINKUP.CONNECTION_ERROR)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
if CableEntry.matched(Link.clientCall("room"), stale) then
|
||||
UI.close()
|
||||
unwatch()
|
||||
joining = nil
|
||||
if not Link.openRelay({ linkType = spec.linkType, game = spec.game, hello = spec.hello }) then
|
||||
finish(Link.LINKUP.CONNECTION_ERROR)
|
||||
return true
|
||||
end
|
||||
phase, ticks = "ready", 0
|
||||
return false
|
||||
end
|
||||
local g = Link.clientCall("group")
|
||||
if role == "leader" then leaderTick(g) elseif role == "join" then joinTick(g) end
|
||||
if not done and phase == "starting" and ticks > CableEntry.CONNECT_TICKS then
|
||||
finish(Link.LINKUP.CONNECTION_ERROR)
|
||||
end
|
||||
return done
|
||||
end
|
||||
ctx.nativePoll = poll
|
||||
UI.show({
|
||||
rows = rows,
|
||||
select = select,
|
||||
tick = function() if ctx.nativePoll == poll then poll() end end,
|
||||
title = function()
|
||||
if phase == "ask" then return Strings("ADD %s?", (pending.avatar or {}).name or pending.name or "") end
|
||||
return Strings((wire:upper():gsub("_", " ")))
|
||||
end,
|
||||
cancel = function()
|
||||
if phase == "waiting" or phase == "joining" then
|
||||
if phase == "joining" then Link.clientCall("leaveRoom") else Link.clientCall("leaveGroup") end
|
||||
joining, phase = nil, "list"
|
||||
UI.cursor = 1
|
||||
return
|
||||
end
|
||||
finish(Link.LINKUP.FAILED)
|
||||
end,
|
||||
})
|
||||
return true
|
||||
end
|
||||
|
||||
return CableEntry
|
||||
@@ -38,6 +38,18 @@ local function linkSeat(link)
|
||||
return tonumber(link.seat) or 0
|
||||
end
|
||||
|
||||
-- pokeemerald/src/contest_link.c:88
|
||||
function ContestLink.flagsFor(link, wireless)
|
||||
local flags = ContestLink.FLAG.IS_LINK + (wireless and ContestLink.FLAG.IS_WIRELESS or 0)
|
||||
local ok, players = false, nil
|
||||
if type(link) == "table" and type(link.players) == "function" then ok, players = pcall(link.players, link) end
|
||||
local Rs = require("src.core.game3.link.rs")
|
||||
for _, p in ipairs(ok and type(players) == "table" and players or {}) do
|
||||
if type(p) == "table" and Rs.is(p.version) then return flags + ContestLink.FLAG.HAS_RS_PLAYER end
|
||||
end
|
||||
return flags
|
||||
end
|
||||
|
||||
local function linkCount(link)
|
||||
if type(link.players) == "function" then
|
||||
local ok, list = pcall(link.players, link)
|
||||
@@ -153,8 +165,14 @@ function Session:fromLeader(key, data)
|
||||
return self:waiting(key)
|
||||
end
|
||||
|
||||
function Session:hasRsPlayer()
|
||||
return math.floor(self.flags / ContestLink.FLAG.HAS_RS_PLAYER) % 2 == 1
|
||||
end
|
||||
|
||||
-- pokeemerald/src/contest_util.c:2718
|
||||
function Session:standby()
|
||||
-- pokeemerald/src/contest_link.c:100
|
||||
if self:hasRsPlayer() then return true end
|
||||
if not self.standbyKey then
|
||||
self.standbyN = self.standbyN + 1
|
||||
self.standbyKey = "standby:" .. self.standbyN
|
||||
|
||||
@@ -225,9 +225,24 @@ local function spawnAll(lk, own)
|
||||
snap(r, M._base.x + seat, M._base.y)
|
||||
M._remotes[seat] = r
|
||||
end
|
||||
local r = M._remotes[seat]
|
||||
if r then r.name = type(player.name) == "string" and player.name or r.name end
|
||||
end
|
||||
end
|
||||
|
||||
local BUSY_KIND = { tradeCenter = "trade", colosseum2P = "battle", colosseum4P = "battle", recordCorner = "busy" }
|
||||
|
||||
function M.statusKind(seat)
|
||||
local r = M._remotes[seat]
|
||||
if not (r and r.busy) then return nil end
|
||||
return BUSY_KIND[M._mapId] or "busy"
|
||||
end
|
||||
|
||||
function M.nameOf(seat)
|
||||
local r = M._remotes[seat]
|
||||
return r and r.name or nil
|
||||
end
|
||||
|
||||
function M.update()
|
||||
local lk = liveLink()
|
||||
if not lk then
|
||||
@@ -254,6 +269,7 @@ function M.update()
|
||||
end
|
||||
if warping then return true end
|
||||
M._mapId = mapId
|
||||
if not package.loaded["src.ui.game3.link_tags"] then pcall(require, "src.ui.game3.link_tags") end
|
||||
spawnAll(lk, own)
|
||||
sendLocal(lk, mapId)
|
||||
for seat, r in pairs(M._remotes) do
|
||||
|
||||
@@ -0,0 +1,171 @@
|
||||
local Wander = {}
|
||||
|
||||
Wander.CELL = 16
|
||||
Wander.STEP_FRAMES = 32
|
||||
Wander.FIRST_REST = 360
|
||||
Wander.REST_MIN, Wander.REST_MAX = 240, 600
|
||||
Wander.PAUSE_MIN, Wander.PAUSE_MAX = 40, 120
|
||||
Wander.MAX_STEPS = 3
|
||||
Wander.RADIUS = 1
|
||||
Wander.GIVE_UP = 240
|
||||
|
||||
local ORDER = { "down", "up", "left", "right" }
|
||||
local DELTA = { down = { 0, 1 }, up = { 0, -1 }, left = { -1, 0 }, right = { 1, 0 } }
|
||||
local OPPOSITE = { down = "up", up = "down", left = "right", right = "left" }
|
||||
|
||||
Wander.DELTA = DELTA
|
||||
Wander.OPPOSITE = OPPOSITE
|
||||
|
||||
local function nextRand(w)
|
||||
w.seed = (w.seed * 16807) % 2147483647
|
||||
return w.seed
|
||||
end
|
||||
|
||||
local function randRange(w, lo, hi)
|
||||
return lo + nextRand(w) % (hi - lo + 1)
|
||||
end
|
||||
|
||||
function Wander.new(seed, homeX, homeY)
|
||||
local s = math.floor(math.abs(tonumber(seed) or 1)) % 2147483647
|
||||
if s == 0 then s = 1 end
|
||||
local w = {
|
||||
seed = s, homeX = homeX, homeY = homeY, x = homeX, y = homeY,
|
||||
prevX = homeX, prevY = homeY, path = {}, mode = "rest", want = 0,
|
||||
moving = false, progress = 0, animClock = 0, stepFlip = false, blocked = 0,
|
||||
facing = false, px = false, py = false, timer = 0,
|
||||
}
|
||||
w.timer = Wander.FIRST_REST + randRange(w, 0, Wander.REST_MAX - Wander.REST_MIN)
|
||||
return w
|
||||
end
|
||||
|
||||
function Wander.atHome(w)
|
||||
return w ~= nil and not w.moving and w.x == w.homeX and w.y == w.homeY
|
||||
end
|
||||
|
||||
function Wander.away(w)
|
||||
return w ~= nil and not Wander.atHome(w)
|
||||
end
|
||||
|
||||
local function inRange(w, x, y)
|
||||
return math.abs(x - w.homeX) <= Wander.RADIUS and math.abs(y - w.homeY) <= Wander.RADIUS
|
||||
end
|
||||
|
||||
local function beginStep(w, dir)
|
||||
local d = DELTA[dir]
|
||||
w.prevX, w.prevY = w.x, w.y
|
||||
w.x, w.y = w.x + d[1], w.y + d[2]
|
||||
w.facing = dir
|
||||
w.moving, w.progress, w.animClock = true, 0, 0
|
||||
w.px, w.py = w.prevX * Wander.CELL, w.prevY * Wander.CELL
|
||||
end
|
||||
|
||||
local function advance(w)
|
||||
w.progress = w.progress + 1
|
||||
w.animClock = w.animClock + 1
|
||||
local off = math.floor(Wander.CELL * math.min(w.progress, Wander.STEP_FRAMES) / Wander.STEP_FRAMES)
|
||||
w.px = w.prevX * Wander.CELL + (w.x - w.prevX) * off
|
||||
w.py = w.prevY * Wander.CELL + (w.y - w.prevY) * off
|
||||
if w.progress >= Wander.STEP_FRAMES then
|
||||
w.moving, w.progress, w.px, w.py = false, 0, false, false
|
||||
w.prevX, w.prevY = w.x, w.y
|
||||
w.stepFlip = not w.stepFlip
|
||||
end
|
||||
end
|
||||
|
||||
local function clearPath(w)
|
||||
for i = #w.path, 1, -1 do w.path[i] = nil end
|
||||
end
|
||||
|
||||
local function rest(w)
|
||||
clearPath(w)
|
||||
w.mode, w.want, w.blocked = "rest", 0, 0
|
||||
w.facing = false
|
||||
w.timer = randRange(w, Wander.REST_MIN, Wander.REST_MAX)
|
||||
end
|
||||
|
||||
local function pickStep(w, free)
|
||||
local last = w.path[#w.path]
|
||||
local start = randRange(w, 1, #ORDER)
|
||||
for i = 0, #ORDER - 1 do
|
||||
local dir = ORDER[(start + i - 1) % #ORDER + 1]
|
||||
if dir ~= (last and OPPOSITE[last]) then
|
||||
local d = DELTA[dir]
|
||||
local tx, ty = w.x + d[1], w.y + d[2]
|
||||
if inRange(w, tx, ty) and free(tx, ty, w.x, w.y, dir) then return dir end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Wander.tick(w, env)
|
||||
if w.moving then
|
||||
advance(w)
|
||||
return true
|
||||
end
|
||||
env = env or {}
|
||||
if env.frozen then return false end
|
||||
local free = env.free or function() return false end
|
||||
if not env.canWander and w.mode ~= "rest" then w.mode = "back" end
|
||||
if w.mode == "rest" then
|
||||
if not Wander.atHome(w) then
|
||||
w.mode = "back"
|
||||
return false
|
||||
end
|
||||
if not env.canWander then return false end
|
||||
w.timer = w.timer - 1
|
||||
if w.timer > 0 then return false end
|
||||
clearPath(w)
|
||||
w.mode, w.want = "out", randRange(w, 1, Wander.MAX_STEPS)
|
||||
end
|
||||
if w.mode == "out" then
|
||||
local dir = #w.path < w.want and pickStep(w, free) or nil
|
||||
if not dir and #w.path == 0 then
|
||||
rest(w)
|
||||
return false
|
||||
end
|
||||
if not dir then
|
||||
w.mode = "pause"
|
||||
w.timer = randRange(w, Wander.PAUSE_MIN, Wander.PAUSE_MAX)
|
||||
local turn = ORDER[randRange(w, 1, #ORDER)]
|
||||
if turn ~= w.facing then
|
||||
w.facing = turn
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
w.path[#w.path + 1] = dir
|
||||
beginStep(w, dir)
|
||||
return true
|
||||
end
|
||||
if w.mode == "pause" then
|
||||
w.timer = w.timer - 1
|
||||
if w.timer <= 0 then w.mode = "back" end
|
||||
return false
|
||||
end
|
||||
local last = w.path[#w.path]
|
||||
if not last then
|
||||
if w.x ~= w.homeX or w.y ~= w.homeY then
|
||||
if not free(w.homeX, w.homeY) then return false end
|
||||
w.x, w.y, w.prevX, w.prevY = w.homeX, w.homeY, w.homeX, w.homeY
|
||||
end
|
||||
rest(w)
|
||||
return true
|
||||
end
|
||||
local dir = OPPOSITE[last]
|
||||
local d = DELTA[dir]
|
||||
if free(w.x + d[1], w.y + d[2], w.x, w.y, dir) then
|
||||
w.path[#w.path] = nil
|
||||
w.blocked = 0
|
||||
beginStep(w, dir)
|
||||
return true
|
||||
end
|
||||
w.blocked = w.blocked + 1
|
||||
if w.blocked >= Wander.GIVE_UP and free(w.homeX, w.homeY) then
|
||||
w.x, w.y, w.prevX, w.prevY = w.homeX, w.homeY, w.homeX, w.homeY
|
||||
rest(w)
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
return Wander
|
||||
@@ -58,8 +58,28 @@ local function deep(v)
|
||||
return out
|
||||
end
|
||||
|
||||
RecordMix.BYTES_KEY = "$b"
|
||||
RecordMix.BYTES_MIN, RecordMix.BYTES_MAX = 16, 256
|
||||
|
||||
local function byteRun(v)
|
||||
local n = #v
|
||||
if n < RecordMix.BYTES_MIN or n > RecordMix.BYTES_MAX then return nil end
|
||||
local count = 0
|
||||
for _ in pairs(v) do count = count + 1 end
|
||||
if count ~= n then return nil end
|
||||
local hex = {}
|
||||
for i = 1, n do
|
||||
local b = v[i]
|
||||
if type(b) ~= "number" or b < 0 or b > 255 or b % 1 ~= 0 then return nil end
|
||||
hex[i] = string.format("%02x", b)
|
||||
end
|
||||
return table.concat(hex)
|
||||
end
|
||||
|
||||
function RecordMix.toWire(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local hex = byteRun(v)
|
||||
if hex then return { [RecordMix.BYTES_KEY] = hex } end
|
||||
local out = {}
|
||||
for k, x in pairs(v) do
|
||||
if type(k) == "number" then out["#" .. tostring(k)] = RecordMix.toWire(x)
|
||||
@@ -70,6 +90,12 @@ end
|
||||
|
||||
function RecordMix.fromWire(v)
|
||||
if type(v) ~= "table" then return v end
|
||||
local hex = v[RecordMix.BYTES_KEY]
|
||||
if type(hex) == "string" and next(v, next(v)) == nil and #hex % 2 == 0 and not hex:find("[^%x]") then
|
||||
local out = {}
|
||||
for i = 1, #hex / 2 do out[i] = tonumber(hex:sub(i * 2 - 1, i * 2), 16) end
|
||||
return out
|
||||
end
|
||||
local out = {}
|
||||
for k, x in pairs(v) do
|
||||
local n = type(k) == "string" and tonumber(k:match("^#(%-?%d+)$")) or nil
|
||||
|
||||
@@ -30,8 +30,6 @@ Union.LINK_GROUP = {
|
||||
POKEMON_JUMP = 4,
|
||||
BERRY_CRUSH = 5,
|
||||
BERRY_PICKING = 6,
|
||||
WONDER_CARD = 7,
|
||||
WONDER_NEWS = 8,
|
||||
UNION_ROOM_RESUME = 9,
|
||||
UNION_ROOM_INIT = 10,
|
||||
-- pokeemerald/include/constants/union_room.h:70
|
||||
@@ -77,15 +75,20 @@ Union.INVITE_ITEMS = {
|
||||
{ key = "EXIT", activity = Union.ACTIVITY.NONE, union = true },
|
||||
}
|
||||
|
||||
-- pokefirered/src/data/union_room.h:1 sLinkGroupActivityNameTexts
|
||||
Union.ACTIVITY_NAMES = RomText.lazy({
|
||||
[1] = "sLinkGroupActivityNameTexts[1]",
|
||||
[2] = "sLinkGroupActivityNameTexts[2]",
|
||||
[3] = "sLinkGroupActivityNameTexts[3]",
|
||||
[4] = "sLinkGroupActivityNameTexts[4]",
|
||||
[5] = "sLinkGroupActivityNameTexts[5]",
|
||||
[8] = "sLinkGroupActivityNameTexts[8]",
|
||||
[12] = "sLinkGroupActivityNameTexts[12]",
|
||||
Union.ACTIVITY_NAME_IDS = {
|
||||
-- pokefirered/src/data/union_room.h:1
|
||||
frlg = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 21, 22 },
|
||||
-- pokeemerald/src/data/union_room.h:592
|
||||
rse = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 14, 15, 16, 21, 22, 23, 24, 25, 26, 27, 28 },
|
||||
}
|
||||
Union.ACTIVITY_NAMES = setmetatable({}, {
|
||||
__index = function(_, raw)
|
||||
raw = tonumber(raw)
|
||||
for _, id in ipairs(Union.ACTIVITY_NAME_IDS[Family.of()] or {}) do
|
||||
if id == raw then return RomText.plain(RomText.key("sLinkGroupActivityNameTexts", id)) end
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
})
|
||||
|
||||
Union.AVATARS_FILE = "data/generated/gba/union_room/avatars.lua"
|
||||
@@ -395,10 +398,113 @@ function Union.retryWaitingAvatars()
|
||||
Union._avatarWaiting = waiting
|
||||
end
|
||||
|
||||
Union.WANDER = true
|
||||
Union.WANDER_NEAR_PLAYER = 2
|
||||
|
||||
local function wanderMod()
|
||||
return require("src.core.game3.link.plaza_wander")
|
||||
end
|
||||
|
||||
local STATUS_KIND = { chatting = "chat", trading = "trade", battling = "battle" }
|
||||
|
||||
function Union.memberStatusKind(p)
|
||||
if type(p) ~= "table" then return nil end
|
||||
local kind = STATUS_KIND[p.status]
|
||||
if kind or p.status ~= nil then return kind end
|
||||
local raw = math.floor(tonumber(p.activity) or 0) % Union.IN_UNION_ROOM
|
||||
local A = Union.ACTIVITY
|
||||
if raw == A.CHAT or raw == A.CARD then return "chat" end
|
||||
if raw == A.TRADE or raw == A.SPIN_TRADE then return "trade" end
|
||||
if raw == A.BATTLE_SINGLE or raw == A.BATTLE_DOUBLE or raw == A.BATTLE_MULTI then return "battle" end
|
||||
return nil
|
||||
end
|
||||
|
||||
function Union.memberIdle(p)
|
||||
if type(p) ~= "table" or p.gone then return false end
|
||||
if p.status ~= nil and p.status ~= "idle" then return false end
|
||||
return idleActivity(p.activity)
|
||||
end
|
||||
|
||||
local ENTER_OPTS = { surfing = false, elevation = 3 }
|
||||
|
||||
function Union.wanderWalkable(x, y, fromX, fromY, dir)
|
||||
local Coll = package.loaded["src.core.game3.collision"]
|
||||
if not (Coll and Coll._mapId == plazaMap().MAP_ID and Coll.canEnter) then return false end
|
||||
if Coll.inBounds and not Coll.inBounds(x, y) then return false end
|
||||
if Coll.isWater and Coll.isWater(x, y) then return false end
|
||||
ENTER_OPTS.fromX, ENTER_OPTS.fromY, ENTER_OPTS.dir = fromX, fromY, dir
|
||||
local ok = Coll.canEnter(link().game(), x, y, ENTER_OPTS)
|
||||
return ok == true
|
||||
end
|
||||
|
||||
local function wanderCellFree(x, y, fromX, fromY, dir)
|
||||
if playerOn(x, y) then return false end
|
||||
local Obj = objects()
|
||||
if Obj and Obj.blocks and Obj.blocks(x, y) then return false end
|
||||
return Union.wanderWalkable(x, y, fromX or x, fromY or y, dir)
|
||||
end
|
||||
|
||||
local function playerNear(x, y)
|
||||
local P = playerMod()
|
||||
if not P then return false end
|
||||
local near = Union.WANDER_NEAR_PLAYER
|
||||
local px, py = tonumber(P.cellX), tonumber(P.cellY)
|
||||
if px and py and math.abs(px - x) <= near and math.abs(py - y) <= near then return true end
|
||||
if not P.moving then return false end
|
||||
px, py = tonumber(P.targetX), tonumber(P.targetY)
|
||||
return px ~= nil and py ~= nil and math.abs(px - x) <= near and math.abs(py - y) <= near
|
||||
end
|
||||
|
||||
local WANDER_ENV = { free = wanderCellFree }
|
||||
|
||||
function Union.tickWander()
|
||||
local dirty = false
|
||||
local onPlaza = Union.WANDER and link().currentMap() == plazaMap().MAP_ID
|
||||
for slot = 1, Union.capacity() do
|
||||
local v = Union.vobj(slot)
|
||||
local p = Union.players[slot]
|
||||
if v and v.visible then
|
||||
local w = v.wander
|
||||
if not w or w.homeX ~= v.x or w.homeY ~= v.y then
|
||||
w = wanderMod().new(slot * 7919 + (tonumber(p and p.trainerId) or 0) + 1, v.x, v.y)
|
||||
v.wander = w
|
||||
end
|
||||
if v.anim == nil then
|
||||
WANDER_ENV.frozen = Union._talkSlot == slot or Union.partnerId == slot
|
||||
WANDER_ENV.canWander = onPlaza and Union.memberIdle(p) and not playerNear(v.x, v.y)
|
||||
if wanderMod().tick(w, WANDER_ENV) then dirty = true end
|
||||
end
|
||||
elseif v then
|
||||
v.wander = nil
|
||||
end
|
||||
end
|
||||
return dirty
|
||||
end
|
||||
|
||||
function Union.avatarCell(slot)
|
||||
local v = Union.vobj(slot)
|
||||
if not v then return nil end
|
||||
local w = v.wander
|
||||
if w then return w.x, w.y, w.moving end
|
||||
return v.x, v.y, false
|
||||
end
|
||||
|
||||
function Union.slotAtCell(x, y)
|
||||
for slot = 1, Union.capacity() do
|
||||
local v = Union.vobj(slot)
|
||||
if v and v.visible then
|
||||
local cx, cy, moving = Union.avatarCell(slot)
|
||||
if cx == x and cy == y and not moving then return slot end
|
||||
end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/event_object_movement.c:9354
|
||||
function Union.animateVobjs()
|
||||
local dirty = Union._vobjDirty
|
||||
local vobjs = Union._vobjs
|
||||
if Union.tickWander() then dirty = true end
|
||||
for _, v in pairs(vobjs) do
|
||||
if v.anim == "in" then
|
||||
v.y2 = math.min(0, v.y2 + Union.FLY_STEP)
|
||||
@@ -431,9 +537,22 @@ function Union.animateVobjs()
|
||||
if v.visible then
|
||||
local rec = VirtualObjects.get(id) or VirtualObjects.spawn(id, v.gfx, v.x, v.y, 3, v.dir)
|
||||
if rec then
|
||||
local w = v.wander
|
||||
rec.graphicsId = v.gfx
|
||||
rec.x, rec.y = v.x, v.y
|
||||
rec.direction = v.dir
|
||||
rec.prevX, rec.prevY, rec.px, rec.py, rec.moving = nil, nil, nil, nil, nil
|
||||
if w then
|
||||
rec.x, rec.y = w.x, w.y
|
||||
if w.facing and Union._talkSlot ~= v.slot then rec.direction = Union.FACE_DIR[w.facing] or v.dir end
|
||||
if w.moving then
|
||||
rec.prevX, rec.prevY, rec.px, rec.py, rec.moving = w.prevX, w.prevY, w.px, w.py, true
|
||||
rec.targetX, rec.targetY = w.x, w.y
|
||||
else
|
||||
rec.targetX, rec.targetY = nil, nil
|
||||
end
|
||||
rec.animClock, rec.stepFrames, rec.stepFlip = w.animClock, wanderMod().STEP_FRAMES, w.stepFlip
|
||||
end
|
||||
rec.y2 = v.y2
|
||||
rec.solid = v.anim ~= "out"
|
||||
end
|
||||
@@ -469,7 +588,7 @@ function Union.tryInteractWithMember()
|
||||
local d = P and FACING_DELTA[P.facing or "down"]
|
||||
if not d then return nil end
|
||||
local fx, fy = (tonumber(P.cellX) or 0) + d[1], (tonumber(P.cellY) or 0) + d[2]
|
||||
local slot = plazaMap().slotAt(fx, fy)
|
||||
local slot = Union.slotAtCell(fx, fy)
|
||||
if not slot then return nil end
|
||||
local p = Union.players[slot]
|
||||
local v = Union.vobj(slot)
|
||||
@@ -1603,6 +1722,7 @@ end
|
||||
|
||||
function Union.relayTick(_dt)
|
||||
local L = link()
|
||||
if not package.loaded["src.ui.game3.link_tags"] then pcall(require, "src.ui.game3.link_tags") end
|
||||
if not L.online() then
|
||||
Union.plazaOffline()
|
||||
Union.animateAll()
|
||||
@@ -2583,6 +2703,8 @@ function Union.roomPartnerAvatar(room)
|
||||
return nil
|
||||
end
|
||||
|
||||
Union.GROUP_JOIN_TICKS = 600
|
||||
|
||||
function Union.directRows(wire)
|
||||
local out = {}
|
||||
for _, e in ipairs(link().clientCall("directEntries", wire) or {}) do
|
||||
@@ -2596,6 +2718,11 @@ function Union.directRows(wire)
|
||||
out[#out + 1] = { key = "p:" .. tostring(e.id), kind = "player", id = e.id,
|
||||
name = av.name or e.name, trainerId = av.trainerId, gender = av.gender, version = av.version,
|
||||
canLinkNationally = av.canLinkNationally == true }
|
||||
elseif e.kind == "group" and e.leader ~= nil
|
||||
and (tonumber(e.joined) or 0) < (tonumber(e.max) or 2) then
|
||||
out[#out + 1] = { key = "g:" .. tostring(e.leader), kind = "group", id = e.leader, leader = e.leader,
|
||||
name = av.name or e.name, trainerId = av.trainerId, gender = av.gender,
|
||||
version = av.version or e.version, canLinkNationally = av.canLinkNationally == true }
|
||||
end
|
||||
end
|
||||
end
|
||||
@@ -2698,6 +2825,11 @@ function Union.chooseDirect(ctx, adapters, group, _direct)
|
||||
if D then D.setFrozen(true) end
|
||||
st.handle = L.clientCall("invite", row.id, wire, { ruleset = ruleset }, profile)
|
||||
if M then M.show(Union.askedToJoinText(wire, row.name), { stay = true }) end
|
||||
elseif row.kind == "group" then
|
||||
st.phase, st.row, st.sawGroup, st.waited = "grouping", row, false, 0
|
||||
if D then D.setFrozen(true) end
|
||||
L.clientCall("joinGroup", row.leader, profile, L.avatar())
|
||||
if M then M.show(Union.askedToJoinText(wire, row.name), { stay = true }) end
|
||||
elseif row.locked then
|
||||
askPin(row)
|
||||
else
|
||||
@@ -2723,6 +2855,15 @@ function Union.chooseDirect(ctx, adapters, group, _direct)
|
||||
local row = st.row or {}
|
||||
notice(h.why == "declined" and Union.rejectText(Union.GROUP_ACTIVITY[group].activity, row.gender) or busy)
|
||||
end
|
||||
elseif st.phase == "grouping" then
|
||||
local g = L.clientCall("group")
|
||||
st.waited = st.waited + 1
|
||||
if type(g) == "table" and g.leader == st.row.leader then
|
||||
st.sawGroup = true
|
||||
elseif st.sawGroup or st.waited > Union.GROUP_JOIN_TICKS then
|
||||
L.clientCall("leaveGroup")
|
||||
notice(st.sawGroup and Union.rejectText(Union.GROUP_ACTIVITY[group].activity, st.row.gender) or busy)
|
||||
end
|
||||
elseif st.phase == "joining" then
|
||||
local p = st.pending
|
||||
if type(p) ~= "table" then
|
||||
@@ -2806,7 +2947,10 @@ function Union.directFlow(ctx, adapters, group, role)
|
||||
L.setStatus("idle")
|
||||
local _, _, linkType = Union.directDest(group)
|
||||
return Union.waitForMatch(ctx, adapters, {
|
||||
cancel = function() L.clientCall("leaveDirect") end,
|
||||
cancel = function()
|
||||
L.clientCall("leaveDirect")
|
||||
if type(L.clientCall("group")) == "table" then L.clientCall("leaveGroup") end
|
||||
end,
|
||||
refuse = function(room)
|
||||
if wire ~= "trade" then return nil end
|
||||
local ready = Union.tradeReadyWith(Union.roomPartnerAvatar(room))
|
||||
|
||||
+206
-43
@@ -238,6 +238,27 @@ local function copyFlagList(list, version, bad)
|
||||
return out
|
||||
end
|
||||
|
||||
MysteryGift.VERSIONS = { "firered", "leafgreen", "ruby", "sapphire", "emerald" }
|
||||
MysteryGift.FAMILY_VERSIONS = { frlg = { "firered", "leafgreen" }, rse = { "emerald" }, rs = { "ruby", "sapphire" } }
|
||||
|
||||
local function copyVersions(list)
|
||||
if type(list) ~= "table" then return nil end
|
||||
local known, out = {}, {}
|
||||
for _, v in ipairs(MysteryGift.VERSIONS) do known[v] = true end
|
||||
for _, v in ipairs(list) do
|
||||
if known[v] then out[#out + 1] = v end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function MysteryGift.offeredIn(list, version)
|
||||
if type(list) ~= "table" or version == nil then return true end
|
||||
for _, v in ipairs(list) do
|
||||
if v == version then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
local function resolved(version, kind, value, bad)
|
||||
if value == nil then return nil end
|
||||
local n = MysteryGift.resolveId(version, kind, value)
|
||||
@@ -289,8 +310,22 @@ function MysteryGift.normalizeCard(card, version)
|
||||
requireStat = tonumber(gift.requireStat),
|
||||
requireValue = tonumber(gift.requireValue),
|
||||
script = type(gift.script) == "string" and gift.script:match("^[%w_]+$") and gift.script or nil,
|
||||
metLocation = tonumber(gift.metLocation),
|
||||
metLevel = tonumber(gift.metLevel),
|
||||
metGame = tonumber(gift.metGame),
|
||||
ball = tonumber(gift.ball),
|
||||
language = tonumber(gift.language),
|
||||
otNames = type(gift.otNames) == "table" and copyList(gift.otNames, math.min(#gift.otNames, 6), 7) or nil,
|
||||
otIdMax = tonumber(gift.otIdMax),
|
||||
secretIdRandom = gift.secretIdRandom == true or nil,
|
||||
otGender = tonumber(gift.otGender),
|
||||
shiny = (gift.shiny == "never" or gift.shiny == "always" or gift.shiny == "random") and gift.shiny or nil,
|
||||
nationalRibbon = gift.nationalRibbon == true or nil,
|
||||
movesOnly = gift.movesOnly == true or nil,
|
||||
},
|
||||
versions = copyVersions(card.versions),
|
||||
}
|
||||
if gift.fateful == false then out.gift.fateful = false end
|
||||
if bad.any then return nil, "unresolved" end
|
||||
return out
|
||||
end
|
||||
@@ -331,12 +366,19 @@ local function cardBytes(card)
|
||||
tostring(gift.requireStat or 0), tostring(gift.requireValue or 0),
|
||||
}
|
||||
if gift.script then parts[#parts + 1] = "R" .. tostring(gift.script) end
|
||||
for _, k in ipairs({ "metLocation", "metLevel", "metGame", "ball", "language", "otIdMax", "secretIdRandom",
|
||||
"otGender", "shiny", "nationalRibbon", "movesOnly" }) do
|
||||
if gift[k] ~= nil then parts[#parts + 1] = k .. "=" .. tostring(gift[k]) end
|
||||
end
|
||||
if gift.fateful == false then parts[#parts + 1] = "fateful=false" end
|
||||
for _, n in ipairs(type(gift.otNames) == "table" and gift.otNames or {}) do parts[#parts + 1] = "O" .. n end
|
||||
for _, slot in ipairs(moveKeys) do
|
||||
parts[#parts + 1] = tostring(slot) .. "=" .. tostring(gift.moves[slot])
|
||||
end
|
||||
for _, id in ipairs(gift.setFlags or {}) do parts[#parts + 1] = "S" .. tostring(id) end
|
||||
for _, id in ipairs(gift.haveFlags or {}) do parts[#parts + 1] = "H" .. tostring(id) end
|
||||
if gift.heldItem then parts[#parts + 1] = "I" .. tostring(gift.heldItem) end
|
||||
for _, v in ipairs(type(card.versions) == "table" and card.versions or {}) do parts[#parts + 1] = "V" .. tostring(v) end
|
||||
return table.concat(parts, "\2")
|
||||
end
|
||||
|
||||
@@ -651,20 +693,6 @@ function MysteryGift.isNewsSameAsSaved(session, news)
|
||||
return newsBytes(MysteryGift.ensure(session).news) == newsBytes(other)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift.c:120 IsSendingSavedWonderNewsAllowed
|
||||
function MysteryGift.isSendingNewsAllowed(session)
|
||||
local news = MysteryGift.ensure(session).news
|
||||
if not news then return false end
|
||||
return num(news.sendType) ~= MysteryGift.SEND_TYPE_DISALLOWED
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift.c:208 IsSendingSavedWonderCardAllowed
|
||||
function MysteryGift.isSendingCardAllowed(session)
|
||||
local card = MysteryGift.ensure(session).card
|
||||
if not card then return false end
|
||||
return num(card.sendType) ~= MysteryGift.SEND_TYPE_DISALLOWED
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift.c:235 DisableWonderCardSending
|
||||
function MysteryGift.disableCardSending(card)
|
||||
if type(card) == "table" and num(card.sendType) == MysteryGift.SEND_TYPE_ALLOWED then
|
||||
@@ -694,8 +722,30 @@ function MysteryGift.receivedGiftFlag(flagId, session)
|
||||
return first + (flagId - MysteryGift.WONDER_CARD_FLAG_OFFSET)
|
||||
end
|
||||
|
||||
local function tracksById(card)
|
||||
local gift = type(card) == "table" and type(card.gift) == "table" and card.gift or {}
|
||||
return (gift.kind == "mon" or gift.kind == "egg") and not gift.doneFlag
|
||||
end
|
||||
|
||||
local function deliveredIds(session)
|
||||
local rec = MysteryGift.ensure(session)
|
||||
rec.deliveredIds = type(rec.deliveredIds) == "table" and rec.deliveredIds or {}
|
||||
return rec.deliveredIds
|
||||
end
|
||||
|
||||
function MysteryGift.wasDelivered(session, card)
|
||||
local id = num(type(card) == "table" and card.idNumber)
|
||||
for _, v in ipairs(deliveredIds(session)) do
|
||||
if num(v) == id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift.c:248 IsSavedWonderCardGiftNotReceived
|
||||
function MysteryGift.isGiftNotReceived(session)
|
||||
if MysteryGift.validateSavedCard(session) and tracksById(MysteryGift.ensure(session).card) then
|
||||
return not MysteryGift.wasDelivered(session, MysteryGift.ensure(session).card)
|
||||
end
|
||||
local flagId = MysteryGift.getCardFlagId(session)
|
||||
local giftFlag = MysteryGift.receivedGiftFlag(flagId, session)
|
||||
if not giftFlag then return false end
|
||||
@@ -954,13 +1004,22 @@ function MysteryGift.getNewsRewardInfo(session)
|
||||
return rewardType, item
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_client.c:210 CLI_SAVE_NEWS
|
||||
function MysteryGift.saveNewsIfNew(session, news)
|
||||
if MysteryGift.isNewsSameAsSaved(session, news) then return false, "had" end
|
||||
if not MysteryGift.saveNews(session, news) then return false, "invalid" end
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_client.c:213
|
||||
function MysteryGift.receiveNews(session, news, newsType)
|
||||
if not MysteryGift.saveNews(session, news) then return false end
|
||||
local ok, why = MysteryGift.saveNewsIfNew(session, news)
|
||||
if not ok then return false, why end
|
||||
MysteryGift.setNewsReward(session, newsType or MysteryGift.WONDER_NEWS_RECV_WIRELESS)
|
||||
return true
|
||||
end
|
||||
|
||||
|
||||
-- pokefirered/data/mystery_event_msg.s:69 SurfPichu_GiveEgg
|
||||
local function createEventMon(session, gift)
|
||||
local Party = require("src.core.game3.party")
|
||||
@@ -980,8 +1039,43 @@ local function createEventMon(session, gift)
|
||||
ok, code, mon = Party.giveMon(session, species, level, gift.nickname)
|
||||
end
|
||||
if not (ok and mon) then return nil, code end
|
||||
if gift.personality then
|
||||
mon.personality = num(gift.personality)
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local otName = gift.otName
|
||||
if type(gift.otNames) == "table" and #gift.otNames > 0 then
|
||||
otName = gift.otNames[(Rng.Random() % #gift.otNames) + 1]
|
||||
end
|
||||
if otName then
|
||||
mon.ot = otName
|
||||
mon.otName = otName
|
||||
end
|
||||
if gift.otIdMax then
|
||||
mon.otId = (Rng.Random() % num(gift.otIdMax)) + 1
|
||||
mon.otSecretId = 0
|
||||
elseif gift.otId then
|
||||
mon.otId = num(gift.otId) % 65536
|
||||
mon.otSecretId = math.floor(num(gift.otId) / 65536) % 65536
|
||||
end
|
||||
if gift.secretIdRandom then mon.otSecretId = Rng.Random() end
|
||||
if gift.otGender == 2 then mon.otGender = Rng.Random() % 2
|
||||
elseif gift.otGender then mon.otGender = gift.otGender end
|
||||
local personality = gift.personality and num(gift.personality) or mon.personality
|
||||
-- pokefirered/src/pokemon.c:6062 IsShinyOtIdPersonality
|
||||
local function shiny(p)
|
||||
local bit = require("bit")
|
||||
local tid, sid = num(mon.otId) % 65536, num(mon.otSecretId) % 65536
|
||||
return bit.bxor(bit.bxor(tid, sid), bit.bxor(math.floor(p / 65536), p % 65536)) < 8
|
||||
end
|
||||
if not gift.personality and gift.shiny == "never" then
|
||||
while shiny(personality) do personality = Rng.Random32() % 0x100000000 end
|
||||
elseif not gift.personality and gift.shiny == "always" and not shiny(personality) then
|
||||
local bit = require("bit")
|
||||
local hi = math.floor(personality / 65536)
|
||||
local lo = bit.band(bit.bxor(bit.bxor(num(mon.otId) % 65536, num(mon.otSecretId) % 65536), hi), 0xFFF8)
|
||||
+ personality % 8
|
||||
personality = hi * 65536 + lo
|
||||
end
|
||||
if personality ~= mon.personality then
|
||||
mon.personality = personality
|
||||
if Pokemon.natureId then mon.nature = Pokemon.natureId(mon.personality) end
|
||||
if Pokemon.abilityId then
|
||||
mon.ability = Pokemon.abilityId(species, mon.personality)
|
||||
@@ -990,25 +1084,24 @@ local function createEventMon(session, gift)
|
||||
if Pokemon.gender then mon.gender = Pokemon.gender(species, mon.personality) end
|
||||
if Pokemon.applyStats then Pokemon.applyStats(mon) end
|
||||
end
|
||||
if gift.otName then
|
||||
mon.ot = gift.otName
|
||||
mon.otName = gift.otName
|
||||
end
|
||||
if gift.otId then
|
||||
-- pokefirered/src/pokemon.c:6062 IsShinyOtIdPersonality
|
||||
mon.otId = num(gift.otId) % 65536
|
||||
mon.otSecretId = math.floor(num(gift.otId) / 65536) % 65536
|
||||
end
|
||||
if gift.heldItem and num(gift.heldItem) > 0 then
|
||||
mon.item, mon.heldItem = num(gift.heldItem), num(gift.heldItem)
|
||||
end
|
||||
-- pokefirered/src/scrcmd.c:2244 ScrCmd_setmonmodernfatefulencounter
|
||||
mon.modernFatefulEncounter = true
|
||||
mon.modernFatefulEncounter = gift.fateful ~= false
|
||||
if gift.nationalRibbon then
|
||||
require("src.core.game3.rse.ribbons").set(mon, "national", 1)
|
||||
end
|
||||
-- pokefirered/data/mystery_event_msg.s:72 setmonmetlocation METLOC_FATEFUL_ENCOUNTER
|
||||
mon.metLocation = METLOC_FATEFUL_ENCOUNTER
|
||||
mon.metLocation = gift.metLocation or METLOC_FATEFUL_ENCOUNTER
|
||||
if gift.metLevel then mon.metLevel = gift.metLevel end
|
||||
if gift.metGame then mon.metGame = gift.metGame end
|
||||
if gift.ball then mon.pokeball = gift.ball end
|
||||
if gift.language then mon.language = gift.language end
|
||||
mon.moves = mon.moves or {}
|
||||
mon.pp = mon.pp or {}
|
||||
mon.maxPp = mon.maxPp or {}
|
||||
if gift.movesOnly then mon.moves, mon.pp, mon.maxPp = {}, {}, {} end
|
||||
for slot, move in pairs(gift.moves or {}) do
|
||||
mon.moves[slot] = move
|
||||
local pp = Pokemon.movePp and Pokemon.movePp(move) or nil
|
||||
@@ -1034,12 +1127,14 @@ function MysteryGift.deliverGift(session, card)
|
||||
card = card or MysteryGift.getSavedCard(session)
|
||||
if type(card) ~= "table" then return MysteryGift.DELIVER_NOTHING end
|
||||
local gift = type(card.gift) == "table" and card.gift or {}
|
||||
local receivedFlag = MysteryGift.receivedGiftFlag(card.flagId, session)
|
||||
local byId = tracksById(card)
|
||||
local receivedFlag = not byId and MysteryGift.receivedGiftFlag(card.flagId, session) or nil
|
||||
|
||||
-- pokefirered/src/mystery_gift.c:248 IsSavedWonderCardGiftNotReceived
|
||||
if receivedFlag and getFlag(session, receivedFlag) then
|
||||
return MysteryGift.DELIVER_ALREADY
|
||||
end
|
||||
if byId and MysteryGift.wasDelivered(session, card) then return MysteryGift.DELIVER_ALREADY end
|
||||
if gift.doneFlag and getFlag(session, gift.doneFlag) then
|
||||
return MysteryGift.DELIVER_ALREADY
|
||||
end
|
||||
@@ -1085,6 +1180,7 @@ function MysteryGift.deliverGift(session, card)
|
||||
end
|
||||
if gift.doneFlag then setFlag(session, gift.doneFlag, true) end
|
||||
if receivedFlag then setFlag(session, receivedFlag, true) end
|
||||
if byId then table.insert(deliveredIds(session), num(card.idNumber)) end
|
||||
return MysteryGift.DELIVER_GIVEN
|
||||
end
|
||||
|
||||
@@ -1229,6 +1325,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("It is for use at VERMILION CITY port.")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
gift = {
|
||||
kind = "item",
|
||||
item = MysteryGift.ITEM_MYSTIC_TICKET,
|
||||
@@ -1261,6 +1358,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("It is for use at VERMILION CITY port.")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
gift = {
|
||||
kind = "item",
|
||||
item = MysteryGift.ITEM_AURORA_TICKET,
|
||||
@@ -1294,6 +1392,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("kindness.")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
gift = {
|
||||
kind = "egg",
|
||||
species = 172,
|
||||
@@ -1326,6 +1425,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("your STAMP CARD.")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
gift = { kind = "none" },
|
||||
},
|
||||
},
|
||||
@@ -1350,6 +1450,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("Look for them and battle!")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
-- pokefirered/data/mystery_event_msg.s:173 giveitem ITEM_POTION
|
||||
gift = {
|
||||
kind = "item",
|
||||
@@ -1382,6 +1483,7 @@ function MysteryGift.builtins(family)
|
||||
Strings("on SIX ISLAND.")),
|
||||
footerLine1Text = Strings("Speak to the deliveryman"),
|
||||
footerLine2Text = Strings("at a POKéMON CENTER."),
|
||||
versions = { "firered", "leafgreen" },
|
||||
-- pokefirered/data/mystery_event_msg.s:325
|
||||
gift = {
|
||||
kind = "var",
|
||||
@@ -1403,6 +1505,7 @@ local function rseCard(key, label, t)
|
||||
t.subtitleText = t.subtitleText or Strings("MYSTERY GIFT")
|
||||
t.footerLine1Text = t.footerLine1Text or Strings("Speak to the deliveryman")
|
||||
t.footerLine2Text = t.footerLine2Text or Strings("at a POKéMON CENTER.")
|
||||
t.versions = t.versions or { "emerald" }
|
||||
return { key = key, family = "rse", label = label, card = t }
|
||||
end
|
||||
|
||||
@@ -1579,10 +1682,60 @@ end
|
||||
|
||||
local function familyVersion(family)
|
||||
if family == MysteryGift.familyOf(nil) then return versionOf(nil) end
|
||||
return family == "rse" and "emerald" or "firered"
|
||||
return MysteryGift.FAMILY_VERSIONS[family] and MysteryGift.FAMILY_VERSIONS[family][1] or "firered"
|
||||
end
|
||||
|
||||
function MysteryGift.parseFeed(payload, family)
|
||||
-- pokeruby/src/mystery_event_script.c:46 CheckCompatibility
|
||||
function MysteryGift.eventHeaderOk(bytes, version)
|
||||
local Event = require("src.core.game3.rs.mystery_event")
|
||||
if type(bytes) ~= "string" or #bytes < 17 or #bytes > Event.MAX_BYTES or bytes:byte(1) ~= 1 then return false end
|
||||
local bit = require("bit")
|
||||
local function u16(at) return bytes:byte(at) + bytes:byte(at + 1) * 256 end
|
||||
local function u32(at) return u16(at) + u16(at + 2) * 65536 end
|
||||
local mask = ({ ruby = 0x80, sapphire = 0x100 })[version] or 0x180
|
||||
return bit.band(u16(6), 2) ~= 0 and bit.band(u32(8), 2) ~= 0 and bit.band(u16(12), 4) ~= 0
|
||||
and bit.band(u32(14), mask) ~= 0
|
||||
end
|
||||
|
||||
local EVENT_OP_SIZE = { [0] = 1, 17, 1, 6, 2, 5, 12, 5, 3, 1, 2, 5, 5, 5, 1, 13, 13 }
|
||||
|
||||
-- pokeruby/src/mystery_event_script.c:356 MEScrCmd_setrecordmixinggift
|
||||
function MysteryGift.eventRecordMixingItem(bytes)
|
||||
if type(bytes) ~= "string" then return nil end
|
||||
local pos = 1
|
||||
while pos <= #bytes do
|
||||
local op = bytes:byte(pos)
|
||||
local size = EVENT_OP_SIZE[op]
|
||||
if not size or op == 2 then return nil end
|
||||
if op == 11 and pos + 4 <= #bytes then
|
||||
local unk, qty = bytes:byte(pos + 1), bytes:byte(pos + 2)
|
||||
local item = bytes:byte(pos + 3) + bytes:byte(pos + 4) * 256
|
||||
if unk ~= 0 and qty ~= 0 and item ~= 0 then return item end
|
||||
end
|
||||
pos = pos + size
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function parseEvents(data, version)
|
||||
local out = {}
|
||||
if type(data.events) ~= "table" then return out end
|
||||
local Base64 = require("src.core.Base64")
|
||||
for _, raw in ipairs(data.events) do
|
||||
local ok, bytes = pcall(Base64.decode, type(raw) == "table" and raw.payload or nil)
|
||||
if ok and rowFamily(raw) == "rs" and MysteryGift.offeredIn(copyVersions(raw.versions), version)
|
||||
and MysteryGift.eventHeaderOk(bytes, version) then
|
||||
out[#out + 1] = {
|
||||
key = type(raw.key) == "string" and raw.key or tostring(#out + 1),
|
||||
label = clampText(raw.title, WONDER_CARD_TEXT_LENGTH),
|
||||
bytes = bytes,
|
||||
}
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function MysteryGift.parseFeed(payload, family, gameVersion)
|
||||
family = family or MysteryGift.familyOf(nil)
|
||||
local version = familyVersion(family)
|
||||
local Json = require("src.link.Json")
|
||||
@@ -1590,9 +1743,11 @@ function MysteryGift.parseFeed(payload, family)
|
||||
if type(data) ~= "table" or data.v ~= MysteryGift.FEED_VERSION then return nil, "bad_feed" end
|
||||
if type(data.cards) ~= "table" or type(data.news) ~= "table" then return nil, "bad_feed" end
|
||||
local out = { issued = num(data.issued), cards = {}, news = {} }
|
||||
if family == "rs" then out.events = parseEvents(data, gameVersion) end
|
||||
for _, raw in ipairs(data.cards) do
|
||||
local card = rowFamily(raw) == family and MysteryGift.normalizeCard(raw, version) or nil
|
||||
if card and validateCard(card) and isFlagIdInValidRange(card.flagId) and card.idNumber ~= 0 then
|
||||
local card = rowFamily(raw) == family and MysteryGift.normalizeCard(raw, gameVersion or version) or nil
|
||||
if card and validateCard(card) and isFlagIdInValidRange(card.flagId) and card.idNumber ~= 0
|
||||
and MysteryGift.offeredIn(card.versions, gameVersion) then
|
||||
out.cards[#out.cards + 1] = {
|
||||
key = type(raw.key) == "string" and raw.key or tostring(card.idNumber),
|
||||
label = card.titleText,
|
||||
@@ -1601,7 +1756,7 @@ function MysteryGift.parseFeed(payload, family)
|
||||
end
|
||||
end
|
||||
for _, raw in ipairs(data.news) do
|
||||
local news = rowFamily(raw) == family and MysteryGift.normalizeNews(raw) or nil
|
||||
local news = MysteryGift.normalizeNews(raw)
|
||||
if news and validateNews(news) then
|
||||
out.news[#out.news + 1] = {
|
||||
key = type(raw.key) == "string" and raw.key or tostring(news.id),
|
||||
@@ -1613,7 +1768,7 @@ function MysteryGift.parseFeed(payload, family)
|
||||
return out
|
||||
end
|
||||
|
||||
function MysteryGift.verifyFeed(feed, family)
|
||||
function MysteryGift.verifyFeed(feed, family, gameVersion)
|
||||
if type(feed) ~= "table" or type(feed.payload) ~= "string" or type(feed.sig) ~= "string" then
|
||||
return nil, "bad_feed"
|
||||
end
|
||||
@@ -1621,17 +1776,25 @@ function MysteryGift.verifyFeed(feed, family)
|
||||
if not Ed25519.verify(MysteryGift.GIFT_PUBKEY, feed.payload, feed.sig) then
|
||||
return nil, "bad_signature"
|
||||
end
|
||||
return MysteryGift.parseFeed(feed.payload, family)
|
||||
return MysteryGift.parseFeed(feed.payload, family, gameVersion)
|
||||
end
|
||||
|
||||
function MysteryGift.feedPath(family)
|
||||
if family == "frlg" then return MysteryGift.FEED_PATH end
|
||||
return MysteryGift.FEED_PATH .. "?family=" .. tostring(family)
|
||||
function MysteryGift.feedPath(family, gameVersion)
|
||||
local q = {}
|
||||
if family ~= "frlg" then q[#q + 1] = "family=" .. tostring(family) end
|
||||
if gameVersion then q[#q + 1] = "version=" .. tostring(gameVersion) end
|
||||
if #q == 0 then return MysteryGift.FEED_PATH end
|
||||
return MysteryGift.FEED_PATH .. "?" .. table.concat(q, "&")
|
||||
end
|
||||
|
||||
function MysteryGift.fetchOnline(opts)
|
||||
opts = opts or {}
|
||||
local family = opts.family or MysteryGift.familyOf(opts.session)
|
||||
local gameVersion = opts.version
|
||||
if gameVersion == nil then
|
||||
gameVersion = versionOf(opts.session)
|
||||
if not MysteryGift.offeredIn(MysteryGift.FAMILY_VERSIONS[family], gameVersion) then gameVersion = nil end
|
||||
end
|
||||
local client = opts.client
|
||||
if not client then
|
||||
local okS, SyncClient = pcall(require, "src.sync.SyncClient")
|
||||
@@ -1640,12 +1803,12 @@ function MysteryGift.fetchOnline(opts)
|
||||
if not okN then return { status = "error", reason = "offline" } end
|
||||
client = made
|
||||
end
|
||||
local okR, handle = pcall(client.send, client, "GET", MysteryGift.feedPath(family), nil,
|
||||
local okR, handle = pcall(client.send, client, "GET", MysteryGift.feedPath(family, gameVersion), nil,
|
||||
{ noAuth = true, maxSeconds = MysteryGift.FETCH_SECONDS })
|
||||
if not okR or handle == nil then
|
||||
return { status = "error", reason = "offline", client = client, family = family }
|
||||
return { status = "error", reason = "offline", client = client, family = family, version = gameVersion }
|
||||
end
|
||||
return { status = "pending", client = client, handle = handle, family = family }
|
||||
return { status = "pending", client = client, handle = handle, family = family, version = gameVersion }
|
||||
end
|
||||
|
||||
function MysteryGift.pollOnline(job)
|
||||
@@ -1662,7 +1825,7 @@ function MysteryGift.pollOnline(job)
|
||||
job.reason = (num(res.code) >= 400) and "server" or "offline"
|
||||
return "error", job.reason
|
||||
end
|
||||
local list, why = MysteryGift.verifyFeed(res.data, job.family)
|
||||
local list, why = MysteryGift.verifyFeed(res.data, job.family, job.version)
|
||||
if not list then
|
||||
job.status, job.reason = "error", why
|
||||
return "error", why
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
local Link = require("src.core.game3.link.init")
|
||||
local Strings = require("src.core.Strings")
|
||||
local CableEntry = require("src.core.game3.link.cable_entry")
|
||||
local M = {}
|
||||
-- pokeruby/cable_club.c:558
|
||||
-- pokeruby/src/cable_club.c:558
|
||||
M.SERVICES = {
|
||||
[1] = {wire = "battle_single", linkType = 0x2233, min = 2, max = 2},
|
||||
[2] = {wire = "battle_double", linkType = 0x2244, min = 2, max = 2},
|
||||
@@ -10,149 +9,9 @@ M.SERVICES = {
|
||||
records = {wire = "record_corner", linkType = 0x3311, min = 2, max = 4},
|
||||
blender = {wire = "berry_blender", linkType = 0x4411, min = 2, max = 4},
|
||||
}
|
||||
-- pokeruby/src/cable_club.c:651
|
||||
function M.contest(category)
|
||||
local names = {"cool", "beauty", "cute", "smart", "tough"}
|
||||
return {wire = "contest_" .. assert(names[(tonumber(category) or 0) + 1]), linkType = 0x6601, min = 4, max = 4}
|
||||
end
|
||||
function M.run(ctx, adapters, spec, handshake)
|
||||
assert(spec, "RS cable service missing")
|
||||
local live = Link.link
|
||||
if live and live.isOpen and live:isOpen() then return handshake(ctx, adapters) end
|
||||
local N = require("src.core.game3.scripting.natives")
|
||||
local UI = require("src.ui.game3.rs.cable_lobby")
|
||||
local phase, ticks, done, role, pending, answered = "connect", 0, false, nil, nil, {}
|
||||
local seenGroup, goneTicks, awayTicks = false, 0, 0
|
||||
local profile = assert(spec.profile or Link.liveProfile(), "RS link profile missing")
|
||||
local avatar = spec.avatar or Link.avatar()
|
||||
local function finish(code)
|
||||
Link.clientCall("leaveGroup"); Link.closeLink("rs_cable_failed")
|
||||
UI.close(); Link.clientCall("groupList", nil); Link.setStatus("busy")
|
||||
Link.setResult(ctx, code); done = true
|
||||
end
|
||||
local function offline(member)
|
||||
if type(member) ~= "table" then return false end
|
||||
if member.online == false then return true end
|
||||
for _, entry in ipairs(Link.clientCall("lobby") or {}) do
|
||||
if type(entry) == "table" and entry.id ~= nil and entry.id == member.id then return entry.online == false end
|
||||
end
|
||||
return false
|
||||
end
|
||||
local function liveMembers(group)
|
||||
local members = type(group) == "table" and type(group.members) == "table" and group.members or {}
|
||||
local live = {}
|
||||
for _, member in ipairs(members) do
|
||||
if not offline(member) then live[#live + 1] = member end
|
||||
end
|
||||
return live, #live == #members
|
||||
end
|
||||
local function rows()
|
||||
if phase == "mode" then return {{id = "leader", label = Strings("CREATE GROUP")}, {id = "join", label = Strings("JOIN GROUP")}} end
|
||||
if phase == "ask" then return {{id = "yes", label = Strings("YES")}, {id = "no", label = Strings("NO")}} end
|
||||
local out, group = {}, Link.clientCall("group")
|
||||
if role == "leader" then
|
||||
local members, all = liveMembers(group)
|
||||
for _, member in ipairs(members) do
|
||||
local av = member.avatar or {}
|
||||
out[#out + 1] = {label = av.name or member.name or "", disabled = true}
|
||||
end
|
||||
out[#out + 1] = {id = "start", label = Strings("START"), disabled = not all or #members < spec.min or #members > spec.max}
|
||||
elseif phase == "list" then
|
||||
for _, groupRow in ipairs(Link.clientCall("groups", spec.wire) or {}) do
|
||||
local av = groupRow.avatar or {}
|
||||
out[#out + 1] = {id = groupRow.leader, label = av.name or groupRow.name or "",
|
||||
disabled = (tonumber(groupRow.joined) or 0) >= (tonumber(groupRow.max) or spec.max)}
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
local poll
|
||||
local function select(row)
|
||||
if phase == "mode" then
|
||||
role, phase = row.id, "list"
|
||||
if role == "leader" then Link.clientCall("openGroup", spec.wire, profile, avatar)
|
||||
else Link.clientCall("groupList", spec.wire, profile) end
|
||||
elseif phase == "ask" then
|
||||
answered[pending.id] = true
|
||||
Link.clientCall("acceptGroup", pending.id, row.id == "yes"); pending, phase = nil, "list"
|
||||
elseif role == "leader" and row.id == "start" then
|
||||
Link.clientCall("startGroup"); phase, ticks = "starting", 0
|
||||
elseif role == "join" then Link.clientCall("joinGroup", row.id, profile, avatar); phase, ticks = "waiting", 0 end
|
||||
end
|
||||
N.yieldHost(ctx, adapters, function() end)
|
||||
Link.setResult(ctx, 0)
|
||||
if not Link.online() then
|
||||
local ok = (spec.connect or Link.connect)()
|
||||
if not ok then finish(Link.LINKUP.CONNECTION_ERROR); return false, Link.LINKUP.CONNECTION_ERROR end
|
||||
else phase = "mode" end
|
||||
poll = function()
|
||||
if done then return true end
|
||||
ticks = ticks + 1
|
||||
if phase == "connect" then
|
||||
if Link.online() then phase, ticks = "mode", 0
|
||||
elseif ticks > 1200 or Link.connectState() == "error" then finish(Link.LINKUP.CONNECTION_ERROR) end
|
||||
return done
|
||||
end
|
||||
if not Link.online() then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
if phase == "ready" then
|
||||
local lk = Link.link
|
||||
if not lk or not lk:isOpen() then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
if lk:isReady() then
|
||||
UI.close(); Link.clientCall("groupList", nil); Link.setStatus("busy")
|
||||
local yielded, value = handshake(ctx, adapters)
|
||||
if not yielded then Link.setResult(ctx, value or Link.getVar(ctx, Link.VAR_RESULT)); return true end
|
||||
return false
|
||||
elseif ticks > 600 then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
return false
|
||||
end
|
||||
local room = Link.clientCall("room")
|
||||
if type(room) == "table" and room.stage == "battling" and room.match ~= nil then
|
||||
UI.close()
|
||||
if not Link.openRelay({linkType = spec.linkType, game = spec.game, hello = spec.hello}) then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
phase, ticks = "ready", 0
|
||||
return false
|
||||
end
|
||||
local g = Link.clientCall("group")
|
||||
if role == "leader" and phase == "ask" then
|
||||
local still = false
|
||||
for _, request in ipairs(type(g) == "table" and type(g.pending) == "table" and g.pending or {}) do
|
||||
if pending and request.id == pending.id and not offline(request) then still = true end
|
||||
end
|
||||
if not still then
|
||||
if pending then answered[pending.id] = true end
|
||||
pending, phase = nil, "list"
|
||||
UI.cursor = 1
|
||||
end
|
||||
elseif role == "leader" and phase == "list" then
|
||||
for _, request in ipairs(type(g) == "table" and type(g.pending) == "table" and g.pending or {}) do
|
||||
if not answered[request.id] and not offline(request) then pending, phase = request, "ask"; break end
|
||||
end
|
||||
elseif role == "leader" and phase == "starting" and type(g) == "table" then
|
||||
local members, all = liveMembers(g)
|
||||
if not all or #members < spec.min then phase, ticks = "list", 0 end
|
||||
elseif role == "join" and phase == "waiting" then
|
||||
if type(g) == "table" then
|
||||
seenGroup, goneTicks = true, 0
|
||||
local leader
|
||||
for _, member in ipairs(type(g.members) == "table" and g.members or {}) do
|
||||
if member.id == g.leader then leader = member end
|
||||
end
|
||||
awayTicks = offline(leader) and awayTicks + 1 or 0
|
||||
if awayTicks > 600 then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
ticks = 0
|
||||
elseif seenGroup then
|
||||
goneTicks = goneTicks + 1
|
||||
if goneTicks > 120 then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
end
|
||||
end
|
||||
if not done and (phase == "starting" or phase == "waiting") and ticks > 1200 then finish(Link.LINKUP.CONNECTION_ERROR); return true end
|
||||
return false
|
||||
end
|
||||
ctx.nativePoll = poll
|
||||
UI.show({rows = rows, select = select, tick = function()
|
||||
if ctx.nativePoll == poll then poll() end
|
||||
end,
|
||||
title = function() return phase == "ask" and Strings("ADD %s?", (pending.avatar or {}).name or pending.name or "") or Strings(spec.wire:upper():gsub("_", " ")) end,
|
||||
cancel = function() finish(Link.LINKUP.FAILED) end})
|
||||
return true
|
||||
return {wire = assert(CableEntry.CONTEST_WIRES[tonumber(category) or -1]), linkType = 0x6601, min = 4, max = 4}
|
||||
end
|
||||
M.run = CableEntry.run
|
||||
return M
|
||||
|
||||
@@ -26,6 +26,11 @@ EventIslands.GIFT_FLAGS = {
|
||||
|
||||
local cache = {}
|
||||
|
||||
function EventIslands.available()
|
||||
local ok, man = pcall(EventIslands.manifest)
|
||||
return ok and type(man) == "table" and type(man.gifts) == "table"
|
||||
end
|
||||
|
||||
function EventIslands.manifest()
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
local key = tostring(GameVersion.get())
|
||||
@@ -70,28 +75,75 @@ local function hasItem(sess, name)
|
||||
return Bag.has(sess.bag, C(sess):require("items", name), 1)
|
||||
end
|
||||
|
||||
local TICKET_IDS = { eon = true, aurora = true, mystic = true, oldSeaMap = true }
|
||||
EventIslands.REFRESH_SECONDS = 600
|
||||
|
||||
function EventIslands.relayTickets(sess)
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
local rec = MysteryGift.ensure(sess)
|
||||
local out = {}
|
||||
for _, id in ipairs(type(rec.relayTickets) == "table" and rec.relayTickets or {}) do out[id] = true end
|
||||
return out
|
||||
end
|
||||
|
||||
function EventIslands.applyFeed(sess, list)
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
local ids, seen = {}, {}
|
||||
for _, entry in ipairs(type(list) == "table" and list.cards or {}) do
|
||||
local id = MysteryGift.ramScriptId(sess, entry.card)
|
||||
if TICKET_IDS[id] and not seen[id] then
|
||||
seen[id] = true
|
||||
ids[#ids + 1] = id
|
||||
end
|
||||
end
|
||||
MysteryGift.ensure(sess).relayTickets = ids
|
||||
return ids
|
||||
end
|
||||
|
||||
function EventIslands.refresh(sess, opts)
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
opts = opts or {}
|
||||
if not EventIslands.job then
|
||||
local now = os.time()
|
||||
if EventIslands.fetchedAt and not opts.force and now - EventIslands.fetchedAt < EventIslands.REFRESH_SECONDS then
|
||||
return "cached"
|
||||
end
|
||||
EventIslands.fetchedAt = now
|
||||
EventIslands.job = MysteryGift.fetchOnline({ family = "rse", session = sess, transport = opts.transport,
|
||||
client = opts.client })
|
||||
end
|
||||
local status, result = MysteryGift.pollOnline(EventIslands.job)
|
||||
if status == "pending" then return "pending" end
|
||||
EventIslands.job = nil
|
||||
if status == "ok" then EventIslands.applyFeed(sess, result) end
|
||||
return status, result
|
||||
end
|
||||
|
||||
function EventIslands.eonPending(sess)
|
||||
return not hasItem(sess, "ITEM_EON_TICKET") and not Rse.flag("FLAG_ENABLE_SHIP_SOUTHERN_ISLAND", sess)
|
||||
return EventIslands.relayTickets(sess).eon == true
|
||||
and not hasItem(sess, "ITEM_EON_TICKET") and not Rse.flag("FLAG_ENABLE_SHIP_SOUTHERN_ISLAND", sess)
|
||||
end
|
||||
|
||||
-- pokeemerald/src/mystery_gift.c:156
|
||||
function EventIslands.pendingGift(sess)
|
||||
sess = sess or Rse.session()
|
||||
if not EventIslands.enabled(sess) then return nil end
|
||||
if not (sess and EventIslands.enabled(sess)) then return nil end
|
||||
local relay = EventIslands.relayTickets(sess)
|
||||
for _, g in ipairs(EventIslands.manifest().gifts) do
|
||||
local flag = EventIslands.GIFT_FLAGS[g.id]
|
||||
if flag and not Rse.flag(flag, sess) then return g end
|
||||
if flag and relay[g.id] and not Rse.flag(flag, sess) then return g end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokeemerald/src/field_specials.c:3630
|
||||
function EventIslands.sync(sess)
|
||||
function EventIslands.sync(sess, opts)
|
||||
sess = sess or Rse.session()
|
||||
if not (sess and Rse.isRse(sess) and Rse.store()) then return false end
|
||||
if not EventIslands.enabled(sess) then return false end
|
||||
if not (opts and opts.offline) then pcall(EventIslands.refresh, sess, opts) end
|
||||
local pending = EventIslands.eonPending(sess) or EventIslands.pendingGift(sess) ~= nil
|
||||
Rse.setVar("VAR_DISTRIBUTE_EON_TICKET", pending and 1 or 0, sess)
|
||||
if pending then Rse.setVar("VAR_DISTRIBUTE_EON_TICKET", 1, sess) end
|
||||
return pending
|
||||
end
|
||||
|
||||
@@ -106,7 +158,7 @@ function EventIslands.optionRow()
|
||||
step = function(c)
|
||||
local o = require("src.core.game3.options").block(c.options)
|
||||
o[EventIslands.OPTION_KEY] = (tonumber(o[EventIslands.OPTION_KEY]) or 0) ~= 0 and 0 or 1
|
||||
pcall(EventIslands.sync, c.session)
|
||||
pcall(EventIslands.sync, c.session, { force = true })
|
||||
return true
|
||||
end,
|
||||
}
|
||||
|
||||
@@ -258,8 +258,7 @@ function NativesContest.contestLinkTransfer(vm)
|
||||
if not job then
|
||||
if live.isReady and not live:isReady() then return false end
|
||||
-- pokeemerald/src/contest_link.c:74
|
||||
local flags = CL.FLAG.IS_LINK + (CL.wireless and CL.FLAG.IS_WIRELESS or 0)
|
||||
local s = CL.newSession(live, { flags = flags })
|
||||
local s = CL.newSession(live, { flags = CL.flagsFor(live, CL.wireless) })
|
||||
local contestant = Util().contestantFromMon(mon, sess)
|
||||
if contestant.nickname == nil or contestant.nickname == "" then
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
@@ -317,7 +316,8 @@ end
|
||||
-- pokeemerald/src/contest_util.c:2718
|
||||
function NativesContest.linkContestWaitForConnection(ctx, adapters)
|
||||
local s = linkSession()
|
||||
if not (s and isWireless()) then return false, 0 end
|
||||
-- pokeemerald/src/contest_link.c:100
|
||||
if not (s and isWireless()) or s:hasRsPlayer() then return false, 0 end
|
||||
local Natives = Natives()
|
||||
if not Natives.yieldHost(ctx, adapters, function() end) then return false, 1 end
|
||||
ctx.nativePoll = function()
|
||||
@@ -517,7 +517,10 @@ NativesContest.BY_NAME = {
|
||||
-- pokeemerald/src/contest_util.c:2754
|
||||
LinkContestTryHideWirelessIndicator = function() return false end,
|
||||
-- pokeemerald/src/contest_util.c:2763
|
||||
IsContestWithRSPlayer = function() return false, 0 end,
|
||||
IsContestWithRSPlayer = function()
|
||||
local s = linkSession()
|
||||
return false, s and s:hasRsPlayer() and 1 or 0
|
||||
end,
|
||||
-- pokeemerald/src/contest_util.c:2771
|
||||
ClearLinkContestFlags = function()
|
||||
NativesContest.linkFlags = 0
|
||||
|
||||
@@ -92,6 +92,10 @@ end
|
||||
|
||||
local function helloFor(game, linkType, player)
|
||||
local hello = Handshake.hello(handshakeView(game), nil)
|
||||
local okA, ArenaData = pcall(require, "src.online.ArenaData")
|
||||
if okA and type(ArenaData) == "table" and ArenaData.stripCosmeticMods then
|
||||
ArenaData.stripCosmeticMods(hello, game)
|
||||
end
|
||||
hello.generation = Game3Link.GENERATION
|
||||
hello.type = Game3Link.HELLO
|
||||
hello.ruleset = Handshake.DEFAULT_RULESET
|
||||
|
||||
@@ -326,17 +326,95 @@ local function liveVersion(game)
|
||||
return GameVersion.get()
|
||||
end
|
||||
|
||||
ArenaData.COSMETIC_REGISTRIES = {
|
||||
text = true, rom_text = true, strings = true, font = true,
|
||||
sprites = true, palettes = true, icons = true, battle_anims = true,
|
||||
render_pipelines = true, battle_sprite_scales = true, transitions = true,
|
||||
music = true, sfx = true, cries = true, audio = true, map_songs = true,
|
||||
}
|
||||
|
||||
ArenaData.NAME_FIELDS = {
|
||||
pokemon = { id = true, name = true, description = true, dexEntry = true, category = true },
|
||||
moves = { id = true, name = true, description = true },
|
||||
items = { id = true, name = true, description = true },
|
||||
}
|
||||
|
||||
local function cosmeticOp(registry, entry)
|
||||
if ArenaData.COSMETIC_REGISTRIES[registry] then return true end
|
||||
local fields = ArenaData.NAME_FIELDS[registry]
|
||||
if not fields or entry.op ~= "patch" or type(entry.value) ~= "table" then return false end
|
||||
for key in pairs(entry.value) do
|
||||
if not fields[key] then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
local function owns(list, id)
|
||||
for _, entry in ipairs(list or {}) do
|
||||
if entry.owner == id then return true end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function ArenaData.modTouchesGameplay(loader, id)
|
||||
if type(loader) ~= "table" then return false end
|
||||
local record = loader.mods and loader.mods[id]
|
||||
local manifest = record and record.manifest
|
||||
if manifest and #(manifest.permissions or {}) > 0 then return true end
|
||||
for name, registry in pairs(loader.content or {}) do
|
||||
for _, list in pairs(registry.ops or {}) do
|
||||
for _, entry in ipairs(list) do
|
||||
if entry.owner == id and not cosmeticOp(name, entry) then return true end
|
||||
end
|
||||
end
|
||||
end
|
||||
for _, chain in pairs((loader.hooks and loader.hooks.chains) or {}) do
|
||||
if owns(chain, id) then return true end
|
||||
end
|
||||
for _, list in pairs((loader.events and loader.events.listeners) or {}) do
|
||||
if owns(list, id) then return true end
|
||||
end
|
||||
local migrations = loader.migrations and loader.migrations[id]
|
||||
if type(migrations) == "table" and next(migrations) ~= nil then return true end
|
||||
local input = loader.modInput and loader.modInput[id]
|
||||
if type(input) == "table" and (tonumber(input.seq) or 0) > 0 then return true end
|
||||
if loader.stepsQueues and loader.stepsQueues[id] ~= nil then return true end
|
||||
return false
|
||||
end
|
||||
|
||||
function ArenaData.onlineBlockers3(game)
|
||||
local out = {}
|
||||
local loader = type(game) == "table" and game.mods or nil
|
||||
if not loader then return out end
|
||||
for _, entry in ipairs(Handshake.mods(game)) do
|
||||
if entry.affectsLink or ArenaData.modTouchesGameplay(loader, entry.id) then out[#out + 1] = entry end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local NO_MODS = {}
|
||||
|
||||
function ArenaData.stripCosmeticMods(hello, game)
|
||||
if type(hello) ~= "table" or type(game) ~= "table" or not game.mods then return hello end
|
||||
local blockers = ArenaData.onlineBlockers3(game)
|
||||
if #blockers == #(hello.mods or NO_MODS) then return hello end
|
||||
hello.mods = blockers
|
||||
if #blockers == 0 then hello.linkModified = false end
|
||||
local ok, fingerprint = pcall(Fingerprint.compute, game.data, blockers, 3)
|
||||
if ok then hello.fingerprint = fingerprint end
|
||||
return hello
|
||||
end
|
||||
|
||||
function ArenaData.liveProfile3(game, rulesetId)
|
||||
if type(game) ~= "table" then return nil, "no game" end
|
||||
if Handshake.linkModified(game) then return nil, "mods" end
|
||||
if #ArenaData.onlineBlockers3(game) > 0 then return nil, "mods" end
|
||||
local version = liveVersion(game)
|
||||
if not GameVersion.VERSIONS[version] or GameVersion.generation(version) ~= 3 then
|
||||
return nil, "unknown game"
|
||||
end
|
||||
rulesetId = rulesetId or G3_DEFAULT_RULESET
|
||||
if not isG3Ruleset(rulesetId) then return nil, "unknown ruleset" end
|
||||
local ok, fingerprint = pcall(Fingerprint.compute, game.data,
|
||||
Handshake.mods(game), 3)
|
||||
local ok, fingerprint = pcall(Fingerprint.compute, game.data, NO_MODS, 3)
|
||||
if not ok then return nil, tostring(fingerprint) end
|
||||
return copyProfile({
|
||||
engine = 3,
|
||||
|
||||
@@ -0,0 +1,336 @@
|
||||
local LinkTags = {}
|
||||
|
||||
LinkTags.PLATE_H = 11
|
||||
LinkTags.PAD = 2
|
||||
LinkTags.ICON_GAP = 1
|
||||
LinkTags.MAX_TEXT_W = 44
|
||||
LinkTags.TEXT_DY = -2
|
||||
LinkTags.HEAD_OVERLAP = 2
|
||||
LinkTags.EDGE = 1
|
||||
LinkTags.BLENDER_EM_BASE = 0x7F00
|
||||
LinkTags.BLENDER_RS_BASE = 0xF0
|
||||
LinkTags.MAX_SEATS = 5
|
||||
|
||||
LinkTags.PLATE = { 0.06, 0.07, 0.12, 0.62 }
|
||||
LinkTags.PLATE_SELF = { 0.08, 0.2, 0.46, 0.7 }
|
||||
|
||||
LinkTags.COLORS = {
|
||||
["#"] = { 0.12, 0.12, 0.16, 1 },
|
||||
w = { 0.97, 0.97, 0.97, 1 },
|
||||
b = { 0.3, 0.6, 1, 1 },
|
||||
o = { 1, 0.58, 0.18, 1 },
|
||||
r = { 0.92, 0.22, 0.2, 1 },
|
||||
y = { 1, 0.86, 0.3, 1 },
|
||||
}
|
||||
|
||||
LinkTags.ICONS = {
|
||||
chat = {
|
||||
".#######.",
|
||||
"#wwwwwww#",
|
||||
"#w#w#w#w#",
|
||||
"#wwwwwww#",
|
||||
".#ww####.",
|
||||
".#w#.....",
|
||||
".##......",
|
||||
},
|
||||
trade = {
|
||||
"......#...",
|
||||
"#######b#.",
|
||||
"#bbbbbbbb#",
|
||||
"#######b#.",
|
||||
"...#..#...",
|
||||
".#o#######",
|
||||
"#oooooooo#",
|
||||
".#o#######",
|
||||
"...#......",
|
||||
},
|
||||
battle = {
|
||||
"##.....##",
|
||||
"#w#...#w#",
|
||||
".#w#.#w#.",
|
||||
"..#w#w#..",
|
||||
"...#w#...",
|
||||
"..##.##..",
|
||||
".rr#.#rr.",
|
||||
"#r#...#r#",
|
||||
"##.....##",
|
||||
},
|
||||
busy = {
|
||||
"#######",
|
||||
"#yyyyy#",
|
||||
".#yyy#.",
|
||||
"..#y#..",
|
||||
".#y.y#.",
|
||||
"#yyyyy#",
|
||||
"#######",
|
||||
},
|
||||
}
|
||||
|
||||
LinkTags._images = nil
|
||||
LinkTags._widths = {}
|
||||
LinkTags._widthCount = 0
|
||||
LinkTags._src = { byVobj = {}, player = nil }
|
||||
LinkTags._rows = {}
|
||||
LinkTags._self = {}
|
||||
|
||||
function LinkTags.iconSize(kind)
|
||||
local rows = LinkTags.ICONS[kind]
|
||||
if not rows then return 0, 0 end
|
||||
return #rows[1], #rows
|
||||
end
|
||||
|
||||
local function union()
|
||||
return package.loaded["src.core.game3.link.union_room"]
|
||||
end
|
||||
|
||||
local function linkPlayers()
|
||||
return package.loaded["src.core.game3.link.link_players"]
|
||||
end
|
||||
|
||||
local function link()
|
||||
return package.loaded["src.core.game3.link"]
|
||||
end
|
||||
|
||||
local function tagRow(i)
|
||||
local row = LinkTags._rows[i]
|
||||
if not row then
|
||||
row = {}
|
||||
LinkTags._rows[i] = row
|
||||
end
|
||||
return row
|
||||
end
|
||||
|
||||
local function localKind()
|
||||
local LT = package.loaded["src.core.game3.link.trade"]
|
||||
if LT and LT.isActive and LT.isActive() then return "trade" end
|
||||
local LB = package.loaded["src.core.game3.link.battle"]
|
||||
if LB and LB.isActive and LB.isActive() then return "battle" end
|
||||
local Chat = package.loaded["src.core.game3.link.chat"]
|
||||
if Chat and Chat.isActive and Chat.isActive() then return "chat" end
|
||||
local U = union()
|
||||
if U and U.isActive and U.isActive() and U.state == "in_activity" then
|
||||
return U.memberStatusKind({ activity = U.activity })
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
local function blenderPartners(src, n)
|
||||
local L = link()
|
||||
local lk = L and L.link
|
||||
if not (lk and lk.isOpen and lk:isOpen() and lk.players) then return n, false end
|
||||
local V = package.loaded["src.core.game3.virtual_objects"]
|
||||
if not V then return n, false end
|
||||
local ok, players = pcall(lk.players, lk)
|
||||
if not ok or type(players) ~= "table" then return n, false end
|
||||
local any = false
|
||||
for i, player in ipairs(players) do
|
||||
local seat = tonumber(player.seat) or (i - 1)
|
||||
if seat >= 0 and seat < LinkTags.MAX_SEATS and type(player.name) == "string" then
|
||||
for _, id in ipairs({ LinkTags.BLENDER_EM_BASE + seat, LinkTags.BLENDER_RS_BASE - seat }) do
|
||||
if V.get(id) then
|
||||
n = n + 1
|
||||
local row = tagRow(n)
|
||||
row.name, row.kind = player.name, nil
|
||||
src.byVobj[id] = row
|
||||
any = true
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return n, any
|
||||
end
|
||||
|
||||
function LinkTags.sources()
|
||||
local src = LinkTags._src
|
||||
for k in pairs(src.byVobj) do src.byVobj[k] = nil end
|
||||
src.player = nil
|
||||
local active, n = false, 0
|
||||
local U = union()
|
||||
if U and U.isActive and U.isActive() and U.onUnionRoomMap and U.onUnionRoomMap() then
|
||||
active = true
|
||||
for slot = 1, U.capacity() do
|
||||
local p = U.players[slot]
|
||||
if p and not p.gone and type(p.name) == "string" then
|
||||
n = n + 1
|
||||
local row = tagRow(n)
|
||||
row.name, row.kind = p.name, U.memberStatusKind(p)
|
||||
src.byVobj[U.vobjId(slot)] = row
|
||||
end
|
||||
end
|
||||
end
|
||||
local LP = linkPlayers()
|
||||
if LP and LP._mapId then
|
||||
active = true
|
||||
for seat in pairs(LP.remotes()) do
|
||||
local name = LP.nameOf(seat)
|
||||
if name then
|
||||
n = n + 1
|
||||
local row = tagRow(n)
|
||||
row.name, row.kind = name, LP.statusKind(seat)
|
||||
src.byVobj[LP.VOBJ_BASE + seat] = row
|
||||
end
|
||||
end
|
||||
end
|
||||
local blender
|
||||
n, blender = blenderPartners(src, n)
|
||||
active = active or blender
|
||||
if not active then return nil end
|
||||
local L = link()
|
||||
local s = L and L.session and L.session()
|
||||
local me = LinkTags._self
|
||||
me.name = type(s) == "table" and tostring(s.name or s.playerName or ""):sub(1, 7) or nil
|
||||
me.kind = localKind()
|
||||
if me.name and me.name ~= "" then src.player = me end
|
||||
return src
|
||||
end
|
||||
|
||||
function LinkTags.tagFor(src, a)
|
||||
if not (src and a) then return nil end
|
||||
if a.kind == "player" then return src.player end
|
||||
local eo = a.eventObject
|
||||
local id = eo and eo.virtualId
|
||||
return id and src.byVobj[id] or nil
|
||||
end
|
||||
|
||||
function LinkTags.textWidth(name, measure)
|
||||
local w = LinkTags._widths[name]
|
||||
if w then return w end
|
||||
if measure then
|
||||
w = measure(name)
|
||||
else
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
local ok, got = pcall(FrlgFont.measure, name, { small = true })
|
||||
w = ok and tonumber(got) or (#name * 6)
|
||||
end
|
||||
w = math.min(LinkTags.MAX_TEXT_W, math.max(0, w))
|
||||
if LinkTags._widthCount > 256 then
|
||||
LinkTags._widths, LinkTags._widthCount = {}, 0
|
||||
end
|
||||
LinkTags._widths[name] = w
|
||||
LinkTags._widthCount = LinkTags._widthCount + 1
|
||||
return w
|
||||
end
|
||||
|
||||
function LinkTags.layout(tag, headX, headY, viewW, measure, out)
|
||||
local textW = LinkTags.textWidth(tag.name, measure)
|
||||
local iconW = tag.kind and LinkTags.iconSize(tag.kind) or 0
|
||||
local lead = iconW > 0 and (iconW + LinkTags.ICON_GAP) or 0
|
||||
local w = LinkTags.PAD * 2 + textW + lead
|
||||
local x = math.floor(headX - w / 2 + 0.5)
|
||||
if viewW then
|
||||
x = math.max(LinkTags.EDGE, math.min(viewW - w - LinkTags.EDGE, x))
|
||||
end
|
||||
out = out or {}
|
||||
out.x, out.y, out.w, out.h = x, math.floor(headY - LinkTags.PLATE_H + LinkTags.HEAD_OVERLAP), w, LinkTags.PLATE_H
|
||||
out.textW, out.iconW, out.textX = textW, iconW, x + LinkTags.PAD + lead
|
||||
return out
|
||||
end
|
||||
|
||||
local LAYOUT = {}
|
||||
local TEXT_OPTS = { small = true, colors = nil, maxWidth = 0 }
|
||||
|
||||
local function iconImages()
|
||||
if LinkTags._images then return LinkTags._images end
|
||||
local out = {}
|
||||
for kind, rows in pairs(LinkTags.ICONS) do
|
||||
local w, h = #rows[1], #rows
|
||||
local data = love.image.newImageData(w, h)
|
||||
for y = 1, h do
|
||||
for x = 1, w do
|
||||
local c = LinkTags.COLORS[rows[y]:sub(x, x)]
|
||||
if c then data:setPixel(x - 1, y - 1, c[1], c[2], c[3], c[4]) end
|
||||
end
|
||||
end
|
||||
local img = love.graphics.newImage(data)
|
||||
img:setFilter("nearest", "nearest")
|
||||
out[kind] = img
|
||||
end
|
||||
LinkTags._images = out
|
||||
return out
|
||||
end
|
||||
|
||||
local function plate(x, y, w, h, color)
|
||||
love.graphics.setColor(color)
|
||||
love.graphics.rectangle("fill", x + 1, y, w - 2, h)
|
||||
love.graphics.rectangle("fill", x, y + 1, 1, h - 2)
|
||||
love.graphics.rectangle("fill", x + w - 1, y + 1, 1, h - 2)
|
||||
end
|
||||
|
||||
local opaqueTops = setmetatable({}, { __mode = "k" })
|
||||
|
||||
local function opaqueTop(spr)
|
||||
local top = opaqueTops[spr]
|
||||
if top then return top end
|
||||
top = 0
|
||||
local data = spr.imageData
|
||||
local w, h = tonumber(spr.width), tonumber(spr.height)
|
||||
if data and data.getPixel and w and h then
|
||||
pcall(function()
|
||||
for y = 0, h - 1 do
|
||||
for x = 0, w - 1 do
|
||||
local _, _, _, alpha = data:getPixel(x, y)
|
||||
if alpha > 0 then
|
||||
top = y
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
end)
|
||||
end
|
||||
opaqueTops[spr] = top
|
||||
return top
|
||||
end
|
||||
|
||||
local function headTop(a, camY)
|
||||
local h, offY, top = 32, 0, 0
|
||||
local Ow = package.loaded["src.core.game3.ow_sprites"]
|
||||
if Ow and Ow.getDraw and a.graphicsId ~= nil then
|
||||
local ok, spr = pcall(Ow.getDraw, a.graphicsId)
|
||||
if ok and type(spr) == "table" then
|
||||
h = tonumber(spr.height) or h
|
||||
offY = tonumber(spr.drawOffY) or 0
|
||||
top = opaqueTop(spr)
|
||||
end
|
||||
end
|
||||
return a.y - camY + 16 - h + offY + top
|
||||
end
|
||||
|
||||
local function drawTag(a, tag, camX, camY, viewW, isSelf)
|
||||
local L = LinkTags.layout(tag, a.x - camX + 8, headTop(a, camY), viewW, nil, LAYOUT)
|
||||
plate(L.x, L.y, L.w, L.h, isSelf and LinkTags.PLATE_SELF or LinkTags.PLATE)
|
||||
if tag.kind then
|
||||
local img = iconImages()[tag.kind]
|
||||
if img then
|
||||
local _, ih = LinkTags.iconSize(tag.kind)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
love.graphics.draw(img, L.x + LinkTags.PAD, L.y + math.floor((L.h - ih) / 2))
|
||||
end
|
||||
end
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
TEXT_OPTS.colors, TEXT_OPTS.maxWidth = FrlgFont.COLOR.WHITE, L.textW
|
||||
pcall(FrlgFont.draw, tag.name, L.textX, L.y + LinkTags.TEXT_DY, TEXT_OPTS)
|
||||
end
|
||||
|
||||
local function drawList(src, list, camX, camY, viewW, billboard)
|
||||
if not list then return end
|
||||
for _, a in ipairs(list) do
|
||||
local tag = LinkTags.tagFor(src, a)
|
||||
if tag and tag.name and tag.name ~= "" then
|
||||
local pushed = billboard and billboard(a.x, a.y, camX, camY)
|
||||
drawTag(a, tag, camX, camY, viewW, a.kind == "player")
|
||||
if pushed then love.graphics.pop() end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LinkTags.draw(under, over, camX, camY, viewW, _viewH, billboard)
|
||||
if not (love and love.graphics) then return false end
|
||||
local src = LinkTags.sources()
|
||||
if not src then return false end
|
||||
drawList(src, under, camX, camY, viewW, billboard)
|
||||
drawList(src, over, camX, camY, viewW, billboard)
|
||||
love.graphics.setColor(1, 1, 1, 1)
|
||||
return true
|
||||
end
|
||||
|
||||
return LinkTags
|
||||
+159
-10
@@ -29,6 +29,9 @@ Ui.STATE = {
|
||||
SAVE_DONE = "save_done",
|
||||
GIFT_INPUT = "gift_input",
|
||||
NEWS_VIEW = "news_view",
|
||||
SAVED_VIEW = "saved_view",
|
||||
GIFT_MENU = "gift_menu",
|
||||
ASK_TOSS = "ask_toss",
|
||||
EXIT = "exit",
|
||||
}
|
||||
|
||||
@@ -40,6 +43,13 @@ local THREE_WIN = Window.template(8, 5, 14, 5)
|
||||
local OFFER_WIN = Window.template(1, 3, 17, 10)
|
||||
-- pokefirered/src/mystery_gift_menu.c:157 sWindowTemplate_YesNoBox
|
||||
local YESNO_WIN = Window.template(23, 15, 6, 4)
|
||||
-- pokefirered/src/mystery_gift_menu.c:137 sWindowTemplate_GiftSelect
|
||||
local GIFT_SELECT_WIN = Window.template(1, 15, 19, 4)
|
||||
-- pokefirered/src/mystery_gift_menu.c:177 sWindowTemplate_GiftSelect_2Options
|
||||
local GIFT_SELECT_OPTIONS = {
|
||||
[3] = Window.template(22, 14, 7, 5),
|
||||
[2] = Window.template(22, 15, 7, 4),
|
||||
}
|
||||
-- pokefirered/src/mystery_gift_show_card.c:67 sWindowTemplates
|
||||
local CARD_HEADER = Window.template(1, 1, 25, 4)
|
||||
local CARD_BODY = Window.template(1, 6, 28, 8)
|
||||
@@ -264,7 +274,8 @@ function Ui.news(st)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:855 SaveOnMysteryGiftMenu
|
||||
local function beginSave(st)
|
||||
local function beginSave(st, afterSave)
|
||||
st.afterSave = afterSave
|
||||
st.state = Ui.STATE.SAVE
|
||||
-- pokefirered/src/strings.c:1321 gText_DataWillBeSaved
|
||||
say(st, RomText.plain("gText_DataWillBeSaved"), nil, true)
|
||||
@@ -302,11 +313,12 @@ local function toMainMenuOffline(st)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1352
|
||||
local function clientResult(st, textKey, success)
|
||||
local function clientResult(st, textKey, success, onShown)
|
||||
st.state = Ui.STATE.RESULT_MSG
|
||||
if success then
|
||||
fanfare(Song.MUS_OBTAIN_ITEM)
|
||||
say(st, RomText.plain(textKey), function(s)
|
||||
if onShown then onShown(s) end
|
||||
beginSave(s)
|
||||
end, true, Ui.SUCCESS_FRAMES)
|
||||
else
|
||||
@@ -315,10 +327,29 @@ local function clientResult(st, textKey, success)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1344
|
||||
local function commCompleted(st, textKey, success)
|
||||
local function commCompleted(st, textKey, success, onShown)
|
||||
iconOff()
|
||||
closeOffers(st)
|
||||
clientResult(st, textKey, success)
|
||||
clientResult(st, textKey, success, onShown)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_client.c:210 CLI_SAVE_NEWS
|
||||
local function receiveNewsFrom(st, entry)
|
||||
local sess = session(st)
|
||||
st.viewNews = nil
|
||||
local ok, why = MysteryGift.saveNewsIfNew(sess, entry.news)
|
||||
if not ok then
|
||||
-- pokefirered/src/mystery_gift_menu.c:921
|
||||
commCompleted(st, why == "had" and "gText_AlreadyHadNews" or "gText_CommunicationError", false)
|
||||
return false
|
||||
end
|
||||
MysteryGift.claimNews(sess, entry.news)
|
||||
-- pokefirered/src/mystery_gift_menu.c:905
|
||||
commCompleted(st, "gText_WonderNewsReceived", true, function(s)
|
||||
-- pokefirered/src/mystery_gift_menu.c:1369
|
||||
MysteryGift.setNewsReward(session(s), MysteryGift.WONDER_NEWS_RECV_WIRELESS)
|
||||
end)
|
||||
return true
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_client.c:208 CLI_SAVE_CARD
|
||||
@@ -368,7 +399,9 @@ function Ui.offerRows(st)
|
||||
local ListMenu = require("src.ui.game3.list_menu")
|
||||
local items = {}
|
||||
for i, entry in ipairs(st.offers or {}) do
|
||||
local claimed = not st.isNews and MysteryGift.hasClaimedCard(sess, entry.card)
|
||||
local claimed
|
||||
if st.isNews then claimed = MysteryGift.hasClaimedNews(sess, entry.news)
|
||||
else claimed = MysteryGift.hasClaimedCard(sess, entry.card) end
|
||||
items[i] = {
|
||||
label = entry.label,
|
||||
id = i,
|
||||
@@ -450,6 +483,76 @@ local function pickOffer(st, entry)
|
||||
deliverOffer(st, entry)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:765 ValidateCardOrNews
|
||||
local function hasSaved(st)
|
||||
if st.isNews then return MysteryGift.validateSavedNews(session(st)) end
|
||||
return MysteryGift.validateSavedCard(session(st))
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1390 MG_STATE_LOAD_GIFT
|
||||
local function openSaved(st)
|
||||
st.viewCard, st.viewNews, st.viewOffer = nil, nil, nil
|
||||
st.newsScroll = 0
|
||||
st.prompt = nil
|
||||
st.state = Ui.STATE.SAVED_VIEW
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1147 MG_STATE_DONT_HAVE_ANY
|
||||
local function openBranch(st)
|
||||
if hasSaved(st) then return openSaved(st) end
|
||||
st.state = Ui.STATE.RESULT_MSG
|
||||
say(st, RomText.plain(st.isNews and "gText_DontHaveNewsNewOneInput" or "gText_DontHaveCardNewOneInput"),
|
||||
startSearch)
|
||||
end
|
||||
Ui.openBranch = openBranch
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1417 MG_STATE_HANDLE_GIFT_SELECT
|
||||
function Ui.giftMenuRows(st)
|
||||
local list, actions
|
||||
if st.isNews then
|
||||
list, actions = "sListMenuItems_Receive", { "receive", "cancel" }
|
||||
else
|
||||
list, actions = "sListMenuItems_ReceiveToss", { "receive", "toss", "cancel" }
|
||||
end
|
||||
st.giftActions = actions
|
||||
return RomText.list(list)
|
||||
end
|
||||
|
||||
local function openGiftMenu(st)
|
||||
st.state = Ui.STATE.GIFT_MENU
|
||||
-- pokefirered/src/mystery_gift_menu.c:716
|
||||
st.prompt = RomText.plain(st.isNews and "gText_WhatToDoWithNews" or "gText_WhatToDoWithCards")
|
||||
setRows(st, Ui.giftMenuRows(st), 1)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1484 MG_STATE_TOSS
|
||||
local function tossSaved(st)
|
||||
st.prompt = nil
|
||||
if st.isNews then MysteryGift.clearNewsAndRelated(session(st))
|
||||
else MysteryGift.clearCardAndRelated(session(st)) end
|
||||
beginSave(st, function(s)
|
||||
s.state = Ui.STATE.RESULT_MSG
|
||||
-- pokefirered/src/mystery_gift_menu.c:847 PrintThrownAway
|
||||
say(s, RomText.plain(s.isNews and "gText_WonderNewsThrownAway" or "gText_WonderCardThrownAway"), toMainMenu)
|
||||
end)
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1454 MG_STATE_ASK_TOSS
|
||||
local function askToss(st)
|
||||
st.state = Ui.STATE.ASK_TOSS
|
||||
st.prompt = nil
|
||||
-- pokefirered/src/mystery_gift_menu.c:839 AskDiscardGift
|
||||
ask(st, RomText.plain(st.isNews and "gText_OkayToDiscardNews" or "gText_IfThrowAwayCardEventWontHappen"),
|
||||
function(s)
|
||||
if not s.isNews and MysteryGift.isGiftNotReceived(session(s)) then
|
||||
-- pokefirered/src/mystery_gift_menu.c:1470 MG_STATE_ASK_TOSS_UNRECEIVED
|
||||
ask(s, RomText.plain("gText_HaventReceivedGiftOkayToDiscard"), tossSaved, openGiftMenu)
|
||||
return
|
||||
end
|
||||
tossSaved(s)
|
||||
end, openGiftMenu)
|
||||
end
|
||||
|
||||
function Ui.close(st)
|
||||
if type(st) == "table" and st.job then
|
||||
MysteryGift.cancelOnline(st.job)
|
||||
@@ -562,10 +665,10 @@ function Ui.update(st, pressed, dt)
|
||||
local action = pick == -1 and "exit" or (st.mainActions or {})[pick]
|
||||
if action == "news" then
|
||||
st.isNews = true
|
||||
startSearch(st)
|
||||
openBranch(st)
|
||||
elseif action == "cards" then
|
||||
st.isNews = false
|
||||
startSearch(st)
|
||||
openBranch(st)
|
||||
elseif action == "exit" then
|
||||
st.state = S.EXIT
|
||||
Ui.close(st)
|
||||
@@ -615,6 +718,7 @@ function Ui.update(st, pressed, dt)
|
||||
local item = list:selected()
|
||||
if item and item.entry and st.isNews then
|
||||
st.viewNews = item.entry.news
|
||||
st.viewNewsEntry = item.entry
|
||||
st.newsScroll = 0
|
||||
st.prompt = nil
|
||||
st.state = S.NEWS_VIEW
|
||||
@@ -634,22 +738,59 @@ function Ui.update(st, pressed, dt)
|
||||
|
||||
if st.state == S.NEWS_VIEW then
|
||||
tickNewsScroll(st, pressed)
|
||||
if pressed("a") or pressed("b") then
|
||||
if pressed("a") and st.viewNewsEntry then
|
||||
se(Song.SE_SELECT)
|
||||
local entry = st.viewNewsEntry
|
||||
st.viewNewsEntry = nil
|
||||
receiveNewsFrom(st, entry)
|
||||
elseif pressed("a") or pressed("b") then
|
||||
se(Song.SE_SELECT)
|
||||
st.viewNews = nil
|
||||
st.viewNewsEntry = nil
|
||||
st.state = S.OFFER_LIST
|
||||
st.prompt = Ui.listPrompt(st)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
-- pokefirered/src/mystery_gift_menu.c:1394 MG_STATE_HANDLE_GIFT_INPUT
|
||||
if st.state == S.SAVED_VIEW then
|
||||
if st.isNews then tickNewsScroll(st, pressed) end
|
||||
if pressed("a") then
|
||||
se(Song.SE_SELECT)
|
||||
openGiftMenu(st)
|
||||
elseif pressed("b") then
|
||||
se(Song.SE_SELECT)
|
||||
toMainMenu(st)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if st.state == S.GIFT_MENU then
|
||||
local pick = tickList(st, pressed)
|
||||
if pick == nil then return nil end
|
||||
local action = pick == -1 and "cancel" or (st.giftActions or {})[pick]
|
||||
if action == "receive" then
|
||||
-- pokefirered/src/mystery_gift_menu.c:1506 MG_STATE_RECEIVE
|
||||
st.prompt = nil
|
||||
startSearch(st)
|
||||
elseif action == "toss" then
|
||||
askToss(st)
|
||||
else
|
||||
openSaved(st)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
if st.state == S.SAVE then
|
||||
local ok = true
|
||||
if st.onSave then ok = st.onSave(session(st)) ~= false end
|
||||
local after = st.afterSave
|
||||
st.afterSave = nil
|
||||
st.state = S.SAVE_DONE
|
||||
-- pokefirered/src/strings.c:1322 gText_SaveCompletedPressA
|
||||
say(st, ok and RomText.plain("gText_SaveCompletedPressA")
|
||||
or Strings("Save failed."), toMainMenu)
|
||||
or Strings("Save failed."), ok and after or toMainMenu)
|
||||
return nil
|
||||
end
|
||||
|
||||
@@ -824,9 +965,11 @@ function Ui.drawNews(st)
|
||||
end
|
||||
end
|
||||
|
||||
local CARD_STATES = { gift_input = true, news_view = true, saved_view = true, gift_menu = true, ask_toss = true }
|
||||
|
||||
function Ui.draw(st)
|
||||
local S = Ui.STATE
|
||||
if st.state == S.GIFT_INPUT or st.state == S.NEWS_VIEW then
|
||||
if CARD_STATES[st.state] then
|
||||
if st.isNews then Ui.drawNews(st) else Ui.drawCard(st) end
|
||||
else
|
||||
drawMenuBg()
|
||||
@@ -843,6 +986,12 @@ function Ui.draw(st)
|
||||
drawYesNo(st)
|
||||
elseif st.msg then
|
||||
drawMessage(st.msg.text, math.min(st.msg.revealed, st.msg.total))
|
||||
elseif st.state == S.GIFT_MENU then
|
||||
-- pokefirered/src/mystery_gift_menu.c:719
|
||||
Window.fixedStdFrame(GIFT_SELECT_WIN)
|
||||
Window.printPx(st.prompt or "", GIFT_SELECT_WIN.left * T, GIFT_SELECT_WIN.top * T + 2,
|
||||
{ colors = TEXT, maxWidth = GIFT_SELECT_WIN.width * T })
|
||||
drawList(st, GIFT_SELECT_OPTIONS[#(st.rows or {})] or GIFT_SELECT_OPTIONS[3])
|
||||
elseif st.prompt then
|
||||
drawMessage(st.prompt, nil)
|
||||
end
|
||||
|
||||
@@ -359,9 +359,8 @@ function Rows.build(ctx, skip)
|
||||
return true
|
||||
end,
|
||||
})
|
||||
if require("src.core.game3.profile").family(ctx and ctx.session) == "rse" then
|
||||
add(require("src.core.game3.rse.event_islands").optionRow())
|
||||
end
|
||||
local EventIslands = require("src.core.game3.rse.event_islands")
|
||||
if EventIslands.available() then add(EventIslands.optionRow()) end
|
||||
|
||||
add({
|
||||
id = "touchControls", label = Strings("TOUCH PAD"),
|
||||
|
||||
@@ -4,6 +4,8 @@ local Font = require("src.ui.game3.frlg_font")
|
||||
local Text = require("src.core.game3.rom_text")
|
||||
local Strings = require("src.core.Strings")
|
||||
local M = {ID = "rs_cable_lobby", open = false}
|
||||
-- pokeemerald/src/strings.c:1242
|
||||
M.HEADERS = {"gOtherText_LinkStandby", "gText_CommunicationStandby"}
|
||||
function M.isOpen() return M.open end
|
||||
function M.show(opts)
|
||||
M.opts, M.cursor, M.open = opts, 1, true
|
||||
@@ -38,8 +40,11 @@ end
|
||||
function M.draw()
|
||||
if not M.open then return end
|
||||
Window.stdFrame(Window.template(1, 1, 28, 18))
|
||||
local rows, opts = M.rows(), {font = "native_3"}
|
||||
Font.draw(Text.plain("gOtherText_LinkStandby"), 16, 16, opts)
|
||||
local Family = require("src.core.game3.link.family")
|
||||
local rows, opts = M.rows(), Family.isRubySapphire(Family.activeVersion()) and {font = "native_3"} or {}
|
||||
for _, key in ipairs(M.HEADERS) do
|
||||
if Text.has(key) then Font.draw(Text.plain(key), 16, 16, opts); break end
|
||||
end
|
||||
local title = M.opts.title and M.opts.title() or ""
|
||||
Font.draw(title, 16, 40, opts)
|
||||
for i, row in ipairs(rows) do
|
||||
|
||||
@@ -1,27 +1,18 @@
|
||||
local Kit = require("src.ui.game3.rse.scene_kit")
|
||||
local Pal = require("src.core.game3.pal_fade")
|
||||
local Chrome = require("src.ui.game3.chrome")
|
||||
local Link = require("src.core.game3.link.init")
|
||||
local Event = require("src.core.game3.rs.mystery_event")
|
||||
local M = {MESSAGE = "rs_mystery_event", REQUEST = "rs_mystery_event_request"}
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
local Strings = require("src.core.Strings")
|
||||
local M = {VISIBLE = 4, NEWS_LINES = 8}
|
||||
M.__index = M
|
||||
function M.send(link, bytes)
|
||||
if type(bytes) == "string" then bytes = {bytes:byte(1, #bytes)} end
|
||||
if type(bytes) ~= "table" or #bytes == 0 or #bytes > Event.MAX_BYTES then return false end
|
||||
local copy = {}
|
||||
for i, b in ipairs(bytes) do
|
||||
if type(b) ~= "number" or b < 0 or b > 255 or b % 1 ~= 0 then return false end
|
||||
copy[i] = b
|
||||
end
|
||||
return link:send({type = M.MESSAGE, bytes = copy})
|
||||
end
|
||||
function M.new(opts)
|
||||
opts = opts or {}
|
||||
local saved = opts.save or Kit.loadRawSave()
|
||||
local saved = opts.save or (not opts.session and Kit.loadRawSave())
|
||||
local session = opts.session or (saved and require("src.core.game3.save_schema_firered").fromSaveTable(saved))
|
||||
assert(session, "RS Mystery Events require an existing save")
|
||||
local self = setmetatable({game = opts.game, session = session, opts = opts, state = "fade_in",
|
||||
pal = Pal.new(), ticks = 0, step = Kit.stepper()}, M)
|
||||
pal = Pal.new(), ticks = 0, step = Kit.stepper(), cursor = 1, scroll = 0}, M)
|
||||
self.pal:beginFade(Pal.ALL, 0, 16, 0, Pal.BLACK)
|
||||
return self
|
||||
end
|
||||
@@ -29,8 +20,7 @@ function M:message(source, vars)
|
||||
self.printer = Kit.printer(source, {speed = 2, canSpeedUp = false, ctx = {stringVars = vars or {}}})
|
||||
end
|
||||
function M:close()
|
||||
require("src.ui.game3.rs.cable_lobby").close()
|
||||
Link.clientCall("leaveGroup"); Link.closeLink("rs_mystery_event_exit")
|
||||
if self.job then MysteryGift.cancelOnline(self.job); self.job = nil end
|
||||
end
|
||||
function M:exit()
|
||||
self:close(); self.state = "exit"
|
||||
@@ -40,83 +30,108 @@ function M:failed(why)
|
||||
self.error = why; self.loading = false
|
||||
self:close(); self:message("gSystemText_LoadingError"); self.state = "result_print"
|
||||
end
|
||||
function M:startEntry()
|
||||
local profile = self.opts.profile
|
||||
if not profile then profile = require("src.online.ArenaData").liveProfile3(self.game, "g3_link") end
|
||||
function M:startFetch()
|
||||
local s = self.session
|
||||
local avatar = {name = s.name or s.playerName or "", gender = s.gender or 0, version = s.version,
|
||||
trainerId = require("src.core.game3.link.rs").trainerId(s)}
|
||||
local spec = {wire = "mystery_event", linkType = 0x5501, min = 2, max = 2, profile = profile,
|
||||
avatar = avatar, game = self.game,
|
||||
hello = require("src.link.Game3Link").hello(self.game, 0x5501, {session = s, version = s.version,
|
||||
name = avatar.name, gender = avatar.gender, trainerId = avatar.trainerId}),
|
||||
connect = function()
|
||||
return require("src.online.Connect").start({source = "game", version = s.version,
|
||||
trainerName = avatar.name, profiles = {assert(profile, "Mystery Event live profile missing")},
|
||||
presence = {where = "game", status = "busy", version = s.version}})
|
||||
end}
|
||||
self.ctx = require("src.core.game3.scripting.ctx").new()
|
||||
local function ready()
|
||||
Kit.playSe("SE_PIN")
|
||||
self.state = "ready_print"; self:message("gSystemText_LoadEventPressA")
|
||||
return false, 1
|
||||
self.job = MysteryGift.fetchOnline({family = "rs", version = s.version, session = s,
|
||||
transport = self.opts.transport, client = self.opts.client})
|
||||
self.state = "fetching"
|
||||
end
|
||||
function M:selected()
|
||||
return self.rows and self.rows[self.cursor]
|
||||
end
|
||||
local function plainIr(text)
|
||||
return require("src.core.game3.scripting.text_ir").fromAscii(text)
|
||||
end
|
||||
function M:writeSave()
|
||||
local save = require("src.core.game3.save_schema_firered").toSaveTable(self.session)
|
||||
local writer = self.opts.writeSave or require("src.core.SaveData").save
|
||||
local ok, written = pcall(writer, save)
|
||||
if not ok or written == false then return false end
|
||||
self.saved = save; if self.game then self.game.save = save end
|
||||
return true
|
||||
end
|
||||
function M:buildRows(result)
|
||||
local rows = {}
|
||||
if MysteryGift.validateSavedNews(self.session) then
|
||||
rows[#rows + 1] = {kind = "news", saved = true, label = MysteryGift.getSavedNews(self.session).titleText,
|
||||
news = MysteryGift.getSavedNews(self.session)}
|
||||
end
|
||||
local yielded, result = require("src.core.game3.rs.link_entry").run(self.ctx, {}, spec, ready)
|
||||
if self.state == "ready_print" then return end
|
||||
if yielded then self.state = "lobby" else self:failed(result) end
|
||||
for _, ev in ipairs(result.events or {}) do rows[#rows + 1] = {kind = "event", label = ev.label, bytes = ev.bytes} end
|
||||
for _, n in ipairs(result.news or {}) do
|
||||
if not MysteryGift.hasClaimedNews(self.session, n.news) then
|
||||
rows[#rows + 1] = {kind = "news", label = n.label, news = n.news}
|
||||
end
|
||||
end
|
||||
return rows
|
||||
end
|
||||
function M:receiveNews(row)
|
||||
local ok, why = MysteryGift.saveNewsIfNew(self.session, row.news)
|
||||
if not ok then
|
||||
self:message(plainIr(why == "had" and Strings("You already have this WONDER NEWS.") or Strings("This WONDER NEWS can't be read.")))
|
||||
self.state = "result_print"
|
||||
return
|
||||
end
|
||||
MysteryGift.claimNews(self.session, row.news)
|
||||
if not self:writeSave() then return self:failed("save_failed") end
|
||||
self:message(plainIr(Strings("The WONDER NEWS was saved.")))
|
||||
self.state = "result_print"
|
||||
end
|
||||
function M:frame(inp)
|
||||
self.ticks = self.ticks + 1
|
||||
local lk = Link.link
|
||||
if lk then lk:update(1 / 60) end
|
||||
if self.opts.payload and lk and lk:isReady() and lk:take(M.REQUEST) then M.send(lk, self.opts.payload) end
|
||||
if self.state == "fade_in" then
|
||||
-- pokeruby/src/mystery_event_menu.c:98
|
||||
if not self.pal:fadeActive() then self:message("gSystemText_LinkStandby"); self.state = "standby_print" end
|
||||
elseif self.state == "standby_print" then
|
||||
self.printer:run(inp)
|
||||
if not self.printer:isActive() then self:startEntry() end
|
||||
elseif self.state == "lobby" then
|
||||
require("src.ui.game3.rs.cable_lobby").update(inp)
|
||||
if self.ctx.nativePoll and self.ctx.nativePoll() and self.state == "lobby" then
|
||||
local result = self.ctx.specialVars[0x800D]
|
||||
if result == 5 then self:exit() else self:failed(result) end
|
||||
if not self.printer:isActive() then self:startFetch() end
|
||||
elseif self.state == "fetching" then
|
||||
if inp.new.b then Kit.playSe("SE_SELECT"); self:exit(); return end
|
||||
local status, result = MysteryGift.pollOnline(self.job)
|
||||
if status == "pending" then return end
|
||||
self.job = nil
|
||||
if status ~= "ok" then return self:failed(result) end
|
||||
self.rows = self:buildRows(result)
|
||||
if #self.rows == 0 then return self:failed("no_events") end
|
||||
self.cursor, self.scroll, self.state = 1, 0, "list"
|
||||
elseif self.state == "list" then
|
||||
if inp.new.b then Kit.playSe("SE_SELECT"); self:exit()
|
||||
elseif inp.new.up and self.cursor > 1 then
|
||||
Kit.playSe("SE_SELECT"); self.cursor = self.cursor - 1
|
||||
if self.cursor <= self.scroll then self.scroll = self.cursor - 1 end
|
||||
elseif inp.new.down and self.cursor < #self.rows then
|
||||
Kit.playSe("SE_SELECT"); self.cursor = self.cursor + 1
|
||||
if self.cursor > self.scroll + M.VISIBLE then self.scroll = self.cursor - M.VISIBLE end
|
||||
elseif inp.new.a and self:selected().kind == "news" then
|
||||
Kit.playSe("SE_SELECT"); self.newsScroll, self.state = 0, "news_view"
|
||||
elseif inp.new.a then
|
||||
-- pokeruby/src/mystery_event_menu.c:112
|
||||
Kit.playSe("SE_PIN"); self:message("gSystemText_LoadEventPressA"); self.state = "ready_print"
|
||||
end
|
||||
elseif self.state == "news_view" then
|
||||
local row = self:selected()
|
||||
local last = 0
|
||||
for i, line in ipairs(row.news.bodyText or {}) do if line ~= "" then last = i end end
|
||||
if inp.new.up and self.newsScroll > 0 then self.newsScroll = self.newsScroll - 1
|
||||
elseif inp.new.down and self.newsScroll < math.max(0, last - M.NEWS_LINES) then self.newsScroll = self.newsScroll + 1
|
||||
elseif inp.new.a and not row.saved then Kit.playSe("SE_SELECT"); self:receiveNews(row)
|
||||
elseif inp.new.a or inp.new.b then Kit.playSe("SE_SELECT"); self.state = "list" end
|
||||
elseif self.state == "ready_print" then
|
||||
self.printer:run(inp); if not self.printer:isActive() then self.state = "ready" end
|
||||
elseif self.state == "ready" then
|
||||
if inp.new.b then Kit.playSe("SE_SELECT"); self:exit()
|
||||
elseif not lk or not lk:isOpen() then self:failed("peer_closed")
|
||||
if inp.new.b then Kit.playSe("SE_SELECT"); self.printer = nil; self.state = "list"
|
||||
elseif inp.new.a then
|
||||
Kit.playSe("SE_SELECT")
|
||||
local players = lk:players()
|
||||
if #players ~= 2 or players[1].language ~= players[2].language then self:failed("peer_language_or_count")
|
||||
else self:message("gSystemText_DontCutLink"); self.state = "receive_print"; self.loading = true end
|
||||
-- pokeruby/src/mystery_event_menu.c:168
|
||||
Kit.playSe("SE_SELECT"); self:message("gSystemText_DontCutLink"); self.state = "receive_print"; self.loading = true
|
||||
end
|
||||
elseif self.state == "receive_print" then
|
||||
if not lk or not lk:isOpen() then self:failed("peer_closed")
|
||||
else self.printer:run(inp) end
|
||||
if self.state == "receive_print" and not self.printer:isActive() then
|
||||
lk:send({type = M.REQUEST}); self.state, self.ticks = "receive", 0
|
||||
end
|
||||
elseif self.state == "receive" then
|
||||
if not lk or not lk:isOpen() then self:failed("peer_closed")
|
||||
else
|
||||
local block = lk:take(M.MESSAGE)
|
||||
if block then self.block, self.state = block.bytes, "received"
|
||||
elseif self.ticks > 1200 then self:failed("receive_timeout") end
|
||||
end
|
||||
elseif self.state == "received" then
|
||||
self:close(); self.state = "execute"
|
||||
self.printer:run(inp)
|
||||
if not self.printer:isActive() then self.state = "execute" end
|
||||
elseif self.state == "execute" then
|
||||
self.result = Event.run(self.block, self.session, {adapters = self.opts.adapters,
|
||||
-- pokeruby/src/mystery_event_menu.c:291 RunMysteryEventScript
|
||||
local picked = self:selected()
|
||||
self.result = Event.run(picked.bytes, self.session, {adapters = self.opts.adapters,
|
||||
saveBlock1Address = self.opts.saveBlock1Address})
|
||||
self.block = nil
|
||||
if self.result.save then
|
||||
local save = require("src.core.game3.save_schema_firered").toSaveTable(self.session)
|
||||
local ok, written = pcall(require("src.core.SaveData").save, save)
|
||||
if not ok or written == false then return self:failed("save_failed") end
|
||||
self.saved = save; if self.game then self.game.save = save end
|
||||
end
|
||||
if self.result.save and not self:writeSave() then return self:failed("save_failed") end
|
||||
self.loading = false
|
||||
self:message(self.result.message, self.result.stringVars); self.state = "result_print"
|
||||
elseif self.state == "result_print" then
|
||||
@@ -130,17 +145,41 @@ function M:update(input, dt)
|
||||
self.step:collect(input)
|
||||
return self.step:run(dt, function(inp) return self:frame(inp) end)
|
||||
end
|
||||
function M:drawList()
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
local RomText = require("src.core.game3.rom_text")
|
||||
local colors = Kit.messageColors()
|
||||
local rows = math.min(M.VISIBLE, #self.rows)
|
||||
Chrome.stdFrame(1, 1, 28, rows * 2)
|
||||
for i = 1, rows do
|
||||
local ev = self.rows[i + self.scroll]
|
||||
FrlgFont.draw(ev.label or "", 16, 8 + (i - 1) * 16, {colors = colors})
|
||||
end
|
||||
FrlgFont.draw(RomText.plain("gText_SelectorArrow3"), 8, 8 + (self.cursor - self.scroll - 1) * 16, {colors = colors})
|
||||
end
|
||||
function M:drawNews()
|
||||
local FrlgFont = require("src.ui.game3.frlg_font")
|
||||
local colors = Kit.messageColors()
|
||||
local news = self:selected().news
|
||||
Chrome.stdFrame(1, 1, 28, 2)
|
||||
FrlgFont.draw(news.titleText or "", 8, 8, {colors = colors})
|
||||
Chrome.stdFrame(1, 5, 28, M.NEWS_LINES * 2)
|
||||
for i = 1, M.NEWS_LINES do
|
||||
FrlgFont.draw((news.bodyText or {})[i + self.newsScroll] or "", 8, 40 + (i - 1) * 16, {colors = colors})
|
||||
end
|
||||
end
|
||||
function M:draw()
|
||||
love.graphics.clear(0, 0, 0, 1)
|
||||
if self.state == "lobby" then require("src.ui.game3.rs.cable_lobby").draw()
|
||||
else
|
||||
if self.state == "news_view" then self:drawNews()
|
||||
elseif (self.state == "list" or self.state == "ready_print" or self.state == "ready") and self.rows then self:drawList() end
|
||||
if self.printer and self.state ~= "list" then
|
||||
Chrome.stdFrame(1, 15, 28, 4)
|
||||
if self.printer then self.printer:draw(8, 120, {colors = Kit.messageColors()}) end
|
||||
if self.loading then
|
||||
Chrome.stdFrame(7, 6, 16, 2)
|
||||
require("src.ui.game3.frlg_font").draw(require("src.core.game3.rom_text").plain("gSystemText_LoadingEvent"),
|
||||
56, 48, {colors = Kit.messageColors()})
|
||||
end
|
||||
self.printer:draw(8, 120, {colors = Kit.messageColors()})
|
||||
end
|
||||
if self.loading then
|
||||
Chrome.stdFrame(7, 6, 16, 2)
|
||||
require("src.ui.game3.frlg_font").draw(require("src.core.game3.rom_text").plain("gSystemText_LoadingEvent"),
|
||||
56, 48, {colors = Kit.messageColors()})
|
||||
end
|
||||
Kit.drawFade(self.pal, 0)
|
||||
end
|
||||
|
||||
@@ -70,6 +70,13 @@ return function(game)
|
||||
local row = EI.optionRow()
|
||||
row.step({ options = game.options, session = session })
|
||||
check(EI.enabled(session), "the EVENT TICKETS option row turns the distribution on")
|
||||
check(EI.pendingGift(session) == nil, "no ticket is handed out until the relay publishes one")
|
||||
local feedCards = {}
|
||||
for _, b in ipairs(require("src.core.game3.mystery_gift").builtins("rse")) do
|
||||
feedCards[#feedCards + 1] = { key = b.key, card = b.card }
|
||||
end
|
||||
EI.applyFeed(session, { cards = feedCards })
|
||||
EI.sync(session, { offline = true })
|
||||
check(S.var("VAR_DISTRIBUTE_EON_TICKET") == 1, "VAR_DISTRIBUTE_EON_TICKET raised for the Mystery Gift man")
|
||||
|
||||
if not check(teleport("EM_LILYCOVE_CITY_POKEMON_CENTER_2F", 1, 6, "up"), "Lilycove Pokemon Center 2F loads") then
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
local U = require("tests.drivers.util")
|
||||
local DIR = os.getenv("POKEPORT_SHOT_DIR") or "/tmp/g3link_presence"
|
||||
|
||||
local VERSION = os.getenv("POKEPORT_VERSION") or "firered"
|
||||
local PER = {
|
||||
firered = { center = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F", trade = "FR_TRADE_CENTER" },
|
||||
leafgreen = { center = "FR_VIRIDIAN_CITY_POKEMON_CENTER_2F", trade = "FR_TRADE_CENTER" },
|
||||
emerald = { center = "EM_OLDALE_TOWN_POKEMON_CENTER_2F", trade = "EM_TRADE_CENTER" },
|
||||
}
|
||||
local CENTER_2F = PER[VERSION].center
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
|
||||
local failures = 0
|
||||
local function result(ok, label)
|
||||
print((ok and "PASS " or "FAIL ") .. label)
|
||||
if not ok then failures = failures + 1 end
|
||||
return ok
|
||||
end
|
||||
|
||||
local function finish()
|
||||
local Link = package.loaded["src.core.game3.link"]
|
||||
if Link then pcall(Link.reset) end
|
||||
local Client = package.loaded["src.online.Client"]
|
||||
if Client then pcall(Client.disconnect) end
|
||||
if failures == 0 then
|
||||
print("PASS g3link_room_presence")
|
||||
love.event.quit(0)
|
||||
else
|
||||
print("FAIL g3link_room_presence failures=" .. failures)
|
||||
love.event.quit(1)
|
||||
end
|
||||
end
|
||||
|
||||
local function now() return love.timer.getTime() end
|
||||
|
||||
return function(game)
|
||||
for _ = 1, 900 do
|
||||
if game.phase == "boot" and game.boot then break end
|
||||
U.wait(1)
|
||||
end
|
||||
game:_handleBootAction({ action = "new_game", name = "RED" })
|
||||
U.wait(240)
|
||||
|
||||
local Runtime = require("src.core.game3.runtime")
|
||||
local Map = require("src.core.game3.map")
|
||||
local Space = require("src.core.game3.scripting.space")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Player = require("src.core.game3.player")
|
||||
local Message = require("src.ui.game3.message")
|
||||
local Choice = require("src.ui.game3.choice")
|
||||
local SaveMenu = require("src.ui.game3.save_menu")
|
||||
local Party = require("src.core.game3.party")
|
||||
local Link = require("src.core.game3.link")
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
local Client = require("src.online.Client")
|
||||
local Relay = require("tests.support.fake_relay")
|
||||
local VirtualObjects = require("src.core.game3.virtual_objects")
|
||||
local Family = require("src.core.game3.link.family")
|
||||
local UNION_ROOM = Plaza.MAP_ID
|
||||
|
||||
local session = Runtime.getSession()
|
||||
if not result(session ~= nil, "new game reached the game3 field") then return finish() end
|
||||
Party.giveMon(session, 25, 12)
|
||||
Party.giveMon(session, 1, 10)
|
||||
|
||||
local relay = Relay.new({ clock = function() return now() end })
|
||||
local me = relay:seat("a0000001", "RED")
|
||||
Client.configure({ relayAddress = "fake:1", connect = function() return me.transport end })
|
||||
|
||||
local function ctx() return Space.vm and Space.vm.ctx end
|
||||
local function place(x, y, facing)
|
||||
Player.cellX, Player.cellY = x, y
|
||||
Player.px, Player.py = x * 16, y * 16
|
||||
Player.targetX, Player.targetY = x, y
|
||||
Player.facing = facing
|
||||
game.session.x, game.session.y, game.session.facing = x, y, facing
|
||||
end
|
||||
local function wait(n)
|
||||
for _ = 1, n do
|
||||
relay:pump()
|
||||
U.wait(1)
|
||||
end
|
||||
end
|
||||
local function waitFor(cond, seconds, frames)
|
||||
local t0, n = now(), 0
|
||||
while not cond() do
|
||||
relay:pump()
|
||||
U.wait(1)
|
||||
n = n + 1
|
||||
if now() - t0 > (seconds or 5) and n > (frames or 60) then return false end
|
||||
end
|
||||
return true
|
||||
end
|
||||
local function drive(cond, seconds)
|
||||
local t0 = now()
|
||||
while not cond() and now() - t0 < (seconds or 10) do
|
||||
if Choice.active or SaveMenu.isOpen() or Message.isOpen() then U.tap(game, "a") end
|
||||
wait(6)
|
||||
end
|
||||
return cond()
|
||||
end
|
||||
|
||||
local live = Link.liveProfile()
|
||||
if not result(live ~= nil, "the live g3 profile computes") then return finish() end
|
||||
local function peer(id, name, trainerId, gender, version, status)
|
||||
local s = relay:seat(id, name)
|
||||
relay:handle(s, { type = "lobby_hello", protocol = 3, name = name, profiles = { live },
|
||||
presence = { where = "launcher", status = "idle", version = version } })
|
||||
s.avatar = { name = name, trainerId = trainerId, gender = gender, version = version }
|
||||
s.status = status
|
||||
relay:handle(s, { type = "plaza_join", kind = "union", cap = 40, profile = live, avatar = s.avatar })
|
||||
return s
|
||||
end
|
||||
|
||||
Link.connect()
|
||||
result(waitFor(function() return Client.state() == "online" end, 5, 120), "online on the relay")
|
||||
result(Link.adapterConnected(), "the adapter reads connected")
|
||||
Map.load(nil, game, CENTER_2F, { x = 6, y = 4, facing = "up" })
|
||||
wait(60)
|
||||
Flags.setFlag(Space.store, ctx(), Family.flag(VERSION, "FLAG_SYS_POKEDEX_GET"), true)
|
||||
if VERSION == "emerald" then
|
||||
-- pokeemerald/data/scripts/cable_club.inc:104
|
||||
Flags.setVar(Space.store, ctx(), Family.var(VERSION, "VAR_CABLE_CLUB_TUTORIAL_STATE"), 2)
|
||||
end
|
||||
place(6, 4, "up")
|
||||
wait(12)
|
||||
U.tap(game, "a")
|
||||
local entered = drive(function() return Space.mapId == UNION_ROOM and Union.state == "main" end, 25)
|
||||
if not result(entered, "the attendant walks the player into the Union Room") then
|
||||
print("[driver] map=" .. tostring(Space.mapId) .. " union=" .. tostring(Union.state))
|
||||
U.shot(game, DIR .. "/" .. VERSION .. "_presence_enter_failed.png")
|
||||
return finish()
|
||||
end
|
||||
|
||||
local statuses = { "chatting", "trading", "battling", "idle", "idle", "idle", "idle", "idle", "idle", "idle", "idle", "idle" }
|
||||
local names = { "BLUE", "GREEN", "GOLD", "SILVER", "MAY", "LEAF", "WALLY", "BRENDAN", "KRIS", "ETHAN", "LYRA", "DAWN" }
|
||||
local versions = { "firered", "leafgreen", "emerald", "ruby", "sapphire" }
|
||||
local crowd = {}
|
||||
for i, name in ipairs(names) do
|
||||
crowd[i] = peer(string.format("c%07x", i), name, 0x100 + i * 13, i % 2, versions[(i - 1) % #versions + 1], statuses[i])
|
||||
end
|
||||
result(waitFor(function() return Union.playerCount() == #names end, 10, 120), "the peers appear in the plaza")
|
||||
result(waitFor(function() return VirtualObjects.count() == #names end, 5, 60), "as avatars")
|
||||
wait(40)
|
||||
|
||||
local LinkTags = package.loaded["src.ui.game3.link_tags"]
|
||||
result(LinkTags ~= nil, "the badge layer is loaded in the Union Room")
|
||||
local src = LinkTags and LinkTags.sources()
|
||||
local kinds, named = {}, 0
|
||||
for slot = 1, Plaza.CAP do
|
||||
local p = Union.players[slot]
|
||||
local tag = p and src and src.byVobj[Union.vobjId(slot)]
|
||||
if tag then
|
||||
if tag.name == p.name then named = named + 1 end
|
||||
if tag.kind then kinds[tag.kind] = true end
|
||||
end
|
||||
end
|
||||
result(named == #names, "every avatar carries its relay name (" .. named .. ")")
|
||||
result(kinds.chat and kinds.trade and kinds.battle, "chatting, trading and battling each get an icon")
|
||||
result(src and src.player and src.player.name == "RED", "the player's own badge reads RED")
|
||||
|
||||
place(12, 15, "up")
|
||||
wait(10)
|
||||
U.still(game, DIR .. "/" .. VERSION .. "_presence_01_badges_and_icons.png")
|
||||
|
||||
local homes = {}
|
||||
for slot = 1, Plaza.CAP do
|
||||
if Union.players[slot] then homes[slot] = { Union.cellFor(slot) } end
|
||||
end
|
||||
place(1, 22, "up")
|
||||
local moved, stepping, busyMoved = {}, false, false
|
||||
for _ = 1, 1500 do
|
||||
wait(1)
|
||||
for slot, h in pairs(homes) do
|
||||
local rec = VirtualObjects.get(Union.vobjId(slot))
|
||||
if rec and (rec.x ~= h[1] or rec.y ~= h[2]) then
|
||||
moved[slot] = true
|
||||
if Union.memberStatusKind(Union.players[slot]) then busyMoved = true end
|
||||
if rec.moving and not stepping then
|
||||
stepping = true
|
||||
place(12, 15, "up")
|
||||
wait(8)
|
||||
U.still(game, DIR .. "/" .. VERSION .. "_presence_02_wander_mid_step.png")
|
||||
place(1, 22, "up")
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
local n = 0
|
||||
for _ in pairs(moved) do n = n + 1 end
|
||||
result(n >= 3, "idle avatars wander off their cells (" .. n .. ")")
|
||||
result(not busyMoved, "chatting, trading and battling avatars stay put")
|
||||
|
||||
place(12, 15, "up")
|
||||
wait(30)
|
||||
U.still(game, DIR .. "/" .. VERSION .. "_presence_03_after_wander.png")
|
||||
|
||||
place(12, 22, "down")
|
||||
wait(10)
|
||||
for _ = 1, 20 do
|
||||
if Map.current == CENTER_2F then break end
|
||||
U.hold(game, "down", 8)
|
||||
relay:pump()
|
||||
end
|
||||
result(waitFor(function() return Map.current == CENTER_2F end, 8, 300), "the exit warps back to the 2F")
|
||||
wait(20)
|
||||
result(LinkTags.sources() == nil, "no badges outside the link rooms")
|
||||
|
||||
local LinkPlayers = require("src.core.game3.link.link_players")
|
||||
local inbox = {}
|
||||
local fake = {
|
||||
role = "host",
|
||||
update = function() end,
|
||||
isOpen = function() return true end,
|
||||
isReady = function() return true end,
|
||||
getSeat = function() return 0 end,
|
||||
send = function() return true end,
|
||||
take = function(_, kind)
|
||||
for i, m in ipairs(inbox) do
|
||||
if m.type == kind then return table.remove(inbox, i) end
|
||||
end
|
||||
return nil
|
||||
end,
|
||||
players = function()
|
||||
return { { seat = 0, name = "RED", gender = 0, version = "firered" },
|
||||
{ seat = 1, name = "MAY", gender = 1, version = "emerald" } }
|
||||
end,
|
||||
}
|
||||
Client.disconnect()
|
||||
Map.load(nil, game, PER[VERSION].trade, { x = 4, y = 5, facing = "right" })
|
||||
place(4, 5, "right")
|
||||
wait(30)
|
||||
Link.link = fake
|
||||
Link.startPump()
|
||||
inbox[#inbox + 1] = { type = LinkPlayers.MSG, seat = 1, map = "tradeCenter", x = 7, y = 5, facing = "left", busy = true }
|
||||
wait(30)
|
||||
local tc = LinkTags.sources()
|
||||
local mayTag = tc and tc.byVobj[LinkPlayers.VOBJ_BASE + 1]
|
||||
result(mayTag and mayTag.name == "MAY", "the Trade Center partner wears her name")
|
||||
result(mayTag and mayTag.kind == "trade", "and a trade icon while she is busy at the table")
|
||||
result(tc and tc.player and tc.player.name == "RED", "and the player keeps his badge in the link room")
|
||||
U.still(game, DIR .. "/" .. VERSION .. "_presence_04_trade_center.png")
|
||||
Link.link = nil
|
||||
wait(4)
|
||||
finish()
|
||||
end
|
||||
@@ -373,8 +373,17 @@ eq(Rse.var("VAR_DEOXYS_ROCK_LEVEL", sess), 0, "level back to 0")
|
||||
sess.options = {}
|
||||
check(Islands.pendingGift(sess) == nil, "no gifts while the option is off")
|
||||
Islands.setEnabled(sess, true)
|
||||
check(Islands.pendingGift(sess) == nil, "the option alone hands out nothing; the relay controls tickets")
|
||||
eq(Rse.var("VAR_DISTRIBUTE_EON_TICKET", sess), 0, "no Eon Ticket without the relay")
|
||||
local feedCards = {}
|
||||
for _, b in ipairs(require("src.core.game3.mystery_gift").builtins("rse")) do feedCards[#feedCards + 1] = { key = b.key, card = b.card } end
|
||||
Islands.applyFeed(sess, { cards = { feedCards[2] } })
|
||||
eq(Islands.pendingGift(sess).id, "aurora", "a relay AURORA TICKET card is offered")
|
||||
check(not Islands.eonPending(sess), "the relay feed without the Eon card leaves the Eon Ticket off")
|
||||
Islands.applyFeed(sess, { cards = feedCards })
|
||||
Islands.sync(sess, { offline = true })
|
||||
eq(Islands.pendingGift(sess).id, "aurora", "the Aurora Ticket is the first wonder card")
|
||||
eq(Rse.var("VAR_DISTRIBUTE_EON_TICKET", sess), 1, "the Eon Ticket distribution var is raised")
|
||||
eq(Rse.var("VAR_DISTRIBUTE_EON_TICKET", sess), 1, "the relay Eon Ticket raises the distribution var")
|
||||
Rse.setFlag("FLAG_RECEIVED_AURORA_TICKET", true, sess)
|
||||
eq(Islands.pendingGift(sess).id, "mystic", "then the Mystic Ticket")
|
||||
Rse.setFlag("FLAG_RECEIVED_MYSTIC_TICKET", true, sess)
|
||||
|
||||
@@ -21,6 +21,23 @@ for _, version in ipairs({ "ruby", "sapphire", "emerald", "firered", "leafgreen"
|
||||
tables[prefix.."data/generated/gba/items/pack.lua"] = Serializer.encode({items=items})
|
||||
end
|
||||
CacheFs.readAt=function(path) return tables[path] end;Catalog.reset()
|
||||
Events.autoFetch=false
|
||||
local Gift=require("src.core.game3.mystery_gift")
|
||||
local EON_EVENT=require("src.core.Base64").decode("AQAAAAICAAIAAAAEAIABAAAQfhYAAB4AAAKaAgACBggBAWEAAAL7AQACCwEFEwEF+wEAAgLB4wDn2dkA7ePp5gDa1ejc2eYA1egA6NzZAMHTxwDd4v7Kv867xrzPzMGt/7hhAAACRxMBAQAhDYABALsBvwAAAkoTAQEAIQ2AAQC7Ab8AAAIrzgC7Ab8AAAJqWr3JAAACZm1GEwEBACENgAAAuwHAAAACGgCAEwEaAYABAAkAKVMIvQYBAAJmbWwNvYYBAAJmbWwCvru+8AD9AasAwePj2ADo4wDn2dkA7ePpq/7O3Nnm2bTnANUA4Nno6NnmANzZ5tkA2uPmAO3j6bgA/QGt/767vvAAw+gA1eTk2dXm5wDo4wDW2QDVANrZ5ubtAM7DvcW/zrj+1unoAMO06tkA4tnq2eYA59nZ4gDj4tkA4N3f2QDd6ADW2drj5tmt+9Pj6QDn3OPp4NgA6t3n3egAxsPG073J0L8A1eLYANXn3/7V1uPp6ADd6ADo3Nnm2a3/vru+8AD9AbgA6NzZAMW/0wDDzr/HzQDKyb3Fv84A3eL+7ePp5gC8u8EA3ecA2ung4K37x+Pq2QDn4+HZAN/Z7QDd6Nnh5wDa4+YA59Xa2d/Z2eTd4tv+3eIA7ePp5gDKvbgA6NzZ4gDX4+HZAOfZ2QDh2a3/uPsBAAJHEwEBACENgAEAuwFBAgACShMBAQAhDYABALsBQQIAAivOALsBQQIAAkYTAQEAIQ2AAAC7AUkCAAK+NQAAAg4CAr5RAgACDgMCvnUCAAIOAwLO3N3nAL/Qv8jOAOHV7QDW2QDk4NXt2dgA4+Lg7QDj4tfZrf/T4+nmALy7wbTnAMW/0wDDzr/HzQDKyb3Fv84A3ecA2ung4K3/")
|
||||
T.eq(Gift.eventRecordMixingItem(EON_EVENT),275,"the relay eon event hands out ITEM_EON_TICKET by record mixing")
|
||||
for _,version in ipairs({"firered","leafgreen","emerald","ruby","sapphire"}) do
|
||||
T.check(not Events.receive({version=version,flags={},vars={},modData={},bag=require("src.core.game3.bag").new()},
|
||||
version=="emerald" and "aurora" or (Events.FAMILY[version]=="rs" and "eon" or "aurora"),2),
|
||||
version.." hands out no ticket before the relay feed arrives")
|
||||
end
|
||||
local function feedFor(version)
|
||||
local fam=Events.FAMILY[version]
|
||||
if fam=="rs" then return {cards={},news={},events={{key="rs_eon_ticket",label="EON TICKET",bytes=EON_EVENT}}} end
|
||||
local cards={}
|
||||
for _,b in ipairs(Gift.builtins(fam)) do cards[#cards+1]={key=b.key,card=b.card} end
|
||||
return {cards=cards,news={}}
|
||||
end
|
||||
for _,version in ipairs({"firered","leafgreen","emerald","ruby","sapphire"}) do Events.setFeed(version,feedFor(version)) end
|
||||
T.check(not Events.allowed("oldSeaMap","emerald",2),"English Emerald cannot receive Old Sea Map")
|
||||
T.check(Events.allowed("oldSeaMap","emerald",1)~=nil,"Japanese Emerald has historical Old Sea Map distribution")
|
||||
T.check(not Events.allowed("aurora","emerald",1),"Japanese Emerald has no historical AuroraTicket distribution")
|
||||
@@ -104,5 +121,13 @@ for _,version in ipairs({"emerald","firered","leafgreen"}) do
|
||||
T.eq(Events.status(save,row),"Event already completed",version.." completed encounter is visible before receiving card")
|
||||
T.check(not Events.receive(save,"mystic",2),"completed encounter cannot create undeliverable pending card")
|
||||
end
|
||||
Events.setFeed("firered",{cards={},news={}})
|
||||
do
|
||||
local save,why=Events.receive({version="firered",flags={},vars={},modData={},bag=require("src.core.game3.bag").new()},"aurora",2)
|
||||
T.check(save==nil and tostring(why):find("not offering"),"a ticket the relay does not publish cannot be received: "..tostring(why))
|
||||
end
|
||||
Events.setFeed("ruby",{cards={},news={},events={}})
|
||||
T.check(not Events.receive({version="ruby",flags={},vars={},modData={},bag=require("src.core.game3.bag").new()},"eon",2),
|
||||
"Ruby gets no Eon Ticket without the relay e-Reader event")
|
||||
CacheFs.readAt=oldRead;Catalog.reset()
|
||||
T.finish("Box events")
|
||||
|
||||
@@ -0,0 +1,45 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
|
||||
local version, seat, outFile, inFile = arg[1], tonumber(arg[2]), arg[3], arg[4]
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
require("src.core.game3.profile").reset()
|
||||
GameVersion.set(version)
|
||||
require("src.import.gba.versions").select(version)
|
||||
require("src.core.game3.pokemon").install(nil)
|
||||
|
||||
local Json = require("src.link.Json")
|
||||
local Wire = require("src.link.Wire")
|
||||
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 RecordMix = require("src.core.game3.link.record_mix")
|
||||
|
||||
local other = version == "emerald" and "ruby" or "emerald"
|
||||
local s = Schema.newGame({ version = version, name = version == "emerald" and "MAY" or "BRENDAN", rngSeed = 7 + seat })
|
||||
s.trainerId, s.secretId, s.gender = 0x1000 + seat, 0, seat % 2
|
||||
s.party = { { species = 1, level = 5, nickname = "", heldItem = 0 } }
|
||||
Space.store = Flags.newStore()
|
||||
local partners = {}
|
||||
partners[seat + 1] = { seat = seat, version = version }
|
||||
partners[2 - seat] = { seat = 1 - seat, version = other }
|
||||
|
||||
local mine = RecordMix.packet(s, seat, partners)
|
||||
local text = Json.encode({ type = RecordMix.MSG.PACKET, spot = seat, packet = RecordMix.toWire(mine) })
|
||||
local f = assert(io.open(outFile, "wb"))
|
||||
f:write(text)
|
||||
f:close()
|
||||
print("bytes " .. #text)
|
||||
print("layout " .. tostring(mine.recordLayout))
|
||||
if not inFile then return end
|
||||
local g = assert(io.open(inFile, "rb"))
|
||||
local theirs = Wire.sanitize(Json.decode(g:read("*a")))
|
||||
g:close()
|
||||
local list = {}
|
||||
list[seat + 1] = mine
|
||||
list[2 - seat] = RecordMix.fromWire(theirs.packet)
|
||||
local applied = RecordMix.receive(s, list, seat + 1)
|
||||
print("unsupported " .. tostring(applied and applied.unsupportedReason))
|
||||
for _, key in ipairs({ "secretBases", "tvShows", "pokeNews", "oldMan", "dewfordTrends", "daycareMail", "battleTower" }) do
|
||||
print(key .. " " .. tostring(applied and applied[key]))
|
||||
end
|
||||
File diff suppressed because one or more lines are too long
@@ -121,7 +121,7 @@ local job = MysteryGift.fetchOnline({ transport = okT })
|
||||
eq(job.status, "pending", "the fetch starts")
|
||||
local req = okT.begun[1]
|
||||
check(req ~= nil and req.method == "GET", "it is a GET")
|
||||
check(req ~= nil and req.url:sub(-#"/gifts/gen3") == "/gifts/gen3", "to /gifts/gen3: " .. tostring(req and req.url))
|
||||
check(req ~= nil and req.url:find("/gifts/gen3", 1, true) ~= nil, "to /gifts/gen3: " .. tostring(req and req.url))
|
||||
check(req ~= nil and req.headers["x-sync-token"] == nil, "without sync credentials")
|
||||
eq(req and req.maxSeconds, 10, "with a ten second budget")
|
||||
eq((MysteryGift.pollOnline(job)), "pending", "pending while the transport is")
|
||||
|
||||
+132
-14
@@ -21,7 +21,7 @@ love = love or require("tests.love_stub")
|
||||
local romBundle = require("tests.game3_cache").bundle()
|
||||
if not romBundle then
|
||||
local LIST_ROWS = { sListMenuItems_CardsOrNews = 3, sListMenuItems_ReceiveSendToss = 4, sListMenuItems_ReceiveToss = 3,
|
||||
sListMenuItems_WirelessOrFriend = 3 }
|
||||
sListMenuItems_WirelessOrFriend = 3, sListMenuItems_ReceiveSend = 3, sListMenuItems_Receive = 2 }
|
||||
package.loaded["src.core.game3.rom_text"] = {
|
||||
plain = function(key) return key end, box = function(key) return key end,
|
||||
ascii = function(key) return key end, has = function() return true end,
|
||||
@@ -145,12 +145,12 @@ local function giftSession(save)
|
||||
end
|
||||
|
||||
local function openWireless(st, d, isNews)
|
||||
if isNews then
|
||||
d.step("down")
|
||||
d.step("a")
|
||||
return
|
||||
end
|
||||
if isNews then d.step("down") end
|
||||
d.step("a")
|
||||
if st.state == Ui.STATE.SAVED_VIEW then
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
end
|
||||
d.waitFor(function() return st.state == Ui.STATE.SEARCHING end)
|
||||
end
|
||||
|
||||
@@ -259,7 +259,11 @@ do
|
||||
teq(rows[3], "EXIT", "EXIT is the third row")
|
||||
|
||||
step("a")
|
||||
eq(st.state, Ui.STATE.SEARCHING, "WONDER CARDS fetches the card list at once")
|
||||
-- pokefirered/src/mystery_gift_menu.c:1147 MG_STATE_DONT_HAVE_ANY
|
||||
eq(st.state, Ui.STATE.RESULT_MSG, "with no card held the don't-have text comes first")
|
||||
eq(feedTransport.calls, 0, "nothing is fetched yet")
|
||||
d.waitFor(function() return st.state == Ui.STATE.SEARCHING end)
|
||||
eq(st.state, Ui.STATE.SEARCHING, "then WONDER CARDS fetches the card list")
|
||||
eq(feedTransport.calls, 1, "one request goes out")
|
||||
eq(WirelessIcon.anim(), "searching", "the wireless icon plays its searching anim")
|
||||
tcheck(type(st.prompt) == "string" and st.prompt:find("Searching for a WIRELESS"),
|
||||
@@ -306,7 +310,22 @@ print("[test] 5. The list marks the held card and opens it")
|
||||
do
|
||||
local d = driver(st)
|
||||
d.step("a")
|
||||
eq(st.state, Ui.STATE.SEARCHING, "WONDER CARDS fetches again")
|
||||
-- pokefirered/src/mystery_gift_menu.c:1390 MG_STATE_LOAD_GIFT
|
||||
eq(st.state, Ui.STATE.SAVED_VIEW, "WONDER CARDS shows the held card first")
|
||||
eq(Ui.card(st).titleText, "MYSTIC TICKET", "the saved card is on screen")
|
||||
local okSaved, errSaved = pcall(Ui.draw, st)
|
||||
tcheck(okSaved, "the saved card draws: " .. tostring(errSaved))
|
||||
d.step("a")
|
||||
eq(st.state, Ui.STATE.GIFT_MENU, "A opens the card's menu")
|
||||
eq(#st.rows, 3, "a keep-only card offers RECEIVE, TOSS and CANCEL")
|
||||
eq(st.giftActions[2], "toss", "the second row tosses")
|
||||
local okMenu, errMenu = pcall(Ui.draw, st)
|
||||
tcheck(okMenu, "the card menu draws: " .. tostring(errMenu))
|
||||
d.step("b")
|
||||
eq(st.state, Ui.STATE.SAVED_VIEW, "B goes back to the card")
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
eq(st.state, Ui.STATE.SEARCHING, "RECEIVE fetches again")
|
||||
d.waitFor(function() return st.state ~= Ui.STATE.SEARCHING end)
|
||||
eq(st.state, Ui.STATE.OFFER_LIST, "the list opens")
|
||||
local lm = ListStub.last
|
||||
@@ -378,7 +397,7 @@ print("[test] 7. Wonder News over WIRELESS COMMUNICATION")
|
||||
do
|
||||
local st2, d, sess2 = freshScreen(okFeed())
|
||||
openWireless(st2, d, true)
|
||||
eq(st2.state, Ui.STATE.SEARCHING, "WONDER NEWS fetches the list at once")
|
||||
eq(st2.state, Ui.STATE.SEARCHING, "WONDER NEWS fetches the list")
|
||||
searchResult(st2, d)
|
||||
eq(st2.state, Ui.STATE.OFFER_LIST, "the news list opens")
|
||||
eq(#ListStub.last.items, 1, "only news rows are listed")
|
||||
@@ -404,9 +423,10 @@ do
|
||||
d.step("a")
|
||||
d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end)
|
||||
eq(MysteryGift.getSavedCard(sess2).idNumber, 7, "the MEW card is installed")
|
||||
local giftFlag = MysteryGift.receivedGiftFlag(MysteryGift.getSavedCard(sess2).flagId)
|
||||
check(giftFlag ~= nil, "the MEW card has a received-gift flag")
|
||||
MysteryGift.setFlag(sess2, giftFlag, true)
|
||||
check(MysteryGift.isGiftNotReceived(sess2), "the MEW is not collected yet")
|
||||
local rec = MysteryGift.ensure(sess2)
|
||||
rec.deliveredIds = { 7 }
|
||||
check(MysteryGift.wasDelivered(sess2, MysteryGift.getSavedCard(sess2)), "a Pokemon card is tracked by its ID number")
|
||||
MysteryGift.clearCardAndRelated(sess2)
|
||||
check(not MysteryGift.validateSavedCard(sess2), "then tossed")
|
||||
|
||||
@@ -439,7 +459,7 @@ print("[test] 9. Replacing a held card asks first")
|
||||
do
|
||||
local st2, d, sess2 = freshScreen(okFeed())
|
||||
MysteryGift.receiveCard(sess2, MysteryGift.builtins()[2].card)
|
||||
d.step("a")
|
||||
openWireless(st2, d, false)
|
||||
eq(st2.state, Ui.STATE.SEARCHING, "WONDER CARDS fetches the list")
|
||||
searchResult(st2, d)
|
||||
d.step("a")
|
||||
@@ -582,13 +602,111 @@ do
|
||||
teq(locked.rows[2], "EXIT", "row 2 is EXIT")
|
||||
Ui.update(locked, function(k) return k == "a" end, 1 / 60)
|
||||
check(locked.isNews, "the first row opens the news branch")
|
||||
eq(locked.state, Ui.STATE.SEARCHING, "which fetches the news list")
|
||||
eq(locked.state, Ui.STATE.RESULT_MSG, "which says no news is held before fetching")
|
||||
|
||||
local open = Ui.new({ session = giftSession({}), onSave = function() return true end })
|
||||
eq(#open.rows, 3, "the passphrase adds WONDER CARDS back")
|
||||
teq(open.rows[1], "WONDER CARDS", "on the first row")
|
||||
end
|
||||
|
||||
print("[test] 14. Receiving Wonder News from the relay saves it and sets the reward")
|
||||
do
|
||||
local save = {}
|
||||
local st2, d, sess2 = freshScreen(okFeed(), save)
|
||||
local before = #fanfares
|
||||
openWireless(st2, d, true)
|
||||
searchResult(st2, d)
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.NEWS_VIEW, "the news opens")
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.RESULT_MSG, "A receives it")
|
||||
check(MysteryGift.validateSavedNews(sess2), "the news is saved")
|
||||
check(MysteryGift.hasClaimedNews(sess2, 1), "and claimed")
|
||||
eq(#fanfares, before + 1, "the received fanfare plays")
|
||||
tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("NEWS"), "with the news-received text")
|
||||
eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_NONE,
|
||||
"the reward is not set before the message ends")
|
||||
d.waitFor(function() return st2.state == Ui.STATE.SAVE end)
|
||||
-- pokefirered/src/mystery_gift_menu.c:1369 WonderNews_SetReward(WONDER_NEWS_RECV_WIRELESS)
|
||||
eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_RECV_WIRELESS,
|
||||
"the wireless reward is set once the message is done")
|
||||
d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end)
|
||||
check(type(save.modData) == "table" and save.modData.mysteryGift.news ~= nil, "the news reached the save")
|
||||
|
||||
openWireless(st2, d, true)
|
||||
eq(st2.state, Ui.STATE.SEARCHING, "the saved news offers RECEIVE")
|
||||
searchResult(st2, d)
|
||||
eq(ListStub.last.items[1].colors, ListStub.COLOR_WHITE, "the held news prints greyed")
|
||||
MysteryGift.getSavedNewsMetadata(sess2).newsType = MysteryGift.WONDER_NEWS_NONE
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.RESULT_MSG, "the same news again ends the link")
|
||||
tcheck(st2.msg ~= nil and tostring(st2.msg.text):find("already"), "with the already-had text")
|
||||
d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU end)
|
||||
eq(MysteryGift.getSavedNewsMetadata(sess2).newsType, MysteryGift.WONDER_NEWS_NONE,
|
||||
"already-had news earns no reward")
|
||||
end
|
||||
|
||||
print("[test] 15. The saved card menu tosses a card")
|
||||
do
|
||||
local save = {}
|
||||
local saves = 0
|
||||
local sess2 = giftSession(save)
|
||||
local st2 = Ui.new({ session = sess2, fetch = { transport = okFeed() },
|
||||
onSave = function(s) saves = saves + 1 return MysteryGift.applyToSave(s, save) end })
|
||||
local d = driver(st2)
|
||||
check(MysteryGift.receiveCard(sess2, MysteryGift.builtins()[2].card), "an uncollected AURORA card is held")
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.SAVED_VIEW, "the held card shows")
|
||||
d.step("a")
|
||||
d.step("down")
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.ASK_TOSS, "TOSS asks first")
|
||||
check(st2.yesno ~= nil, "with a yes/no")
|
||||
d.step("a")
|
||||
check(st2.yesno ~= nil and st2.state == Ui.STATE.ASK_TOSS, "an uncollected gift asks a second time")
|
||||
d.step("b")
|
||||
eq(st2.state, Ui.STATE.GIFT_MENU, "NO goes back to the menu")
|
||||
check(MysteryGift.validateSavedCard(sess2), "and keeps the card")
|
||||
d.step("down")
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
check(not MysteryGift.validateSavedCard(sess2), "YES twice throws the card away")
|
||||
d.waitFor(function() return st2.state == Ui.STATE.MAIN_MENU and not st2.msg end)
|
||||
eq(saves, 1, "the toss is saved once")
|
||||
check(save.modData.mysteryGift.card == nil, "the save has no card")
|
||||
end
|
||||
|
||||
print("[test] 16. A shareable card has no SEND row")
|
||||
do
|
||||
local sess2 = giftSession({})
|
||||
local st2 = Ui.new({ session = sess2, onSave = function() return true end })
|
||||
local d = driver(st2)
|
||||
check(MysteryGift.receiveCard(sess2, MysteryGift.builtins()[4].card), "a shareable STAMP CARD is held")
|
||||
d.step("a")
|
||||
d.step("a")
|
||||
eq(#st2.rows, 3, "the menu is RECEIVE, TOSS and CANCEL")
|
||||
eq(table.concat(st2.giftActions, ","), "receive,toss,cancel", "with no send action")
|
||||
check(MysteryGift.sendToFriend == nil, "there is no friend-send hook")
|
||||
d.step("b")
|
||||
d.step("b")
|
||||
eq(st2.state, Ui.STATE.MAIN_MENU, "B twice goes back to the menu")
|
||||
end
|
||||
|
||||
print("[test] 17. Held news offers RECEIVE and CANCEL only")
|
||||
do
|
||||
local sess2 = giftSession({})
|
||||
local st2 = Ui.new({ session = sess2, onSave = function() return true end })
|
||||
local d = driver(st2)
|
||||
check(MysteryGift.receiveNews(sess2, { id = 3, sendType = 1, titleText = "NEWS", bodyText = {} }), "shareable news is held")
|
||||
d.step("down")
|
||||
d.step("a")
|
||||
eq(st2.state, Ui.STATE.SAVED_VIEW, "the held news shows")
|
||||
d.step("a")
|
||||
eq(table.concat(st2.giftActions, ","), "receive,cancel", "news has no send or toss")
|
||||
end
|
||||
|
||||
if failed == 0 then
|
||||
print("PASS game3_gift_menu")
|
||||
else
|
||||
|
||||
@@ -218,10 +218,10 @@ do
|
||||
eq(rse and #rse.cards, 1, "rse sees one card")
|
||||
eq(rse and rse.cards[1].key, "em", "the rse row")
|
||||
eq(rse and rse.cards[1].card.gift.setFlags[1], F("FLAG_ENABLE_SHIP_BIRTH_ISLAND"), "names resolve with Emerald ids")
|
||||
eq(rse and #rse.news, 1, "rse sees one news")
|
||||
eq(rse and #rse.news, 2, "rse sees every news, news has no family limit")
|
||||
local frlg = MysteryGift.parseFeed(payload, "frlg")
|
||||
eq(frlg and #frlg.cards, 2, "frlg sees its row and the unlabeled one")
|
||||
eq(frlg and #frlg.news, 1, "frlg sees the unlabeled news")
|
||||
eq(frlg and #frlg.news, 2, "frlg sees every news too")
|
||||
|
||||
local begun = {}
|
||||
local transport = {
|
||||
@@ -231,10 +231,15 @@ do
|
||||
}
|
||||
local job = MysteryGift.fetchOnline({ transport = transport, session = newSession("emerald") })
|
||||
eq(job.family, "rse", "the Emerald job carries its family")
|
||||
check(begun[1] and begun[1].url:sub(-#"/gifts/gen3?family=rse") == "/gifts/gen3?family=rse",
|
||||
"Emerald fetches ?family=rse: " .. tostring(begun[1] and begun[1].url))
|
||||
local emPath = "/gifts/gen3?family=rse&version=emerald"
|
||||
check(begun[1] and begun[1].url:sub(-#emPath) == emPath,
|
||||
"Emerald fetches ?family=rse and names its game: " .. tostring(begun[1] and begun[1].url))
|
||||
MysteryGift.fetchOnline({ transport = transport, session = newSession("firered") })
|
||||
check(begun[2] and begun[2].url:sub(-#"/gifts/gen3") == "/gifts/gen3", "FireRed keeps the bare path")
|
||||
check(begun[2] and begun[2].url:sub(-#"/gifts/gen3?version=firered") == "/gifts/gen3?version=firered",
|
||||
"FireRed keeps the frlg path and names its game")
|
||||
MysteryGift.fetchOnline({ transport = transport, session = newSession("leafgreen") })
|
||||
check(begun[3] and begun[3].url:sub(-#"/gifts/gen3?version=leafgreen") == "/gifts/gen3?version=leafgreen",
|
||||
"LeafGreen names its own game")
|
||||
end
|
||||
|
||||
print("[test] 11. LINK TOGETHER WITH ALL")
|
||||
|
||||
@@ -0,0 +1,91 @@
|
||||
#!/usr/bin/env luajit
|
||||
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
require("tests.fixture_data.game3_items").install()
|
||||
love = love or require("tests.love_stub")
|
||||
local T = require("tests.harness")
|
||||
local check, eq = T.check, T.eq
|
||||
|
||||
local FIXTURE_PUB = "b084317448d99b4451ec0b3f5a4ec746a0de25800d8e5bdce4bedbcae6b7ce1d"
|
||||
local FEED_FRLG = [==[{"v":1,"payload":"{\"cards\":[{\"bgType\":3,\"bodyText\":[\"Thank you for using the MYSTERY\",\"GIFT System.\",\"There is a ticket here for you.\",\"It is for use at VERMILION CITY port.\"],\"family\":\"frlg\",\"flagId\":1000,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"haveFlags\":[679,740],\"item\":371,\"kind\":\"item\",\"quantity\":1,\"setFlags\":[2123,679]},\"iconSpecies\":0,\"idNumber\":2,\"key\":\"aurora_ticket\",\"maxStamps\":0,\"sendType\":0,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"AURORA TICKET\",\"type\":0,\"versions\":[\"firered\",\"leafgreen\"]},{\"bgType\":7,\"bodyText\":[\"A mythical POKéMON has been\",\"sent to you from the\",\"POKéMON CENTER.\",\"Please take good care of it.\"],\"family\":\"frlg\",\"flagId\":1007,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"kind\":\"mon\",\"level\":10,\"moves\":{\"1\":1},\"otId\":20078,\"otName\":\"AURA\",\"species\":151},\"iconSpecies\":151,\"idNumber\":7,\"key\":\"lg_mew\",\"maxStamps\":0,\"sendType\":0,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"MEW\",\"type\":0,\"versions\":[\"leafgreen\"]}],\"family\":\"frlg\",\"issued\":5,\"news\":[{\"bgType\":0,\"bodyText\":[\"Every game reads this.\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],\"family\":\"frlg\",\"id\":9,\"key\":\"kanto_news\",\"sendType\":0,\"titleText\":\"RELAY NEWS\"}],\"v\":1}","sig":"f21a2cff1f284e7f31f507d27a2190b765f4839ff72077fe31321dce8bb655d460522748b79ef455500699012940a3509286b56820b8a2a657676226d5e9840c","key":"b084317448d99b4451ec0b3f5a4ec746a0de25800d8e5bdce4bedbcae6b7ce1d"}]==]
|
||||
local FEED_RSE = [==[{"v":1,"payload":"{\"cards\":[{\"bgType\":6,\"bodyText\":[\"Thank you for using the MYSTERY\",\"GIFT System.\",\"We received this OLD SEA MAP\",\"addressed to you.\"],\"family\":\"rse\",\"flagId\":1002,\"footerLine1Text\":\"Speak to the deliveryman\",\"footerLine2Text\":\"at a POKéMON CENTER.\",\"gift\":{\"haveFlags\":[316,458],\"item\":376,\"kind\":\"item\",\"quantity\":1,\"setFlags\":[2262,316]},\"iconSpecies\":0,\"idNumber\":104,\"key\":\"rse_old_sea_map\",\"maxStamps\":0,\"sendType\":0,\"subtitleText\":\"MYSTERY GIFT\",\"titleText\":\"OLD SEA MAP\",\"type\":0,\"versions\":[\"emerald\"]}],\"family\":\"rse\",\"issued\":5,\"news\":[{\"bgType\":0,\"bodyText\":[\"Every game reads this.\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],\"family\":\"frlg\",\"id\":9,\"key\":\"kanto_news\",\"sendType\":0,\"titleText\":\"RELAY NEWS\"}],\"v\":1}","sig":"e10196799b4ff259bbf3b20f9ed133d5041dabf4f78bba4a3612d9464ecd2a693f95035e324697f0c31ae00efbcf33c43496882b7af03aeee2a5c99fe0a28f03","key":"b084317448d99b4451ec0b3f5a4ec746a0de25800d8e5bdce4bedbcae6b7ce1d"}]==]
|
||||
local FEED_RS = [==[{"v":1,"payload":"{\"cards\":[],\"events\":[{\"family\":\"rs\",\"key\":\"rs_eon_ticket\",\"payload\":\"AQAAAAICAAIAAAAEAIABAAAQfhYAAB4AAAKaAgACBggBAWEAAAL7AQACCwEFEwEF+wEAAgLB4wDn2dkA7ePp5gDa1ejc2eYA1egA6NzZAMHTxwDd4v7Kv867xrzPzMGt/7hhAAACRxMBAQAhDYABALsBvwAAAkoTAQEAIQ2AAQC7Ab8AAAIrzgC7Ab8AAAJqWr3JAAACZm1GEwEBACENgAAAuwHAAAACGgCAEwEaAYABAAkAKVMIvQYBAAJmbWwNvYYBAAJmbWwCvru+8AD9AasAwePj2ADo4wDn2dkA7ePpq/7O3Nnm2bTnANUA4Nno6NnmANzZ5tkA2uPmAO3j6bgA/QGt/767vvAAw+gA1eTk2dXm5wDo4wDW2QDVANrZ5ubtAM7DvcW/zrj+1unoAMO06tkA4tnq2eYA59nZ4gDj4tkA4N3f2QDd6ADW2drj5tmt+9Pj6QDn3OPp4NgA6t3n3egAxsPG073J0L8A1eLYANXn3/7V1uPp6ADd6ADo3Nnm2a3/vru+8AD9AbgA6NzZAMW/0wDDzr/HzQDKyb3Fv84A3eL+7ePp5gC8u8EA3ecA2ung4K37x+Pq2QDn4+HZAN/Z7QDd6Nnh5wDa4+YA59Xa2d/Z2eTd4tv+3eIA7ePp5gDKvbgA6NzZ4gDX4+HZAOfZ2QDh2a3/uPsBAAJHEwEBACENgAEAuwFBAgACShMBAQAhDYABALsBQQIAAivOALsBQQIAAkYTAQEAIQ2AAAC7AUkCAAK+NQAAAg4CAr5RAgACDgMCvnUCAAIOAwLO3N3nAL/Qv8jOAOHV7QDW2QDk4NXt2dgA4+Lg7QDj4tfZrf/T4+nmALy7wbTnAMW/0wDDzr/HzQDKyb3Fv84A3ecA2ung4K3/\",\"title\":\"EON TICKET\",\"versions\":[\"ruby\",\"sapphire\"]},{\"family\":\"rs\",\"key\":\"rs_gift_ribbon\",\"payload\":\"AZsIAAICAAIAAAAEAIABAAAQTGwAALkIAAK9CAACCAABAg==\",\"title\":\"GIFT RIBBON\",\"versions\":[\"ruby\",\"sapphire\"]}],\"family\":\"rs\",\"issued\":5,\"news\":[{\"bgType\":0,\"bodyText\":[\"Every game reads this.\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\",\"\"],\"family\":\"frlg\",\"id\":9,\"key\":\"kanto_news\",\"sendType\":0,\"titleText\":\"RELAY NEWS\"}],\"v\":1}","sig":"ff759a880d1e32cd6e8962c86e3cb2f56b729b973545f4b748eaa7d571961c6690fae7034d36ef819f7127791d9bf972621097eab735f782558fa8cd1e9a680d","key":"b084317448d99b4451ec0b3f5a4ec746a0de25800d8e5bdce4bedbcae6b7ce1d"}]==]
|
||||
|
||||
|
||||
local Json = require("src.link.Json")
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
local Bag = require("src.core.game3.bag")
|
||||
local Schema = require("src.core.game3.save_schema_firered")
|
||||
MysteryGift.GIFT_PUBKEY = FIXTURE_PUB
|
||||
|
||||
local function keys(list, field)
|
||||
local out = {}
|
||||
for _, e in ipairs(list or {}) do out[#out + 1] = e[field or "key"] end
|
||||
return table.concat(out, ",")
|
||||
end
|
||||
|
||||
local function verify(body, family, version)
|
||||
return MysteryGift.verifyFeed(Json.decode(body), family, version)
|
||||
end
|
||||
|
||||
print("[test] 1. Cards only reach the games that had them")
|
||||
local fr = verify(FEED_FRLG, "frlg", "firered")
|
||||
local lg = verify(FEED_FRLG, "frlg", "leafgreen")
|
||||
eq(fr and keys(fr.cards), "aurora_ticket", "FireRed does not see the LeafGreen-only card")
|
||||
eq(lg and keys(lg.cards), "aurora_ticket,lg_mew", "LeafGreen sees its own card")
|
||||
eq(lg and lg.cards[1].card.versions and table.concat(lg.cards[1].card.versions, ","), "firered,leafgreen",
|
||||
"the card keeps its game list")
|
||||
local em = verify(FEED_RSE, "rse", "emerald")
|
||||
eq(em and keys(em.cards), "rse_old_sea_map", "Emerald sees the OLD SEA MAP")
|
||||
local any = verify(FEED_FRLG, "frlg", nil)
|
||||
eq(any and #any.cards, 2, "with no game named nothing is dropped")
|
||||
|
||||
print("[test] 2. Wonder News reaches every game")
|
||||
for _, row in ipairs({ { "firered", FEED_FRLG, "frlg" }, { "leafgreen", FEED_FRLG, "frlg" },
|
||||
{ "emerald", FEED_RSE, "rse" }, { "ruby", FEED_RS, "rs" }, { "sapphire", FEED_RS, "rs" } }) do
|
||||
local list = verify(row[2], row[3], row[1])
|
||||
eq(list and keys(list.news), "kanto_news", "news reaches " .. row[1])
|
||||
end
|
||||
|
||||
print("[test] 3. LeafGreen receives and collects its Wonder Card")
|
||||
do
|
||||
local session = Schema.newGame({ rngSeed = 0x77 })
|
||||
session.version = "leafgreen"
|
||||
session.store = { flags = {}, vars = {} }
|
||||
session.bag = Bag.new()
|
||||
eq(MysteryGift.versionOf(session), "leafgreen", "the session is LeafGreen")
|
||||
local entry = lg.cards[1]
|
||||
check(MysteryGift.receiveCard(session, entry.card), "the relay AURORA TICKET card is saved on LeafGreen")
|
||||
eq(MysteryGift.deliverGift(session, MysteryGift.getSavedCard(session)), MysteryGift.DELIVER_GIVEN,
|
||||
"the deliveryman hands it over")
|
||||
check(Bag.has(session.bag, MysteryGift.ITEM_AURORA_TICKET, 1), "the AURORA TICKET is in the bag")
|
||||
-- pokefirered/data/mystery_event_msg.s:222
|
||||
check(MysteryGift.getFlag(session, 0x84B) and MysteryGift.getFlag(session, 0x2A7),
|
||||
"FLAG_ENABLE_SHIP_BIRTH_ISLAND and FLAG_RECEIVED_AURORA_TICKET are set")
|
||||
end
|
||||
|
||||
print("[test] 4. The rs feed carries signed Mystery Events")
|
||||
local ruby = verify(FEED_RS, "rs", "ruby")
|
||||
local sapphire = verify(FEED_RS, "rs", "sapphire")
|
||||
eq(ruby and keys(ruby.events), "rs_eon_ticket,rs_gift_ribbon", "Ruby lists both e-Reader events")
|
||||
eq(sapphire and keys(sapphire.events), "rs_eon_ticket,rs_gift_ribbon", "and so does Sapphire")
|
||||
eq(ruby and #ruby.cards, 0, "Ruby gets no Wonder Cards")
|
||||
eq(ruby and ruby.events[1].label, "EON TICKET", "the event keeps its title")
|
||||
check(ruby and #ruby.events[1].bytes > 17 and #ruby.events[1].bytes <= 0x7D4, "the bytes fit an e-Reader block")
|
||||
-- pokeruby/data/debug_mystery_event_scripts.s:37 me_setrecordmixinggift 0x1, 0x5, ITEM_EON_TICKET
|
||||
eq(MysteryGift.eventRecordMixingItem(ruby.events[1].bytes), 275, "the Eon event spreads ITEM_EON_TICKET")
|
||||
eq(MysteryGift.eventRecordMixingItem(ruby.events[2].bytes), nil, "the ribbon event has no record mixing gift")
|
||||
local forged = FEED_RS:gsub("EON TICKET", "EON TICKEU")
|
||||
local bad, why = verify(forged, "rs", "ruby")
|
||||
check(bad == nil and why == "bad_signature", "a tampered rs feed is refused")
|
||||
local data = Json.decode(Json.decode(FEED_RS).payload)
|
||||
local raw = require("src.core.Base64").decode(data.events[1].payload)
|
||||
local rubyOnly = raw:sub(1, 13) .. string.char(0x80, 0, 0, 0) .. raw:sub(18)
|
||||
data.events[1].payload = require("src.core.Base64").encode(rubyOnly)
|
||||
local payload = Json.encode(data)
|
||||
eq(#MysteryGift.parseFeed(payload, "rs", "ruby").events, 2, "a Ruby-only block still runs on Ruby")
|
||||
eq(#MysteryGift.parseFeed(payload, "rs", "sapphire").events, 1, "but is dropped on Sapphire by its header")
|
||||
data.events[2].versions = { "ruby" }
|
||||
eq(#MysteryGift.parseFeed(Json.encode(data), "rs", "sapphire").events, 0, "and the feed game list drops the other")
|
||||
|
||||
T.finish("game3_gift_versions")
|
||||
@@ -221,19 +221,19 @@ session.party = { charizard() }
|
||||
Link.reset()
|
||||
freshCtx()
|
||||
setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE)
|
||||
local connects = 0
|
||||
local Connect = { start = function() connects = connects + 1; return false, "offline" end,
|
||||
state = function() return "offline" end }
|
||||
package.loaded["src.online.Connect"] = Connect
|
||||
package.loaded["src.online.ArenaData"] = {
|
||||
liveProfile3 = function(_, rulesetId) return { engine = 3, version = "firered", rulesetId = rulesetId } end,
|
||||
}
|
||||
Link._live = nil
|
||||
local waited = Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
-- pokefirered/src/cable_club.c:208-222
|
||||
check(waited, "with no cable yet the counter parks the script instead of answering")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING, "and reports LINKUP_ONGOING while it waits")
|
||||
local spun = 0
|
||||
for _ = 1, LB.LINKUP_TICKS + 1 do
|
||||
if ctx.nativePoll() then break end
|
||||
spun = spun + 1
|
||||
end
|
||||
-- pokefirered/src/cable_club.c:482 TryLinkTimeout
|
||||
eq(spun, LB.LINKUP_TICKS, "it waits the full TryLinkTimeout window before giving up")
|
||||
check(not waited, "with no cable and no relay the counter answers at once")
|
||||
eq(connects, 1, "after trying the relay once")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.CONNECTION_ERROR,
|
||||
"and a partner that never arrives is LINKUP_CONNECTION_ERROR")
|
||||
"and a relay that cannot be reached is LINKUP_CONNECTION_ERROR")
|
||||
|
||||
local host, guest = FakeRelay.pair({ game = game })
|
||||
pumpLink(host, guest)
|
||||
@@ -243,51 +243,67 @@ setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE)
|
||||
local yielded = Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
check(yielded, "the script yields while the linkup runs")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING, "VAR_RESULT is LINKUP_ONGOING while it waits")
|
||||
check(ctx.nativePoll() == false, "nothing is decided before the peer says what it picked")
|
||||
guest:update(0)
|
||||
local peerPick = guest:take(LB.MSG.LINKUP)
|
||||
eq(peerPick and peerPick.linkType, LB.LINKTYPE.SINGLE_BATTLE,
|
||||
"the peer was told this machine picked LINKTYPE_SINGLE_BATTLE")
|
||||
guest:send({ type = LB.MSG.LINKUP, linkType = LB.LINKTYPE.SINGLE_BATTLE, players = 2 })
|
||||
host:update(0)
|
||||
check(ctx.nativePoll() == true, "the poll settles once both picks are in")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.SUCCESS, "matching picks report LINKUP_SUCCESS")
|
||||
eq(guest:take(LB.MSG.LINKUP), nil, "a relay link sends no linkup message: the hellos already carry the picks")
|
||||
check(ctx.nativePoll() == true, "the poll settles from the hellos")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.SUCCESS, "a peer on the generic wireless link type is LINKUP_SUCCESS")
|
||||
eq(LB.state, "seat", "and the session is waiting for a colosseum seat")
|
||||
Link.closeLink("test")
|
||||
|
||||
local Game3Link = require("src.link.Game3Link")
|
||||
local function relayPair(hostType, guestType, seats)
|
||||
local room = FakeRelay.room({ seats = seats or 2 })
|
||||
local a = Game3Link.attach(FakeRelay.transport(room, 0), { game = game, linkType = hostType, seat = 0, seats = room.seats })
|
||||
local b = Game3Link.attach(FakeRelay.transport(room, 1), { game = game, linkType = guestType, seat = 1, seats = room.seats })
|
||||
pumpLink(a, b)
|
||||
return a, b
|
||||
end
|
||||
host, guest = relayPair(LB.LINKTYPE.SINGLE_BATTLE, LB.LINKTYPE.DOUBLE_BATTLE)
|
||||
Link.attach(host)
|
||||
freshCtx()
|
||||
setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE)
|
||||
Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
guest:update(0)
|
||||
guest:take(LB.MSG.LINKUP)
|
||||
guest:send({ type = LB.MSG.LINKUP, linkType = LB.LINKTYPE.MULTI_BATTLE, players = 4 })
|
||||
host:update(0)
|
||||
ctx.nativePoll()
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.DIFF_SELECTIONS,
|
||||
"a peer that picked another mode reports LINKUP_DIFF_SELECTIONS")
|
||||
"a cable peer that picked another mode reports LINKUP_DIFF_SELECTIONS")
|
||||
Link.closeLink("test")
|
||||
|
||||
host, guest = relayPair(LB.LINKTYPE.MULTI_BATTLE, LB.LINKTYPE.MULTI_BATTLE)
|
||||
Link.attach(host)
|
||||
freshCtx()
|
||||
setVar(Link.VAR_0x8004, Link.USING.MULTI_BATTLE)
|
||||
Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
guest:update(0)
|
||||
local multiPick = guest:take(LB.MSG.LINKUP)
|
||||
eq(multiPick and multiPick.linkType, LB.LINKTYPE.MULTI_BATTLE, "MULTI BATTLE picks LINKTYPE_MULTI_BATTLE")
|
||||
guest:send({ type = LB.MSG.LINKUP, linkType = LB.LINKTYPE.MULTI_BATTLE, players = 4 })
|
||||
host:update(0)
|
||||
ctx.nativePoll()
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.WRONG_NUM_PLAYERS,
|
||||
"a four player mode on a two machine cable reports LINKUP_WRONG_NUM_PLAYERS")
|
||||
"a four player mode on a two machine link reports LINKUP_WRONG_NUM_PLAYERS")
|
||||
Link.closeLink("test")
|
||||
|
||||
local lhost, lguest = Game3Link.loopback({ game = game })
|
||||
pumpLink(lhost, lguest)
|
||||
Link.attach(lhost)
|
||||
freshCtx()
|
||||
setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE)
|
||||
Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
check(ctx.nativePoll() == false, "a direct cable waits for the peer's pick")
|
||||
lguest:update(0)
|
||||
local peerPick = lguest:take(LB.MSG.LINKUP)
|
||||
eq(peerPick and peerPick.linkType, LB.LINKTYPE.SINGLE_BATTLE,
|
||||
"and tells the peer this machine picked LINKTYPE_SINGLE_BATTLE")
|
||||
lguest:send({ type = LB.MSG.LINKUP, linkType = LB.LINKTYPE.MULTI_BATTLE, players = 4 })
|
||||
lhost:update(0)
|
||||
ctx.nativePoll()
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.DIFF_SELECTIONS, "a different pick over the cable is LINKUP_DIFF_SELECTIONS")
|
||||
|
||||
Link.closeLink("test")
|
||||
freshCtx()
|
||||
setVar(Link.VAR_0x8004, Link.USING.SINGLE_BATTLE)
|
||||
connects = 0
|
||||
Natives.special(ctx, NativesLink.SPECIAL.TryBattleLinkup, adapters)
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING,
|
||||
"a torn down link puts the counter back to waiting for a machine")
|
||||
for _ = 1, LB.LINKUP_TICKS + 1 do
|
||||
if ctx.nativePoll() then break end
|
||||
end
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.CONNECTION_ERROR,
|
||||
"and times out to LINKUP_CONNECTION_ERROR")
|
||||
eq(connects, 1, "a torn down link sends the counter back to the relay")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.CONNECTION_ERROR, "which answers LINKUP_CONNECTION_ERROR when unreachable")
|
||||
package.loaded["src.online.Connect"] = nil
|
||||
package.loaded["src.online.ArenaData"] = nil
|
||||
Link._live = nil
|
||||
Link.reset()
|
||||
|
||||
print("[test] 4. the shared seed drives one RNG stream on both machines")
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
#!/usr/bin/env luajit
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
local function eq(a, b, msg)
|
||||
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
||||
end
|
||||
|
||||
local session = { name = "RED", gender = 0, trainerId = 0x1234, version = "firered" }
|
||||
package.loaded["src.core.game3.runtime"] = {
|
||||
getSession = function() return session end,
|
||||
_game = { session = session, data = { maps = {} } },
|
||||
}
|
||||
local Player = { cellX = 1, cellY = 22, facing = "up" }
|
||||
package.loaded["src.core.game3.player"] = Player
|
||||
|
||||
local Plaza = require("src.core.game3.link.union_plaza_map")
|
||||
package.loaded["src.core.game3.scripting.space"] = { mapId = Plaza.MAP_ID }
|
||||
package.loaded["src.core.game3.collision"] = {
|
||||
_mapId = Plaza.MAP_ID,
|
||||
inBounds = function(x, y) return x >= 0 and y >= 0 and x < Plaza.WIDTH and y < Plaza.HEIGHT end,
|
||||
isWater = function() return false end,
|
||||
canEnter = function(_, x, y) return Plaza.roleAt(x, y) == "." end,
|
||||
}
|
||||
local VirtualObjects = require("src.core.game3.virtual_objects")
|
||||
package.loaded["src.core.game3.objects"] = {
|
||||
blocks = function(x, y)
|
||||
for _, vo in ipairs(VirtualObjects.list()) do
|
||||
if vo.solid and ((vo.x == x and vo.y == y) or (vo.prevX == x and vo.prevY == y)) then return true end
|
||||
end
|
||||
return false
|
||||
end,
|
||||
}
|
||||
|
||||
local Wander = require("src.core.game3.link.plaza_wander")
|
||||
|
||||
do
|
||||
local function trace(seed)
|
||||
local w = Wander.new(seed, 5, 5)
|
||||
local env = { canWander = true, free = function() return true end }
|
||||
local out = {}
|
||||
for i = 1, 3000 do
|
||||
Wander.tick(w, env)
|
||||
out[i] = w.x * 100 + w.y
|
||||
end
|
||||
return out, w
|
||||
end
|
||||
local a = trace(42)
|
||||
local b = trace(42)
|
||||
local same, left, far = true, false, 0
|
||||
for i = 1, #a do
|
||||
if a[i] ~= b[i] then same = false end
|
||||
local x, y = math.floor(a[i] / 100), a[i] % 100
|
||||
if x ~= 5 or y ~= 5 then left = true end
|
||||
far = math.max(far, math.abs(x - 5), math.abs(y - 5))
|
||||
end
|
||||
check(same, "the same seed walks the same path")
|
||||
check(left, "an idle avatar leaves its cell now and then")
|
||||
check(far <= Wander.RADIUS, "and never strays past its radius (" .. far .. ")")
|
||||
|
||||
local w = Wander.new(7, 5, 5)
|
||||
local env = { canWander = true, free = function() return true end }
|
||||
local guard = 0
|
||||
while Wander.atHome(w) and guard < 5000 do Wander.tick(w, env); guard = guard + 1 end
|
||||
check(not Wander.atHome(w), "the avatar starts a walk")
|
||||
env.canWander = false
|
||||
for _ = 1, 400 do Wander.tick(w, env) end
|
||||
check(Wander.atHome(w), "and walks back home when it may not wander")
|
||||
local still = Wander.new(9, 5, 5)
|
||||
for _ = 1, 3000 do Wander.tick(still, { canWander = false, free = function() return true end }) end
|
||||
check(Wander.atHome(still), "a busy avatar never moves")
|
||||
end
|
||||
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
Union.graphicsIdFor = function() return 1 end
|
||||
Union._avatars = { opposite_facing = { 0, 2, 1, 4, 3 }, member_facing = { 1, 3, 1, 4, 2 } }
|
||||
Union.state = "main"
|
||||
Union._vobjs = {}
|
||||
Union.players = {
|
||||
[1] = { id = "b", name = "BLUE", gender = 0, trainerId = 2, status = "idle", activity = Union.ACTIVITY.NONE + Union.IN_UNION_ROOM },
|
||||
[2] = { id = "g", name = "GREEN", gender = 1, trainerId = 3, status = "chatting", activity = Union.ACTIVITY.CHAT + Union.IN_UNION_ROOM },
|
||||
[3] = { id = "y", name = "YELLOW", gender = 1, trainerId = 4, status = "trading", activity = Union.ACTIVITY.TRADE + Union.IN_UNION_ROOM },
|
||||
[4] = { id = "p", name = "PINK", gender = 1, trainerId = 5, status = "battling", activity = Union.ACTIVITY.BATTLE_SINGLE + Union.IN_UNION_ROOM },
|
||||
}
|
||||
Union.refreshPlaza()
|
||||
|
||||
eq(Union.memberStatusKind(Union.players[1]), nil, "an idle member shows no status")
|
||||
eq(Union.memberStatusKind(Union.players[2]), "chat", "chatting shows the chat icon")
|
||||
eq(Union.memberStatusKind(Union.players[3]), "trade", "trading shows the trade icon")
|
||||
eq(Union.memberStatusKind(Union.players[4]), "battle", "battling shows the battle icon")
|
||||
eq(Union.memberStatusKind({ activity = Union.ACTIVITY.CARD + Union.IN_UNION_ROOM }), "chat",
|
||||
"with no relay status the activity decides")
|
||||
|
||||
local hx, hy = Union.cellFor(1)
|
||||
local bx, by = Union.cellFor(2)
|
||||
local leftHome, moved2, maxOff = false, false, 0
|
||||
local awayAt
|
||||
for _ = 1, 4000 do
|
||||
Union.animateAll()
|
||||
local rec = VirtualObjects.get(Union.vobjId(1))
|
||||
if rec and (rec.x ~= hx or rec.y ~= hy) then
|
||||
leftHome = true
|
||||
maxOff = math.max(maxOff, math.abs(rec.x - hx), math.abs(rec.y - hy))
|
||||
if not rec.moving and not awayAt then awayAt = { rec.x, rec.y } end
|
||||
end
|
||||
local rec2 = VirtualObjects.get(Union.vobjId(2))
|
||||
if rec2 and (rec2.x ~= bx or rec2.y ~= by) then moved2 = true end
|
||||
if awayAt then break end
|
||||
end
|
||||
check(leftHome, "an idle plaza avatar wanders off its cell")
|
||||
check(maxOff <= 1, "inside its own 3x3 box")
|
||||
check(not moved2, "a chatting avatar stays put")
|
||||
|
||||
if awayAt then
|
||||
Player.cellX, Player.cellY, Player.facing, Player.moving = awayAt[1], awayAt[2] + 1, "up", false
|
||||
if Plaza.roleAt(Player.cellX, Player.cellY) ~= "." then
|
||||
Player.cellY, Player.facing = awayAt[2] - 1, "down"
|
||||
end
|
||||
eq(Union.tryInteractWithMember(), 1, "talking reaches the avatar where it stands now")
|
||||
for _ = 1, 200 do Union.animateAll() end
|
||||
local rec = VirtualObjects.get(Union.vobjId(1))
|
||||
check(rec and rec.x == awayAt[1] and rec.y == awayAt[2], "and it holds still while talked to")
|
||||
Union.updateMemberFacing(1)
|
||||
Player.cellX, Player.cellY = 1, 22
|
||||
local home = false
|
||||
for _ = 1, 4000 do
|
||||
Union.animateAll()
|
||||
local r = VirtualObjects.get(Union.vobjId(1))
|
||||
if r and r.x == hx and r.y == hy and not r.moving then home = true break end
|
||||
end
|
||||
check(home, "after the talk it walks back to its own cell")
|
||||
else
|
||||
check(false, "the idle avatar never stopped away from home")
|
||||
end
|
||||
|
||||
local LinkTags = require("src.ui.game3.link_tags")
|
||||
local src = LinkTags.sources()
|
||||
check(src ~= nil, "the Union Room has tags")
|
||||
local t1 = src and src.byVobj[Union.vobjId(1)]
|
||||
eq(t1 and t1.name, "BLUE", "the badge carries the relay avatar name")
|
||||
eq(t1 and t1.kind, nil, "idle has no icon")
|
||||
eq(src and src.byVobj[Union.vobjId(2)] and src.byVobj[Union.vobjId(2)].kind, "chat", "chatting badge")
|
||||
eq(src and src.player and src.player.name, "RED", "the local player gets a badge too")
|
||||
eq(LinkTags.tagFor(src, { kind = "player" }), src and src.player, "the player actor maps to its badge")
|
||||
eq(LinkTags.tagFor(src, { kind = "npc", eventObject = { virtualId = Union.vobjId(3) } }), src and src.byVobj[Union.vobjId(3)],
|
||||
"a plaza avatar maps to its badge")
|
||||
|
||||
local measure = function(name) return #name * 6 end
|
||||
local left = LinkTags.layout({ name = "LONGNAME", kind = "trade" }, 0, 40, 240, measure)
|
||||
check(left.x >= LinkTags.EDGE, "a badge at the left edge stays on screen")
|
||||
local right = LinkTags.layout({ name = "LONGNAME", kind = "battle" }, 240, 40, 240, measure)
|
||||
check(right.x + right.w <= 240 - LinkTags.EDGE, "a badge at the right edge stays on screen")
|
||||
local plain = LinkTags.layout({ name = "WALLACEMORE" }, 120, 40, 240, function() return 999 end)
|
||||
eq(plain.textW, LinkTags.MAX_TEXT_W, "long names are clipped to the badge width")
|
||||
for kind in pairs(LinkTags.ICONS) do
|
||||
local w, h = LinkTags.iconSize(kind)
|
||||
check(h <= LinkTags.PLATE_H and w > 0, kind .. " icon fits the badge")
|
||||
end
|
||||
|
||||
local LP = require("src.core.game3.link.link_players")
|
||||
LP._mapId = "tradeCenter"
|
||||
LP._remotes[1] = { busy = true, name = "GREEN", x = 4, y = 5 }
|
||||
LP._remotes[2] = { busy = false, name = "GOLD", x = 7, y = 5 }
|
||||
eq(LP.statusKind(1), "trade", "a busy player in the Trade Center is trading")
|
||||
eq(LP.statusKind(2), nil, "an idle one has no icon")
|
||||
LP._mapId = "colosseum2P"
|
||||
eq(LP.statusKind(1), "battle", "a busy player in the Colosseum is battling")
|
||||
src = LinkTags.sources()
|
||||
eq(src and src.byVobj[LP.VOBJ_BASE + 1] and src.byVobj[LP.VOBJ_BASE + 1].name, "GREEN", "link room avatars get badges")
|
||||
LP._remotes, LP._mapId = {}, nil
|
||||
|
||||
if failed > 0 then
|
||||
print(("FAIL game3_link_room_presence_test (%d)"):format(failed))
|
||||
os.exit(1)
|
||||
end
|
||||
print("PASS game3_link_room_presence_test")
|
||||
@@ -0,0 +1,262 @@
|
||||
#!/usr/bin/env luajit
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
local function eq(a, b, msg)
|
||||
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
||||
end
|
||||
|
||||
local FakeRelay = require("tests.g3link_fake_relay")
|
||||
|
||||
local store = { flags = {}, vars = {} }
|
||||
local session = {
|
||||
version = "ruby", store = store, map = "RS_TEST_POKEMON_CENTER_2F", x = 5, y = 3,
|
||||
name = "BRENDAN", gender = 0, trainerId = 0x2222,
|
||||
party = { { species = 280, level = 12 }, { species = 283, level = 9 } },
|
||||
bag = { pockets = { items = {} } },
|
||||
}
|
||||
local game = { data = { maps = {} }, session = session, input = { wasPressed = function() return false end },
|
||||
save = { player = { name = "BRENDAN" } } }
|
||||
package.loaded["src.core.game3.runtime"] = {
|
||||
getSession = function() return session end,
|
||||
isActive = function() return true end,
|
||||
_game = game,
|
||||
}
|
||||
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 3, facing = "up" }
|
||||
package.loaded["src.core.game3.map"] = { load = function() end }
|
||||
package.loaded["src.core.game3.rom_text"] = {
|
||||
plain = function(key) return key end, box = function(key) return key end,
|
||||
ascii = function(key) return key end, has = function() return true end,
|
||||
ir = function(key) return { { t = "text", s = key } } end,
|
||||
translate = function(ir) return ir end,
|
||||
key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
|
||||
at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
|
||||
count = function() return 0 end, list = function() return {} end,
|
||||
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
|
||||
}
|
||||
local ctx = { specialVars = {}, stringVars = {} }
|
||||
local adapters = { log = function() end, playSe = function() end }
|
||||
package.loaded["src.core.game3.scripting.space"] = {
|
||||
store = store, mapId = session.map, vm = { ctx = ctx, adapters = adapters },
|
||||
ensureBundle = function() return nil end,
|
||||
}
|
||||
package.loaded["src.online.ArenaData"] = {
|
||||
liveProfile3 = function(_, rulesetId)
|
||||
return { engine = 3, engineVersion = "1.0.0", fingerprint = "f00dcafe", kind = "vanilla",
|
||||
version = session.version, rulesetId = rulesetId }
|
||||
end,
|
||||
}
|
||||
local Client = FakeRelay.client({ state = "online", id = "0000beef" })
|
||||
package.loaded["src.online.Client"] = Client
|
||||
|
||||
local Link = require("src.core.game3.link")
|
||||
local LB = require("src.core.game3.link.battle")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Lobby = require("src.ui.game3.rs.cable_lobby")
|
||||
local Game3Link = require("src.link.Game3Link")
|
||||
local RsNatives = require("src.core.game3.scripting.natives_link_rs").BY_NAME
|
||||
local RseNatives = require("src.core.game3.scripting.natives_link_rse").BY_NAME
|
||||
local ContestNatives = require("src.core.game3.scripting.natives_contest").BY_NAME
|
||||
local CL = require("src.core.game3.link.contest_link")
|
||||
|
||||
local function setVar(id, v) Flags.setVar(store, ctx, id, v) end
|
||||
local function getVar(id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end
|
||||
local function pick(id)
|
||||
for _, row in ipairs(Lobby.rows()) do
|
||||
if row.id == id and not row.disabled then return Lobby.opts.select(row) end
|
||||
end
|
||||
error("no enabled row " .. tostring(id))
|
||||
end
|
||||
local function pump(peers)
|
||||
for _ = 1, 4 do
|
||||
for _, p in ipairs(peers) do p:update(0) end
|
||||
Link.update(0)
|
||||
end
|
||||
end
|
||||
|
||||
local function linkup(version, start, peerVersions, peerLinkType, max)
|
||||
session.version = version
|
||||
ctx.specialVars, ctx.nativePoll = {}, nil
|
||||
Client.clear()
|
||||
Client._group, Client._room, Client._seat = nil, nil, nil
|
||||
Lobby.reset()
|
||||
Link.reset()
|
||||
Link._live = nil
|
||||
local yielded = start()
|
||||
if not yielded then return nil, "did not yield" end
|
||||
pick("leader")
|
||||
local members = { { id = "0000beef", name = session.name } }
|
||||
for i = 1, #peerVersions do members[#members + 1] = { id = string.format("00e0000%d", i), name = "P" .. i } end
|
||||
Client._group = { leader = "0000beef", min = 2, max = max, members = members, pending = {} }
|
||||
ctx.nativePoll()
|
||||
if Client.count("startGroup") == 0 then pick("start") end
|
||||
local room = FakeRelay.room({ seats = #peerVersions + 1 })
|
||||
Client.bindRoom(room, 0)
|
||||
local peers = {}
|
||||
for i, v in ipairs(peerVersions) do
|
||||
local hello = Game3Link.hello(game, peerLinkType, { version = v, name = v:upper() .. i, trainerId = 0x3000 + i })
|
||||
peers[i] = Game3Link.attach(FakeRelay.transport(room, i), {
|
||||
game = game, linkType = peerLinkType, seat = i, seats = room.seats, hello = hello,
|
||||
})
|
||||
end
|
||||
ctx.nativePoll()
|
||||
pump(peers)
|
||||
for _ = 1, LB.LINKUP_TICKS + 5 do
|
||||
if ctx.nativePoll() then break end
|
||||
end
|
||||
return getVar(Link.VAR_RESULT), peers
|
||||
end
|
||||
|
||||
local LT = Game3Link.LINKTYPE
|
||||
|
||||
print("[test] 1. trade: Ruby cable vs Emerald, both directions")
|
||||
local code = linkup("ruby", function() return RsNatives.sub_80834E4(ctx, adapters) end,
|
||||
{ "emerald" }, LT.TRADE_SETUP, 2)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Ruby's Trade Center links with an Emerald trader")
|
||||
code = linkup("emerald", function() return RseNatives.TryTradeLinkup(ctx, adapters) end,
|
||||
{ "sapphire" }, LT.TRADE_SETUP, 2)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable Trade Center links with a Sapphire trader")
|
||||
|
||||
print("[test] 2. battles: Ruby cable vs Emerald and FireRed wireless players")
|
||||
code = linkup("ruby", function() setVar(0x8004, 1); return RsNatives.sub_808347C(ctx, adapters) end, { "emerald" }, LT.BATTLE, 2)
|
||||
eq(code, Link.LINKUP.SUCCESS, "single battle with an Emerald Direct Corner player")
|
||||
code = linkup("sapphire", function() setVar(0x8004, 2); return RsNatives.sub_808347C(ctx, adapters) end, { "firered" }, LT.BATTLE, 2)
|
||||
eq(code, Link.LINKUP.SUCCESS, "double battle with a FireRed Direct Corner player")
|
||||
code = linkup("ruby", function() setVar(0x8004, 5); return RsNatives.sub_808347C(ctx, adapters) end,
|
||||
{ "emerald", "leafgreen", "sapphire" }, LT.BATTLE, 4)
|
||||
eq(code, Link.LINKUP.SUCCESS, "multi battle with Emerald, LeafGreen and Sapphire")
|
||||
code = linkup("emerald", function() setVar(0x8004, 1); return RseNatives.TryBattleLinkup(ctx, adapters) end, { "ruby" }, LT.SINGLE_BATTLE, 2)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable colosseum with a Ruby cable player")
|
||||
code = linkup("emerald", function() setVar(0x8004, 1); return RseNatives.TryBattleLinkup(ctx, adapters) end, { "ruby" }, LT.DOUBLE_BATTLE, 2)
|
||||
eq(code, Link.LINKUP.DIFF_SELECTIONS, "a single vs double cable pick is still LINKUP_DIFF_SELECTIONS")
|
||||
|
||||
print("[test] 3. record corner: Ruby with Emerald, three players")
|
||||
code = linkup("ruby", function() return RsNatives.sub_808350C(ctx, adapters) end,
|
||||
{ "emerald", "sapphire" }, LT.RECORD_MIX_BEFORE, 4)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Ruby's Record Corner links three players")
|
||||
local players = Link.link and Link.link:players() or {}
|
||||
eq(#players, 3, "all three seats are linked")
|
||||
eq(players[2] and players[2].version, "emerald", "seat 1 is the Emerald player")
|
||||
check(require("src.core.game3.link.rs_record_cross").anyRS(players), "Emerald sees an RS partner and uses the RS layout")
|
||||
|
||||
print("[test] 4. berry blender: Ruby and Emerald blend in step")
|
||||
local blendPeers
|
||||
code, blendPeers = linkup("ruby", function() return RsNatives.sub_8083614(ctx, adapters) end,
|
||||
{ "emerald" }, LT.BERRY_BLENDER_SETUP, 4)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Ruby's blender links with an Emerald blender")
|
||||
eq(Link.link and Link.link.linkType, LT.BERRY_BLENDER_SETUP, "the link runs as LINKTYPE_BERRY_BLENDER_SETUP")
|
||||
local Blend = require("src.core.game3.rse.berry_blender_link")
|
||||
local mine = Blend.new(Link.link, Link.link:players())
|
||||
local theirs = Blend.new(blendPeers[1], blendPeers[1]:players())
|
||||
eq(mine.rngState, theirs.rngState, "both machines seed the same blender RNG")
|
||||
local a, b
|
||||
for _ = 1, 8 do
|
||||
a = a or mine:submitBerry(133)
|
||||
b = b or theirs:submitBerry(140)
|
||||
pump(blendPeers)
|
||||
end
|
||||
eq(a and a[1], 140, "Ruby sees the Emerald berry")
|
||||
eq(b and b[0], 133, "Emerald sees the Ruby berry")
|
||||
local fa, fb
|
||||
for _ = 1, 8 do
|
||||
fa = fa or mine:exchangeFrame(1, 2)
|
||||
fb = fb or theirs:exchangeFrame(1, 3)
|
||||
pump(blendPeers)
|
||||
end
|
||||
eq(fa and fa[1], 3, "the per-frame scores cross over")
|
||||
eq(fb and fb[0], 2, "both ways")
|
||||
|
||||
print("[test] 5. contests: Ruby with three Emerald players")
|
||||
code = linkup("ruby", function() setVar(0x8011, 0); return RsNatives.sub_808363C(ctx, adapters) end,
|
||||
{ "emerald", "emerald", "emerald" }, 0x6602, 4)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Ruby's COOL contest links with three Emerald wireless contestants")
|
||||
local flags = CL.flagsFor(Link.link, false)
|
||||
eq(flags, CL.FLAG.IS_LINK + CL.FLAG.HAS_RS_PLAYER, "the contest is flagged HAS_RS_PLAYER")
|
||||
code = linkup("emerald", function() setVar(0x8011, 0); return ContestNatives.TryContestEModeLinkup(ctx, adapters) end,
|
||||
{ "ruby" }, 0x6601, 4)
|
||||
eq(code, Link.LINKUP.SUCCESS, "Emerald's cable contest links with a Ruby contestant")
|
||||
eq(CL.flagsFor(Link.link, true), CL.FLAG.IS_LINK + CL.FLAG.IS_WIRELESS + CL.FLAG.HAS_RS_PLAYER,
|
||||
"and a wireless Emerald contest with a Ruby player skips the standby too")
|
||||
session.version = "ruby"
|
||||
ctx.specialVars, ctx.nativePoll = {}, nil
|
||||
Client.clear()
|
||||
Client._group, Client._room, Client._seat = nil, nil, nil
|
||||
Lobby.reset()
|
||||
Link.reset()
|
||||
setVar(0x8011, 0)
|
||||
RsNatives.sub_808363C(ctx, adapters)
|
||||
pick("join")
|
||||
Client._groups = { { leader = "00e00009", name = "MAY", avatar = { name = "MAY" }, joined = 1, max = 4 } }
|
||||
pick("00e00009")
|
||||
eq(Client.last("joinGroup") and Client.last("joinGroup")[1], "00e00009", "Ruby joins an Emerald wireless contest group")
|
||||
local room2 = FakeRelay.room({ seats = 2 })
|
||||
Client.bindRoom(room2, 1)
|
||||
local lead = Game3Link.attach(FakeRelay.transport(room2, 0), { game = game, linkType = 0x6602, seat = 0, seats = 2,
|
||||
hello = Game3Link.hello(game, 0x6602, { version = "emerald", name = "MAY", trainerId = 0x4444 }) })
|
||||
ctx.nativePoll()
|
||||
pump({ lead })
|
||||
for _ = 1, LB.LINKUP_TICKS + 5 do
|
||||
if ctx.nativePoll() then break end
|
||||
end
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.WRONG_NUM_PLAYERS,
|
||||
"a two player contest started by Emerald is LINKUP_WRONG_NUM_PLAYERS on Ruby")
|
||||
|
||||
Link.reset()
|
||||
Lobby.reset()
|
||||
|
||||
print("[test] 6. record mixing data crosses Ruby and Emerald")
|
||||
local function cacheRoot(version)
|
||||
local env = os.getenv("POKEPORT_" .. version:upper() .. "_CACHE")
|
||||
if env and env ~= "" then return env end
|
||||
local identity, home = os.getenv("POKEPORT_" .. version:upper() .. "_IDENTITY") or os.getenv("POKEPORT_IDENTITY"), os.getenv("HOME")
|
||||
if not (identity and identity ~= "" and home) then return nil end
|
||||
for _, base in ipairs({ home .. "/Library/Application Support/LOVE/", home .. "/.local/share/love/" }) do
|
||||
local root = base .. identity .. "/" .. version .. "/data/generated/gba"
|
||||
local f = io.open(root .. "/meta.json", "rb")
|
||||
if f then f:close(); return root end
|
||||
end
|
||||
return nil
|
||||
end
|
||||
local rubyRoot, emeraldRoot = cacheRoot("ruby"), cacheRoot("emerald")
|
||||
if not (rubyRoot and emeraldRoot) then
|
||||
print("[skip] 6. needs a Ruby and an Emerald cache (POKEPORT_RUBY_CACHE / POKEPORT_EMERALD_CACHE)")
|
||||
else
|
||||
local tmp = os.tmpname()
|
||||
local function side(version, root, seat, out, inp)
|
||||
local cmd = string.format("POKEPORT_GBA_CACHE=%q luajit tests/g3link_rs_mix_side.lua %s %d %q %s 2>&1",
|
||||
root, version, seat, out, inp and string.format("%q", inp) or "")
|
||||
local p = io.popen(cmd)
|
||||
local text = p:read("*a")
|
||||
p:close()
|
||||
local out2 = {}
|
||||
for k, v in text:gmatch("\n?(%w+) ([^\n]*)") do out2[k] = v end
|
||||
return out2, text
|
||||
end
|
||||
local rubyOut, emOut = tmp .. ".ruby", tmp .. ".em"
|
||||
local r1 = side("ruby", rubyRoot, 1, rubyOut)
|
||||
local e = side("emerald", emeraldRoot, 0, emOut, rubyOut)
|
||||
local r = side("ruby", rubyRoot, 1, rubyOut, emOut)
|
||||
eq(e.layout, "rs", "Emerald sends the RS record layout to a Ruby partner")
|
||||
check((tonumber(e.bytes) or 1e9) < 65536, "and the packet fits the relay's rse_record_mix cap (" .. tostring(e.bytes) .. ")")
|
||||
check((tonumber(r1.bytes) or 1e9) < 65536, "Ruby's packet fits too (" .. tostring(r1.bytes) .. ")")
|
||||
eq(e.unsupported, "nil", "Emerald takes Ruby's records")
|
||||
eq(e.secretBases, "true", "Emerald mixes the secret bases")
|
||||
eq(e.tvShows, "true", "and the TV shows")
|
||||
eq(r.unsupported, "nil", "Ruby takes Emerald's records")
|
||||
eq(r.oldMan, "true", "Ruby swaps the old man")
|
||||
eq(r.daycareMail, "true", "and the daycare mail")
|
||||
os.remove(tmp)
|
||||
os.remove(rubyOut)
|
||||
os.remove(emOut)
|
||||
end
|
||||
|
||||
print(string.format("%s game3_link_rs_cross_test", failed == 0 and "PASS" or ("FAIL " .. failed)))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -0,0 +1,304 @@
|
||||
#!/usr/bin/env luajit
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local failed = 0
|
||||
local function check(cond, msg)
|
||||
if cond then
|
||||
print("[ok] " .. msg)
|
||||
else
|
||||
failed = failed + 1
|
||||
print("[FAIL] " .. msg)
|
||||
end
|
||||
end
|
||||
local function eq(a, b, msg)
|
||||
check(a == b, string.format("%s (%s == %s)", msg, tostring(a), tostring(b)))
|
||||
end
|
||||
|
||||
local FakeRelay = require("tests.g3link_fake_relay")
|
||||
|
||||
local store = { flags = {}, vars = {} }
|
||||
local session = {
|
||||
version = "ruby", store = store, map = "RS_OLDALE_TOWN_POKEMON_CENTER_2F", x = 5, y = 3,
|
||||
name = "BRENDAN", gender = 0, trainerId = 0x2222,
|
||||
party = { { species = 280, level = 12 }, { species = 283, level = 9 } },
|
||||
bag = { pockets = { items = {} } },
|
||||
}
|
||||
local input = { pressed = {} }
|
||||
function input:wasPressed(k) return self.pressed[k] == true end
|
||||
function input:isDown(k) return self.pressed[k] == true end
|
||||
local game = { data = { maps = {} }, session = session, input = input, save = { player = { name = "BRENDAN" } } }
|
||||
|
||||
package.loaded["src.core.game3.runtime"] = {
|
||||
getSession = function() return session end,
|
||||
isActive = function() return true end,
|
||||
_game = game,
|
||||
}
|
||||
package.loaded["src.core.game3.player"] = { cellX = 5, cellY = 3, facing = "up" }
|
||||
package.loaded["src.core.game3.map"] = { load = function() end }
|
||||
package.loaded["src.core.game3.rom_text"] = {
|
||||
plain = function(key) return key end, box = function(key) return key end,
|
||||
ascii = function(key) return key end, has = function() return true end,
|
||||
ir = function(key) return { { t = "text", s = key } } end,
|
||||
translate = function(ir) return ir end,
|
||||
key = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
|
||||
at = function(n, i, j) return j and (n .. "[" .. i .. "][" .. j .. "]") or (n .. "[" .. i .. "]") end,
|
||||
count = function() return 0 end, list = function() return {} end,
|
||||
lazy = function(map) return setmetatable({}, { __index = function(_, k) return map[k] end }) end,
|
||||
}
|
||||
|
||||
local ctx = { specialVars = {}, stringVars = {} }
|
||||
local adapters = { log = function() end, playSe = function() end }
|
||||
package.loaded["src.core.game3.scripting.space"] = {
|
||||
store = store, mapId = session.map, vm = { ctx = ctx, adapters = adapters },
|
||||
ensureBundle = function() return nil end,
|
||||
}
|
||||
|
||||
local LIVE = { engine = 3, engineVersion = "1.0.0", fingerprint = "f00dcafe", kind = "vanilla" }
|
||||
package.loaded["src.online.ArenaData"] = {
|
||||
liveProfile3 = function(_, rulesetId)
|
||||
local p = { version = session.version }
|
||||
for k, v in pairs(LIVE) do p[k] = v end
|
||||
p.rulesetId = rulesetId
|
||||
return p
|
||||
end,
|
||||
}
|
||||
local Client = FakeRelay.client({ state = "online", id = "0000beef" })
|
||||
package.loaded["src.online.Client"] = Client
|
||||
|
||||
local Link = require("src.core.game3.link")
|
||||
local LB = require("src.core.game3.link.battle")
|
||||
local Union = require("src.core.game3.link.union_room")
|
||||
local Flags = require("src.core.game3.scripting.flags")
|
||||
local Lobby = require("src.ui.game3.rs.cable_lobby")
|
||||
local Game3Link = require("src.link.Game3Link")
|
||||
local NativesLinkRs = require("src.core.game3.scripting.natives_link_rs")
|
||||
|
||||
local function getVar(id) return tonumber(Flags.getVar(store, ctx, id)) or 0 end
|
||||
local function setVar(id, v) Flags.setVar(store, ctx, id, v) end
|
||||
local function fresh()
|
||||
ctx.specialVars = {}
|
||||
ctx.nativePoll, ctx.mode, ctx.status = nil, nil, nil
|
||||
Client.clear()
|
||||
Client._group, Client._groups, Client._directEntries = nil, {}, {}
|
||||
Client._room, Client._seat = nil, nil
|
||||
Lobby.reset()
|
||||
Link.reset()
|
||||
Link._live = nil
|
||||
end
|
||||
local function rowIds()
|
||||
local out = {}
|
||||
for i, row in ipairs(Lobby.rows()) do out[i] = row.id end
|
||||
return out
|
||||
end
|
||||
local function pick(id)
|
||||
for _, row in ipairs(Lobby.rows()) do
|
||||
if row.id == id then return Lobby.opts.select(row) end
|
||||
end
|
||||
error("no row " .. tostring(id))
|
||||
end
|
||||
local function poll(n)
|
||||
local r
|
||||
for _ = 1, n or 1 do
|
||||
r = ctx.nativePoll()
|
||||
if r then break end
|
||||
end
|
||||
return r
|
||||
end
|
||||
local function me(id) return { id = id or "0000beef", name = "BRENDAN", avatar = { name = "BRENDAN" } } end
|
||||
local function peerOn(room, seat, version, linkType)
|
||||
local hello = Game3Link.hello(game, linkType, { version = version, name = version:upper(), trainerId = 0x1000 + seat })
|
||||
return Game3Link.attach(FakeRelay.transport(room, seat), {
|
||||
game = game, linkType = linkType, seat = seat, seats = room.seats, hello = hello,
|
||||
})
|
||||
end
|
||||
local function pump(peers)
|
||||
for _ = 1, 4 do
|
||||
for _, p in ipairs(peers) do p:update(0) end
|
||||
Link.update(0)
|
||||
end
|
||||
end
|
||||
|
||||
print("[test] 1. a Ruby cable leader auto-accepts and starts a two-seat group")
|
||||
fresh()
|
||||
setVar(0x8004, 1)
|
||||
local yielded = NativesLinkRs.BY_NAME.sub_808347C(ctx, adapters)
|
||||
check(yielded, "sub_808347C parks the script")
|
||||
check(Lobby.isOpen(), "the cable lobby is up")
|
||||
eq(table.concat(rowIds(), ","), "leader,join", "CREATE GROUP / JOIN GROUP")
|
||||
pick("leader")
|
||||
local og = Client.last("openGroup")
|
||||
eq(og and og[1], "battle_single", "the leader opens a battle_single group")
|
||||
eq(og and og[2] and og[2].rulesetId, "g3_single", "with the battle ruleset profile")
|
||||
Client._group = { leader = "0000beef", min = 2, max = 2, members = { me() },
|
||||
pending = { { id = "00e00001", name = "MAY", avatar = { name = "MAY" } } } }
|
||||
poll()
|
||||
local acc = Client.last("acceptGroup")
|
||||
eq(acc and acc[1], "00e00001", "the first group_request is accepted without a prompt")
|
||||
eq(acc and acc[2], true, "with ok = true")
|
||||
Client._group = { leader = "0000beef", min = 2, max = 2, members = { me(), { id = "00e00001", name = "MAY" } }, pending = {} }
|
||||
poll()
|
||||
eq(Client.count("startGroup"), 1, "a full two-seat group starts by itself")
|
||||
|
||||
print("[test] 2. an Emerald wireless peer seated into the group links up with no linkup message")
|
||||
local room = FakeRelay.room({ seats = 2 })
|
||||
Client.bindRoom(room, 0)
|
||||
local peer = peerOn(room, 1, "emerald", Game3Link.LINKTYPE.BATTLE)
|
||||
check(poll() == false, "the match opens the relay link")
|
||||
check(Link.link ~= nil, "Link.openRelay ran")
|
||||
pump({ peer })
|
||||
check(Link.link and Link.link:isReady(), "the hellos agree")
|
||||
local done = poll(LB.LINKUP_TICKS + 5)
|
||||
check(done == true, "the linkup finishes")
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.SUCCESS, "Ruby vs an Emerald wireless player is LINKUP_SUCCESS")
|
||||
eq(LB.state, "seat", "and the colosseum seat wait begins")
|
||||
eq(Link.link and Link.link.linkType, 0x2233, "the link runs as LINKTYPE_SINGLE_BATTLE")
|
||||
check(not Lobby.isOpen(), "the lobby closed")
|
||||
|
||||
print("[test] 3. a Ruby joiner sees Direct Corner hosts next to cable groups")
|
||||
fresh()
|
||||
setVar(0x8004, 2)
|
||||
NativesLinkRs.BY_NAME.sub_808347C(ctx, adapters)
|
||||
pick("join")
|
||||
eq(Client.last("groupList") and Client.last("groupList")[1], "battle_double", "it watches battle_double groups")
|
||||
eq(Client.last("directList") and Client.last("directList")[1], "battle_double", "and the Direct Corner list")
|
||||
Client._groups = { { leader = "00c00001", name = "RED", avatar = { name = "RED" }, joined = 1, max = 2 } }
|
||||
Client._directEntries.battle_double = {
|
||||
{ kind = "room", room = "r00000000000000aa", host = "00d00001", name = "LEAF", avatar = { name = "LEAF" } },
|
||||
{ kind = "room", room = "r00000000000000bb", host = "00d00002", name = "PIN", locked = true },
|
||||
{ kind = "player", id = "00d00003", name = "AUTO" },
|
||||
}
|
||||
local rows = Lobby.rows()
|
||||
eq(#rows, 3, "one group and two hosted rooms (queued players are not joinable)")
|
||||
eq(rows[2].kind, "room", "the Direct Corner host is listed")
|
||||
eq(rows[3].disabled, true, "a PIN room is not joinable from the cable")
|
||||
pick("r00000000000000aa")
|
||||
local jr = Client.last("joinRoom")
|
||||
eq(jr and jr[1], "r00000000000000aa", "room_join to the Direct Corner host")
|
||||
eq(jr and jr[2], "player", "as a player")
|
||||
Lobby.opts.cancel()
|
||||
eq(Client.count("leaveRoom"), 1, "B while joining leaves the room")
|
||||
check(Lobby.isOpen(), "and goes back to the list")
|
||||
|
||||
print("[test] 4. FireRed's cable counter falls back to the relay instead of timing out")
|
||||
fresh()
|
||||
session.version = "firered"
|
||||
local NativesLink = require("src.core.game3.scripting.natives_link")
|
||||
setVar(0x8004, Link.USING.TRADE_CENTER)
|
||||
yielded = NativesLink.BY_NAME.TryTradeLinkup(ctx, adapters)
|
||||
check(yielded, "TryTradeLinkup parks the script")
|
||||
check(Lobby.isOpen(), "with the cable lobby, not a dead wait")
|
||||
for _ = 1, LB.LINKUP_TICKS + 5 do ctx.nativePoll() end
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.ONGOING, "and it does not time out to CONNECTION_ERROR")
|
||||
pick("leader")
|
||||
eq(Client.last("openGroup") and Client.last("openGroup")[1], "trade", "the trade group")
|
||||
Lobby.opts.cancel()
|
||||
eq(getVar(Link.VAR_RESULT), Link.LINKUP.FAILED, "B is LINKUP_FAILED")
|
||||
eq(Client.count("leaveGroup"), 1, "and leaves the group")
|
||||
|
||||
print("[test] 5. Emerald's cable counters pick the matching relay activity")
|
||||
session.version = "emerald"
|
||||
local cases = {
|
||||
{ "TryBattleLinkup", Link.USING.SINGLE_BATTLE, "battle_single" },
|
||||
{ "TryBattleLinkup", Link.USING.MULTI_BATTLE, "battle_multi" },
|
||||
{ "TryBattleLinkup", Link.USING.BATTLE_TOWER, "battle_tower" },
|
||||
{ "TryRecordMixLinkup", 0, "record_corner" },
|
||||
}
|
||||
local NativesLinkRse = require("src.core.game3.scripting.natives_link_rse")
|
||||
for _, c in ipairs(cases) do
|
||||
fresh()
|
||||
setVar(0x8004, c[2])
|
||||
NativesLinkRse.BY_NAME[c[1]](ctx, adapters)
|
||||
pick("leader")
|
||||
eq(Client.last("openGroup") and Client.last("openGroup")[1], c[3], c[1] .. " " .. c[2] .. " opens " .. c[3])
|
||||
end
|
||||
fresh()
|
||||
NativesLinkRse.BY_NAME.TryBerryBlenderLinkup(ctx, adapters)
|
||||
pick("leader")
|
||||
eq(Client.last("openGroup") and Client.last("openGroup")[1], "berry_blender", "TryBerryBlenderLinkup opens berry_blender")
|
||||
fresh()
|
||||
setVar(0x8011, 2)
|
||||
local NativesContest = require("src.core.game3.scripting.natives_contest")
|
||||
NativesContest.BY_NAME.TryContestEModeLinkup(ctx, adapters)
|
||||
pick("leader")
|
||||
eq(Client.last("openGroup") and Client.last("openGroup")[1], "contest_cute", "a CUTE link contest opens contest_cute")
|
||||
eq(LB.towerSpec().linkType, 0x2266, "a level 50 tower challenge links as LINKTYPE_BATTLE_TOWER_50")
|
||||
|
||||
print("[test] 6. a 2..4 group still asks the leader")
|
||||
fresh()
|
||||
setVar(0x8004, 0)
|
||||
NativesLinkRse.BY_NAME.TryRecordMixLinkup(ctx, adapters)
|
||||
pick("leader")
|
||||
Client._group = { leader = "0000beef", min = 2, max = 4, members = { me() },
|
||||
pending = { { id = "00e00002", name = "WALLY", avatar = { name = "WALLY" } } } }
|
||||
poll()
|
||||
eq(table.concat(rowIds(), ","), "yes,no", "ADD WALLY? YES / NO")
|
||||
eq(Client.count("acceptGroup"), 0, "nothing accepted yet")
|
||||
pick("yes")
|
||||
eq(Client.last("acceptGroup") and Client.last("acceptGroup")[2], true, "YES accepts")
|
||||
|
||||
print("[test] 7. the activity names follow each game's sLinkGroupActivityNameTexts")
|
||||
session.version = "emerald"
|
||||
eq(Union.ACTIVITY_NAMES[15], "sLinkGroupActivityNameTexts[15]", "Emerald RECORD CORNER")
|
||||
eq(Union.ACTIVITY_NAMES[23], "sLinkGroupActivityNameTexts[23]", "Emerald COOL CONTEST")
|
||||
eq(Union.ACTIVITY_NAMES[28], "sLinkGroupActivityNameTexts[28]", "Emerald BATTLE TOWER LV. 50")
|
||||
eq(Union.ACTIVITY_NAMES[13], nil, "Emerald SPIN TRADE is an empty string")
|
||||
eq(Union.ACTIVITY_NAMES[3], "sLinkGroupActivityNameTexts[3]", "MULTI BATTLE")
|
||||
session.version = "firered"
|
||||
eq(Union.ACTIVITY_NAMES[13], "sLinkGroupActivityNameTexts[13]", "FireRed SPIN TRADE")
|
||||
eq(Union.ACTIVITY_NAMES[15], nil, "FireRed has no RECORD CORNER name")
|
||||
eq(Union.ACTIVITY_NAMES[10], "sLinkGroupActivityNameTexts[10]", "BERRY CRUSH")
|
||||
|
||||
print("[test] 8. the Direct Corner CHOOSE list shows open cable groups")
|
||||
Client._directEntries.trade = {
|
||||
{ kind = "group", leader = "00f00001", name = "BRENDAN", avatar = { name = "BRENDAN", version = "ruby" }, joined = 1, max = 2 },
|
||||
{ kind = "group", leader = "00f00002", name = "FULL", avatar = { name = "FULL" }, joined = 2, max = 2 },
|
||||
}
|
||||
local drows = Union.directRows("trade")
|
||||
eq(#drows, 1, "the open group is listed, the full one is not")
|
||||
eq(drows[1] and drows[1].kind, "group", "as a group row")
|
||||
eq(drows[1] and drows[1].leader, "00f00001", "with its leader id")
|
||||
eq(drows[1] and drows[1].version, "ruby", "and the Ruby version for the trade check")
|
||||
|
||||
print("[test] 9. record mixing packs byte runs so a full Emerald packet fits the relay cap")
|
||||
local RecordMix = require("src.core.game3.link.record_mix")
|
||||
local Json = require("src.link.Json")
|
||||
local Wire = require("src.link.Wire")
|
||||
local raw = {}
|
||||
for i = 1, 160 do raw[i] = (i * 37) % 256 end
|
||||
local packet = { secretBases = { { trainerName = "MAY", _recordMixNativeBytes = raw } }, tvShows = { [0] = { kind = 1 } } }
|
||||
local msg = Wire.sanitize(Json.decode(Json.encode({ type = RecordMix.MSG.PACKET, spot = 1, packet = RecordMix.toWire(packet) })))
|
||||
local back = RecordMix.fromWire(msg.packet)
|
||||
local same = true
|
||||
for i = 1, 160 do if back.secretBases[1]._recordMixNativeBytes[i] ~= raw[i] then same = false end end
|
||||
check(same, "160 native bytes survive the wire")
|
||||
eq(back.tvShows[0].kind, 1, "zero-indexed rows still round trip")
|
||||
check(#Json.encode(RecordMix.toWire(packet)) < 700, "and the byte run travels as one hex string")
|
||||
|
||||
print("[test] 10. a contest with a Ruby player is flagged and skips the wireless standby")
|
||||
local CL = require("src.core.game3.link.contest_link")
|
||||
local fakeLink = {
|
||||
isOpen = function() return true end,
|
||||
players = function() return { { seat = 0, version = "emerald" }, { seat = 1, version = "ruby" } } end,
|
||||
send = function(self, m) self.sent = (self.sent or 0) + 1 end,
|
||||
take = function() return nil end,
|
||||
getSeat = function() return 0 end,
|
||||
}
|
||||
local flags = CL.flagsFor(fakeLink, true)
|
||||
eq(flags, CL.FLAG.IS_LINK + CL.FLAG.IS_WIRELESS + CL.FLAG.HAS_RS_PLAYER, "IS_LINK | IS_WIRELESS | HAS_RS_PLAYER")
|
||||
local s = CL.newSession(fakeLink, { flags = flags })
|
||||
eq(s:standby(), true, "LinkContest_TryLinkStandby skips the standby with an RS player")
|
||||
eq(fakeLink.sent, nil, "without sending a standby block")
|
||||
CL.active = s
|
||||
NativesContest.linkFlags = flags
|
||||
eq(select(2, NativesContest.BY_NAME.IsContestWithRSPlayer()), 1, "IsContestWithRSPlayer answers TRUE")
|
||||
eq(select(1, NativesContest.linkContestWaitForConnection(ctx, adapters)), false,
|
||||
"LinkContestWaitForConnection does not park the script for an RS contest")
|
||||
eq(CL.flagsFor({ players = function() return { { seat = 0, version = "emerald" } } end }, false), CL.FLAG.IS_LINK,
|
||||
"an all-Emerald cable contest is just IS_LINK")
|
||||
CL.reset()
|
||||
NativesContest.linkFlags = 0
|
||||
|
||||
Link.reset()
|
||||
Lobby.reset()
|
||||
print(string.format("%s game3_link_rs_entry_test", failed == 0 and "PASS" or ("FAIL " .. failed)))
|
||||
os.exit(failed == 0 and 0 or 1)
|
||||
@@ -0,0 +1,121 @@
|
||||
#!/usr/bin/env luajit
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
|
||||
local T = require("tests.harness")
|
||||
if not _G.love then _G.love = require("tests.love_stub") end
|
||||
|
||||
local Cache = require("tests.game3_cache")
|
||||
if not Cache.mount("meta.json") then
|
||||
print("[skip] game3_link_translation_mods_test: " .. tostring(Cache.reason))
|
||||
os.exit(0)
|
||||
end
|
||||
|
||||
local files = {}
|
||||
local memfs = {
|
||||
getInfo = function(name) return files[name] and { type = "file" } or nil end,
|
||||
read = function(name) return files[name] end,
|
||||
write = function(name, body) files[name] = body return true end,
|
||||
remove = function(name) files[name] = nil return true end,
|
||||
createDirectory = function() return true end,
|
||||
getDirectoryItems = function() return {} end,
|
||||
}
|
||||
local SaveData = require("src.core.SaveData")
|
||||
SaveData.portableFs = function() return memfs end
|
||||
|
||||
require("src.core.GameVersion").set("firered")
|
||||
local ArenaData = require("src.online.ArenaData")
|
||||
local Handshake = require("src.link.Handshake")
|
||||
local Fingerprint = require("src.link.Fingerprint")
|
||||
local Protocol = require("src.link.Protocol")
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
Pokemon.install(nil)
|
||||
|
||||
local vanilla = ArenaData.liveProfile3({ data = { generation = 3 }, version = "firered" })
|
||||
T.check(vanilla ~= nil, "a vanilla FireRed has a live profile")
|
||||
|
||||
local function ops(list)
|
||||
local out = {}
|
||||
for _, row in ipairs(list) do
|
||||
out[row.registry] = out[row.registry] or { ops = {} }
|
||||
local reg = out[row.registry].ops
|
||||
reg[row.id] = reg[row.id] or {}
|
||||
local l = reg[row.id]
|
||||
l[#l + 1] = { op = row.op, value = row.value, owner = row.owner }
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
local function loader(id, content, extra)
|
||||
extra = extra or {}
|
||||
return {
|
||||
mods = { [id] = { manifest = { id = id, permissions = extra.permissions or {} } } },
|
||||
content = content,
|
||||
hooks = { chains = extra.chains or {} },
|
||||
events = { listeners = {} },
|
||||
migrations = {}, modInput = {}, stepsQueues = {},
|
||||
status = function()
|
||||
return { loaded = { { id = id, version = "1.0", affects_link = extra.affectsLink == true } } }
|
||||
end,
|
||||
}
|
||||
end
|
||||
|
||||
local function game(mods)
|
||||
return { data = { generation = 3 }, version = "firered", mods = mods }
|
||||
end
|
||||
|
||||
local translation = loader("espanol", ops({
|
||||
{ registry = "rom_text", id = "gText_UR_EggTrade", op = "override", value = "HUEVO", owner = "espanol" },
|
||||
{ registry = "strings", id = "Connect to the Wireless Club?", op = "override", value = "Conectar?", owner = "espanol" },
|
||||
{ registry = "pokemon", id = "BULBASAUR", op = "patch", value = { name = "BULBIZARRE" }, owner = "espanol" },
|
||||
{ registry = "moves", id = "TACKLE", op = "patch", value = { name = "PLACAJE" }, owner = "espanol" },
|
||||
{ registry = "items", id = "POTION", op = "patch", value = { name = "POCION", description = "Cura 20 PS." }, owner = "espanol" },
|
||||
}))
|
||||
|
||||
T.eq(ArenaData.modTouchesGameplay(translation, "espanol"), false, "names and text are cosmetic")
|
||||
local live, why = ArenaData.liveProfile3(game(translation))
|
||||
T.check(live ~= nil, "a translation mod keeps the game online: " .. tostring(why))
|
||||
T.eq(live and live.fingerprint, vanilla and vanilla.fingerprint, "and leaves the dataset fingerprint alone")
|
||||
|
||||
local rebalance = loader("rebalance", ops({
|
||||
{ registry = "pokemon", id = "BULBASAUR", op = "patch", value = { name = "BULBA", baseStats = { hp = 99 } }, owner = "rebalance" },
|
||||
}))
|
||||
T.eq(ArenaData.modTouchesGameplay(rebalance, "rebalance"), true, "a stat change is gameplay")
|
||||
local none, noneWhy = ArenaData.liveProfile3(game(rebalance))
|
||||
T.eq(none, nil, "a gameplay mod still blocks online")
|
||||
T.eq(noneWhy, "mods", "with the mods reason")
|
||||
|
||||
local encounters = loader("wild", ops({
|
||||
{ registry = "encounters", id = "ROUTE1", op = "override", value = {}, owner = "wild" },
|
||||
}))
|
||||
T.eq(select(2, ArenaData.liveProfile3(game(encounters))), "mods", "an encounter table is gameplay too")
|
||||
|
||||
local hooked = loader("hooky", ops({
|
||||
{ registry = "strings", id = "Hi", op = "override", value = "Hola", owner = "hooky" },
|
||||
}), { chains = { ["battle.damage"] = { { owner = "hooky" } } } })
|
||||
T.eq(select(2, ArenaData.liveProfile3(game(hooked))), "mods", "a text mod that also hooks code blocks")
|
||||
|
||||
local permitted = loader("net", ops({}), { permissions = { "network" } })
|
||||
T.eq(select(2, ArenaData.liveProfile3(game(permitted))), "mods", "a mod asking for permissions blocks")
|
||||
|
||||
local claimed = loader("overhaul", ops({}), { affectsLink = true })
|
||||
T.eq(select(2, ArenaData.liveProfile3(game(claimed))), "mods", "a mod that says it affects link blocks")
|
||||
|
||||
local data = { generation = 3 }
|
||||
local hello = Handshake.hello({ data = data, mods = translation }, nil)
|
||||
ArenaData.stripCosmeticMods(hello, { data = data, mods = translation })
|
||||
T.eq(hello.linkModified, false, "the room hello does not flag a translation as link-modified")
|
||||
T.eq(#hello.mods, 0, "and lists no link mods")
|
||||
T.eq(hello.fingerprint, Fingerprint.compute(data, {}, 3), "and hashes like vanilla")
|
||||
local plainHello = Handshake.hello({ data = data }, nil)
|
||||
T.eq(Handshake.checkCompat(hello, plainHello), "full", "so it pairs fully with a vanilla game")
|
||||
|
||||
local gameplayHello = Handshake.hello({ data = data, mods = rebalance }, nil)
|
||||
ArenaData.stripCosmeticMods(gameplayHello, { data = data, mods = rebalance })
|
||||
T.eq(gameplayHello.linkModified, true, "a gameplay mod still flags the hello")
|
||||
|
||||
local mon = { species = 1, nickname = "", level = 5, personality = 1, otId = 1, otName = "RED" }
|
||||
local packed = Protocol.packMon3(mon)
|
||||
T.eq(packed.species, 1, "a traded mon travels as its species id")
|
||||
T.eq(packed.nickname, "", "an unnamed mon sends no species name text")
|
||||
|
||||
T.finish()
|
||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,256 @@
|
||||
#!/usr/bin/env python3
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import struct
|
||||
import sys
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
DEV = os.path.abspath(os.path.join(HERE, "..", ".."))
|
||||
PKHEX = os.environ.get("PKHEX", os.path.join(DEV, "pkhex-master", "PKHeX.Core"))
|
||||
SERVER = os.environ.get("POKESERVER", os.path.join(DEV, "pokeserver"))
|
||||
WC3 = "Legality/Encounters/Data/Gen3/EncountersWC3.cs"
|
||||
RSE = "Legality/Encounters/Data/Gen3/Encounters3RSE.cs"
|
||||
NY = "Legality/Encounters/Templates/Gen3/Gifts/Distribution3NY.cs"
|
||||
NY_PKL = "Resources/legality/wild/Gen3/encounter_pcny.pkl"
|
||||
JPN = "Legality/Encounters/Templates/Gen3/Gifts/Distribution3JPN.cs"
|
||||
|
||||
LANG = {"Japanese": 1, "English": 2, "French": 3, "Italian": 4, "German": 5, "Spanish": 7}
|
||||
LANG_FILE = {1: "ja", 3: "fr", 4: "it", 5: "de", 7: "es"}
|
||||
GAME_ID = {"S": 1, "R": 2, "E": 3, "FR": 4, "LG": 5}
|
||||
ALL = ["firered", "leafgreen", "ruby", "sapphire", "emerald"]
|
||||
GAMES = {"R": ALL, "S": ALL, "RS": ALL, "Gen3": ALL, "FRLG": ["firered", "leafgreen"],
|
||||
"EFL": ["firered", "leafgreen", "emerald"]}
|
||||
GENDER = {"Only0": 0, "RandD3_0": 0, "Only1": 1, "RandD3_1": 1, "Recipient": None}
|
||||
CONSTS = {"PCJPEggTrainerName": "オヤNAME", "M2WishEggOT": ""}
|
||||
OWNER = {("Encounters3RSE", 251): {"otId": 31121, "secretIdZero": True,
|
||||
"note": "owner: SID 0; PKHeX leaves the SID unspecified"}}
|
||||
|
||||
|
||||
def read(rel):
|
||||
with open(os.path.join(PKHEX, rel), encoding="utf-8-sig") as f:
|
||||
return f.read().split("\n")
|
||||
|
||||
|
||||
CATALOG = json.load(open(os.path.join(SERVER, "gifts_catalog.json"), encoding="utf-8"))
|
||||
INTERNAL = {s["dex"]: s["id"] for s in CATALOG["species"]}
|
||||
NAME = {s["dex"]: s["name"] for s in CATALOG["species"]}
|
||||
CHARSET = set(CATALOG["charset"])
|
||||
SPECIES_TEXT = {}
|
||||
|
||||
|
||||
def species_name(dex, lang):
|
||||
if lang not in LANG_FILE:
|
||||
return None
|
||||
code = LANG_FILE[lang]
|
||||
if code not in SPECIES_TEXT:
|
||||
path = os.path.join(PKHEX, "Resources/text/other/%s/text_Species_%s.txt" % (code, code))
|
||||
SPECIES_TEXT[code] = open(path, encoding="utf-8-sig").read().split("\n")
|
||||
name = SPECIES_TEXT[code][dex].strip()
|
||||
if lang != 1:
|
||||
name = name.upper()
|
||||
if not all(ch in CHARSET for ch in name):
|
||||
return None
|
||||
return name[:10]
|
||||
|
||||
|
||||
def split_top(s):
|
||||
out, depth, cur = [], 0, ""
|
||||
for ch in s:
|
||||
if ch in "([":
|
||||
depth += 1
|
||||
elif ch in ")]":
|
||||
depth -= 1
|
||||
if ch == "," and depth == 0:
|
||||
out.append(cur.strip())
|
||||
cur = ""
|
||||
else:
|
||||
cur += ch
|
||||
if cur.strip():
|
||||
out.append(cur.strip())
|
||||
return out
|
||||
|
||||
|
||||
def value(v):
|
||||
v = v.strip()
|
||||
if v.startswith('"'):
|
||||
return v.strip('"')
|
||||
if v in CONSTS:
|
||||
return CONSTS[v]
|
||||
if v.startswith("(int)"):
|
||||
return LANG[v[5:]]
|
||||
if re.match(r"^\d+$", v):
|
||||
return int(v, 10)
|
||||
return v
|
||||
|
||||
|
||||
def entry(rel, line_no, line):
|
||||
m = re.match(r"\s*new\((.*?)\)\s*\{(.*)\},?\s*(//\s*(.*))?$", line)
|
||||
if not m:
|
||||
return None
|
||||
args = split_top(m.group(1))
|
||||
props = {}
|
||||
for part in split_top(m.group(2)):
|
||||
if "=" in part:
|
||||
k, v = part.split("=", 1)
|
||||
props[k.strip()] = v.strip()
|
||||
dex, level, version = int(args[0]), int(args[1]), args[2]
|
||||
egg = len(args) > 3 and args[3] == "true"
|
||||
met = None
|
||||
for a in args[3:]:
|
||||
if a.startswith("met:"):
|
||||
met = int(a[4:])
|
||||
elif re.match(r"^\d+$", a):
|
||||
met = int(a)
|
||||
moves = []
|
||||
if "Moves" in props:
|
||||
moves = [int(x) for x in re.findall(r"\d+", props["Moves"])]
|
||||
ev = {
|
||||
"source": "%s:%d" % (rel, line_no),
|
||||
"comment": (m.group(4) or "").strip(),
|
||||
"dex": dex, "species": INTERNAL[dex], "level": level, "egg": egg, "version": version,
|
||||
"moves": [x for x in moves if x],
|
||||
"ot": value(props.get("OriginalTrainerName", '""')),
|
||||
"language": value(props["Language"]) if "Language" in props else None,
|
||||
"shiny": {"Never": "never", "Always": "always"}.get(props.get("Shiny", "Random"), "random"),
|
||||
"fateful": props.get("FatefulEncounter") == "true",
|
||||
"nationalRibbon": props.get("RibbonNational") == "true",
|
||||
"metLevel": met if met is not None else (0 if egg else level),
|
||||
}
|
||||
gender = props.get("OriginalTrainerGender")
|
||||
ev["otGender"] = GENDER[gender] if gender in GENDER else (None if gender is None else 2)
|
||||
if "ID32" in props:
|
||||
id32 = int(props["ID32"], 10)
|
||||
ev["otId"], ev["secretIdRandom"] = id32, False
|
||||
elif "TID16" in props:
|
||||
ev["otId"], ev["secretIdRandom"] = int(props["TID16"], 10), True
|
||||
return ev
|
||||
|
||||
|
||||
def wc3():
|
||||
out, section = [], None
|
||||
lines = read(WC3)
|
||||
for i, line in enumerate(lines, 1):
|
||||
s = re.match(r"\s*private static readonly EncounterGift3\[\] (\w+)", line)
|
||||
if s:
|
||||
section = s.group(1)
|
||||
ev = entry(WC3, i, line)
|
||||
if ev:
|
||||
ev["group"] = section
|
||||
if ev["language"] is None and not ev["egg"]:
|
||||
ev["language"] = 2
|
||||
if ev["language"] is None and any(ord(ch) > 0x7F for ch in ev["ot"]):
|
||||
ev["language"] = 1
|
||||
out.append(ev)
|
||||
return out
|
||||
|
||||
|
||||
def colo():
|
||||
out = []
|
||||
lines = read(RSE)
|
||||
names = {}
|
||||
for line in lines:
|
||||
m = re.match(r'\s*private static readonly string\[\] (\w+) = \[(.*)\];', line)
|
||||
if m:
|
||||
names[m.group(1)] = [x.strip().strip('"') for x in m.group(2).split(",")]
|
||||
for i, line in enumerate(lines, 1):
|
||||
m = re.match(r"\s*new\((\d+), (\d+), (\w+), (\w+)\)\s*\{(.*)\},?\s*//\s*(.*)$", line)
|
||||
if not m or "Bonus" not in "".join(lines[i - 4:i]) or "without" in "".join(lines[i - 4:i]):
|
||||
continue
|
||||
dex = int(m.group(1))
|
||||
props = dict((k.strip(), v.strip()) for k, v in (p.split("=", 1) for p in split_top(m.group(5))))
|
||||
ev = {"source": "%s:%d" % (RSE, i), "comment": m.group(6).strip(), "group": "ColoBonus",
|
||||
"dex": dex, "species": INTERNAL[dex], "level": int(m.group(2)), "egg": False, "version": m.group(4),
|
||||
"moves": [], "ot": names[m.group(3)][1], "language": 1, "shiny": "never", "fateful": False,
|
||||
"nationalRibbon": False, "metLevel": int(m.group(2)), "otGender": int(props["OriginalTrainerGender"]),
|
||||
"otId": int(props["TID16"]), "secretIdRandom": True}
|
||||
fix = OWNER.get(("Encounters3RSE", dex))
|
||||
if fix:
|
||||
ev["otId"], ev["secretIdRandom"], ev["note"] = fix["otId"], False, fix["note"]
|
||||
out.append(ev)
|
||||
return out
|
||||
|
||||
|
||||
def pcny():
|
||||
lines = read(NY)
|
||||
order = []
|
||||
enum_at = next(i for i, l in enumerate(lines) if "public enum Distribution3NY" in l)
|
||||
for line in lines[enum_at + 2:]:
|
||||
m = re.match(r"\s*(\w+),\s*$", line)
|
||||
if not m:
|
||||
break
|
||||
order.append(m.group(1))
|
||||
names = {}
|
||||
for i, line in enumerate(lines, 1):
|
||||
m = re.match(r"\s*(\w+) => (pivot \? (\w) : (\w)|(\w)),", line)
|
||||
if m and m.group(1) in order and m.group(1) not in names:
|
||||
picks = [m.group(4), m.group(3)] if m.group(3) else [m.group(5)]
|
||||
names[m.group(1)] = (["PCNY" + p.lower() for p in picks], i)
|
||||
data = open(os.path.join(PKHEX, NY_PKL), "rb").read()
|
||||
out = []
|
||||
for k in range(len(data) // 12):
|
||||
dex, level, dist = struct.unpack_from("<HBB", data, k * 12)
|
||||
moves = [x for x in struct.unpack_from("<4H", data, k * 12 + 4) if x]
|
||||
dname = order[dist]
|
||||
ots, line = names[dname]
|
||||
out.append({"source": "%s record %d, %s:%d" % (NY_PKL, k, NY, line), "comment": "PCNY " + dname,
|
||||
"group": "PCNY", "dex": dex, "species": INTERNAL[dex], "level": level, "egg": False,
|
||||
"version": "RS", "moves": moves, "otNames": ots, "language": 2, "shiny": "never",
|
||||
"fateful": False, "nationalRibbon": False, "metLevel": level, "otGender": None,
|
||||
"otIdMax": 2999})
|
||||
return out
|
||||
|
||||
|
||||
def pcjp():
|
||||
lines = read(JPN)
|
||||
text = "\n".join(lines)
|
||||
cities = dict(re.findall(r'private const string (\w+) = "(.*?)";', text))
|
||||
tids = dict(re.findall(r"(\w+)\s+=> (\d{5}),", text))
|
||||
out = []
|
||||
for i, line in enumerate(lines, 1):
|
||||
m = re.match(r"\s*private static ReadOnlySpan<ushort> Distro(\d) => \[(.*)\];", line)
|
||||
if not m:
|
||||
continue
|
||||
dist = ["First", "Second", "Third", "Fourth", "Fifth", "Sixth"][int(m.group(1)) - 1]
|
||||
ots = [cities[c] for c in ["Tokyo", "Yokohama", "Nagoya", "Osaka", "Fukuoka", "Sapporo"]]
|
||||
if dist == "Sixth":
|
||||
ots = ots[:5]
|
||||
for dex in [int(x) for x in m.group(2).split(",")]:
|
||||
out.append({"source": "%s:%d" % (JPN, i), "comment": "PCJP " + dist, "group": "PCJP", "dex": dex,
|
||||
"species": INTERNAL[dex], "level": 10, "egg": False, "version": "R", "moves": [],
|
||||
"otNames": ots, "language": 1, "shiny": "never", "fateful": False,
|
||||
"nationalRibbon": False, "metLevel": 10, "otGender": None, "otId": int(tids[dist]),
|
||||
"secretIdRandom": False})
|
||||
return out
|
||||
|
||||
|
||||
def slug(text):
|
||||
return re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
||||
|
||||
|
||||
def main():
|
||||
events = wc3() + colo() + pcny() + pcjp()
|
||||
seen = {}
|
||||
for n, ev in enumerate(events):
|
||||
base = "ev%03d_%s" % (n, slug(NAME[ev["dex"]]))
|
||||
ev["key"] = base[:28]
|
||||
assert ev["key"] not in seen
|
||||
seen[ev["key"]] = True
|
||||
ev["idNumber"] = 1000 + n
|
||||
ev["games"] = GAMES[ev["version"]]
|
||||
if ev["version"] in GAME_ID:
|
||||
ev["metGame"] = GAME_ID[ev["version"]]
|
||||
lang = ev.get("language")
|
||||
nick = species_name(ev["dex"], lang) if lang and lang != 2 and not ev["egg"] else None
|
||||
if nick:
|
||||
ev["nickname"] = nick
|
||||
title = NAME[ev["dex"]] if not ev["egg"] else "POKéMON EGG"
|
||||
ev["title"] = title
|
||||
label = ev["comment"] or title
|
||||
ev["label"] = ("%s (%s)" % (label, ev.get("ot") or "/".join(ev.get("otNames", [])) or "recipient"))
|
||||
json.dump(events, sys.stdout, ensure_ascii=False, indent=1)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,107 @@
|
||||
package.path = "./?.lua;./?/init.lua;" .. package.path
|
||||
love = love or require("tests.love_stub")
|
||||
rawset(_G, "print", function() end)
|
||||
|
||||
local root, templatesPath = arg[1], arg[2]
|
||||
assert(root and templatesPath,
|
||||
"usage: luajit tools/rs_mystery_event_mons.lua <ruby cache data/generated/gba root> <gen3_event_catalog.py output>")
|
||||
|
||||
local GameVersion = require("src.core.GameVersion")
|
||||
GameVersion.set("ruby")
|
||||
local Dataset = require("src.core.game3.dataset")
|
||||
Dataset.cacheRootOverride = root
|
||||
Dataset.mountExtractRoots()
|
||||
local Pokemon = require("src.core.game3.pokemon")
|
||||
Pokemon.install(nil)
|
||||
local Rng = require("src.core.game3.rng")
|
||||
local Base64 = require("src.core.Base64")
|
||||
local Json = require("src.link.Json")
|
||||
local MysteryGift = require("src.core.game3.mystery_gift")
|
||||
local bit = require("bit")
|
||||
local codec = require("src.save_convert.Gen3Save").forVersion("ruby")
|
||||
|
||||
local BASE = 0x02000000
|
||||
local FATEFUL = 0xFF
|
||||
local POKE_BALL = 4
|
||||
|
||||
local function u8(n) return string.char(n % 256) end
|
||||
local function u16(n) return u8(n) .. u8(math.floor(n / 256)) end
|
||||
local function u32(n) return u16(n % 65536) .. u16(math.floor(n / 65536)) end
|
||||
|
||||
local function crc16(s)
|
||||
local crc = 0x1121
|
||||
for i = 1, #s do
|
||||
crc = bit.bxor(crc, s:byte(i))
|
||||
for _ = 1, 8 do
|
||||
if bit.band(crc, 1) ~= 0 then crc = bit.bxor(bit.rshift(crc, 1), 0x8408) else crc = bit.rshift(crc, 1) end
|
||||
end
|
||||
end
|
||||
return bit.band(bit.bnot(crc), 0xFFFF)
|
||||
end
|
||||
|
||||
local function giftOf(t)
|
||||
local g = { kind = t.egg and "egg" or "mon", species = t.species, ball = POKE_BALL }
|
||||
if not t.egg then
|
||||
g.level, g.metLocation, g.metLevel = t.level, FATEFUL, t.metLevel
|
||||
end
|
||||
if #t.moves > 0 then
|
||||
g.moves, g.movesOnly = {}, true
|
||||
for i, m in ipairs(t.moves) do g.moves[tostring(i)] = m end
|
||||
end
|
||||
if t.ot and t.ot ~= "" then g.otName = t.ot end
|
||||
if t.otNames then g.otNames = t.otNames end
|
||||
if t.otIdMax then g.otIdMax = t.otIdMax elseif t.otId then g.otId = t.otId end
|
||||
if t.secretIdRandom then g.secretIdRandom = true end
|
||||
if t.otGender then g.otGender = t.otGender end
|
||||
if t.language then g.language = t.language end
|
||||
if t.metGame then g.metGame = t.metGame end
|
||||
if t.nickname then g.nickname = t.nickname end
|
||||
g.shiny = t.shiny
|
||||
if not t.fateful then g.fateful = false end
|
||||
if t.nationalRibbon then g.nationalRibbon = true end
|
||||
return g
|
||||
end
|
||||
|
||||
local function partyMon(t, gift, seed)
|
||||
Rng.SeedRng(seed)
|
||||
local session = { version = "ruby", party = {}, name = "PLAYER", trainerId = 0, secretId = 0, gender = 0 }
|
||||
local card = MysteryGift.normalizeCard({ flagId = 1019, idNumber = 1, type = 0, gift = gift }, "ruby")
|
||||
assert(card, "gift does not normalize: " .. t.key)
|
||||
local mon = assert(MysteryGift.createEventMon(session, card.gift), "could not build " .. t.key)
|
||||
local cart = codec.fromPortMon(mon, { trainerId = mon.otId, secretId = mon.otSecretId, playerName = mon.otName,
|
||||
speciesName = Pokemon.name }, true)
|
||||
local bytes = codec.encodePartyMon(cart)
|
||||
assert(#bytes == 100, "party mon is not 100 bytes")
|
||||
local back = codec.decodePartyMon(bytes)
|
||||
assert(back.species == t.species and back.otId == mon.otId, "party mon does not round trip: " .. t.key)
|
||||
if t.language == 1 and mon.otName ~= "" then
|
||||
assert(codec.decodeString(back.otNameRaw, 0, 7, 1, true) == mon.otName, "Japanese OT lost: " .. t.key)
|
||||
end
|
||||
return bytes
|
||||
end
|
||||
|
||||
-- pokeruby/include/macros/mystery_event_script.inc:5 me_checkcompat
|
||||
local function payload(monBytes)
|
||||
local mail = string.rep("\0", 36)
|
||||
local monAt = 17 + 13 + 5 + 1
|
||||
local total = monAt + #monBytes + #mail
|
||||
local header = u8(1) .. u32(BASE) .. u16(2) .. u32(2) .. u16(4) .. u32(0x180)
|
||||
local body = u8(12) .. u32(BASE + monAt) .. u8(2) .. monBytes .. mail
|
||||
local crcOp = u8(16) .. u32(crc16(body)) .. u32(BASE + 30) .. u32(BASE + total)
|
||||
local out = header .. crcOp .. body
|
||||
assert(#out == total and #out <= 0x7D4)
|
||||
return out
|
||||
end
|
||||
|
||||
local templates = Json.decode(io.open(templatesPath, "rb"):read("*a"))
|
||||
local out = {}
|
||||
for i, t in ipairs(templates) do
|
||||
local gift = giftOf(t)
|
||||
local row = { key = t.key, label = t.label, title = t.title, games = t.games, source = t.source,
|
||||
idNumber = t.idNumber, egg = t.egg, gift = gift, note = t.note }
|
||||
local rs = false
|
||||
for _, g in ipairs(t.games) do if g == "ruby" or g == "sapphire" then rs = true end end
|
||||
if rs then row.payload = Base64.encode(payload(partyMon(t, gift, 0x47334500 + i))) end
|
||||
out[#out + 1] = row
|
||||
end
|
||||
io.write(Json.encode(out), "\n")
|
||||
@@ -0,0 +1,97 @@
|
||||
#!/usr/bin/env python3
|
||||
import base64
|
||||
import json
|
||||
import os
|
||||
import shutil
|
||||
import struct
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
RUBY = os.path.abspath(os.environ.get("POKERUBY", os.path.join(HERE, "..", "..", "pokeruby")))
|
||||
SOURCE = "data/debug_mystery_event_scripts.s"
|
||||
BASE = 0x02000000
|
||||
BOTH_VERSIONS = 0x80 | 0x100
|
||||
|
||||
EVENTS = [
|
||||
("rs_eon_ticket", "EON TICKET", "gUnknown_Debug_845DAE1", "gUnknown_Debug_845DAE1End"),
|
||||
("rs_gift_ribbon", "GIFT RIBBON", "gUnknown_Debug_845E3E0", "gUnknown_Debug_845E3E0End"),
|
||||
]
|
||||
|
||||
|
||||
def tool(name):
|
||||
for prefix in ("arm-none-eabi-",):
|
||||
path = shutil.which(prefix + name)
|
||||
if path:
|
||||
return path
|
||||
sys.exit("missing arm-none-eabi-" + name)
|
||||
|
||||
|
||||
def crc16(data):
|
||||
crc = 0x1121
|
||||
for b in data:
|
||||
crc ^= b
|
||||
for _ in range(8):
|
||||
crc = (crc >> 1) ^ 0x8408 if crc & 1 else crc >> 1
|
||||
return ~crc & 0xFFFF
|
||||
|
||||
|
||||
def assemble(work):
|
||||
preproc = os.path.join(RUBY, "tools", "preproc", "preproc")
|
||||
charmap = os.path.join(RUBY, "charmap.txt")
|
||||
src = os.path.join(RUBY, SOURCE)
|
||||
obj = os.path.join(work, "events.o")
|
||||
flags = ["-mcpu=arm7tdmi", "-I", "include", "--defsym", "RUBY=1", "--defsym", "REVISION=0",
|
||||
"--defsym", "DEBUG_FIX=0", "--defsym", "ENGLISH=1", "--defsym", "DEBUG=1",
|
||||
"--defsym", "MODERN=0"]
|
||||
a = subprocess.run([preproc, src, charmap], cwd=RUBY, check=True, capture_output=True).stdout
|
||||
b = subprocess.run([tool("cpp"), "-I", "include"], cwd=RUBY, input=a, check=True,
|
||||
capture_output=True).stdout
|
||||
c = subprocess.run([preproc, "-ie", src, charmap], cwd=RUBY, input=b, check=True,
|
||||
capture_output=True).stdout
|
||||
subprocess.run([tool("as")] + flags + ["-o", obj], cwd=RUBY, input=c, check=True)
|
||||
elf = os.path.join(work, "events.elf")
|
||||
subprocess.run([tool("ld"), "--section-start", ".rodata=0x%08X" % BASE, "-e", "0", "-o", elf, obj],
|
||||
check=True)
|
||||
binary = os.path.join(work, "events.bin")
|
||||
subprocess.run([tool("objcopy"), "-O", "binary", "-j", ".rodata", elf, binary], check=True)
|
||||
syms = {}
|
||||
out = subprocess.run([tool("nm"), elf], check=True, capture_output=True, text=True).stdout
|
||||
for line in out.splitlines():
|
||||
parts = line.split()
|
||||
if len(parts) == 3:
|
||||
syms[parts[2]] = int(parts[0], 16)
|
||||
with open(binary, "rb") as f:
|
||||
return f.read(), syms
|
||||
|
||||
|
||||
def fix_crc(block, start):
|
||||
if block[0] != 1 or block[0x11] != 16 or struct.unpack_from("<I", block, 0x12)[0] != 0:
|
||||
return
|
||||
first = struct.unpack_from("<I", block, 0x16)[0] - start
|
||||
last = struct.unpack_from("<I", block, 0x1A)[0] - start
|
||||
struct.pack_into("<I", block, 0x12, crc16(block[first:last]))
|
||||
|
||||
|
||||
def main():
|
||||
work = tempfile.mkdtemp()
|
||||
try:
|
||||
image, syms = assemble(work)
|
||||
finally:
|
||||
shutil.rmtree(work, ignore_errors=True)
|
||||
out = []
|
||||
for key, title, first, last in EVENTS:
|
||||
start, end = syms[first], syms[last]
|
||||
block = bytearray(image[start - BASE:end - BASE])
|
||||
assert struct.unpack_from("<I", block, 1)[0] == start
|
||||
struct.pack_into("<I", block, 13, struct.unpack_from("<I", block, 13)[0] | BOTH_VERSIONS)
|
||||
fix_crc(block, start)
|
||||
assert 0 < len(block) <= 0x7D4
|
||||
out.append({"key": key, "title": title, "payload": base64.b64encode(bytes(block)).decode()})
|
||||
json.dump(out, sys.stdout, indent=2)
|
||||
sys.stdout.write("\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user