Skip to content

fix!(lua.endpoints): buy/pack hang when Celestial pack contains Black Hole #199

Description

@S1M0N38

Description

Buying or selecting from a Celestial booster pack causes the endpoint to hang indefinitely when the pack contains Black Hole (c_black_hole, a Spectral card) as its first card. The response is never sent and the request times out on the client side.

Both buy (opening the pack) and pack (selecting a card from an already-opened pack) are affected.

Root Cause

Both src/lua/endpoints/buy.lua (~line 260) and src/lua/endpoints/pack.lua (~line 305) infer whether hand cards need to be dealt by checking the first card's ability.set:

local pack_key = G.pack_cards.cards[1].ability and G.pack_cards.cards[1].ability.set
local needs_hand = pack_key == "Tarot" or pack_key == "Spectral"
  • Black Hole has ability.set = "Spectral" (it is a Spectral-type consumable)
  • But Black Hole can appear in Celestial packs via the soul mechanism: when create_card("Planet", ...) is called for a Celestial pack, there is a ~0.3% chance per card (pseudorandom > 0.997) that it becomes Black Hole
  • Celestial packs do NOT have draw_hand = true in SMODS's booster config (only Arcana and Spectral packs do)
  • So when Black Hole is the first card, the code sets needs_hand = true and waits for G.hand.cards to be populated
  • Hand cards are never dealt → the condition event spins forever → endpoint hangs

Steps to Reproduce

Seed S001250 naturally produces Black Hole in a Celestial pack via the soul mechanism after cycling 12 Celestial packs in the first shop.

  1. Start a run: start({"deck": "b_red", "stake": "stake_white", "seed": "S001250"})
  2. Select a blind: select()
  3. Set chips and money: set({"chips": 100000, "money": 999})
  4. Play a hand: play({"cards": [0,1,2,3,4]})
  5. Cash out: cash_out()
  6. Free a booster slot: buy({"pack": 0})pack({"skip": true})
  7. Cycle 12 Celestial packs: add({"key": "p_celestial_normal_1"})buy({"pack": 1})pack({"skip": true}) × 12
  8. Add the 13th Celestial pack: add({"key": "p_celestial_normal_1"})
  9. Buy it: buy({"pack": 1})
  10. The request hangs indefinitely — Black Hole (set=Spectral) is the first card

For the pack endpoint: load the attached fixture to reach SMODS_BOOSTER_OPENED state with Black Hole at index 0, then call pack({"card": 0}) — this also hangs.

Expected Behavior

The endpoints should return the gamestate response after the pack opens / selection completes, regardless of whether the first card is a Spectral type. Celestial packs never deal hand cards, so the endpoints should not wait for them.

Actual Behavior

The request never returns. The log shows Buying Booster 'Celestial Pack' for $4 but no corresponding OK or ERR response. The event loop spins on the hand_ready condition that never becomes true.

Why This Bug Is Difficult to Reproduce

  • Black Hole only appears in Celestial packs through the soul mechanism: a ~0.3% chance per card when creating Planet cards
  • The bug only triggers when Black Hole is the first card in the pack, because the code only checks G.pack_cards.cards[1]
  • Combined probability: roughly 0.05% per shop — automated bots hit it eventually, humans almost never
  • Found via parallel search across 8 headless instances testing 2000 seeds

Environment

  • OS: macOS
  • Lovely version: 0.8.0
  • SMODS version: v1.0.0~BETA-1221a
  • BalatroBot commit: 7a33403

Files

Metadata

Metadata

Assignees

Labels

completed-in-devThis issue have been solved in dev branch

Type

No fields configured for Bug.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions