Answer
Gate all external writes to the server‑only path and correlate the predicted client action with the final server‑authoritative resolution using a unique idempotency key (e.g., the Ability’s PredictionKey or a custom GUID) that is echoed back by the server.
Confirmed facts
- In GAS the server has authority over state changes; clients use PredictionKeys for local prediction and rollback during reconciliation.
- Any code that performs an external write (database call, web service, etc.) must be executed only where
HasAuthority() returns true to avoid client‑side duplicates.
- The PredictionKey is replicated from client to server with the ability activation and is available on the server after reconciliation.
Likely explanation
If external writes are triggered from both the predicted client path and the reconciled server path, the same logical action can be committed twice. By ensuring the write occurs only after the server has reconciled the state and by checking that the idempotency key has not been processed before, duplicate commits are avoided.
Steps for this case
- When the client activates the ability, generate or copy an IdempotencyKey (e.g., the ability’s PredictionKey or a UUID) and include it in the ability’s activation payload (custom struct passed to
ServerActivateAbility or replicated via a replicated variable).
- On the client, you may optimistically store the key locally for UI feedback, but do not perform the external write here.
- On the server, in the ability’s server‑only logic (guarded by
if (HasAuthority()) { … }):
- Receive the IdempotencyKey from the activation payload.
- Check a server‑side set or external table (with a unique constraint) to see if this key has already been processed.
- If not processed, perform the external write and then record the key as processed (insert into the set/table).
- If already processed, skip the write – this is the idempotency guard.
- Optionally replicate the processed key back to all clients (via a replicated variable or a multicast RPC) so clients can clear their predictive state and avoid stale UI.
Missing diagnostic detail
To confirm that the above steps are sufficient, we need to know where the external write is invoked: is it called directly from the Ability’s activation logic, or from a GameplayEffect’s execution callback? If the write occurs inside a GameplayEffect, the same authority check and idempotency key must be moved into the effect’s execution policy (e.g., a custom GameplayEffectExecutionPolicy that runs only on authority). Please confirm the invocation point so we can tailor the implementation.