Skip to content

Fix portal gun portals disappearing - #3320

Open
ZakFahey wants to merge 2 commits into
general-develfrom
portal-fix
Open

ZakFahey wants to merge 2 commits into
general-develfrom
portal-fix

Conversation

@ZakFahey

Copy link
Copy Markdown
Contributor

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

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.

Changelog

@ZakFahey: fixed portal gun portals disappearing

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.
@greptile-apps

greptile-apps Bot commented Oct 11, 2026 •

Copy link
Copy Markdown
Contributor

RetriggerConfidence Score: 3/5

[Medium impact] Fixes portal gun projectile tracking logic.

Consume the stored bolt record before merging so one bolt cannot authorize repeated portals.

Findings

  1. P1 Security One bolt allows repeated portals ▶

Summary

Fixes disappearing portals by checking the recorded projectile type and keeping separate records for newly created projectiles whose server slot is not yet known.

  • Portal gates can find their bolt before it gets a slot.

Reviews (1) · Last reviewed commit: "Fix portal gun portals disappearing" · Reviewed by Greptile

Comment thread TShockAPI/Bouncer.cs Outdated

// 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);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 security 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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this a regression from the pre-PR code?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant