feat(fifa17): opt-in commerce settings, the server half of the transfer-list fix
"Place on Transfer List" is greyed for two reasons. This crate already fixes one
(owned copies emit `untradeable: false`). The other is `tradingEnabled`: the
client's struct defaults it to 0 — it is not a flag we have been overwriting, it
is a flag nobody has ever sent — and it gates the service half of the
TO_TRADE_PILE predicate (vtable slot +0x270, gate byte 0x1fd2e, measured 0 live).
`GET /settings` has always answered `{"configs": []}`.
The schema is high-confidence: FutGetSettingsServerResponse (deser 0x18013c6d0,
read end to end) has a single `configs` key holding `{type, value}` rows, and the
key ladder holds nothing else. `type` is the setting NAME. The row set is ported
from the shape the Python oracle would emit rather than invented.
Default OFF (`OPENFUT_FIFA17_COMMERCE_SETTINGS=1` opts in), because the flags are
RECOVERED BUT UNTESTED and the empty list is the live-proven body — the house
rule is that a flag defaults to the live-proven value. This also moves the
capability out of the oracle we are retiring and into Rust, where it can actually
be reached once Python is gone.
Verified against a real host on both settings: OFF returns {"configs":[]}
byte-identical to today, ON returns the 8-row body with tradingEnabled. It
explains why the menu entry is greyed; it does not promise the market works.
This commit is contained in:
@@ -24,9 +24,50 @@ pub fn accountinfo_body() -> Value {
|
||||
json!({})
|
||||
}
|
||||
|
||||
/// `GET …/settings` — production oracle returns an empty config list.
|
||||
pub fn settings_body() -> Value {
|
||||
json!({ "configs": [] })
|
||||
/// The commerce flags the client leaves OFF unless a `configs` row turns them on.
|
||||
///
|
||||
/// `FutGetSettingsServerResponse` (deser `0x18013c6d0`, read end to end) has a
|
||||
/// single wrapper key `configs` (0xa2) holding an array of
|
||||
/// `{ type (0x354), value (0x377) }`, and nothing else. `type` is the setting
|
||||
/// NAME, not an index.
|
||||
///
|
||||
/// These matter because the struct's defaults are NOT uniform: several fields
|
||||
/// default to 1, but `tradingEnabled` defaults to **0**. It is not a flag we have
|
||||
/// been overwriting — it is a flag nobody has ever sent. It gates the service
|
||||
/// half of the `TO_TRADE_PILE` predicate (vtable slot `+0x270` =
|
||||
/// `FUN_18011c670`, reading gate byte `0x1fd2e`, measured 0 in the live client),
|
||||
/// which is one of the two reasons "Place on Transfer List" is greyed. The other
|
||||
/// reason — `untradeable` — this crate already handles: [`crate::fut::item`]
|
||||
/// emits `false` for owned copies.
|
||||
const COMMERCE_SETTINGS: [&str; 8] = [
|
||||
"storeEnabled",
|
||||
"storeEnabled_JP",
|
||||
"coinEnabled",
|
||||
"coinEnabled_JP",
|
||||
"cardPackStoreEnabled",
|
||||
"cardPackStoreEnabled_JP",
|
||||
"pointsPackStoreEnabled",
|
||||
"tradingEnabled",
|
||||
];
|
||||
|
||||
/// `GET …/settings` — an empty config list by default, matching the production
|
||||
/// oracle and the live-proven behaviour.
|
||||
///
|
||||
/// `enable_commerce` opts in to [`COMMERCE_SETTINGS`]. It defaults OFF because
|
||||
/// the flags are RECOVERED BUT UNTESTED: the schema is high-confidence and this
|
||||
/// is the exact row shape the oracle would emit, but no launch has yet confirmed
|
||||
/// what the client does with them. Turning it on is the server half of the
|
||||
/// transfer-list fix; it explains why the menu entry is greyed and does NOT
|
||||
/// promise that the transfer market behind it works.
|
||||
pub fn settings_body(enable_commerce: bool) -> Value {
|
||||
if !enable_commerce {
|
||||
return json!({ "configs": [] });
|
||||
}
|
||||
let rows: Vec<Value> = COMMERCE_SETTINGS
|
||||
.iter()
|
||||
.map(|name| json!({ "type": name, "value": 1 }))
|
||||
.collect();
|
||||
json!({ "configs": rows })
|
||||
}
|
||||
|
||||
/// `GET …/leaderboards/options` — production oracle (FUT_MODES off) returns an
|
||||
@@ -426,12 +467,44 @@ mod tests {
|
||||
#[test]
|
||||
fn static_bodies_match_oracle() {
|
||||
assert_eq!(accountinfo_body(), json!({}));
|
||||
assert_eq!(settings_body(), json!({ "configs": [] }));
|
||||
assert_eq!(settings_body(false), json!({ "configs": [] }));
|
||||
assert_eq!(leaderboard_options_body(), json!({}));
|
||||
assert_eq!(match_reset_body(), json!({}));
|
||||
assert_eq!(club_stats_staff_body(), json!({}));
|
||||
}
|
||||
|
||||
/// The default MUST stay the live-proven empty list, and the opt-in body must
|
||||
/// match the schema exactly: `configs` holding `{type, value}` rows where
|
||||
/// `type` is the setting NAME. `tradingEnabled` is the one that matters — the
|
||||
/// client defaults it to 0 and it gates the transfer-list menu entry.
|
||||
#[test]
|
||||
fn commerce_settings_are_opt_in_and_shaped_to_the_schema() {
|
||||
assert_eq!(
|
||||
settings_body(false),
|
||||
json!({ "configs": [] }),
|
||||
"default must remain the live-proven body"
|
||||
);
|
||||
|
||||
let on = settings_body(true);
|
||||
let rows = on["configs"].as_array().expect("configs is an array");
|
||||
assert_eq!(rows.len(), COMMERCE_SETTINGS.len());
|
||||
for row in rows {
|
||||
let obj = row.as_object().expect("each config row is an OBJECT");
|
||||
assert_eq!(
|
||||
obj.keys().collect::<Vec<_>>(),
|
||||
vec!["type", "value"],
|
||||
"the deserializer knows exactly two keys; an extra one is skipped \
|
||||
at best and a type desync at worst"
|
||||
);
|
||||
assert!(obj["type"].is_string(), "type is the setting NAME");
|
||||
assert_eq!(obj["value"], 1);
|
||||
}
|
||||
assert!(
|
||||
rows.iter().any(|r| r["type"] == "tradingEnabled"),
|
||||
"the whole point of the opt-in"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn action_parse() {
|
||||
assert_eq!(
|
||||
|
||||
@@ -3926,8 +3926,9 @@ impl Server {
|
||||
json_status(200, &non_economy::accountinfo_body())
|
||||
}
|
||||
Route::Settings => {
|
||||
eprintln!("utas-host owner=RUST route=settings status=200");
|
||||
json_status(200, &non_economy::settings_body())
|
||||
let commerce = commerce_settings_enabled();
|
||||
eprintln!("utas-host owner=RUST route=settings status=200 commerce={commerce}");
|
||||
json_status(200, &non_economy::settings_body(commerce))
|
||||
}
|
||||
Route::LeaderboardOptions => {
|
||||
eprintln!("utas-host owner=RUST route=leaderboards-options status=200");
|
||||
@@ -4771,6 +4772,20 @@ fn fifa17_sidlog(sid: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether `GET …/settings` should turn the client's commerce flags on.
|
||||
///
|
||||
/// OFF unless `OPENFUT_FIFA17_COMMERCE_SETTINGS=1`, because the flags are
|
||||
/// recovered but UNTESTED and the empty config list is the live-proven body. The
|
||||
/// house rule is that a flag defaults to the live-proven value.
|
||||
///
|
||||
/// Turning it on is the SERVER half of the transfer-list fix — the client's
|
||||
/// `tradingEnabled` gate defaults to 0 and nothing has ever sent it. It needs a
|
||||
/// launch to confirm, and it does not promise the market behind the menu works.
|
||||
fn commerce_settings_enabled() -> bool {
|
||||
static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
|
||||
*ENABLED.get_or_init(|| std::env::var("OPENFUT_FIFA17_COMMERCE_SETTINGS").as_deref() == Ok("1"))
|
||||
}
|
||||
|
||||
/// A JSON response with an explicit status.
|
||||
fn json_status(status: u16, v: &Value) -> WireResponse {
|
||||
let body = serde_json::to_vec(v).unwrap_or_default();
|
||||
|
||||
Reference in New Issue
Block a user