Skip to content

Sync persistent projectiles (satchels, teargas, molotov) on stream-in#4986

Open
TheCrazy17 wants to merge 6 commits into
multitheftauto:masterfrom
TheCrazy17:fix/projectile-streamin-sync
Open

Sync persistent projectiles (satchels, teargas, molotov) on stream-in#4986
TheCrazy17 wants to merge 6 commits into
multitheftauto:masterfrom
TheCrazy17:fix/projectile-streamin-sync

Conversation

@TheCrazy17

@TheCrazy17 TheCrazy17 commented Jun 27, 2026

Copy link
Copy Markdown
Contributor

Summary

Sync persistent projectiles (satchels, teargas, molotov) to players who stream in or join after the projectile was created.

Previously, if a satchel charge was thrown while a player was out of range (or not yet connected), no projectile was ever sent to them; it simply didn't exist on their client. This PR introduces three mechanisms to fix that:

  1. Stream-in resync (ProcessProjectileStreamIn): The server tracks a list of persistent projectiles per-player and periodically sends their creation packet to any nearby player who hasn't been notified yet.
  2. Rest-position packet (PACKET_ID_PROJECTILE_REST_POSITION): Once a satchel settles, the owning client reports its actual resting position and attach offset to the server. Future stream-ins place the satchel directly at that spot instead of replaying the original throw and re-simulating physics (which is non-deterministic).
  3. Pending creation queue (client-side): If a projectile packet arrives before the creator's game entity has streamed in, the creation is queued and retried for up to 60 seconds instead of being silently dropped.

Motivation

Persistent projectiles (satchels in particular) have never been synced to late-joining or streaming-in players. A satchel planted by a player was invisible to anyone not already in range when it was thrown — detonating it would produce an explosion out of thin air on those clients.

Two edge cases drove most of the design:

  • Attached satchels: if the satchel stuck to a moving vehicle, replaying the original throw would leave it at the wrong world position. The rest-position packet carries GTA's own native attach offset so the resync re-attaches it at the exact same point on the entity.
  • Collision not loaded: when a satchel is placed via resync (zero force/velocity), local collision geometry may not be streamed in yet, so SetStaticWaitingForCollision prevents it from falling through the world.

Fixes #369 / #368 (duplicated).

Test plan

Satchel visible to late-joining player

  1. Player A throws a satchel and lets it settle.
  2. Player B connects / streams into range after it has settled.
  3. ✅ The satchel appears on Player B's screen at the correct resting position (no throw animation replayed).
  4. Player A detonates ✅ explosion is at the same spot on both clients.

Satchel attached to a moving vehicle

  1. Player A throws a satchel that sticks to a vehicle. The vehicle moves away.
  2. Player B streams in.
  3. ✅ The satchel appears attached to the vehicle at the correct offset (e.g. the hood), not at the vehicle's origin and not left at the original world position.

Teargas / Molotov visible to streaming-in player

  1. Player A throws a teargas grenade or molotov.
  2. Player B streams into range while the effect is still active.
  3. ✅ The effect is visible on Player B's screen and disappears after its normal lifetime (~20 s).

Creator not yet streamed in

  1. Player A (out of view) throws a satchel, then walks toward Player B.
  2. ✅ As Player A's entity streams in, the satchel is created correctly and not silently dropped.

Checklist

  • Your code should follow the coding guidelines.
  • Smaller pull requests are easier to review. If your pull request is beefy, your pull request should be reviewable commit-by-commit.

…ho come into range later

Packet_ProjectileSync only broadcasts a projectile's creation once, to whoever
is in range at that instant. Satchel charges, teargas clouds and molotov fires
stick around in the world afterwards, so a player who was out of range when
one appeared never sees it, even after walking right up to it.

Server now remembers these per-owner, and periodically checks if any other
player has come within sync range, sending them the original creation packet
when they do. Satchels are tracked until detonated/destroyed explicitly;
teargas/molotov entries expire after 20s, comfortably outlasting their visual
effect.

Fixes multitheftauto#369
Fixes multitheftauto#368
…ed in yet

Packet_ProjectileSync's client handler silently drops the projectile if
CClientProjectileManager::Create() can't get a game-layer entity for the
creator (pPed->GetGameEntity() null), which happens whenever the thrower's
ped/vehicle hasn't streamed into the GTA world yet for the receiving client -
e.g. right after connecting, or right after coming into range of a satchel/
teargas/molotov that was planted earlier while out of view (see the
ProcessProjectileStreamIn resync added previously for issues multitheftauto#369/multitheftauto#368).

Queue the creation and keep retrying every pulse for up to a minute instead,
giving the engine time to stream the creator in.
…playing the throw

Late stream-in (just connected, or just came into range of a satchel planted
earlier) resent the original throw packet - origin + velocity - which replays
the whole toss client-side. Since physics replay isn't perfectly
deterministic, the satchel can visibly fly through the air again and
occasionally settle somewhere slightly different (floating, or in a
different spot) than the one everyone else has been looking at.

The owning client now reports the actual resting position once
CClientProjectile::CorrectPhysics finishes settling the satchel (new
PACKET_ID_PROJECTILE_REST_POSITION packet). The server swaps the tracked
persistent-projectile entry over to that position with zero velocity/force,
so any later stream-in places it directly instead of re-throwing it.

Scoped to satchel charges only - they're the only persistent projectile type
with no lifespan of its own, so a wrong replay stays wrong indefinitely
instead of being a momentary blip like teargas/molotov.

multitheftauto#369
multitheftauto#368
…anded on

Placing a satchel at its resting position via the new rest-position resync
still let normal physics run on it client-side, which caused two more visible
issues once it had to materialize at an arbitrary point in the world instead
of being thrown into it:

- The area's collision may not be streamed in yet when a player walks up to
  a satchel that's been sitting there for a while, so it fell through the
  floor under gravity until something already-loaded caught it.
- A satchel stuck to a vehicle/ped only got a one-off absolute position, so
  it stayed behind, detached, the moment that vehicle/ped moved.

The owning client now also reports what the satchel stuck to (if anything),
via CProjectile::GetAttachedEntity()/GetAttachedOffsets() - the same native
attach GTA already uses for satchels. The server stores that as a position
relative to the attached entity (reusing the m_OriginID/m_vecOrigin
mechanism CProjectileSyncPacket already has for heat-seeking rockets).
On creation, a resynced satchel (recognised by zero velocity/force, which a
live throw never has) is pinned with SetStaticWaitingForCollision() - the
same engine flag used for script-placed objects whose collision isn't loaded
yet - and natively re-attached via AttachEntityToEntity() if it was stuck to
something, so it keeps following it afterwards exactly like the original.

multitheftauto#369
multitheftauto#368
…e entity

Re-attaching a resynced satchel with a zero offset always snapped it to the
attached vehicle/ped's origin, so a satchel stuck to e.g. the hood instead
showed up at the vehicle's centre for anyone who streamed in late.

The owning client now also reports GTA's own native attach offset
(CProjectile::GetAttachedOffsets() - the same position/rotation pair
AttachEntityToEntity expects) when the satchel settles, and the resync
carries it through end to end:
- CProjectileRestPositionPacket (client -> server) gains the offset fields.
- CGame::Packet_ProjectileRestPosition stores them on the tracked entry.
- CProjectileSyncPacket (server -> client) gains a satchel-only optional
  attach-offset block, sent only when resending a settled satchel - live
  throws never have one yet, so their wire format is untouched.
- The receiving client's manual bitstream read (mirroring the above) and
  CClientGame::SendProjectileSync's own write both had to move the satchel
  case out of the shared GRENADE/TEARGAS/MOLOTOV switch arm to add it.

multitheftauto#369
multitheftauto#368
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Satchel charges don't show if you don't see them get placed

1 participant