Repository navigation
Conversation
The portal gun was disappearing after spawn because of a false positive with the Bouncer. It would disappear with the log message "Bouncer / OnNewProjectile rejected from portal gate from <player> (missing active Portal Gun bolt)." With this fix, portals appear again.
### Cause
### Cause
`HandleProjectileNew` handles the projectile index before vanilla creates the projectile, so for a
brand-new projectile `SearchProjectile(ident, owner, generation)` returns `Main.maxProjectiles` (1000). That index is what gets stored in
`RecentlyCreatedProjectiles`, which causes two problems:
1. The gate check finds the bolt with `Main.projectile[p.Index].type == ProjectileID.PortalGunBolt`.
For a bolt recorded at index 1000 this is never true.
2. Entries are deduplicated by index, so only one unresolved projectile is recorded per player until
the 5-second cleanup. The held Portal Gun projectile is created just before the bolt, so it takes
that slot and the bolt isn't recorded at all.
Instead, we compare RecentlyCreatedProjectiles by value and avoid deduplicating projectiles without an id yet set.
Tested and confirmed that true negatives and true positives both still work with the Bouncer.
|
|
|
||
| // Validate we found an active bolt projectile | ||
| var boltProjectileData = args.Player.RecentlyCreatedProjectiles.FirstOrDefault(p => Main.projectile[p.Index].type == ProjectileID.PortalGunBolt); | ||
| var boltProjectileData = args.Player.RecentlyCreatedProjectiles.FirstOrDefault(p => p.Type == ProjectileID.PortalGunBolt); |
There was a problem hiding this comment.
One bolt allows repeated portals
OnNewProjectile now accepts a stored PortalGunBolt record even after the bolt is gone. A modified client can fire one bolt, then send repeated PortalGunGate packets during the five-second record window. ProjectileStruct is a value type, so boltProjectileData.Killed = true changes only the local copy. The stored record remains usable.
The old lookup tied acceptance to the projectile slot's current type. Preserve the unresolved-projectile fix, but consume the stored bolt record when accepting a gate and track its death.
How this was verified: The gate check reads the stored type, its Killed assignment changes a struct copy, and accepted gate packets continue without another live-bolt check.
There was a problem hiding this comment.
Is this a regression from the pre-PR code?
There was a problem hiding this comment.
Yes—the repeated-gate issue is a regression relative to the pre-PR code, not the original portal-gun disappearance.
Before this PR, the lookup also required Main.projectile[p.Index].type to still be PortalGunBolt, so a killed or reused bolt slot stopped matching. With the new p.Type lookup, the five-second record remains a match after the projectile is gone. Since ProjectileStruct is a value type, boltProjectileData.Killed = true only changes the local copy, allowing repeated gate packets until the record expires.
The unresolved-index fix is still needed, but the stored bolt record must also be consumed/removed—or its persisted Killed state updated—when the first gate is accepted.
Tip: You can customize Greptile's behavior for this repo with .greptile/rules.md and .greptile/config.json.
There was a problem hiding this comment.
Fixed: Killed = true is now passed into RecentlyCreatedProjectiles. Manually tested:
┌───────────────────────────────────────────────────────────┬──────────────────────────────┐
│ Sent │ Result │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ portal, no bolt │ rejected │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ bolt, portal, same portal resent │ all accepted │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ another new portal, bolt already used (the review's case) │ rejected │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ two bolts, three portals │ two accepted, third rejected │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ held gun, bolt, portal │ all accepted │
├───────────────────────────────────────────────────────────┼──────────────────────────────┤
│ bolt, portal with a bad angle │ portal rejected │
└───────────────────────────────────────────────────────────┴──────────────────────────────┘
Remove the portal bolt from the usable list of RecentlyCreatedProjectiles to consume it
The portal gun was disappearing after spawn because of a false positive with the Bouncer. It would disappear with the log message "Bouncer / OnNewProjectile rejected from portal gate from (missing active Portal Gun bolt)." With this fix, portals appear again.
2026-10-10.17-36-47.mp4
Cause
HandleProjectileNewhandles the projectile index before vanilla creates the projectile, so for a brand-new projectileSearchProjectile(ident, owner, generation)returnsMain.maxProjectiles(1000). That index is what gets stored inRecentlyCreatedProjectiles, which causes two problems:Main.projectile[p.Index].type == ProjectileID.PortalGunBolt. For a bolt recorded at index 1000 this is never true.Instead, we compare RecentlyCreatedProjectiles by value and avoid deduplicating projectiles without an id yet set.
Tested and confirmed that true negatives and true positives both still work with the Bouncer.
Changelog
@ZakFahey: fixed portal gun portals disappearing