fifa17 SBC: keep repeatable challenges re-enterable after completion

The FIFA 17 client gates challenge re-entry on timesCompleted, not on the
repeatable flag: a nonzero count renders the tile COMPLETED and refuses re-entry
even when repeatable=true. So a repeatable challenge now always projects
timesCompleted=0 (challenges_body) and its set as challengesCompletedCount=0
(sets_body); a non-repeatable challenge keeps its true count and stays locked
once completed. Core keeps the authoritative completion record — economy is
unaffected; this is presentation only.

Live-proven on the retail client 2026-08-18: a completed repeatable Bronze
Upgrade now re-opens for a fresh submit instead of blocking. Regression test
repeatable_completed_challenge_stays_enterable added; adapter + full host suite
(incl. differential and the concurrency race) green.
This commit is contained in:
funman300
2026-08-19 01:18:36 +00:00
parent 8c17f896b4
commit f56aa613da
+59 -2
View File
@@ -66,7 +66,7 @@ pub fn sets_body(challenges: &[ChallengeView]) -> Value {
"description": challenge.description,
"priority": challenge.identity.priority,
"challengesCount": 1,
"challengesCompletedCount": i64::from(challenge.times_completed > 0),
"challengesCompletedCount": i64::from(!challenge.repeatable && challenge.times_completed > 0),
"awards": [],
"hidden": false,
"endTime": 4_102_444_800_i64
@@ -89,6 +89,17 @@ pub fn challenges_body(set_id: i64, challenges: &[ChallengeView]) -> Value {
.iter()
.filter(|challenge| challenge.identity.set_id == set_id)
.map(|challenge| {
// A repeatable challenge is always available to enter again. The FIFA 17
// client gates challenge re-entry on `timesCompleted` (not on `repeatable`):
// a nonzero count renders the tile COMPLETED and refuses re-entry. So a
// repeatable challenge never reports itself as terminally completed here.
// Core keeps the true completion record (economy authority); this is
// presentation only. Live-proven on the retail client 2026-08-18.
let times_completed = if challenge.repeatable {
0
} else {
challenge.times_completed
};
json!({
"challengeId": challenge.identity.challenge_id,
"setId": challenge.identity.set_id,
@@ -103,7 +114,7 @@ pub fn challenges_body(set_id: i64, challenges: &[ChallengeView]) -> Value {
"repeatable": challenge.repeatable,
"trophyId": 0,
"status": "OPEN",
"timesCompleted": challenge.times_completed,
"timesCompleted": times_completed,
"awards": [],
"elgReq": []
})
@@ -246,6 +257,52 @@ mod tests {
assert!(submit["grantedSetAwards"].is_array());
}
#[test]
fn repeatable_completed_challenge_stays_enterable() {
// The FIFA 17 client refuses challenge re-entry when timesCompleted > 0, so
// a repeatable challenge must always project as not-yet-completed while a
// non-repeatable one keeps its true count. Core holds the real record.
let repeatable = ChallengeView {
identity: CHALLENGES[0],
name: "Bronze Upgrade".into(),
description: "Submit players".into(),
repeatable: true,
times_completed: 3,
};
let once = ChallengeView {
identity: CHALLENGES[1],
name: "Hybrid Nations".into(),
description: "Submit a hybrid squad".into(),
repeatable: false,
times_completed: 1,
};
let repeatable_view = challenges_body(CHALLENGES[0].set_id, &[repeatable.clone()]);
assert_eq!(repeatable_view["challenges"][0]["timesCompleted"], 0);
assert_eq!(repeatable_view["challenges"][0]["status"], "OPEN");
let once_view = challenges_body(CHALLENGES[1].set_id, &[once.clone()]);
assert_eq!(once_view["challenges"][0]["timesCompleted"], 1);
let sets = sets_body(&[repeatable, once]);
let sets_arr = sets["categories"][0]["sets"].as_array().unwrap();
let repeatable_set = sets_arr
.iter()
.find(|set| set["setId"] == CHALLENGES[0].set_id)
.unwrap();
let once_set = sets_arr
.iter()
.find(|set| set["setId"] == CHALLENGES[1].set_id)
.unwrap();
assert_eq!(
repeatable_set["challengesCompletedCount"], 0,
"a repeatable set never reports itself terminally completed"
);
assert_eq!(
once_set["challengesCompletedCount"], 1,
"a one-shot set counts its single completion"
);
}
#[test]
fn parser_accepts_only_known_squad_containers_and_item_ids() {
let normal = br#"{"players":[{"index":0,"itemData":{"id":100000001}},{"index":1,"itemData":{"id":0}}]}"#;