diff --git a/openfut-adapter-fifa17/src/fut/sbc.rs b/openfut-adapter-fifa17/src/fut/sbc.rs index f5bc9b5..9ca3173 100644 --- a/openfut-adapter-fifa17/src/fut/sbc.rs +++ b/openfut-adapter-fifa17/src/fut/sbc.rs @@ -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}}]}"#;