![[Atari Quantum - Ghidra.png.png]]
# Overview
Someone was curious how Quantum detects objects within the user's drawn loop, which can be an arbitrary shape. Given how old this hardware is, we were curious what tricks may have been performed.
I spent time reverse-engineering this game using [[Ghidra]].
Here's how this works.
## Loop Management
As you move your player around, drawing a loop behind you, you game tracks your loop as:
1. Segments of loop (I'll call them "Loop Segments").
These are recorded every N game ticks, and cover a somewhat wide section of loop. As you gain score, these segments increase in length (are split off less often), giving you a longer loop.
2. Granular entries of a segment of loop (I'll call them "Segment Entries").
These help break a Loop Segment into smaller chunks that can be used to more carefully manage the loop. There are up to 50 of these at any given point in time.,
Each Loop Segment and Entry has a bounding box. The game tracks these as they're drawn, and normalizes them for comparison on occasion.
![[Atari Quantum - Bounding Boxes.png.png]]
The game also has three entity types:
1. Large particles
2. Small particles
3. Triangle things
Every event handling stage of the game loop, the game checks for collision by comparing your player X, Y to all Large Particles, Triangle Things, and Loop Segments. If it hits a Large Particle or Triangle Thing, you lose a life. If it hits a Loop Segment, the logic for the "what's in the loop?" begins.
## What's in the Loop?
The algorithm is as follows:
1. Once the loop is closed, figure out which segment the player hit.
2. Store that segment and the latest loop segment being drawn.
3. For each segment in-between (inclusive):
1. Generate an outer bounding box. This is a rough "Could I possibly have captured particles?" check.
2. Loop through all Large, Small, and Triangle particles on screen and check if their bounding boxes overlap this outer bounding box.
3. If any hit:
1. Track those as candidate particles.
2. Build an inner bounding box for the loop, covering much of the center (but not the entire area within the loop).
3. Check all particles to see if there's a bounding box hit.
4. For any that hit:
1. Add to a counter and skip slower collision checks for this particle.
2. Mark as a match.
5. If all candidate particles were found:
1. Collision is done.
6. Else:
1. Check each segment's bounding box against each remaining particle and mark any that were hit as a match.
2. Store any that were not explicitly matched.
4. For all large particles in the outer bounding box:
1. If large particle was matched above:
1. Add to the score and flag the large particle as captured.
5. For all small particles in the outer bounding box:
1. If the parent large particle was flagged as captured:
1. Flag small particle as captured (but do not add to score)
2. Else if matched above:
1. Add to score
6. For all triangles in the outer bounding box:
1. If the triangle was matched above:
1. Flag as captured (but do not add to score)
# Memory Addresses
| Address | Used In | Thoughts |
| ------- | ------------------------------------------------------------------- | --------------------------------------------------------------- |
| 0x1b886 | `statistics_screen` | Offset to stats? |
| 0x1b910 | `maybe_check_player_button_press` | Seems to be the dest result for hte function |
| 0x1b912 | `maybe_check_player_button_press` | Result for read when param1 is 1. |
| 0x1b90c | `maybe_check_player_button_press`<br>`wait_for_player_button_press` | Maybe raw read value for button? |
| 0x1c09a | | Somewhere around here might be path or capture info |
| 0x1b5ad | | Cursor X center position |
| 0x1b5e8 | | Cursor Y center position |
| 0x1b6ac | | Maybe loop trail state |
| 0x1b50c | | Some kind of countdown timer state. Maybe for loop tail removal |
| 0x1b6a0 | | Loop length. Up to 0x31 in my test |
# Notes
## maybe_wait_for_player_button_press
Maybe doesn't wait? Maybe sets states?
## maybe_check_player_button_press
Might be button state for any button.
Maybe loop-related:
* FUN_0000daa8
## calc_outer_loop_bounding_box_excluding_active_segment
I found a bug, I think:
```
if (maxX1 < *(short *)&nextSegment->maxExtent) {
maxX1 = *(short *)&nextSegment->maxExtent;
}
if (*(short *)&curSegment[1].minExtent < minX2) {
minX2 = *(short *)&curSegment[1].minExtent;
}
if (maxY1 < (short)curSegment[1].maxExtent) {
maxY1 = (short)curSegment[1].maxExtent;
}
if ((short)curSegment[1].minExtent < minY2) {
minY2 = (short)curSegment[1].minExtent;
}
```
Note how they all check `curSegment` except the first one.