Trusting a number¶
fmsave reads a binary file that nobody documented. Most of it is settled; some of it is not. This page is how you tell which is which before you build anything on a number.
Verified or unconfirmed¶
Every field carries one of two statuses. Verified means the value was checked against what the game itself shows. Unconfirmed means it decodes consistently and looks right, but no screen has confirmed what it means.
import fmsave
fmsave.field_status(fmsave.Player, "attributes.finishing") # "verified"
fmsave.field_status(fmsave.Player, "contract.wage") # "unconfirmed"
Dots reach into groups. A field with no registered status raises KeyError rather than guessing.
Unconfirmed is not a warning to stay away. It is a statement about evidence, and most unconfirmed fields are fine. It does tell you where to look first when a number surprises you.
How much of a column is there at all¶
coverage gives the share of records whose value is not None, per flat column.
with fmsave.open("career.fm") as career_save:
squad = career_save.players().where(club_uid=career_save.managed_clubs()[0].club_uid)
squad.coverage["contract_wage"] # 1.0
squad.coverage["loan_parent_club_name"]
A low number is usually the save, not a bug: see
What a save holds for the things a save only keeps in part. A value fmsave
cannot read is None, never a guess.
Reader checks warn, they do not block¶
Each of the twenty-six readers measures what it decoded against loose bounds drawn from a couple
of reference careers: how many records it found, how many names resolved, how the ratios sit. A
save unlike those careers can miss a bound and still be read perfectly well, so a missed bound is
a ReaderCheckWarning and you get the table anyway.
This is deliberate. Your save is not required to look like someone else’s to be readable.
When you would rather not work with a table that missed a bound, ask for that:
with fmsave.open("career.fm", strict=True) as career_save:
squad = career_save.players() # raises ReaderCheckError instead of warning
strict=True is worth it in a pipeline, where a silently odd table is worse than a stop. One
thing it does not change: a structural failure, where the decode produced no table at all, raises
ReaderCheckError either way.
Ask the save how it did¶
validate_save runs every reader and reports. It neither raises nor warns.
with fmsave.open("career.fm") as career_save:
report = fmsave.validate_save(career_save)
for reader in report.readers:
print(reader.reader, reader.status, reader.record_count)
# "players" "ok" 124310
A reader’s status is ok, failed (a check of its own did not pass) or error (it raised). Each
ReaderValidation also carries its gates, its coverage and its anomalies, counts of the
odd things it saw while decoding.
The same thing from the command line:
fmsave validate career.fm
fmsave validate career.fm --json
players: ok (124310 records)
injuries: failed (630 records)
injury_log_dates = 0.71 expected 0.9..1
game FM26, build 26.1.0+1234567
The report holds structural facts, counts and rates. No names, no uids, no text from your save. That is what makes it the right thing to paste into a GitHub issue when a reader misbehaves. Never attach the save file itself.
Two things that are not errors¶
Money is in the save’s own unit. Wages, values and balances come back exactly as stored, which is not the currency the game displays and not a figure fmsave converts. A wage that looks wrong by a factor is almost certainly this.
A new game build warns. UnknownBuildWarning means the save comes from an FM26 build fmsave
has no layout tables for. The readers still run, and their checks are what tell you whether the
layout moved under them.