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
- classmethod from_raw(enum_type, raw)[source]¶
Label a raw code from the save.
- Raises:
TypeError – enum_type has no UNKNOWN member with the value -1.
- Parameters:
- Return type:
CodedValue[LabelT]
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 the lowest code its word was displayed against; every code the word covers reads as that member and keeps its own raw number.
Fifteen codes are named by a screen. One club’s Facilities screen per code displayed that word against that exact stored number: 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.
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.
Codes 3, 4 and 16 are UNKNOWN, because no club in the save carries them: 3 and 4 are below the lowest Fairly Basic seen and 16 sits between a Good and an Excellent, so neither a screen nor a bracket names them. The reading that the words sit in fixed bands of the scale would name them, and it is a hypothesis rather than an observation, so it is not shipped.
- 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.SponsorType(*values)[source]¶
What kind of sponsorship a contract is.
Every code is UNKNOWN: the save groups its sponsorships into about twenty kinds, and no displayed label has pinned one of those codes yet.
- 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.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. Every code is UNKNOWN: no bit is named until a displayed formation pins it. 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.
- 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.