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:
@@ -66,7 +66,7 @@ pub fn sets_body(challenges: &[ChallengeView]) -> Value {
|
|||||||
"description": challenge.description,
|
"description": challenge.description,
|
||||||
"priority": challenge.identity.priority,
|
"priority": challenge.identity.priority,
|
||||||
"challengesCount": 1,
|
"challengesCount": 1,
|
||||||
"challengesCompletedCount": i64::from(challenge.times_completed > 0),
|
"challengesCompletedCount": i64::from(!challenge.repeatable && challenge.times_completed > 0),
|
||||||
"awards": [],
|
"awards": [],
|
||||||
"hidden": false,
|
"hidden": false,
|
||||||
"endTime": 4_102_444_800_i64
|
"endTime": 4_102_444_800_i64
|
||||||
@@ -89,6 +89,17 @@ pub fn challenges_body(set_id: i64, challenges: &[ChallengeView]) -> Value {
|
|||||||
.iter()
|
.iter()
|
||||||
.filter(|challenge| challenge.identity.set_id == set_id)
|
.filter(|challenge| challenge.identity.set_id == set_id)
|
||||||
.map(|challenge| {
|
.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!({
|
json!({
|
||||||
"challengeId": challenge.identity.challenge_id,
|
"challengeId": challenge.identity.challenge_id,
|
||||||
"setId": challenge.identity.set_id,
|
"setId": challenge.identity.set_id,
|
||||||
@@ -103,7 +114,7 @@ pub fn challenges_body(set_id: i64, challenges: &[ChallengeView]) -> Value {
|
|||||||
"repeatable": challenge.repeatable,
|
"repeatable": challenge.repeatable,
|
||||||
"trophyId": 0,
|
"trophyId": 0,
|
||||||
"status": "OPEN",
|
"status": "OPEN",
|
||||||
"timesCompleted": challenge.times_completed,
|
"timesCompleted": times_completed,
|
||||||
"awards": [],
|
"awards": [],
|
||||||
"elgReq": []
|
"elgReq": []
|
||||||
})
|
})
|
||||||
@@ -246,6 +257,52 @@ mod tests {
|
|||||||
assert!(submit["grantedSetAwards"].is_array());
|
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]
|
#[test]
|
||||||
fn parser_accepts_only_known_squad_containers_and_item_ids() {
|
fn parser_accepts_only_known_squad_containers_and_item_ids() {
|
||||||
let normal = br#"{"players":[{"index":0,"itemData":{"id":100000001}},{"index":1,"itemData":{"id":0}}]}"#;
|
let normal = br#"{"players":[{"index":0,"itemData":{"id":100000001}},{"index":1,"itemData":{"id":0}}]}"#;
|
||||||
|
|||||||
Reference in New Issue
Block a user