fix(contracts): persist raid-zone objective progress (client-hosted raids)
Build & Deploy / build (push) Successful in 36s

Symptom: missions done in raid zones weren't saved -> contracts stuck on
'Objective not met', no claim possible.

Root cause: client-hosted raids have no dedicated game server, so
UpdatePlayerActiveContracts is never called (confirmed: 0 calls in prod logs).
Only Kills and OwnNumOfItem objectives were handled; DeadDrop/VisitArea/
LootContainer had Progress[i]=0 forever -> unclaimable and not shown at station.

Fix: EYContractObjectiveType.IsRaidRuntime() groups the objectives that can only
be observed in a live raid (Kills/DeadDrop/VisitArea/LootContainer). Auto-credit
them on deploy (EnterMatchmakingMatch, persisted) so the station shows them
complete, and accept them in ClaimActiveContract. OwnNumOfItem still validated
against the real stash; FactionLevel/CompletedMission unchanged. Added a
defensive guard against a Progress array shorter than the objective list.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-07-15 01:54:25 +02:00
co-authored by Claude Opus 4.8
parent 71a2c8a1e9
commit dc34233e40
3 changed files with 34 additions and 13 deletions
@@ -72,6 +72,21 @@ public enum EYContractObjectiveType {
MAX = 8
}
public static class ContractObjectiveTypeExtensions {
// Objectives that can only be observed while a raid is running (kills, dead-drops,
// visiting an area, looting a container). The raid is hosted by the player's own
// client and there is no dedicated game server, so their progress is never reported
// to the backend (UpdatePlayerActiveContracts is never called). They are therefore
// auto-credited server-side, otherwise these contracts can never be completed.
// OwnNumOfItem is excluded (validated against the real stash) and so are the meta
// objectives FactionLevel / CompletedMission (validated from persisted player data).
public static bool IsRaidRuntime(this EYContractObjectiveType type) =>
type is EYContractObjectiveType.Kills
or EYContractObjectiveType.DeadDrop
or EYContractObjectiveType.VisitArea
or EYContractObjectiveType.LootContainer;
}
public enum EYContractDifficulty
{
Invalid = 0,
@@ -131,16 +131,17 @@ public class ClaimActiveContract : ICloudScriptFunction<FYClaimCompletedActiveCo
break;
}
}
} else if (objective.Type == EYContractObjectiveType.Kills) {
// Kill objectives can't be tracked in a client-hosted raid (no dedicated
// game server reports kills). They are auto-credited on deploy
// (EnterMatchmakingMatch); accept them here even if a contract was
// activated before that so kill contracts remain completable.
} else if (objective.Type.IsRaidRuntime()) {
// Raid-runtime objectives (kills, dead-drops, visited areas, looted
// containers) can't be tracked in a client-hosted raid — no dedicated
// game server reports them, so their progress is never saved. They are
// auto-credited on deploy (EnterMatchmakingMatch); accept them here too
// (safety net for a contract activated after deploy) so these contracts
// remain completable instead of being stuck on "Objective not met".
remaining = 0;
} else {
// NOTE: A game server must write the correct progress
// for other types of objectives.
// The objectives are mapped by array index.
// Meta objectives (FactionLevel / CompletedMission): validated from the
// persisted progress. The objectives are mapped by array index.
remaining -= targetContract.Progress[i];
}
if (remaining > 0) {
@@ -62,12 +62,17 @@ public class EnterMatchmakingMatchFunction : ICloudScriptFunction<FYEnterMatchAz
continue;
}
for (var i = 0; i < contract.Objectives.Length; i++) {
if (i >= contractActive.Progress.Length) {
// Defensive: player progress array shorter than the objective list.
break;
}
var objective = contract.Objectives[i];
// Kill objectives: the client-hosted raid has no dedicated game server
// to report per-kill progress to the backend, so kills would never be
// saved. Auto-credit them on deploy so the objective can be completed
// (mirrors the player-kill auto-credit done in ActivateContract).
if (objective.Type == EYContractObjectiveType.Kills) {
// Raid-runtime objectives (kills, dead-drops, visited areas, looted
// containers) are simulated inside the client-hosted raid; with no
// dedicated game server their progress is never reported to the backend,
// so they would never be saved. Auto-credit them on deploy so the
// objective persists and the station shows the contract as completable.
if (objective.Type.IsRaidRuntime()) {
contractActive.Progress[i] = objective.MaxProgress;
continue;
}