From f56aa613da8927b0c31c7cb0f8692d0420e98830 Mon Sep 17 00:00:00 2001 From: funman300 Date: Wed, 19 Aug 2026 01:18:36 +0000 Subject: [PATCH] fifa17 SBC: keep repeatable challenges re-enterable after completion MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- openfut-adapter-fifa17/src/fut/sbc.rs | 61 ++++++++++++++++++++++++++- 1 file changed, 59 insertions(+), 2 deletions(-) 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}}]}"#;