Save¶
fmsave.open returns a Save; its reader methods hand back one Table each. validate_save
runs every reader at once and reports how each fared, without raising or warning.
Opening a save¶
- fmsave.open(path, *, strict=False, competition_names=None)¶
Open a Football Manager 26 save file for reading.
- Parameters:
strict (bool) – Whether a failed reader check stops the reader. The checks are bounds measured on a handful of saves, so a save stranger than those is read and warned about by default: a reader whose checks fail issues ReaderCheckWarning and hands its table back. With True it raises ReaderCheckError instead, which is what a pipeline that would rather stop than read a table it cannot vouch for wants. A structural failure, where the decode has no table to hand back, raises either way.
competition_names (Mapping[int, str] | str | PathLike[str] | None) – Names for competitions, keyed on Competition.database_id, either as a mapping or as the path of a two-column CSV that fmsave.read_competition_names reads. No save stores a competition name and fmsave ships none, so without this every Competition.name and every denormalised competition_name is None. Tables are read once and then kept, so the map cannot be changed afterwards: open the save again to read it under a different one.
- Raises:
FileNotFoundError – The file does not exist.
NotAFmSaveError – The file is not a Football Manager save.
CorruptSaveError – The save is damaged or was being written.
UnsupportedGameError – The save is from another Football Manager version.
ReaderCheckError – The save metadata does not match the expected layout.
OSError – A competition name CSV cannot be opened or read.
TypeError – A competition name mapping holds a key that is not an int, or a name that is not a str.
ValueError – A competition name CSV is malformed, or a supplied name is empty.
- Warns:
UnknownBuildWarning – The save comes from an FM26 build without layout tables.
- Return type:
Save¶
- class fmsave.Save(container_index, info, *, strict=False, competition_names={})[source]¶
A Football Manager 26 save opened for reading.
Opening reads only the header, the directory and the save metadata; the file is not kept open. Each reader reopens the file, checks that it has not changed since it was opened, and reads only the parts it needs.
Every reader measures what it decoded against the bounds its checks carry. A failed check warns with ReaderCheckWarning and the reader hands its table back; a save opened with strict=True raises ReaderCheckError instead. A structural failure, where the decode has no table to hand back, raises either way. fmsave.validate_save reports every check of every reader without raising or warning.
A Save is not thread-safe: use one Save per thread. Records and tables it returns are immutable, safe to share, and keep working after the Save is closed.
- Parameters:
- clubs()[source]¶
Every club in the save’s game database, in club index order.
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, or a team id is listed twice (by one club or by two).
- Warns:
ReaderCheckWarning – On a full-size save, the club records fall outside the checks’ bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- players()[source]¶
Every player in the save’s game database, in record offset order.
Person fields (name, birth date, nationality, personality, traits and the rest) are filled in from each player’s person block, or stay None or empty when no block validates. The table is read on the first call to players(), contracts() or suspensions(), from one decode pass; later calls to any of them return the same tables. When a check of that pass fails, none of the three tables is kept, so each of them raises.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written, a player’s relation header or entry list runs past its record window, or a legal name is not valid UTF-8.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice (by one club or by two), no player records were found, two player records share a uid, or the save’s in-game date is unreadable. With a name map, also anything the competition reader raises, since the pass names each ban’s competition from that table.
- Warns:
ReaderCheckWarning – On a full-size save, the players, contracts or suspensions decoded fall outside the checks’ bounds, or, with a name map, the competition table does. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- contracts()[source]¶
Every player’s contract in the save’s game database, in player order.
Only players whose contract is not None appear. The table is read on the first call to players(), contracts() or suspensions(), from one decode pass; later calls to any of them return the same tables. When a check of that pass fails, none of the three tables is kept, so each of them raises.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written, a player’s relation header or entry list runs past its record window, or a legal name is not valid UTF-8.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice (by one club or by two), no player records were found, two player records share a uid, or the save’s in-game date is unreadable. With a name map, also anything the competition reader raises, since the pass names each ban’s competition from that table.
- Warns:
ReaderCheckWarning – On a full-size save, the players, contracts or suspensions decoded fall outside the checks’ bounds, or, with a name map, the competition table does. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- suspensions()[source]¶
Every unserved suspension in the save’s game database, in player order.
Every unserved ban the save holds is listed, including bans the game no longer displays. Each player’s suspensions keep the order the save stores them in, and each row carries the club the player belongs to, which for a player registered with a team another club controls is that controlling club. The table is read on the first call to players(), contracts() or suspensions(), from one decode pass; later calls to any of them return the same tables. When a check of that pass fails, none of the three tables is kept, so each of them raises.
A ban covering one competition carries competition_id in the stage id space, which joins stages(), competitions(), fixtures(), league_tables() and player_match_stats(); a nation-wide ban carries nation_id instead, and scope says which. competition_name is None unless the save was opened with a name map, and stays None for a competition the game itself created during the career, whose database id no name source outside the save carries.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written, a player’s relation header or entry list runs past its record window, or a legal name is not valid UTF-8.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice (by one club or by two), no player records were found, two player records share a uid, or the save’s in-game date is unreadable. With a name map, also anything the competition reader raises, since every competition name a ban carries comes from that table.
- Warns:
ReaderCheckWarning – On a full-size save, the players, contracts or suspensions decoded fall outside the checks’ bounds, or, with a name map, the competition table does. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- managed_clubs()[source]¶
The club run by the save’s first human manager.
An empty table means no managed club was found: the save has no human manager, or the manager has no current club, for example while between jobs. The link from a human manager to a club is proven only on saves with a single human manager; on a save with several, only the first is listed. The club comes from the manager’s current contract and is checked against the save summary, which also stores the manager’s name. The table is read on the first call; later calls return the same table. It does not decode players.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, the save’s in-game date is unreadable, the contract and the save summary link the manager to different clubs, or, with no current contract, the save summary links the manager to more than one club.
- Return type:
- stages()[source]¶
Every stage of every competition, in the order the save’s stage table stores them.
A stage is one part of a competition: a league season is one stage, a cup round is one, and each leg of a two-legged tie is its own stage carrying the same round. Stage ids are what fixtures, league-table groups and per-match records join through. No save stores a competition name, so competition_name is None unless the save was opened with a name map, and then it is filled in for the competitions that map names. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No stage table was found in the tail of the game database, or a stage id appears in two rows. With a name map the competition reader runs first, so anything it raises is raised here too, since a stage’s competition name is read out of that table.
- Warns:
ReaderCheckWarning – On a full-size save, the stage table falls outside the checks’ bounds, or, with a name map, the competition table does. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- competitions()[source]¶
Every competition the save’s stage table names, in ascending competition id.
Each row carries the ids of its stages and, for about nine competitions in ten, the competition’s id in the game’s own editor database, which is the same value on every save and is what an external name source is keyed on. The save stores no competition names, so name is None on every row until a name map is supplied. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No stage table was found in the tail of the game database, or a stage id appears in two rows.
- Warns:
ReaderCheckWarning – On a full-size save, fewer competitions were found than the checks allow. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- fixtures()[source]¶
Every match the save has scheduled or played, in date and kick-off order.
The save holds several copies of the calendar; only the largest is returned, so the block of template matches every save carries is left out. Each row joins to its competition through its stage, and to a club through each team id; an id the save does not resolve leaves its fields empty rather than being guessed.
home_goals and away_goals carry the score of a played match the save still holds one for, which is about a quarter of them: the calendar itself stores no score, and the separate records that do are kept for only part of a career. A played match with empty goals means the save no longer holds that result, not that it finished goalless.
stadium_uid names the ground each match is played at, which comes from the stadium table: this reader therefore needs that table and raises when the game database holds none.
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, no stage table was found in the tail of the game database, a stage id appears in two rows, no stadium table was found, or the save’s in-game date is unreadable. With a competition name map, the competition reader runs first and raises here too, so no row is named from a table that could not be read.
- Warns:
ReaderCheckWarning – On a full-size span, the calendar or the scores joined onto it fall outside the checks’ bounds. The stadium table’s checks run here as well, and with a name map the competition table’s, since a row carries a ground and a name from each. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- stadiums()[source]¶
Every ground the save’s database holds, in the order the save stores them.
A row carries the ground’s capacities, its pitch, when it was built and rebuilt, and the club that owns it. Most rows carry no name: the game takes a ground’s name from its installed database and only a couple of hundred grounds store one in the save.
home_club_uids says which clubs play their home matches there, and it is not a stored link: no club record points at a ground, so it comes from the fixture calendar, as the ground a club used for most of its first-team home matches. A ground no club used often enough lists none, and a shared ground lists every club that uses it. Owning a ground and playing at it are different things and are separate fields.
A ground with no stored owner is the council’s. The game’s facilities screen shows one such ground as owned by the council, so an empty owner_club_uid is what the game displays that way rather than an owner fmsave failed to read.
The last row of the table is a template the save carries rather than a ground anyone plays at, and it is returned like any other row the walk found.
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No stadium table was found, or a stadium uid appears in two rows. The club and fixture readers run first and raise here too, since a row carries club names and a calendar-derived link.
- Warns:
ReaderCheckWarning – On a full-size save, what the table or the calendar link decoded falls outside the checks’ bounds. The club and fixture checks run here as well. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- transfer_windows()[source]¶
Every transfer window the save’s rules database holds, in stored order.
A window says when a transfer window opens and closes in a season. It is database content rather than career state: every save made from one installed database holds the same windows, whatever has happened in the career. The dates are season-relative, so each carries an offset from the season’s start year rather than a calendar date, and a window has no name of its own. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
- Warns:
ReaderCheckWarning – On a full-size save, fewer windows were decoded than the checks allow, or the windows that were found did not decode their dates. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- injury_types()[source]¶
Every injury the game can hand out that the save names, in stored order.
The names are the game’s own text, as the save stores it, in the language the save was written in. They are in no section of the save: each per-match file it holds carries one copy of the table, so a save with no per-match file has no names at all and this table is then empty. Some injury codes have no entry in the table, so a code an injury carries need not be named here. This table holds only what the save itself stores: fmsave ships no injury names of its own, so a code the save leaves out has no name here or anywhere else.
Only as many per-match files are read as it takes to find the table, whatever a save lists, and no game database is read at all. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – A per-match file is damaged or was being written.
- Warns:
ReaderCheckWarning – On a full-size save listing at least one per-match file, fewer records of the table were read than the checks allow. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- injuries()[source]¶
Every injury the save still remembers, in stored order, of both kinds.
A HISTORY row is an injury the save remembers happening: when it happened, the team the person was at, whether it happened in training or in a match, and how bad it was, both of those in the game’s own words. The save keeps only about the last two years of them: the oldest row on every save measured is 742 days before the in-game date, and rows older than that are gone rather than kept, so this is a rolling window and not a whole career.
These are not the rows the game’s Injury History tab lists. That tab reads the full-career store, which fmsave does not ship because nothing in it says which person a history belongs to: it shows injuries from years outside this window, so what is here is the recent part of the career that tab shows whole.
A TYPED row carries the injury type of a recent or current episode, and its date is the day the player is expected back rather than the day he was hurt, which is why so many of these dates sit ahead of the in-game date: adding each displayed injury’s date to the time it kept the player out lands on the date stored here, checked on eleven players. A date behind the in-game date is a return the game has already reached, and how far behind those go depends on the save, so nothing here treats a week as a rule. A typed row is not a list of injuries the player has recovered from: what it names is the type, which no HISTORY row carries. About one type code in fifteen has no entry in Save.injury_types, and type_name is empty for those: fmsave ships no injury names of its own.
A row whose person the save no longer keeps as a player leaves player_uid and player_name empty, which is about one HISTORY row in twenty. A HISTORY row whose team no club lists, or which names no team at all, leaves team_id and the three club fields empty rather than guessing; that is about one row in five hundred. Two views this table has no field for:
history = career_save.injuries() own = history.where(player_uid=player_uid) typed_date = next(row.date for row in own if row.kind == "typed") # The latest history row on or before a typed row's date, for the same player. before = [ row.date for row in own if row.kind == "history" and row.date is not None and row.date <= typed_date ] days_between = (typed_date - max(before)).days if before else None
days_between is the gap between two dates and never how long the player was out for, which no row of this table holds. Filtering by a player’s club today is where(player_uid=…) against Save.players, since club_uid here is the club at the time.
This reader joins through the player records, so a cold call pays the player pass over the game database; a warm one decompresses only this section. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The save’s in-game date is unreadable, so an injury date cannot be judged against it; a count in the section runs past the end of it, the section’s tail is not where it belongs, or bytes follow it; or no club or player record is accepted.
- Warns:
ReaderCheckWarning – On a full-size section, the rows fall outside the checks’ bounds. Every index whose data this reader hands out is read through the reader that enforces that index’s own checks, so those checks are reported here as well. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- affiliates()[source]¶
Every group of clubs the save stores together, in stored order.
What groups a set of clubs is not established. These groups live in a section of their own and carry nothing that says what the grouping means, so every club field is unconfirmed.
They are not the affiliates a club’s Affiliated Clubs screen lists. The club whose screen listed 26 affiliates belongs to no group here, and the one group naming a club that screen also names pairs it with its own academy rather than with the viewing club. A multi-club ownership group is recognisable in the largest group of every save measured, which would suit an ownership reading, but no screen has said so. So a club’s own affiliates, and the kind of each affiliation, are in no field fmsave reads.
This is not the parent link Club.parent_club_uid carries, which comes from the team lists inside the club records; the two relations share no pair at all, although the parent link is what a club’s B and C sides on that screen come through. A member whose stored club index no club record claims leaves that entry’s uid and name empty rather than being guessed, which is about one member in sixty.
The other members of the groups a club belongs to, without the club itself:
groups = career_save.affiliates() own_groups = groups.filter(lambda group: club_uid in group.club_uids) others = tuple( member_uid for group in own_groups for member_uid in group.club_uids if member_uid is not None and member_uid != club_uid )
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The group section is too short to hold its header, a group’s member count is outside what a group may hold, a group runs past the end of the section, the groups do not end on the section’s last byte, no club record is accepted, a club uid or club index appears in two records, or a team id is listed twice.
- Warns:
ReaderCheckWarning – On a full-size save whose groups hold a member, fewer members resolve to a club than the checks allow, or the club table falls outside its own bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- job_vacancies()[source]¶
Every open job the save’s job-centre feed holds, in stored order.
The feed keeps old rows. The earliest advertised date on the saves measured is about ten years before the in-game date, and nothing in a row says whether the job has since been filled, so this is what the save still remembers rather than what the game would list today.
A vacancy is stored against a team, so team_id is the save’s own key and the club fields come from the club that fields that team; a team no club lists leaves them empty, and a record naming no team at all leaves team_id empty too. league_position is the team’s current position in the competition the row names, which is empty on about a quarter of rows because the save stores none. The job title, the second date and two further numbers have no confirmed meaning and ship in unknown rather than under names they have not earned.
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The feed section is too short to hold its header, or its size is not its header and its record size times the count it claims; the save’s in-game date is unreadable, so an advertised date cannot be judged against it; no club record is accepted; a club uid or club index appears in two records; a team id is listed twice; or no stage table was found in the tail of the game database. With a competition name map the competition reader runs first and raises here too, so no row is named from a table that could not be read.
- Warns:
ReaderCheckWarning – On a feed of at least twenty records, the records fall outside the checks’ bounds. The club and stage tables’ checks run here as well, and with a name map the competition table’s. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- league_tables()[source]¶
Every live league table the save holds, in the order the save stores them.
One table is one run of blocks the save stores together, told from the next by the index each block keeps of its own place in its table. The save stores each block several times over, the copies agreeing in every field fmsave decodes and differing only in undecoded head bytes, so repeated content is dropped before the tables are built; keeping it would break the tables apart. Nothing in a table names its competition, so it is voted for from the fixture calendar and is empty on the few tables the vote cannot settle, which are still returned; one competition names several tables, since a cup’s group stage holds a table per group. Rows are in the order the save stores, which is the table’s own standings order, so position is the league position. Each match slot says whether it was played at home or away, from the parity the save alternates venue by; the same calendar checks that parity on every table whose results account for one season of it. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted; a club uid or club index appears in two records; a team id is listed twice; no stage table was found in the tail of the game database; a stage id appears in two rows; or the save’s in-game date is unreadable.
- Warns:
ReaderCheckWarning – On a full-size span, the blocks fall outside the checks’ bounds. Every index whose data this reader hands out is read through the accessor that enforces that index’s own checks, so the checks of the fixture calendar the vote runs on, and of the club, stage and competition tables this reader joins through, are reported here as well. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- competition_rules()[source]¶
Every competition-rules block the save holds, in the order the save stores them.
A block carries a competition’s promotion, play-off and relegation places, its tie-break codes, its prize money by finishing position and its round calendar.
A row’s competition comes from where the save keeps the block, not from a field it stores. The span alternates rules blocks and league-table blocks, and the table stored right after a block is the one that block’s rules govern. competition_id and competition_name are that table’s, and are empty where the run of blocks after the preamble is not exactly one table league_tables() returned with a competition of its own, which on the saves measured is 32% to 45% of rows. It is a position in the file rather than a stored link, and the competition it hands over is itself a vote on the fixture calendar, so both fields are as strong as LeagueTable.competition_id and no stronger. A block’s club count and its points deduction for administration are not read at all, because no fixed position in the block carries either; the linked table’s own club_count is where a club count comes from.
The squad and financial rules the save’s rules database holds are not read: nothing readable ties one of those groups to a competition, and their content is database content, identical on every save of one installed database. So kind is PREAMBLE on every row. Transfer windows are their own table, transfer_windows().
A cold call builds the league tables and the fixture calendar, since that is where the competition comes from. A caller who has already read either pays nothing extra; one who has read neither pays for both here. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted; a club uid or club index appears in two records; a team id is listed twice; no stage table was found in the tail of the game database; a stage id appears in two rows; no stadium table was found; or the save’s in-game date is unreadable, so the span pass cannot run.
- Warns:
ReaderCheckWarning – On a full-size span, the blocks or the link fall outside the checks’ bounds. The checks of the league tables this reader links to, of the fixture calendar their vote runs on, and of the club, stage and competition tables those readers join through, are reported here as well. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- player_match_stats()[source]¶
Every match one of the save’s players played that it still holds a record of.
Rows come in player order and, inside each player, in the order the save stores his matches. This is never a whole season and never a career: the save keeps about twenty matches per player per spell at a team, counted across all competitions at once rather than per competition, and drops the oldest as new ones arrive. A player who has played more than that in his current spell has only his latest matches here, so a per-competition total summed from these rows is short without saying so. Nothing in a row marks it as truncated. Friendlies, internationals and youth matches are kept apart from these records and are not here at all.
Each row carries the competition id the record itself stores, which is in the stage id space, and joins to a club through the opponent’s team id; a team id no club lists leaves the opponent fields empty rather than guessing them. A match the save no longer holds the statistics of carries its date, competition and opponent, has_stats is false, and every field the statistics would fill is empty.
This reader decodes the players to fill player_name, so a cold call pays for the player pass; a caller who has already called players() pays nothing extra for it. The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted; a club uid or club index appears in two records; a team id is listed twice; no player records were found; two player records share a uid; no stage table was found in the tail of the game database; or the save’s in-game date is unreadable, so the years a match may be dated in cannot be worked out.
- Warns:
ReaderCheckWarning – On a full-size save, the records decoded fall outside the checks’ bounds. The checks of the club and player readers this one joins through are reported here as well. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- finances()[source]¶
Every month of club money the save keeps, by club and then oldest month first.
Only some clubs have a series at all. The save keeps one for the clubs of the one or two league nations it tracks, not for every club it holds, and which nations those are changes as a career moves on, so a club with no rows here is ordinary rather than a failure. On the saves measured 57 to 335 clubs of the 48,000 to 51,000 each save holds had one, of between 3 and 60 months each; 60 months is as many as a club keeps. A save whose clubs keep none at all returns an empty table, and on a save with no human manager that is also what a search finding nothing would return: the checks bound the rows that were decoded, and the one count they bound from below applies only where the save lists a managed club. fmsave validate reports the clubs with a series and the club records searched either way.
balance is the balance at the end of the row’s month, and net_transfers is positive for a net spend. Money is a whole number in the save’s base currency, which is not the currency the game displays: on the save whose screens were read the balance, both budgets, the wage bill and the month’s income and expenditure all display at about 1.157 times the stored value, one rate fitting every one of them, which is what says the stored unit is its own unit. Nothing here is converted and no rate is applied. The weekly fields are weekly; every other money field is one month’s amount. transfer_budget_allocated is not the budget the board allocated: that screen showed a budget well above it with almost none of it spent, so what the field is stays unnamed.
month is worked out from the save’s own date rather than stored: the last row of a club is the month before the save’s month, which is the lag that put that club’s two oldest rows on the months its own screen labelled them with. Several useful figures are arithmetic on these rows rather than fields: wage headroom is wage_budget_weekly less wage_payroll_weekly, a yearly figure is a weekly one times 52, a club’s latest month is its last row, and a club summary is its rows grouped by club_uid.
The table is read on the first call to finances() or sponsorships(), from one decode pass; later calls to either return the same tables. When a check of that pass fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, or the save’s in-game date is unreadable, so no month can be dated.
- Warns:
ReaderCheckWarning – On a full-size save, the months and sponsors decoded fall outside the checks’ bounds, or the club table falls outside its own. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- sponsorships()[source]¶
Every sponsorship contract the save lists for a club, in stored order.
The clubs are the ones finances() keeps a series for, in the same order, and each club’s contracts keep the order the save stores them in. The list keeps contracts that have ended, whose annual_value is zero, so a club’s current sponsorship income is the sum of annual_value over the rows whose end is after the save’s date.
Every type code is UNKNOWN: the save groups its sponsorships into about twenty kinds and no displayed label has pinned one of those codes. Money is a whole number in the save’s base currency, as in finances().
The table is read on the first call to finances() or sponsorships(), from one decode pass; later calls to either return the same tables. When a check of that pass fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, a team id is listed twice, or the save’s in-game date is unreadable, so no month can be dated.
- Warns:
ReaderCheckWarning – On a full-size save, the months and sponsors decoded fall outside the checks’ bounds, or the club table falls outside its own. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- facilities()[source]¶
Every club’s corporate facilities rating, in club index order.
The clubs are the ones `finances()` covers, because the rating is stored behind a club’s monthly finance chain and only the clubs of the one or two league nations a save tracks keep one. A club with no rows in finances() has no row here either, which is most clubs of a save and is ordinary rather than a failure; a save whose clubs keep no series returns an empty table, and fmsave validate reports the counts.
corporate_facilities is the rating the game’s Facilities screen shows as a word on its Corporate line, and the words rise with the stored number. Fifteen of the twenty codes carry the word one club’s own screen displayed against that exact number, from 1 Basic to 20 Excellent; two more, 8 and 14, carry the word of the codes either side of them, each of which a screen displayed the same word for. The three codes no club in the save carries – 3, 4 and 16 – read UNKNOWN with the raw number kept, rather than taking a word from where they sit on the scale. The screen’s other facility lines, the stadium and pitch, the training and youth grounds and the academy, are not stored anywhere near this byte and are not read at all.
The table is read on the first call; later calls return the same table.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – No club record is accepted, a club uid or club index appears in two records, or a team id is listed twice.
- Warns:
ReaderCheckWarning – On a full-size save, the ratings decoded fall outside the checks’ bounds, or the club table falls outside its own. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- staff()[source]¶
Everyone a club employs who is not a player, in the order their objects are stored.
A row is one person at one club: the department lists the club keeps him in, what his contract pays and until when, his ability and the preferences a staff profile shows. A person an affiliate side lists whose contract is with that side’s parent is one row at the parent, with the side that lists him in listed_club_uid, the same rule a player on a B team follows. A person a club lists but pays nothing has no contract to read, so his wage and both dates are empty rather than zero; about one listed person in seven of a save is like that.
The save’s human manager is a row of his own, with is_human_manager true. His object is laid out differently, so no ability, no preferences and none of the ability-block unknown keys read for him, while his name, birth date, personality and contract read as anyone’s do.
The contract codes are not player squad statuses. unknown[“contract_e36”] comes from the byte a player’s squad status is read from, but it takes different values on staff and no displayed label has named one, so it and the three codes beside it ship as raw numbers.
The job title is not readable, and `unknown[“r4”]` is not it. A club’s staff screen settled that: over 78 displayed rows, 8 of the 16 codes on show carried two or more different job titles and 7 titles appeared under two or three different codes, so the byte keeps its raw number rather than becoming a named role. The club’s board is not in this table either: the president, the director and the managing director appear on that same screen with no row here, while every other person on it matched a row.
Several useful figures are arithmetic on these rows rather than fields: a club’s non-playing wage bill is the sum of wage over its rows, how many of its staff it lists is where(in_club_lists=True), and who it pays without listing is where(has_contract=True, in_club_lists=False).
The table is read on the first call to staff() or staff_lists(), from one decode pass; later calls to either return the same tables. When a check of that pass fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written, a contract record runs past the end of the game database, or a name block’s relation list runs past the window it was found in.
ReaderCheckError – The club or player readers raised, or the save’s in-game date is unreadable, so no contract can be dated.
- Warns:
ReaderCheckWarning – On a full-size save, the staff decoded fall outside the checks’ bounds, or the club or player checks this reader runs first do. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- staff_lists()[source]¶
The three staff lists each club record holds, in club index order.
A club that lists nobody has no row at all, and a club that lists somebody has all three rows, empty lists included. Only 733 to 1,957 of a save’s 48,000 to 51,000 clubs list anybody, so most clubs are absent, which is ordinary rather than a failure.
The lists split a club’s staff into groups whose codes differ from list to list. They look like the three departments a club’s staff screen shows: on one club the screen’s medical, coaching and recruitment panels and that club’s senior rows in lists 0, 1 and 2 held the same people, person for person by name, save for the human manager and one contracted person no list holds. That is arithmetic on one club rather than a label on a list, so list_index stays a number. People a list names who turn out to be players are dropped, and fmsave validate reports how many; so are the few whose object cannot be told from another’s, so a list’s people are those staff() also has a row for. A list’s size is len(staff_uids).
The table is read on the first call to staff() or staff_lists(), from one decode pass; later calls to either return the same tables. When a check of that pass fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written, a contract record runs past the end of the game database, or a name block’s relation list runs past the window it was found in.
ReaderCheckError – The club or player readers raised, or the save’s in-game date is unreadable, so no contract can be dated.
- Warns:
ReaderCheckWarning – On a full-size save, the staff decoded fall outside the checks’ bounds, or the club or player checks this reader runs first do. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- tactics()[source]¶
Every tactic the manager’s teams store, by team and then in stored order.
A tactic belongs to a team, not to a club. The save keeps each team’s own copy of every tactic the manager has, so the same tactic name appears once per team and a row is one stored copy, keyed on team_id and index. The copies are not byte-identical. Nothing stored says which tactic is selected, so there is no such field: the word that might hold it is the same on every block that carries a tactic at all.
Only the manager’s own teams are here. No other club stores a tactic in this form, and the line-ups the section keeps for thousands of other clubs are a different structure that fmsave does not read.
mentality carries the word the game shows: the seven codes are the seven mentalities it offers, from Very Defensive to Very Attacking, confirmed against a manager’s own tactics. Every position bit is still a raw code with the label UNKNOWN: the bits are read from offsets a strict walk of every record lands on exactly, but no displayed formation has been matched to one of those numbers. The 19 team instructions and the setting units of each slot are unidentified numbers in the same way, and ship in unknown and as TacticSettingUnit values rather than under names they have not earned.
The table is read on the first call to tactics() or set_pieces(), from one walk; later calls to either return the same tables. When a check of that walk fails, neither table is kept, so both raise. A cold call pays for the player pass, because the selectors the reader’s checks judge resolve through the player records.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The tactics section is too short to hold its header, a constant a team block is built around is not where fmsave expects it, a count inside a block runs past the end of the section, a name does not decode, no club record is accepted, a club uid or club index appears in two records, or a team id is listed twice.
- Warns:
ReaderCheckWarning – On a full-size save that lists a managed club, what the walk read falls outside the checks’ bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- set_pieces()[source]¶
Every set-piece routine slot of the manager’s teams, by team and then slot.
Each team has twenty slots and each is a row, whose name is None when the slot holds no routine: that is half the slots of a first team and all twenty of every other team on the saves measured. Which set-piece situation a slot is for is not stored as text, so the slot number is all there is to go on, and a routine is never recognised by its name.
The table is read on the first call to tactics() or set_pieces(), from one walk; later calls to either return the same tables. When a check of that walk fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The tactics section is too short to hold its header, a constant a team block is built around is not where fmsave expects it, a count inside a block runs past the end of the section, a name does not decode, no club record is accepted, a club uid or club index appears in two records, or a team id is listed twice.
- Warns:
ReaderCheckWarning – On a full-size save that lists a managed club, what the walk read falls outside the checks’ bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- training()[source]¶
The training calendar of each team of the club the save’s manager runs.
One row per team, in the order the save stores them: the first team, the reserves, the youth side and each team the club fields at a club it controls, which maps back to the managed club with the slot it holds in that club’s list. No other club has a calendar at all, and a save whose manager runs no club returns an empty table.
Which week runs which schedule is settled. Each weekly record holds the week’s start date and then a schedule name, and the name is the schedule of the week whose date came before it: a manager’s Training screen showed exactly that schedule for four consecutive weeks, where pairing each name with the following week would have been a week out on all four. The dates themselves are solid too: consecutive weeks step exactly seven days on every pair of every calendar measured, and the calendar runs from well before the save’s date to well after it.
The active week of a team is the latest one that has started. A week whose stored date does not decode carries None, and a save whose date is unreadable has nothing to compare against, so both are stepped around:
clock = career_save.info.game_date weeks = career_save.training()[0].weeks started = [ week for week in weeks if week.week_start is not None and clock is not None and week.week_start <= clock ] active = max(started, default=None, key=lambda week: week.week_start)
What a schedule asks of a day is not read: each day of a week holds three codes whose meaning is unknown. schedule_library is the manager’s own saved schedules, which the save keeps once per section rather than per team, so every row carries the same tuple, and the save may hold the same schedule twice over under two ids.
The table is read on the first call to training() or mentoring(), from one walk; later calls to either return the same tables. When a check of that walk fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The training section is too short to hold its header list, a block the walk accepted holds a weekly record or a mentoring group it cannot read, no club record is accepted, a club uid or club index appears in two records, a team id is listed twice, or no player record is found.
- Warns:
ReaderCheckWarning – On a full-size save whose manager runs a club, the blocks and weeks read fall outside the checks’ bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
- mentoring()[source]¶
Every mentoring group of every team of the club the save’s manager runs.
A group belongs to a team, not to the club: the rows come team by team in the order the save stores the teams, and within a team in stored order. Only the managed club has groups at all, and a team with none has no row here, which is ordinary – on the saves measured only the first team and one other side mentor anybody.
label is the stored text. Every group of every save measured carries the game’s own default wording with the group’s number, and whether renaming a group in game changes what is stored has not been checked. A member whose stored selector names no player record leaves that entry’s uid and name empty rather than guessing.
The table is read on the first call to training() or mentoring(), from one walk; later calls to either return the same tables. When a check of that walk fails, neither table is kept, so both raise.
- Raises:
SaveClosedError – The save is closed.
SaveChangedError – The file changed on disk after it was opened.
CorruptSaveError – The save is damaged or was being written.
ReaderCheckError – The training section is too short to hold its header list, a block the walk accepted holds a weekly record or a mentoring group it cannot read, no club record is accepted, a club uid or club index appears in two records, a team id is listed twice, or no player record is found.
- Warns:
ReaderCheckWarning – On a full-size save whose manager runs a club, the members read fall outside the checks’ bounds. A save opened with strict=True raises ReaderCheckError instead.
- Return type:
Save metadata¶
- class fmsave.SaveInfo(game, build, build_number, known_build, db_version, game_date, time_slot, save_name, sections, summary_strings, section_schemas, file_name)[source]¶
Facts about a save, read when it is opened.
The repr leaves out the save name, the summary strings and the section list.
- Parameters:
- game_date¶
In-game date, or None when it cannot be read (unconfirmed).
- Type:
datetime.date | None
- sections¶
Every named section in file order.
- Type:
- summary_strings¶
The readable strings stored in the save summary, in file order, including the build string. They can hold the human manager’s name and club and competition names, so fmsave info does not show them (unconfirmed).
- section_schemas¶
Schema number of every named section, read-only.
- Type:
Competition names¶
Competition.database_id joins the id a name source keyed outside a save uses; this reads such
a source into the map fmsave.open takes.
- fmsave.read_competition_names(path)[source]¶
Read a two-column UTF-8 CSV of database_id,name into a competition name map.
The first column is the competition’s editor database id, which is what Competition.database_id carries. A first row whose first cell is not a whole number is taken for a header and skipped. Blank lines are skipped, and surrounding whitespace is stripped from both cells.
One id may be listed twice with the same name. Two different names for one id are a contradiction and raise, because neither is more likely to be the one meant.
A row is exactly two columns wide. A third column is a fault as much as a missing one, since the extra cell leaves the row ambiguous: a name that holds a comma belongs in quotes rather than spread across cells.
Every error names the file and never the folder holding it, so a message a reader pastes into a bug report carries no part of their directory layout.
- Raises:
OSError – The file cannot be opened or read. It is left to the caller, which knows the path it passed.
ValueError – A row has other than two columns, an id is not a whole number, a name is empty, or one id is listed twice under two names.
- Parameters:
- Return type:
Validation¶
- fmsave.validate_save(career_save)[source]¶
Run every reader on a save and report how each fared.
Readers run in the order clubs, players, contracts, suspensions, managed clubs, stages, competitions, fixtures, league tables, transfer windows, competition rules, per-match player stats, stadiums, finances, sponsorships, affiliates, job vacancies, staff, staff lists, injury types, injury history, training, mentoring, tactics, set pieces, club facilities. The order puts each reader after the ones whose work it reuses, so a shared decode that fails is reported where it failed: stadiums follow the fixtures whose calendar they read, injury types precede the injury history that names its types, and the two readers of each shared pass sit side by side. A reader whose checks fail is reported “failed” with its checks, and one that raises another fmsave error is reported “error” without the error’s text; the remaining readers still run. The report holds only structural facts, counts and rates, never names, uids or other values from the save.
Readers run here exactly as they would for any other caller, with their warnings suppressed for the length of the run: this report lists every check each reader ran, so a warning beside it would only repeat what it already says. Running them leaves the save no different from how a caller who read those tables himself would leave it. On a save opened the default way nothing raises for a failed check, so every table this decodes is kept: the reader calls a caller makes afterwards are free of charge, and say nothing further about a check this report has already reported.
Some readers share one decode: the players, contracts and suspensions of the player pass, the fixtures, league tables, competition rules and stadiums that all need the one streamed pass over the span, and the finance, staff, training and tactics pairs, each built by one pass of its own. When that shared decode raises before it finishes, its error is reported for every reader of the pass and the decode is not attempted again, so the span is streamed once however many of its readers report it. A reader that failed after its pass had been decoded failed on its own, and the others still run.
- Raises:
SaveClosedError – The save is closed.
- Parameters:
career_save (Save)
- Return type:
- class fmsave.ValidationReport(fmsave_version, python_version, os, game, build, known_build, section_schemas, readers, field_statuses)[source]¶
What validate_save found, holding no names, uids or other values from the save.
- Parameters:
- section_schemas¶
Schema number of every named section.
- Type:
- readers¶
Each reader’s outcome, in the order they ran.
- Type:
- field_statuses¶
Whether each public field’s meaning is “verified” or “unconfirmed”.
- Type:
- class fmsave.ReaderValidation(reader, status, record_count, gates, coverage, anomalies)[source]¶
How one reader fared in validate_save.
A reader is “failed” when a check of its own did not pass, and it is the only one reported so: the readers that join through it read the table it returned and are judged on their own checks.
A save opened with strict=True is reported differently, because a failed check stops the reader there and everything downstream of it with it. Players, contracts and suspensions are decoded in one pass, so when a check of any of the three fails, all three are reported “failed” and none of their tables is read, and each of them still lists its own gates: a reader can be “failed” while every one of its own gates passed. Fixtures, league tables, competition rules and stadiums share the one streamed pass over the span the same way, and are reported together when that pass itself is what failed. Four more pairs share a decode in the same way: finances with sponsorships, staff with staff lists, training with mentoring, and tactics with set pieces. A structural failure spreads the same way on any save, since no mode reads a table from a decode that produced none.
- Parameters:
- reader¶
The reader: “clubs”, “players”, “contracts”, “suspensions”, “managed_clubs”, “stages”, “competitions”, “fixtures”, “league_tables”, “transfer_windows”, “competition_rules”, “player_match_stats”, “stadiums”, “finances”, “sponsorships”, “affiliates”, “job_vacancies”, “staff”, “staff_lists”, “injury_types”, “injuries”, “training”, “mentoring”, “tactics”, “set_pieces” or “facilities”.
- Type:
- status¶
“ok” when the reader returned its table and every applied check of its own passed, “failed” when a check did not, and “error” when it raised another fmsave error.
- Type:
fmsave._checks.ReaderStatus
- record_count¶
How many records the reader decoded, or None when it did not get far enough to count them.
- Type:
int | None
- gates¶
The reader’s checks, in a fixed order; empty when the reader has no checks of its own, as the managed-club reader does not, or when it did not get far enough.
- Type:
- coverage¶
The table’s coverage (the share of non-None values per column); empty unless the status is “ok”.
- Type:
- anomalies¶
Counts of records the reader decoded but flags, such as unresolved teams.
- Type:
- class fmsave.ReaderCheck(reader, record_count, gates, anomalies)[source]¶
One reader’s evaluated checks, its record count and its anomaly counts.
It is what a GateCheckError carries, so a caller can read a failure from its numbers rather than from the message text.
- Parameters:
- gates¶
The reader’s checks, in a fixed order; empty when it has none of its own.
- Type:
- anomalies¶
Counts of records the reader decoded but flags, such as unresolved teams.
- Type:
Package metadata¶
- fmsave.__version__ = '0.4.0'¶
str(object=’’) -> str str(bytes_or_buffer[, encoding[, errors]]) -> str
Create a new string object from the given object. If encoding or errors is specified, then the object must expose a data buffer that will be decoded using the given encoding and error handler. Otherwise, returns the result of object.__str__() (if defined) or repr(object). encoding defaults to sys.getdefaultencoding(). errors defaults to ‘strict’.
- fmsave.OUTPUT_SCHEMA_VERSION = 2¶
int([x]) -> integer int(x, base=10) -> integer
Convert a number or string to an integer, or return 0 if no arguments are given. If x is a number, return x.__int__(). For floating point numbers, this truncates towards zero.
If x is not a number or if base is given, then x must be a string, bytes, or bytearray instance representing an integer literal in the given base. The literal can be preceded by ‘+’ or ‘-’ and be surrounded by whitespace. The base defaults to 10. Valid bases are 0 and 2-36. Base 0 means to interpret the base from the string as an integer literal. >>> int(‘0b100’, base=0) 4