Enums¶
The enums a record’s fields carry, the CodedValue wrapper that pairs a stored number with its
label, and the status types that say whether a field’s meaning is verified.
CodedValue¶
- class fmsave.CodedValue(label, raw)[source]¶
A number the save uses as a code, with its label.
Codes without a verified meaning get the enum’s UNKNOWN label and keep their raw number. Every coded enum has an UNKNOWN member with the value -1, which no stored code can equal.
- Parameters:
label (EnumT)
raw (int)
- label¶
The enum member for the code, or UNKNOWN.
- Type:
EnumT
Field status¶
- fmsave.field_status(model_class, field_name)[source]¶
Return whether the meaning of a public record field is verified or unconfirmed.
- Parameters:
- Raises:
KeyError – The field has no registered status, or a dotted path passes through a field that is not a group.
- Return type:
FieldStatus
- fmsave.FieldStatus = FieldStatus¶
Type alias.
Type aliases are created through the type statement:
type Alias = int
In this example, Alias and int will be treated equivalently by static type checkers.
At runtime, Alias is an instance of TypeAliasType. The __name__ attribute holds the name of the type alias. The value of the type alias is stored in the __value__ attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.
Type aliases can also be generic:
type ListOrSet[T] = list[T] | set[T]
In this case, the type parameters of the alias are stored in the __type_params__ attribute.
See PEP 695 for more information.
- fmsave.ReaderStatus = ReaderStatus¶
Type alias.
Type aliases are created through the type statement:
type Alias = int
In this example, Alias and int will be treated equivalently by static type checkers.
At runtime, Alias is an instance of TypeAliasType. The __name__ attribute holds the name of the type alias. The value of the type alias is stored in the __value__ attribute. It is evaluated lazily, so the value is computed only if the attribute is accessed.
Type aliases can also be generic:
type ListOrSet[T] = list[T] | set[T]
In this case, the type parameters of the alias are stored in the __type_params__ attribute.
See PEP 695 for more information.
Enums¶
- class fmsave.ClauseKind(*values)[source]¶
A contract clause’s kind.
Only kinds whose meaning is confirmed in game are named; every other code is UNKNOWN and keeps its raw number. Each code is a distinct kind. MINIMUM_FEE_RELEASE_DOMESTIC (0x12) and MINIMUM_FEE_RELEASE_DOMESTIC_HIGHER_DIVISION (0x11) are separate clauses the game words differently, “Minimum Fee Release Clause (Domestic)” and “Minimum Fee Release Clause (Domestic Clubs in Higher Division)”; no contract in the corpus carries both.
RELEGATION_RELEASE and NON_PROMOTION_RELEASE are the clauses the game calls a relegation release clause and a non promotion release clause, and each one’s value is a release fee like the other release kinds. Neither ever carries a parameter.
A clause’s value is the release fee for MINIMUM_FEE_RELEASE, RELEGATION_RELEASE, NON_PROMOTION_RELEASE, MINIMUM_FEE_RELEASE_FOREIGN, MINIMUM_FEE_RELEASE_DOMESTIC and MINIMUM_FEE_RELEASE_DOMESTIC_HIGHER_DIVISION, and the amount paid for APPEARANCE_FEE, SHUTOUT_BONUS, INTERNATIONAL_CAP_BONUS, UNUSED_SUBSTITUTE_FEE and SEASONAL_LANDMARK_COMBINED_GOALS_AND_ASSISTS. Every clause of those kinds carries one. TOP_DIVISION_RELEGATION_SALARY_DROP and OPTIONAL_EXTENSION_BY_CLUB carry a parameter instead and no value.
A clause’s parameter is a percentage for TOP_DIVISION_RELEGATION_SALARY_DROP, years for OPTIONAL_EXTENSION_BY_CLUB, the number of goals plus assists that earns the bonus for SEASONAL_LANDMARK_COMBINED_GOALS_AND_ASSISTS, and days to expiry from the contract start for MINIMUM_FEE_RELEASE_DOMESTIC and MINIMUM_FEE_RELEASE_DOMESTIC_HIGHER_DIVISION (None means no expiry). A minority of MINIMUM_FEE_RELEASE clauses carry a parameter too, and what it holds there is not confirmed.
- class fmsave.CompetitionRound(*values)[source]¶
A stage’s round in its competition, for the round codes whose meaning is confirmed.
A code is named only where a round label the game itself displayed was matched to that exact code. Every other code is UNKNOWN and keeps its raw number in CodedValue.raw, including codes that look like they continue a named run: a meaning is never carried across from a neighbouring code, because a wrong name is silently wrong while a raw number is plainly incomplete. Most codes a save uses are therefore UNKNOWN, among them the codes group stages and the qualifying rounds use.
A stage with no round at all reads as None rather than UNKNOWN: a league matchday and a league phase carry no round, and the matchday is a property of the fixture, not the stage.
- class fmsave.ContractEndSource(*values)[source]¶
Where a contract’s end date was read from.
CONTRACT is the contract record itself, which is where an end date belongs and where almost every one comes from. PLAYER_RECORD is the player’s own record, which fmsave falls back to when no contract record of the player’s gives up an end date. NONE is a contract whose end date neither source holds.
- class fmsave.CorporateFacilities(*values)[source]¶
How good a club’s corporate facilities are, in the game’s own words.
A member’s value is one code its word was displayed against; every code the word covers reads as that member and keeps its own raw number.
Nineteen codes are named by a screen. One club’s Facilities screen per code displayed that word against that exact stored number. FM26 screens named fifteen: 1 and 2 Basic, 5 Fairly Basic, 6, 7 and 9 Adequate, 10, 11 and 12 Average, 13 and 15 Good, and 17, 18, 19 and 20 Excellent. FM27 screens named four more: 0 None, 3 and 4 Fairly Basic, and 16 Excellent. Those four fit the order the FM26 words rise in, but no FM26 screen has shown them, and no FM27 screen has yet shown the other fifteen.
Two more are named by bracketing, which is not band inference. Code 8 sits between 7 and 9, both displayed Adequate, and code 14 between 13 and 15, both displayed Good, so on a scale whose words rise with the number each of those can only be the word its neighbours carry. Nothing else is filled in that way.
NONE is 0. The club whose screen showed None stores a zero in an otherwise empty block, which is how a club that has no corporate facilities at all reads.
- class fmsave.InjuryCause(*values)[source]¶
Whether an injury happened in training or in a match.
Both stored codes are named, each from the cause a player’s injury history displayed against rows carrying that exact code. The screen also shows a second line saying what the player was doing – jumping, tackling, wear and tear – and that detail is in no field fmsave reads: three different details share the training code alone.
Illness rows are the exception. A row for an illness – a virus, food poisoning, a cold – displays a vocabulary of its own, such as a cause of “Non-soccer”, and its stored codes do not follow this mapping. That is one measured counter-example, on an illness row, against agreement on every injury row checked.
- class fmsave.InjuryRecordKind(*values)[source]¶
Which of the save’s two injury stores a row came out of.
- class fmsave.InjurySeverity(*values)[source]¶
How bad an injury was, in the game’s own words.
All four stored codes are named, each from the severity a player’s injury history displayed against a dated row carrying that exact code.
Illness rows are the exception. A row for an illness – a virus, food poisoning, a cold – displays a vocabulary of its own and its stored codes do not follow this mapping: one such row stores the code an injury displays as Moderate while the game shows it as Minor. That is one measured counter-example, on an illness row, against agreement on every injury row checked.
- class fmsave.MatchOutcome(*values)[source]¶
How a match row ended. Derived by fmsave from the row’s counters, never read as a code.
- class fmsave.MatchPosition(*values)[source]¶
The position a player filled in one match, for the mask bits whose meaning is confirmed.
The save stores the position as a 16-bit mask carrying exactly one bit, and a member’s value here is that bit’s index, not the mask. Fourteen of the sixteen bits carry records on every save measured, so the mask covers a full set of positions; only one of them is named.
A bit earns a name only where a position label the game itself displayed was matched to that exact bit, and only the goalkeeper bit is. Every record of a player the game labels “GK” carries this bit, and all but one of the records carrying it, over every save measured, belongs to a player the save rates a goalkeeper. No other bit has such a label: the players whose displayed label would separate one bit from its neighbours play several positions, so their label fits more than one bit, and a bit named from a neighbouring bit’s meaning would be silently wrong where a raw mask is plainly incomplete.
What the other bits are is therefore left open, and the raw mask is kept on every record so a caller can group by it. Grouping by the raw mask is sound whatever the bits mean; reading a position into one is not.
A mask that is zero, carries a bit no label names, or carries more than one bit reads as UNKNOWN with its raw value kept. Fewer than five records per save carry a zero mask and none at all carries two bits.
- class fmsave.MatchSide(*values)[source]¶
Which side of a match a club was, the home one or the away one.
This is the club’s side, not the ground the match was played at: a match at a neutral ground still has a home club and an away club, and this says which of the two the row’s club was. Fixture.is_neutral_venue is the field about the ground.
Derived by fmsave, never read as a code.
- class fmsave.Mentality(*values)[source]¶
How attacking a tactic’s mentality is, in the game’s own words.
The seven codes are the seven mentalities the game offers, in the order it lists them, and the manager of the save they were read from confirmed the list against his own tactics: the tactic showing Attacking stores 6 and the two showing Balanced store 4. A code outside the seven is UNKNOWN and keeps its raw number.
- class fmsave.RulesBlockKind(*values)[source]¶
Which structure a competition-rules row was read from.
Derived by fmsave from where the row came from, never read as a code.
Every row is PREAMBLE. The save’s rules database does hold tagged groups of squad and financial rules, but nothing readable ties a group to a competition: seven routes were measured, including the positional one a preamble row’s competition comes from, which does not carry over because a group is not stored beside a league table. The groups found carry no name either, their content is database content identical on every save of one installed database, and no displayed label pins the meaning of the values they hold. So fmsave ships none of them rather than shipping guesses, and this enum names no member for them: reading them later adds a member and a kind of row, which takes nothing away from a caller.
- class fmsave.SeasonStatsKind(*values)[source]¶
The competition type a row covers, named after the player profile’s statistics rows.
OVERALL is “Overall (Club)”, the season total. The two calendar-year kinds reset on 1 January. Rows for another team the player played for this season use the first six kinds.
- class fmsave.SponsorType(*values)[source]¶
What kind of sponsorship a contract is, in the words of the game’s sponsorship screen.
The save groups its sponsorships into about twenty kinds. A label is given only where a club’s sponsorship screen displayed it against the row’s own stored kind, and only on the build whose screen did. On FM27 build 27.0.0 one club’s screen named seven codes: 1 Main Kit, 3 and 23 Stadium, 4 General, 8 Away Kit, 9 Additional Kit Sponsor and 11 Back of Shirt. Two codes showing one word may still differ in a way the screen does not show, so each keeps its raw number. Every other code, and every FM26 code, is UNKNOWN: no FM26 screen has named one yet.
A member’s value is the FM27 code it was first displayed against; the save’s build, not the value, decides which codes read as which member.
- class fmsave.SquadStatus(*values)[source]¶
The squad status agreed in a player’s contract, as shown on the contract screen.
It is the role the club agreed to, not a record of how much the player actually plays: a player on CUP_GOALKEEPER terms, for example, can still make league appearances all season. A code is named only where an in-game label confirms that exact code; a code an outside name table would name is not named on that basis alone. Every code that is not named is UNKNOWN and keeps its raw number.
The game offers two sets of labels, one for goalkeepers and one for everyone else, and they do not share every code. FIRST_CHOICE_GOALKEEPER, CUP_GOALKEEPER, DOMESTIC_CUP_GOALKEEPER, CONTINENTAL_CUP_GOALKEEPER, BACKUP and GOALKEEPER_EMERGENCY_BACKUP are offered only for goalkeepers, and every holder of them is one. EMERGENCY_BACKUP and GOALKEEPER_EMERGENCY_BACKUP are separate codes that the game displays with the same words, “Emergency Backup”, one for outfield players and one for goalkeepers, so the code says which list the status came from.
BREAKTHROUGH_PROSPECT, FUTURE_PROSPECT and YOUNGSTER are not in either list: they are youth statuses the senior contract screen does not offer.
- class fmsave.StaffJob(*values)[source]¶
A person’s job at his club, as a club’s All Staff screen shows it.
A code is named only where an in-game label confirms that exact code. Each named code matched one FM27 club’s All Staff screen, person for person, across its senior and under-18 staff and its board. A code no screen we hold has labelled is UNKNOWN and keeps its raw number, even where a label has been reported for it.
MANAGER is the job of whoever manages a team, the first team’s or a youth side’s.
- class fmsave.SuspensionScope(*values)[source]¶
What a ban covers, and so what the id the save stores beside it refers to.
The save writes one code per ban. Code 1 means the ban covers the one competition its id names; codes 5, 6 and 10 mean the ban covers every competition of the nation its id names. Any other code reads as UNKNOWN, and a ban with an unknown code carries no competition id and no nation id, because nothing says which the stored number is.
What separates codes 5, 6 and 10 from one another is not known; all three were measured to carry a nation id, so all three read as NATION and Suspension.scope_code keeps the code itself for a caller who wants to tell them apart.
- class fmsave.TacticPosition(*values)[source]¶
One position of a tactic’s formation.
A member’s value is the bit number set in a slot’s position mask, counting from zero. This is deliberately not the same enum as fmsave.models.matches.MatchPosition, whose codes come from a different field and are ordinals rather than bit numbers.
The eleven named bits were matched to the position labels one FM27 tactics screen shows, in possession and out of possession, through the player picked in each slot. In member order the screen showed them as GK, D (R), D (L), D (C), DM, M (R), M (L), AM (R), AM (L), AM (C) and ST (C). The column flags above the position bits, which tell the two halves of a pair apart, are not positions and stay out of this enum. Bits 1, 5, 6 and 10 appear in stored formations but on no screen compared, so they read UNKNOWN. FM26 stores the same field; there the players picked in each slot fit these labels, which is a corroboration rather than a screen comparison.
- class fmsave.Trait(*values)[source]¶
A named player trait; UNKNOWN keeps the bit number in CodedValue.raw.
A bit is named only where a trait the game itself displayed pins that exact bit. Two readings do that. Six bits come from player profiles, which list a player’s traits in ascending bit order, and that order is what ties a displayed trait to a bit. The other ten come from in-game trait panels read on 2026-09-16, one player per bit, each player’s bitmask holding exactly one bit no earlier reading had reached, so the one unaccounted trait his panel showed is that bit.
A name an outside name table would supply is not evidence for one, so every bit no displayed trait reaches is UNKNOWN and keeps its bit number. Neither is a name that merely looks likely from the bits around it, even though two such guesses were later borne out by a panel: what named those bits is the panel, not the guess.
- class fmsave.TrainingSession(*values)[source]¶
What a training session is, as an FM27 save numbers it.
A member’s value is the code the save stores. Eleven codes are named by a screen: an FM27 manager’s Training screen showed each of these names in full against the code stored for that slot, on every day of three weeks. MATCH is the slot the screen fills with the day’s fixture rather than a word.
Every other code is UNKNOWN and keeps its raw number. Codes 3, 4, 7, 200 to 203 and 304 were on the same screen with their names cut short, so they are not completed by guesswork; the codes other teams and later weeks use have not been seen named on a screen this project has checked.
An FM26 save numbers its sessions differently: its calendars hold neither the FM27 code for rest nor the one for a match, although its match days carry a code of their own. So no FM26 session is labelled: each reads UNKNOWN with its raw number.