Modding Poker Night at the Inventory to Show My Hand Strength (Because I'm Bad at Poker)
Or: how reading raw Lua heap memory at 3am is a normal Saturday.
The Setup
I am, unabashedly, a massive Team Fortress 2 fan. So when I found out there was a game where the Heavy sits down at a poker table with Max, Strong Bad, and Tycho (who I was convinced was the Loss comic guy), I needed to play it. Problem was, Poker Night at the Inventory came out in 2010 and by the time I discovered it the game had already been delisted from Steam for licensing reasons. Just gone. It also handed out exclusive TF2 cosmetics for beating certain characters, items that became completely unobtainable the moment the game disappeared. A piece of TF2 history I never got to touch.
Then the remake dropped. Poker Night at the Inventory came back, cosmetics unlockable again, accessible for the first time in years. I bought it the same day.
Here's the thing though: I am not a great poker player. I know the rules. I can follow along. I've put a genuinely embarrassing number of hours into Balatro, and one of the things Balatro does brilliantly is that it tells you your current best hand at all times. Big bold text right there on screen: "Full House". You always know where you stand.
Poker Night at the Inventory? Nothing. You're on your own. At best it has a static help screen telling you hands but not your current hand. I'd be sitting there with five cards on the table and two in my hand, mentally going through the checklist: do I have a flush? Wait, what suit is that 9? Is that a straight? Hold on and by the time I'd figured it out, it was already my turn and Heavy was staring at me.
The final straw was when I missed a straight flush. I folded a hand thinking I had "two pair, whatever", and only realized what I'd had when the cards flipped. A straight flush. In the wild. The rarest thing I'd ever seen in a real poker game. Gone, because my brain can't hold seven cards and poker rules simultaneously.
So I decided Poker Night needed a hand display overlay. How hard could it be?
(Very. Very hard. But we got there.)
Act I: The Manual Scanner, At Least We Know It's Possible
The first question with any game memory mod is always: is the data even in memory in a readable form? Before automating anything, I needed to confirm I could find the card data at all.
The approach: write a small C# tool that reads the game's process memory, then manually input my hole cards and have it search for their values. If the tool could find addresses that corresponded to cards I told it I had, we'd know the data was there and readable.
scanner.exe
> Enter your cards (e.g. "Ah 7c"): As 10d
[SCAN] Searching for Ace of Spades (rank=14, suit=1)...
[SCAN] Found 3 candidate addresses: 0x1EFA1422B40, 0x1EFA2811050, ...
[SCAN] Cross-referencing with 10 of Diamonds (rank=10, suit=3)...
[SCAN] Likely card pair at: 0x1EFA1422B40
It worked. We could find the cards. The data was there, living in the Lua heap.
But this approach was already broken for a mod's purposes. Nobody is going to run a scanner and manually type their own hole cards before every hand. It completely defeats the purpose. If you already know what cards you have, you don't need a tool to tell you. We needed the scanner to figure it out on its own.
Time to go deeper.
Act II: Archaeology in a Heap, Pattern Hunting
The game runs on a Lua 5.1 VM (Telltale's custom 64-bit build). Every game object (cards, players, hands, the whole poker table) is a Lua table living somewhere in the heap. The challenge: find the right ones among gigabytes of process memory.
The first serious approach was memory dumps. We'd snapshot the game's memory at different points in time, then diff the dumps to find what changed:
# Dump 1: before a hand starts
# Dump 2: after hole cards are dealt
# Diff: what moved? what appeared?
What we found was noise. Lua's garbage collector is constantly moving things around. Between two dumps, hundreds of addresses had changed for reasons having nothing to do with cards. The signal-to-noise ratio was brutal.
We then tried pattern matching on Lua's internal TValue structure, the fundamental value container in Lua 5.1. In this Telltale build, numbers are stored as float32 (not the standard double), so a card's rank of 14 (Ace) would look like this in memory:
00 00 60 41 [float32 = 14.0]
00 00 00 00 [padding]
03 00 00 00 [tt = LUA_TNUMBER]
00 00 00 00 [padding]
So we'd scan for the float byte pattern of rank 14 immediately followed by a valid type tag. We'd find the Ace... and also 47 other things that happened to contain the float 14.0 in memory for completely unrelated reasons. A lighting value. A texture coordinate. A timer. The garbage collector's internal bookkeeping. A cosmic ray flipping a bit on the heap setting the value as 14. Everything.
We also discovered, painfully, that the Lua Table struct offsets we'd been using were wrong. We'd assumed sizearray was at offset 12 and node* was at offset 24. The actual layout in this 64-bit Telltale build is:
[GCnext:8][tt:1][marked:1][flags:1][lsizenode:1][telltale_field:4]
[metatable*:8][array*:8][node*:8][lastfree*:8][gclist*:8][sizearray:4][pad:4]
That means node* is at offset 32, not 24. We were reading array* when we thought we were reading node*. For any hash-only table (like a player object, where array* = 0x0), this was returning null and making the entire navigation chain fail silently. A raw hex dump eventually exposed the truth:
F0 97 46 A1 EF 01 00 00 ← GCnext*
05 04 00 03 ← tt=5(Table), marked, flags, lsizenode=3
EF 01 00 00 ← telltale field
48 52 2D A1 EF 01 00 00 ← metatable* (offset 16) ✓
00 00 00 00 00 00 00 00 ← array* = NULL (offset 24), hash-only table
80 29 53 A1 EF 01 00 00 ← node* (offset 32) ✓ VALID POINTER
Once we fixed the offsets and defined proper named constants for everything (TBL_NODE, NODE_VAL_TT, TV_VALUE, and so on) the reads started making sense.
Act III: The Telltale Explorer, A lucky chip
We'd been guessing at the game's data structures from the outside. Then we found Telltale Explorer, a tool that can extract .ttarch archive files, the packed format Telltale uses to bundle game assets.
One small funny detail: the creator updated the tool the day before we found it to support the remastered version of Poker Night at the Inventory. Serendipity at its finest.
We extracted the game's Lua bytecode and ran it through a decompiler. Out came the actual source code of the game's card system, not perfect (the decompiler left some DECOMPILER ERROR comments where it got confused), but readable enough to understand exactly what we were looking for.
Card.lua:
Card.Initialize = function(self, rank, suit)
self[1] = rank -- stored in Lua array part, index 1
self[2] = suit -- stored in Lua array part, index 2
end
Hand.lua:
Hand.Initialize = function(self)
self.holeCards = CardContainer() -- only the player's 2 dealt cards
self.cards = CardContainer() -- all cards (hole + community)
self.handType = 0
end
Hand.AddHoleCard = function(self, card)
self.cards:Add(card)
self.holeCards:Add(card)
end
Hand.AddCommunityCard = function(self, card)
self.cards:Add(card) -- note: NOT added to holeCards
end
This was the map we'd been missing. Now we knew exactly what to look for.
Act IV: The Game's Architecture
With the decompiled source in hand, here's how the game structures cards and hands:

A few important details that tripped us up:
-
Cards are stored by Lua array index, not string keys.
Card[1]is rank,Card[2]is suit, living in the Lua table's array part, not its hash. This matters because navigating to a card means reading the array part, not scanning hash nodes. -
Hand.SortHand()reordersself.cardsby rank descending every timeEvaluate()is called. So you can never assume the first two entries incards.cardListare the hole cards, after evaluation they might be the two highest-ranked cards in the full hand. We learned this the hard way when community cards started coming back as hole cards. -
holeCardsis the source of truth for hole cards. It's a separate container that never gets sorted. Always use it.
Act V: The Navigation Chain, From Anchor to Cards
Armed with the architecture, we built a navigation chain through the Lua heap:
playerType = 'human' ←── TString scan finds this in ~15s
│
▼
[HumanPlayer hash node] ←── anchor: cached for the session
│
├── cHand ──────────► Hand table (changes each new hand)
│ │
│ ├── holeCards ──► CardContainer
│ │ │
│ │ └── cardList ──► [Card, Card]
│ │ │
│ │ rank, suit (float32)
│ │
│ └── cards ────► CardContainer
│ │
│ └── cardList ──► [Card, Card, Card...]
│ (sorted by rank, subtract holeCards to get community)
│
└── name = 'Player' ←── fallback anchor
The key insight was working backwards from the player, not forward from the cards. Every search-by-card approach drowned in false positives. Float values like 14.0 appear everywhere in game memory. But the string "playerType" with value "human" is unique. Find that single hash node, and you're one pointer dereference away from the current hand.
The TString scan (scanning all committed memory pages for Lua's interned string objects) runs once at startup and takes about 15-20 seconds. After that, every poll cycle is just a handful of targeted memory reads:
[poll ~3s]
ReadMemory(anchorAddr) → validate anchor still live
ReadMemory(anchorAddr ± δ) → find cHand node (±800 byte stride)
ReadMemory(cHandPtr) → Hand table header
ReadMemory(Hand.node*) → hash nodes → holeCardsPtr
ReadMemory(holeCardsPtr.node*) → hash nodes → cardListPtr
ReadMemory(cardListPtr.array*) → 2 × TValue → Card pointers
ReadMemory(Card[0].array*) → rank float, suit float
ReadMemory(Card[1].array*) → rank float, suit float
──────────────────────────────────────────────────────────
Total: ~1ms per poll
One final gotcha: the anchor can get cached from a stale player object during the loading screen, before the in-game structures are fully built. The fix was a 30-second consecutive-failure auto-retry, plus a manual ↺ Retry button on the overlay.
The Result
A single HandTeller.exe, no external libraries, no dependencies. Just a borderless overlay that sits on top of the game and tells you what hand you're holding:
(Work in progress)
The hand evaluator is pure C#, tries every C(n,5) combination from your 2-7 cards, scores each one, returns the best hand name. The whole mod ships as one .exe you drop anywhere and run.
Was It Worth It?
An entire weekend. Lua heap archaeology. Wrong struct offsets. A garbage collector that moves everything constantly. A tool update. Decompiled bytecode. A hand evaluator written from scratch.
All to avoid memorizing what cards make a flush.
...Honestly? Yes. But maybe, maybe, I should have just printed out a poker hand rankings chart.
The mod is open source at github.com/qequ/poker-night-hand-teller. Telltale Explorer: quickandeasysoftware.net