Records

The dataclass each reader’s Table holds, grouped by the reader family that produces it. Table itself, and the value-wrapper and metadata types shared across every reader, live on their own pages: see Table and Enums.

People

players(), contracts(), suspensions(), staff() and staff_lists().

class fmsave.Player(uid, name, first_name, last_name, common_name, full_name, legal_name, birth_date, age, nation_id, second_nation_ids, home_grown_nation_ids, home_grown_club_uids, home_grown_club_names, height_cm, ability, reputation, club_uid, club_name, club_short_name, club_nation_id, club_fa_nation_id, club_reputation, club_last_league_position, team_id, team_slot, team_club_uid, team_club_name, club_join_date, natural_positions, accomplished_positions, personality, attributes, raw_attributes, left_foot, right_foot, raw_left_foot, raw_right_foot, positions, transfer_value, transfer_value_state, raw_condition, raw_match_sharpness, traits, trait_bits, on_loan, loan_parent_club_uid, loan_parent_club_name, loan_start, loan_end, contract, suspensions)[source]

A player record from the save’s game database.

Person fields (name, birth date, nationality, home-grown ids, personality and traits) come from the player’s person block; they stay None or empty when no block validates.

Parameters:
uid

The player’s id in the game database (unconfirmed).

Type:

int

name

Display name: common name first, else first plus last name, else legal name (unconfirmed).

Type:

str | None

first_name

First name (unconfirmed).

Type:

str | None

last_name

Last name (unconfirmed).

Type:

str | None

common_name

Common (nickname) name, when the save stores one (unconfirmed).

Type:

str | None

full_name

First name plus last name, or None when either is missing (unconfirmed).

Type:

str | None

legal_name

The save’s stored legal name, or None when the save stores none (unconfirmed).

Type:

str | None

birth_date

Date of birth (unconfirmed).

Type:

datetime.date | None

age

Age at the save’s in-game clock date, or None when birth_date or the clock date is unreadable (unconfirmed).

Type:

int | None

nation_id

Id of the player’s primary nation (unconfirmed).

Type:

int | None

second_nation_ids

Ids of the player’s other eligible nations.

Type:

tuple[int, …]

home_grown_nation_ids

Ids of nations the player is considered home grown for.

Type:

tuple[int, …]

home_grown_club_uids

Uids of clubs the player is considered home grown for, in relation-list order; an entry is None when its club index does not resolve, but still keeps its position. They are Club.uid values and carry its status (unconfirmed).

Type:

tuple[int | None, …]

home_grown_club_names

Denormalised names for home_grown_club_uids, in the same order; None wherever home_grown_club_uids is None (unconfirmed).

Type:

tuple[str | None, …]

height_cm

Height in centimetres (unconfirmed).

Type:

int

ability

Current and potential ability.

Type:

fmsave.models.players.Ability

reputation

Reputation figures; never threshold on reputation.bucket.

Type:

fmsave.models.players.Reputation

club_uid

Uid of the club the player belongs to: the club fielding his registered team, which for a team an affiliate club stores is the club controlling it. None for a free agent or an unresolved team (unconfirmed).

Type:

int | None

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str | None

club_short_name

Denormalised short name of club_uid (unconfirmed).

Type:

str | None

club_nation_id

Denormalised league nation id of club_uid (unconfirmed).

Type:

int | None

club_fa_nation_id

Denormalised FA nation id of club_uid (unconfirmed).

Type:

int | None

club_reputation

Denormalised reputation of club_uid.

Type:

int | None

club_last_league_position

Denormalised last completed season league position of club_uid.

Type:

int | None

team_id

Id of the player’s registered team, or None for a free agent (unconfirmed).

Type:

int | None

team_slot

The team’s slot in club_uid’s team list, which counts that club’s own slots first and then the teams it controls at affiliate clubs; None when team_id does not resolve to a club (unconfirmed).

Type:

int | None

team_club_uid

Uid of the club whose record stores the player’s registered team, when another club controls that team, else None (unconfirmed).

Type:

int | None

team_club_name

Denormalised full name of team_club_uid; None wherever team_club_uid is (unconfirmed).

Type:

str | None

club_join_date

Date the player joined his current club; it disagrees with the contract start date for about half of players, so it is not a substitute for it (unconfirmed).

Type:

datetime.date | None

natural_positions

Position codes rated at least 18, best first.

Type:

tuple[str, …]

accomplished_positions

Position codes rated 15 to 17, best first. This is an fmsave classifier label, not text the game itself shows.

Type:

tuple[str, …]

personality

Personality profile, or None when no person block validates.

Type:

fmsave.models.players.Personality | None

attributes

The 52 non-foot attributes, on the 1 to 20 display scale.

Type:

fmsave.models.players.Attributes

raw_attributes

The same 52 attributes, on the 1 to 100 raw scale.

Type:

fmsave.models.players.Attributes

left_foot

Left foot strength, on the 1 to 20 display scale.

Type:

int

right_foot

Right foot strength, on the 1 to 20 display scale.

Type:

int

raw_left_foot

Left foot strength, on the 1 to 100 raw scale.

Type:

int

raw_right_foot

Right foot strength, on the 1 to 100 raw scale.

Type:

int

positions

The 15 position ratings.

Type:

fmsave.models.players.Positions

transfer_value

Transfer value in the save’s base currency, set only when transfer_value_state is OK.

Type:

int | None

transfer_value_state

How transfer_value was read from the save (unconfirmed).

Type:

fmsave.models.common.TransferValueState

raw_condition

Condition, on the 0 to 10000 scale the save stores it on. The game shows condition as a percentage, which is this divided by 100.

Type:

int

raw_match_sharpness

Match sharpness, on the 0 to 10000 scale the save stores it on. The game shows it as a percentage, which is this divided by 100.

Type:

int

traits

Named player traits; an unnamed bit is Trait.UNKNOWN with raw set to the bit number.

Type:

tuple[fmsave.models.common.CodedValue[fmsave.models.players.Trait], …]

trait_bits

The raw trait bitmask, or None when no person block validates.

Type:

int | None

on_loan

Whether the player is on loan from the club of his contract in effect: True when he is registered with another club’s team and the save holds a loan for him there that has not ended, False when it does not, and None when his club or his contract in effect is unknown. A player registered with a team his own club controls, such as a B team, is a player of that club, not on loan.

Type:

bool | None

loan_parent_club_uid

Uid of the club of the contract in effect when on_loan is True, else None. It is the contract’s club_uid under another name, so it carries the same status (unconfirmed).

Type:

int | None

loan_parent_club_name

Denormalised name of loan_parent_club_uid (unconfirmed).

Type:

str | None

loan_start

Date the loan began when on_loan is True, else None. It is also None when the loan’s stored start does not decode as a game date, which keeps an unreadable start from costing the player his loan; no save read so far holds such a loan.

Type:

datetime.date | None

loan_end

Date the loan ends when on_loan is True, else None.

Type:

datetime.date | None

contract

The player’s assembled contract, or None when no chain record and no fallback dates were found.

Type:

fmsave.models.contracts.Contract | None

suspensions

The player’s unserved suspensions, in the order the save stores them, including bans the game no longer displays; () when the player has none.

Type:

tuple[fmsave.models.suspensions.PlayerSuspension, …]

class fmsave.Attributes(crossing, dribbling, finishing, heading, long_shots, marking, off_the_ball, passing, penalty_taking, tackling, vision, handling, aerial_reach, command_of_area, communication, kicking, throwing, anticipation, decisions, one_on_ones, positioning, reflexes, first_touch, technique, flair, corners, teamwork, work_rate, long_throws, eccentricity, rushing_out, punching, acceleration, free_kick_taking, strength, stamina, pace, jumping_reach, leadership, dirtiness, balance, bravery, consistency, aggression, agility, important_matches, injury_proneness, versatility, natural_fitness, determination, composure, concentration)[source]

52 non-foot player attributes, shared by the display and raw scales.

Parameters:
  • crossing (int)

  • dribbling (int)

  • finishing (int)

  • heading (int)

  • long_shots (int)

  • marking (int)

  • off_the_ball (int)

  • passing (int)

  • penalty_taking (int)

  • tackling (int)

  • vision (int)

  • handling (int)

  • aerial_reach (int)

  • command_of_area (int)

  • communication (int)

  • kicking (int)

  • throwing (int)

  • anticipation (int)

  • decisions (int)

  • one_on_ones (int)

  • positioning (int)

  • reflexes (int)

  • first_touch (int)

  • technique (int)

  • flair (int)

  • corners (int)

  • teamwork (int)

  • work_rate (int)

  • long_throws (int)

  • eccentricity (int)

  • rushing_out (int)

  • punching (int)

  • acceleration (int)

  • free_kick_taking (int)

  • strength (int)

  • stamina (int)

  • pace (int)

  • jumping_reach (int)

  • leadership (int)

  • dirtiness (int)

  • balance (int)

  • bravery (int)

  • consistency (int)

  • aggression (int)

  • agility (int)

  • important_matches (int)

  • injury_proneness (int)

  • versatility (int)

  • natural_fitness (int)

  • determination (int)

  • composure (int)

  • concentration (int)

crossing

Crossing.

Type:

int

dribbling

Dribbling.

Type:

int

finishing

Finishing.

Type:

int

heading

Heading.

Type:

int

long_shots

Long shots.

Type:

int

marking

Marking.

Type:

int

off_the_ball

Off the ball movement.

Type:

int

passing

Passing.

Type:

int

penalty_taking

Penalty taking.

Type:

int

tackling

Tackling.

Type:

int

vision

Vision.

Type:

int

handling

Goalkeeping handling.

Type:

int

aerial_reach

Goalkeeping aerial reach.

Type:

int

command_of_area

Goalkeeping command of area.

Type:

int

communication

Goalkeeping communication.

Type:

int

kicking

Goalkeeping kicking.

Type:

int

throwing

Goalkeeping throwing.

Type:

int

anticipation

Anticipation.

Type:

int

decisions

Decisions.

Type:

int

one_on_ones

Goalkeeping one-on-ones.

Type:

int

positioning

Positioning (an attribute, not a position rating).

Type:

int

reflexes

Goalkeeping reflexes.

Type:

int

first_touch

First touch.

Type:

int

technique

Technique.

Type:

int

flair

Flair.

Type:

int

corners

Corners.

Type:

int

teamwork

Teamwork.

Type:

int

work_rate

Work rate.

Type:

int

long_throws

Long throws.

Type:

int

eccentricity

Goalkeeping eccentricity.

Type:

int

rushing_out

Goalkeeping tendency to rush out.

Type:

int

punching

Goalkeeping tendency to punch crosses.

Type:

int

acceleration

Acceleration.

Type:

int

free_kick_taking

Free kick taking.

Type:

int

strength

Strength.

Type:

int

stamina

Stamina.

Type:

int

pace

Pace.

Type:

int

jumping_reach

Jumping reach.

Type:

int

leadership

Leadership.

Type:

int

dirtiness

Dirtiness, a hidden attribute.

Type:

int

balance

Balance.

Type:

int

bravery

Bravery.

Type:

int

consistency

Consistency, a hidden attribute.

Type:

int

aggression

Aggression.

Type:

int

agility

Agility.

Type:

int

important_matches

Big-match temperament, a hidden attribute.

Type:

int

injury_proneness

Injury proneness, a hidden attribute.

Type:

int

versatility

Versatility, a hidden attribute.

Type:

int

natural_fitness

Natural fitness.

Type:

int

determination

Determination.

Type:

int

composure

Composure.

Type:

int

concentration

Concentration.

Type:

int

class fmsave.Ability(current, potential, potential_range_code)[source]

A player’s current and potential ability.

Parameters:
  • current (int)

  • potential (int | None)

  • potential_range_code (int | None)

current

Current ability (unconfirmed).

Type:

int

potential

Potential ability, or None when the save stores a range code instead (unconfirmed).

Type:

int | None

potential_range_code

The stored negative code when potential is unknown, else None (unconfirmed).

Type:

int | None

class fmsave.Reputation(bucket, home, current, world)[source]

A player’s reputation figures.

Never threshold on bucket: it is a coarse, lossy bucket that mis-orders players. Use current to rank players instead.

Parameters:
bucket

Coarse reputation bucket that loses precision (unconfirmed).

Type:

int

home

Home reputation.

Type:

int

current

Current reputation.

Type:

int

world

World reputation.

Type:

int

class fmsave.Positions(gk, sw, dl, dc, dr, dm, ml, mc, mr, aml, amc, amr, stc, wbl, wbr)[source]

The 15 position ratings, in the player rating array order.

This is not the tactic or per-match position mask order.

Parameters:
gk

Goalkeeper.

Type:

int

sw

Sweeper.

Type:

int

dl

Left back.

Type:

int

dc

Centre back.

Type:

int

dr

Right back.

Type:

int

dm

Defensive midfielder.

Type:

int

ml

Left midfielder.

Type:

int

mc

Central midfielder.

Type:

int

mr

Right midfielder.

Type:

int

aml

Attacking midfielder, left.

Type:

int

amc

Attacking midfielder, centre.

Type:

int

amr

Attacking midfielder, right.

Type:

int

stc

Striker.

Type:

int

wbl

Left wing back.

Type:

int

wbr

Right wing back.

Type:

int

class fmsave.Personality(adaptability, ambition, loyalty, pressure, professionalism, sportsmanship, temperament, controversy)[source]

A player’s personality profile.

Parameters:
  • adaptability (int)

  • ambition (int)

  • loyalty (int)

  • pressure (int)

  • professionalism (int)

  • sportsmanship (int)

  • temperament (int)

  • controversy (int)

adaptability

Adaptability.

Type:

int

ambition

Ambition.

Type:

int

loyalty

Loyalty.

Type:

int

pressure

Pressure.

Type:

int

professionalism

Professionalism.

Type:

int

sportsmanship

Sportsmanship.

Type:

int

temperament

Temperament.

Type:

int

controversy

Controversy.

Type:

int

class fmsave.Contract(player_uid, player_name, club_uid, club_name, team_id, wage, start, end, end_source, squad_status, kind, clauses, on_loan, loan_parent_club_uid, loan_parent_club_name, loan_start, loan_end, event_count, chain, chain_club_uids, chain_club_names, chain_club_uids_with_terms, chain_club_names_with_terms, unknown)[source]

A player’s contract as it stands at the save’s in-game date.

Every field below that a chain record fills comes from one record: the contract in effect, which is the record at the player’s own club that has started and has not ended, or else the record with the latest start on or before the in-game date. Records that start later are agreed future moves, such as pre-contracts and completed transfers that take effect later; they stay in chain but never fill these fields. When no record has started, those fields are None or empty and chain still lists every record.

Parameters:
player_uid

Uid of the player this contract belongs to (unconfirmed).

Type:

int

player_name

Denormalised name of player_uid (unconfirmed).

Type:

str | None

club_uid

Uid of the contracting club of the record in effect, or None when no record is in effect or its team does not resolve (unconfirmed).

Type:

int | None

club_name

Denormalised name of club_uid (unconfirmed).

Type:

str | None

team_id

Id of the contracting team of the record in effect, or None (unconfirmed).

Type:

int | None

wage

Weekly wage from the record in effect, in the save’s base currency (unconfirmed).

Type:

int | None

start

Contract start date, from the record in effect (unconfirmed).

Type:

datetime.date | None

end

Contract end date.

Type:

datetime.date | None

end_source

Where end was read from.

Type:

fmsave.models.common.ContractEndSource

squad_status

Squad status from the terms of the record in effect, the role agreed in the contract rather than how much the player plays, or None when that record gives no terms up.

Type:

fmsave.models.common.CodedValue[fmsave.models.contracts.SquadStatus] | None

kind

The contract’s type from the terms of the record in effect, or None when it gives no terms up.

Type:

fmsave.models.common.CodedValue[fmsave.models.contracts.ContractType] | None

clauses

Clauses from the terms of the record in effect, or () when it gives no terms up.

Type:

tuple[fmsave.models.contracts.Clause, …]

on_loan

Whether the player is on loan from the club of the contract in effect: True when he is registered with another club’s team and the save holds a loan for him there that has not ended, False when it does not, and None when his club or the contract in effect is unknown. A player registered with a team his own club controls, such as a B team, is a player of that club, not on loan.

Type:

bool | None

loan_parent_club_uid

Uid of the club of the contract in effect when on_loan is True, else None. It is club_uid under another name, so it carries the same status (unconfirmed).

Type:

int | None

loan_parent_club_name

Denormalised name of loan_parent_club_uid (unconfirmed).

Type:

str | None

loan_start

Date the loan began when on_loan is True, else None. It is also None when the loan’s stored start does not decode as a game date, which keeps an unreadable start from costing the player his loan; no save read so far holds such a loan.

Type:

datetime.date | None

loan_end

Date the loan ends when on_loan is True, else None.

Type:

datetime.date | None

event_count

Contract event count from the terms of the record in effect, or None (unconfirmed).

Type:

int | None

chain

Every chain record, oldest first (unconfirmed).

Type:

tuple[fmsave.models.contracts.ContractChainEntry, …]

chain_club_uids

Uid of the contracting club for each chain record, in the same order as chain; None where the team does not resolve (unconfirmed).

Type:

tuple[int | None, …]

chain_club_names

Denormalised names for chain_club_uids, in the same order (unconfirmed).

Type:

tuple[str | None, …]

chain_club_uids_with_terms

Uids of the clubs of the chain records that carry their agreed terms and whose team resolves, in chain order (unconfirmed).

Type:

tuple[int, …]

chain_club_names_with_terms

Denormalised names for chain_club_uids_with_terms, in the same order (unconfirmed).

Type:

tuple[str | None, …]

unknown

Numeric fields with no known meaning, from the terms of the record in effect; a key is present only when its value was read (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.Clause(kind, parameter, value)[source]

One clause attached to a contract, such as a release fee, an extension option or a bonus.

Every named kind is confirmed in game, and so is what a clause’s value holds for each of them (see ClauseKind). A status covers a field across every kind, so parameter still ships unconfirmed: a minority of MINIMUM_FEE_RELEASE clauses carry one whose meaning no in-game reading pins.

Parameters:
kind

The clause’s kind.

Type:

fmsave.models.common.CodedValue[fmsave.models.contracts.ClauseKind]

parameter

The clause’s parameter, whose meaning depends on kind; None when the save stores no parameter (unconfirmed).

Type:

int | None

value

The clause’s money value, in the save’s own money unit and not converted; None when the save stores no value.

Type:

int | None

class fmsave.ContractChainEntry(club_uid, club_name, team_id, wage, start, end, has_terms)[source]

One record in a player’s registration chain.

Parameters:
  • club_uid (int | None)

  • club_name (str | None)

  • team_id (int)

  • wage (int)

  • start (date | None)

  • end (date | None)

  • has_terms (bool)

club_uid

Uid of the contracting club, or None when the team does not resolve (unconfirmed).

Type:

int | None

club_name

Denormalised name of club_uid (unconfirmed).

Type:

str | None

team_id

Id of the contracting team, exactly as stored (unconfirmed).

Type:

int

wage

Weekly wage, in the save’s base currency (unconfirmed).

Type:

int

start

Start date of this spell (unconfirmed).

Type:

datetime.date | None

end

End date from this record’s own terms, or None when it gives no terms up or stores no end.

Type:

datetime.date | None

has_terms

Whether this record carries the terms agreed in it: an end date, a squad status, a contract type and the clauses. A record that does not is still a spell at a club, with its wage and its start date (unconfirmed).

Type:

bool

class fmsave.PlayerSuspension(scope, scope_code, competition_id, competition_name, nation_id, issued_date)[source]

One unserved suspension, as listed on its player.

Parameters:
scope

What the ban covers: one competition, a whole nation, or UNKNOWN for a scope code never measured.

Type:

fmsave.models.suspensions.SuspensionScope

scope_code

The scope code exactly as the save stores it: 1 for a competition-wide ban, 5, 6 or 10 for a nation-wide one. What separates 5, 6 and 10 is not known.

Type:

int

competition_id

Id of the competition the ban applies to, in the stage id space, so it joins stages(), competitions(), fixtures(), league_tables() and player_match_stats(). None unless the scope is COMPETITION.

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names. A competition the game itself created during the career has a database id no name source outside the save carries, so some rows stay unnamed even with a full map (unconfirmed).

Type:

str | None

nation_id

Id of the nation whose competitions the ban covers, in the same id space as Club.nation_id and Player.nation_id. None unless the scope is NATION.

Type:

int | None

issued_date

Date the ban was issued.

Type:

datetime.date

class fmsave.Suspension(player_uid, player_name, club_uid, club_name, scope, scope_code, competition_id, competition_name, nation_id, issued_date, unknown)[source]

One unserved suspension, with its player and the club the player belongs to.

A ban the game no longer displays is listed here too.

Parameters:
player_uid

Uid of the suspended player, copied from the decoded player (unconfirmed).

Type:

int

player_name

Denormalised name of player_uid (unconfirmed).

Type:

str | None

club_uid

Uid of the club the player belongs to, which for a player registered with a team another club controls is that controlling club; None for a free agent or a team that does not resolve to a club. Copied from the decoded player (unconfirmed). It is the club the player belongs to now, which for an old ban need not be the club he was banned at.

Type:

int | None

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str | None

scope

What the ban covers: one competition, a whole nation, or UNKNOWN for a scope code never measured.

Type:

fmsave.models.suspensions.SuspensionScope

scope_code

The scope code exactly as the save stores it: 1 for a competition-wide ban, 5, 6 or 10 for a nation-wide one. What separates 5, 6 and 10 is not known.

Type:

int

competition_id

Id of the competition the ban applies to, in the stage id space, so it joins stages(), competitions(), fixtures(), league_tables() and player_match_stats(). None unless the scope is COMPETITION.

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names. A competition the game itself created during the career has a database id no name source outside the save carries, so some rows stay unnamed even with a full map (unconfirmed).

Type:

str | None

nation_id

Id of the nation whose competitions the ban covers, in the same id space as Club.nation_id and Player.nation_id. None unless the scope is NATION.

Type:

int | None

issued_date

Date the ban was issued.

Type:

datetime.date

unknown

Numeric fields with no known meaning (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.Staff(uid, is_human_manager, name, first_name, last_name, common_name, full_name, legal_name, birth_date, age, nation_id, club_uid, club_name, team_id, team_slot, listed_club_uid, listed_club_name, in_club_lists, list_indexes, has_contract, wage, contract_start, contract_end, ability, personality, attributes, preferences, unknown)[source]

One member of staff at one club.

A person listed by a club’s affiliate side and paid by the club itself is one row at the club, 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 wage, contract_start and contract_end are None rather than zero.

unknown[“contract_e36”] and the three codes beside it are not player squad statuses: they come from the same bytes but take different values on staff, and no displayed label has named one, so they ship as raw numbers and never as a SquadStatus.

The job title is not readable, and `unknown[“r4”]` is not it. That byte was the one candidate for it, and a club’s staff screen ruled it out: over 78 displayed rows, 8 of the 16 codes on show carried two or more different job titles, 7 titles appeared under two or three different codes, and one code carried seven titles on its own. So r4 keeps its raw number for good rather than becoming a named role, and the codes that happen to sit on one title at one club are not evidence of a meaning the field demonstrably does not have.

The human manager is a row with is_human_manager true. His object is laid out differently, so ability, preferences and every unknown key read from the ability block are absent for him, while his name, birth date, personality and contract read normally.

Parameters:
uid

The person’s unique id, as the save stores it; whether the game shows this value or the one above it has not been read (unconfirmed).

Type:

int

is_human_manager

Whether this is the save’s own human manager (unconfirmed).

Type:

bool

name

Common name, else first and last name, else legal name (unconfirmed).

Type:

str | None

first_name

First name (unconfirmed).

Type:

str | None

last_name

Surname (unconfirmed).

Type:

str | None

common_name

The name the game shows in place of the full name (unconfirmed).

Type:

str | None

full_name

First and last name together, when both are known (unconfirmed).

Type:

str | None

legal_name

The stored legal name (unconfirmed).

Type:

str | None

birth_date

Date of birth (unconfirmed).

Type:

datetime.date | None

age

Age at the save’s in-game date (unconfirmed).

Type:

int | None

nation_id

Nation id (unconfirmed).

Type:

int | None

club_uid

Uid of the club the person works for.

Type:

int

club_name

Denormalised full name of club_uid.

Type:

str

team_id

The team his contract registers him with, None without a contract.

Type:

int | None

team_slot

That team’s slot in the club’s team list, None without a contract (unconfirmed).

Type:

int | None

listed_club_uid

The club whose own staff list holds him, which is club_uid unless an affiliate side lists him; None when no list holds him (unconfirmed).

Type:

int | None

listed_club_name

Denormalised full name of listed_club_uid; None wherever listed_club_uid is (unconfirmed).

Type:

str | None

in_club_lists

Whether a club’s staff list holds him at all (unconfirmed).

Type:

bool

list_indexes

Which of the three lists hold him, 0 to 2, empty when none does (unconfirmed).

Type:

tuple[int, …]

has_contract

Whether a contract at club_uid was read for him (unconfirmed).

Type:

bool

wage

What the contract pays, per week, in the save’s base currency; None without a contract.

Type:

int | None

contract_start

The day the contract started.

Type:

datetime.date | None

contract_end

The day it ends, None when the save stores none (unconfirmed).

Type:

datetime.date | None

ability

Current and potential ability, None when the object carries no readable ability block, as the human manager’s does not.

Type:

fmsave.models.players.Ability | None

personality

The eight personality values, None without a name block.

Type:

fmsave.models.players.Personality | None

attributes

The one rated attribute located, None without a name block.

Type:

fmsave.models.staff.StaffAttributes | None

preferences

The named preferences, None when the object carries no readable ability block.

Type:

fmsave.models.staff.StaffPreferences | None

unknown

Numeric fields with no known meaning (unconfirmed). The ability-block keys (entry_count, r4 to r12 and every preference_slot_*) are absent when that block does not read, and the contract_* keys when no contract was read.

Type:

collections.abc.Mapping[str, int]

class fmsave.StaffAttributes(adaptability)[source]

The one rated staff attribute located in the save.

A staff profile rates about twenty attributes; only this one has been found, and it is the same byte as Staff.personality.adaptability, so the two always agree.

Parameters:

adaptability (int)

adaptability

Adaptability, on the same 1 to 20 scale a player’s is.

Type:

int

class fmsave.StaffPreferences(attacking, business, directness, interference, patience, trigger_press, resources, buying_players, mind_games, flexibility, hardness_of_training, squad_rotation, tempo, width)[source]

How a member of staff prefers a team to be run, each on a 1 to 20 scale.

These are preferences, not rated attributes: a high directness says what the person wants from a side, never how good he is at anything. Fourteen of the save’s twenty-six slots have a name; the rest ship as Staff.unknown[“preference_slot_<i>”].

Parameters:
  • attacking (int)

  • business (int)

  • directness (int)

  • interference (int)

  • patience (int)

  • trigger_press (int)

  • resources (int)

  • buying_players (int)

  • mind_games (int)

  • flexibility (int)

  • hardness_of_training (int)

  • squad_rotation (int)

  • tempo (int)

  • width (int)

attacking

How attacking a side he wants.

Type:

int

business

How commercial he wants the club to be.

Type:

int

directness

How direct he wants the passing.

Type:

int

interference

How much he involves himself in other people’s work.

Type:

int

patience

How patient he is with a plan.

Type:

int

trigger_press

How eagerly he wants the side to press.

Type:

int

resources

How freely he wants the club’s resources spent.

Type:

int

buying_players

How readily he wants players bought.

Type:

int

mind_games

How much he plays on an opponent’s mind.

Type:

int

flexibility

How willing he is to change an approach.

Type:

int

hardness_of_training

How hard he wants the side trained.

Type:

int

squad_rotation

How much he wants the side rotated.

Type:

int

tempo

How quickly he wants the side to play.

Type:

int

width

How wide he wants the side to play.

Type:

int

class fmsave.StaffList(club_uid, club_name, list_index, staff_uids, staff_names)[source]

One of the three staff lists a club record holds, for one club.

A club that lists nobody has no row here at all; a club that lists somebody has all three rows, empty lists included. The lists split a club’s staff into groups, and the codes their members carry differ from list to list.

The three lists look like the three departments a club’s staff screen shows. On one club, the screen’s medical, coaching and recruitment panels and the 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 makes list 0 medical, list 1 coaching and list 2 recruitment. It is arithmetic on one club rather than a label on a list, so it is recorded here and list_index stays a number.

People a list holds who turn out to be players are dropped, and 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.

Parameters:
club_uid

Uid of the club whose record holds the list (unconfirmed).

Type:

int

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str

list_index

Which of the club’s three lists this is, 0 to 2 (unconfirmed).

Type:

int

staff_uids

The uid of each person the list holds, in stored order. They are Staff.uid values and join to the staff table (unconfirmed).

Type:

tuple[int, …]

staff_names

Each of those people’s name, aligned with staff_uids (unconfirmed).

Type:

tuple[str | None, …]

Clubs

clubs(), facilities(), stadiums(), finances(), sponsorships(), affiliates() and job_vacancies().

class fmsave.Club(uid, name, short_name, nation_id, fa_nation_id, city_id, teams, parent_club_uid, parent_club_name, reputation, last_league_position)[source]

A club from the save’s game database.

Club names are not unique: look clubs up by uid.

Parameters:
  • uid (int)

  • name (str)

  • short_name (str)

  • nation_id (int)

  • fa_nation_id (int)

  • city_id (int | None)

  • teams (tuple[Team, ...])

  • parent_club_uid (int | None)

  • parent_club_name (str | None)

  • reputation (int | None)

  • last_league_position (int | None)

uid

The club’s id in the game database (unconfirmed).

Type:

int

name

Full club name (unconfirmed).

Type:

str

short_name

Short club name (unconfirmed).

Type:

str

nation_id

Id of the nation whose league the club plays in (unconfirmed).

Type:

int

fa_nation_id

Id of the nation whose football association the club belongs to. It differs from nation_id for a club that plays in another nation’s league (unconfirmed).

Type:

int

city_id

Id of the club’s city, or None when the save stores the missing-id marker instead (unconfirmed).

Type:

int | None

teams

The club’s own teams in stored order, including slots no player is registered with, then the teams of the affiliate clubs it controls; an empty tuple when the team list cannot be read (unconfirmed).

Type:

tuple[fmsave.models.clubs.Team, …]

parent_club_uid

Uid of the club that controls this one, when another club lists one of this club’s teams among its affiliate teams, else None (unconfirmed).

Type:

int | None

parent_club_name

Denormalised full name of parent_club_uid; None wherever parent_club_uid is (unconfirmed).

Type:

str | None

reputation

Club reputation from 1 to 10000, or None when the save holds no valid value.

Type:

int | None

last_league_position

League position at the end of the last completed season, or None when the save holds none. It is not the position in the current table.

Type:

int | None

class fmsave.Team(team_id, slot, club_uid, is_affiliate)[source]

One team in a club’s team list.

A club’s team list holds its own stored team slots, in stored order, then the teams it fields at the clubs it controls, such as a B or C team stored as a club of its own.

Parameters:
team_id

The team’s id, which player records use to name their team (unconfirmed).

Type:

int

slot

Position in the club’s team list, 0 for the first entry. Which side each own slot holds, such as a reserve or youth team, is not known (unconfirmed).

Type:

int

club_uid

Uid of the club whose record stores this team: the club listing it for its own teams, and the affiliate club itself for an affiliate team (unconfirmed).

Type:

int

is_affiliate

Whether the team belongs to an affiliate club the listing club controls (unconfirmed).

Type:

bool

class fmsave.ClubFacilities(club_uid, club_name, corporate_facilities)[source]

One club’s facility ratings, as its own record stores them.

Parameters:
club_uid

Uid of the club (unconfirmed).

Type:

int

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str

corporate_facilities

How good the club’s corporate facilities are. Seventeen of the twenty codes carry a word, and the three no club in the save carries are UNKNOWN with the raw number kept.

Type:

fmsave.models.common.CodedValue[fmsave.models.facilities.CorporateFacilities]

class fmsave.Stadium(uid, name, all_seater_capacity, expansion_capacity, capacity, owner_club_uid, owner_club_name, home_club_uids, home_club_names, pitch_length_dm, pitch_width_dm, pitch_min_length_dm, pitch_min_width_dm, pitch_max_length_dm, pitch_max_width_dm, built_date, rebuilt_date, unknown)[source]

One ground the save’s database holds.

Parameters:
  • uid (int)

  • name (str | None)

  • all_seater_capacity (int)

  • expansion_capacity (int)

  • capacity (int | None)

  • owner_club_uid (int | None)

  • owner_club_name (str | None)

  • home_club_uids (tuple[int, ...])

  • home_club_names (tuple[str, ...])

  • pitch_length_dm (int)

  • pitch_width_dm (int)

  • pitch_min_length_dm (int)

  • pitch_min_width_dm (int)

  • pitch_max_length_dm (int)

  • pitch_max_width_dm (int)

  • built_date (date | None)

  • rebuilt_date (date | None)

  • unknown (Mapping[str, int])

uid

The ground’s uid, which is the stored value plus one, the same convention club uids follow. A stadium uid that equals a club uid is a coincidence: the two are separate id spaces and must never be joined on (unconfirmed).

Type:

int

name

The ground’s name as the save stores it inline, or None when the save stores none, which is the case for nearly every ground: the game takes the name from its installed database instead (unconfirmed).

Type:

str | None

all_seater_capacity

How many seated spectators the ground holds.

Type:

int

expansion_capacity

The capacity the ground would hold expanded, which is at or above the all-seater capacity on all but a handful of grounds.

Type:

int

capacity

A further capacity the save stores, empty where the save stores zero, which is about four grounds in five (unconfirmed).

Type:

int | None

owner_club_uid

Uid of the club that owns the ground, or None when the save names no owner or names a club it does not list. A ground with no stored owner is the council’s: the game’s own facilities screen shows one such ground as owned by the council, so an empty owner is an answer rather than a gap. Owning a ground is not the same as playing at it (unconfirmed).

Type:

int | None

owner_club_name

Denormalised name of owner_club_uid, or None when there is no owner or the owner does not resolve (unconfirmed).

Type:

str | None

home_club_uids

Uids of the clubs whose home ground this is, in ascending order. It comes from the fixture calendar, not from any stored link: a club has a home ground once the calendar records enough first-team home matches for it, and that ground is the one it used most. It is empty for a ground no club uses often enough, and a ground several clubs share lists each of them (unconfirmed).

Type:

tuple[int, …]

home_club_names

Denormalised names of home_club_uids, in the same order (unconfirmed).

Type:

tuple[str, …]

pitch_length_dm

The pitch’s length in decimetres.

Type:

int

pitch_width_dm

The pitch’s width in decimetres.

Type:

int

pitch_min_length_dm

The shortest pitch length the ground allows (unconfirmed).

Type:

int

pitch_min_width_dm

The narrowest pitch width the ground allows (unconfirmed).

Type:

int

pitch_max_length_dm

The longest pitch length the ground allows.

Type:

int

pitch_max_width_dm

The widest pitch width the ground allows.

Type:

int

built_date

The date the ground was built, or None when the save stores none. A few grounds carry a date after the save’s own clock.

Type:

datetime.date | None

rebuilt_date

The date the ground was last rebuilt, or None when the save stores none.

Type:

datetime.date | None

unknown

Numeric fields with no known meaning: two capacity-like words, a small code, a third date as one little-endian int, and the whole flags byte whose 0x10 bit is what says the row carries its name inline (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.FinanceMonth(club_uid, club_name, month, balance, transfer_budget_allocated, transfer_budget_remaining, wage_budget_weekly, wage_payroll_weekly, income_excluding_transfers, net_transfers, wage_bill, net, expenditure_excluding_transfers, total_income, total_expenditure)[source]

One club’s money in one month.

Parameters:
  • club_uid (int)

  • club_name (str)

  • month (date)

  • balance (int)

  • transfer_budget_allocated (int)

  • transfer_budget_remaining (int)

  • wage_budget_weekly (int)

  • wage_payroll_weekly (int)

  • income_excluding_transfers (int)

  • net_transfers (int)

  • wage_bill (int)

  • net (int)

  • expenditure_excluding_transfers (int)

  • total_income (int)

  • total_expenditure (int)

club_uid

Uid of the club (unconfirmed).

Type:

int

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str

month

First day of the month the row covers, worked out from the save’s own date: the last row of a club is the month before the save’s month, and a club’s two oldest rows landed on the months its own finance screen labelled them with.

Type:

datetime.date

balance

The club’s balance at the end of the month.

Type:

int

transfer_budget_allocated

A transfer figure with no confirmed meaning (unconfirmed). It is not the budget the board allocated: a club’s own finances screen showed a budget well above this figure with almost none of it spent, and this figure is below transfer_budget_remaining on about a quarter of rows.

Type:

int

transfer_budget_remaining

What is left of the transfer budget.

Type:

int

wage_budget_weekly

The wage budget, per week; a club’s screen showed this times 52.

Type:

int

wage_payroll_weekly

The wages being paid, per week; likewise times 52 on the screen.

Type:

int

income_excluding_transfers

The month’s income other than transfer fees (unconfirmed).

Type:

int

net_transfers

The month’s transfer flow, positive for a net spend (unconfirmed).

Type:

int

wage_bill

The month’s wage expenditure (unconfirmed).

Type:

int

net

The month’s income less its expenditure.

Type:

int

expenditure_excluding_transfers

The month’s expenditure other than transfer fees (unconfirmed).

Type:

int

total_income

The month’s total income (unconfirmed).

Type:

int

total_expenditure

The month’s total expenditure (unconfirmed).

Type:

int

class fmsave.Sponsorship(club_uid, club_name, kind, start, end, total_value, annual_value, unknown)[source]

One sponsorship contract of one club, running or ended.

Parameters:
club_uid

Uid of the club (unconfirmed).

Type:

int

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str

kind

What kind of sponsorship it is; every code is UNKNOWN (unconfirmed).

Type:

fmsave.models.common.CodedValue[fmsave.models.finances.SponsorType]

start

The day the contract starts.

Type:

datetime.date | None

end

The day it ends.

Type:

datetime.date | None

total_value

The contract’s whole value, which is the annual value times its whole years on about 19 contracts in 20.

Type:

int

annual_value

What it pays a year; zero on a contract that has ended.

Type:

int

unknown

Numeric fields with no known meaning (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.AffiliateGroup(group_index, club_uids, club_names)[source]

One stored group of clubs, with its members resolved to clubs where they resolve.

Parameters:
group_index

The group’s own place in the section, counting from zero in stored order. It is fmsave’s own index and not a value the save keeps: the groups carry no id of their own.

Type:

int

club_uids

Uid of each member, in stored order, None for a member whose stored club index no club record claims, which is about one member in sixty (unconfirmed).

Type:

tuple[int | None, …]

club_names

Denormalised full name of each entry of club_uids, aligned with it and None wherever that uid is (unconfirmed).

Type:

tuple[str | None, …]

class fmsave.JobVacancy(team_id, club_uid, club_name, team_slot, advertised_date, competition_id, competition_name, league_position, unknown)[source]

One job the save’s job-centre feed holds.

Parameters:
  • team_id (int | None)

  • club_uid (int | None)

  • club_name (str | None)

  • team_slot (int | None)

  • advertised_date (date | None)

  • competition_id (int | None)

  • competition_name (str | None)

  • league_position (int | None)

  • unknown (Mapping[str, int])

team_id

The team the job is at, None where the record names no team.

Type:

int | None

club_uid

Uid of the club that fields team_id, None for a team no club lists and wherever team_id is.

Type:

int | None

club_name

Denormalised full name of club_uid, None wherever club_uid is.

Type:

str | None

team_slot

The team’s place in that club’s own team list, 0 for its first team; None when the team resolves to no club (unconfirmed).

Type:

int | None

advertised_date

Date the vacancy was advertised, None when the stored date does not decode. The time of day the save stores with it is in unknown[“advertised_slot”].

Type:

datetime.date | None

competition_id

Id of the competition the job’s team plays in, in the stage id space; None where the record names none (unconfirmed).

Type:

int | None

competition_name

Denormalised name of competition_id, None unless the save was opened with a name map that names it (unconfirmed).

Type:

str | None

league_position

The team’s current position in that competition’s table, None where the record stores none, which is about a quarter of rows (unconfirmed).

Type:

int | None

unknown

Numeric fields with no known meaning: the role code the vacancy’s job title comes from, the advertised date’s time-of-day slot, a second date whose meaning is not settled (its raw four bytes as one little-endian int, not a decoded date), an attribute of the competition, and a 0/1 flag (unconfirmed).

Type:

collections.abc.Mapping[str, int]

Competitions

stages(), competitions(), fixtures(), league_tables(), transfer_windows(), competition_rules() and player_match_stats().

class fmsave.Stage(id, competition_id, competition_name, group_id, round, previous_stage_id, unknown)[source]

One stage of one competition: a league season, a cup round, or one leg of a tie.

Parameters:
id

The stage id, which every fixture, league-table group and per-match record joins through.

Type:

int

competition_id

Id of the competition this stage belongs to, in the stage id space; None when the row stores no competition, and None for the one out-of-band id every save carries, which is a marker rather than a competition.

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names (unconfirmed).

Type:

str | None

group_id

Id of the group or section within the competition, or None when the stage has none.

Type:

int | None

round

The stage’s round, or None when the stage has no round, as a league matchday and a league phase do not. Codes with no confirmed meaning keep their raw number and are labelled UNKNOWN.

Type:

fmsave.models.common.CodedValue[fmsave.models.competitions.CompetitionRound] | None

previous_stage_id

The stage id the row claims comes before this one; None when the row stores the missing value. It equals id - 1 on every row seen, and the reader’s own row test requires that, so it is not independent evidence of a chain between stages (unconfirmed).

Type:

int | None

unknown

Numeric fields with no known meaning. “s25” is data, not a sentinel: it takes small values on several hundred rows per save. “s29” is the missing value on almost every row and carries small numbers on the rest (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.Competition(id, database_id, name, stage_ids)[source]

One competition, as the stage table names it.

Parameters:
id

The competition id, in the stage id space (not the id space a suspension’s competition id belongs to).

Type:

int

database_id

The competition’s id in the game’s own editor database, which is stable across saves and is what an external name source is keyed on; None when the save holds no such link (unconfirmed).

Type:

int | None

name

The competition’s name, which is None unless a name map is supplied: the save stores no competition names, and fmsave ships none (unconfirmed).

Type:

str | None

stage_ids

The ids of this competition’s stages, in the order the stage table stores them, which is ascending on every save seen.

Type:

tuple[int, …]

class fmsave.Fixture(stage_id, competition_id, competition_name, round, round_index, date, kick_off_time, season_start_year, home_team_id, home_club_uid, home_club_name, home_club_short_name, home_team_slot, away_team_id, away_club_uid, away_club_name, away_club_short_name, away_team_slot, home_goals, away_goals, played, is_neutral_venue, stadium_uid, stadium_name, match_record_id, match_rules_template, unknown)[source]

One match in the save’s fixture calendar.

Parameters:
  • stage_id (int | None)

  • competition_id (int | None)

  • competition_name (str | None)

  • round (CodedValue[CompetitionRound] | None)

  • round_index (int | None)

  • date (date | None)

  • kick_off_time (time | None)

  • season_start_year (int | None)

  • home_team_id (int)

  • home_club_uid (int | None)

  • home_club_name (str | None)

  • home_club_short_name (str | None)

  • home_team_slot (int | None)

  • away_team_id (int)

  • away_club_uid (int | None)

  • away_club_name (str | None)

  • away_club_short_name (str | None)

  • away_team_slot (int | None)

  • home_goals (int | None)

  • away_goals (int | None)

  • played (bool)

  • is_neutral_venue (bool | None)

  • stadium_uid (int | None)

  • stadium_name (str | None)

  • match_record_id (int | None)

  • match_rules_template (tuple[int, ...])

  • unknown (Mapping[str, int])

stage_id

The stage this match belongs to, which is the only route to its competition; None when the record stores no stage.

Type:

int | None

competition_id

Id of the competition, in the stage id space (not the id space a suspension’s competition id belongs to), reached through the stage; None when the record has no stage, the stage is not in the stage table, or the stage names no competition.

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names (unconfirmed).

Type:

str | None

round

The stage’s round, or None when the match has no stage or its stage carries no round, as a league matchday and a league phase do not. Codes with no confirmed meaning keep their raw number and are labelled UNKNOWN.

Type:

fmsave.models.common.CodedValue[fmsave.models.competitions.CompetitionRound] | None

round_index

The match’s zero-based index within its round, so a matchday is this plus one; None when the record stores the no-round value.

Type:

int | None

date

The kick-off date, or None when the stored date does not decode.

Type:

datetime.date | None

kick_off_time

The kick-off time of day, or None when the stored slot does not land inside a day.

Type:

datetime.time | None

season_start_year

The calendar year the season started in, or None when the record stores none, as some friendlies do.

Type:

int | None

home_team_id

The home side’s team id, exactly as stored.

Type:

int

home_club_uid

Uid of the club fielding the home team, or None when no club lists that team.

Type:

int | None

home_club_name

Denormalised name of home_club_uid, or None when the team does not resolve.

Type:

str | None

home_club_short_name

Denormalised short name of home_club_uid, or None when the team does not resolve.

Type:

str | None

home_team_slot

The home team’s slot in its club’s team list, 0 for the first entry, or None when the team does not resolve.

Type:

int | None

away_team_id

The away side’s team id, exactly as stored.

Type:

int

away_club_uid

Uid of the club fielding the away team, or None when no club lists that team.

Type:

int | None

away_club_name

Denormalised name of away_club_uid, or None when the team does not resolve.

Type:

str | None

away_club_short_name

Denormalised short name of away_club_uid, or None when the team does not resolve.

Type:

str | None

away_team_slot

The away team’s slot in its club’s team list, 0 for the first entry, or None when the team does not resolve.

Type:

int | None

home_goals

Goals the home side scored, or None when the save no longer holds the score. A played match with no goals means the score was not retained, not that the match finished goalless. The calendar itself stores no score: it is read from separate records the save keeps for only part of a career, so about a quarter of played matches carry one.

Type:

int | None

away_goals

Goals the away side scored, or None when the save no longer holds the score, as for home_goals.

Type:

int | None

played

Whether the save marks the match as played.

Type:

bool

is_neutral_venue

Whether the match was played away from the home club’s usual ground: True when its ground differs from the one that club used most in this season, False when it matches, and None when the ground is unknown or the club played too few home matches this season to say which ground is usual. This is about the ground and not about which club counts as the home one: the home club is home_team_id whether the ground is neutral or not, and LeagueTableMatch.home_or_away is the field that says which side of a match a club was. The season is the stored season_start_year, which some friendlies leave empty, and every such match a club played shares one vote; a club that moved ground mid-career therefore has its earlier seasonless matches judged against the later ground.

Type:

bool | None

stadium_uid

Uid of the ground the match is played at, from the stadium table; None when the record stores no ground or one the table does not hold. The join itself is exact, but a stadium uid is not confirmed by any displayed label, so this field can be no stronger than the uid it carries (unconfirmed).

Type:

int | None

stadium_name

Denormalised name of stadium_uid. Only a couple of hundred grounds store a name, so this is None on nearly every match even where stadium_uid is filled in; Stadium.name says why (unconfirmed).

Type:

str | None

match_record_id

Id of the match record the save keeps for a played match, or None when the match is unplayed (unconfirmed).

Type:

int | None

match_rules_template

The three bytes naming the shared match-rules template this match uses. It is a template many competitions share and is never a competition id (unconfirmed).

Type:

tuple[int, …]

unknown

Numeric fields with no known meaning. “result_r22” is the byte stored beside the score, so only a match whose score the save still holds carries one (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.LeagueTable(competition_id, competition_name, club_count, rows)[source]

One live table: every club the save groups together, with its rows.

The rows nest, so a CSV export puts the whole list in one JSON cell. to_dicts() and the JSON writers are the useful exports for tables.

Parameters:
competition_id

The competition the table was voted to belong to, in the stage id space (not the id space a suspension’s competition id belongs to); None when the vote could not settle it, which on the corpus is one table in a thousand. Read that beside the shape of the tables: 30% to 43% of them hold one block, and a one-block table meets the vote’s half-the-members rule on a majority of one, so it resolves trivially; over the tables of two blocks or more resolution is 0.998 to 1.000. One competition names several tables, because a cup’s group stage is one competition holding a table per group. The save stores no link from a table to its competition, so this is a vote on the fixture calendar rather than a stored value (unconfirmed).

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names (unconfirmed).

Type:

str | None

club_count

How many rows the table holds (unconfirmed).

Type:

int

rows

One row per club, in the order the save stores the group.

Type:

tuple[fmsave.models.league_tables.LeagueTableRow, …]

class fmsave.LeagueTableSplit(played, won, drawn, lost, goals_for, goals_against, points)[source]

One of a row’s four partial records: home, away, first half or second half.

Every field is unconfirmed. The save’s own arithmetic holds on all four (played equals won plus drawn plus lost, and home and away add up to the total, as do the two halves), which is what the block acceptance checks require, but no in-game screen has yet been read back against them, so none of the seven is verified.

Parameters:
played

Matches played in this split (unconfirmed).

Type:

int

won

Matches won (unconfirmed).

Type:

int

drawn

Matches drawn (unconfirmed).

Type:

int

lost

Matches lost (unconfirmed).

Type:

int

goals_for

Goals scored (unconfirmed).

Type:

int

goals_against

Goals conceded (unconfirmed).

Type:

int

points

Points taken (unconfirmed).

Type:

int

class fmsave.LeagueTableMatch(slot, home_or_away, opponent_team_id, opponent_club_uid, opponent_club_name, opponent_club_short_name, goals_for, goals_against, outcome, points)[source]

One match slot of one club’s row: a played match, or a slot never played.

A slot the club has not played keeps its place in the list so the shape of the season is never lost: its opponent fields, goals, outcome and points are all None.

Parameters:
  • slot (int)

  • home_or_away (MatchSide | None)

  • opponent_team_id (int | None)

  • opponent_club_uid (int | None)

  • opponent_club_name (str | None)

  • opponent_club_short_name (str | None)

  • goals_for (int | None)

  • goals_against (int | None)

  • outcome (MatchOutcome | None)

  • points (int | None)

slot

The slot’s index in the row, counting from zero.

Type:

int

home_or_away

Which side of the match the row’s club was, the home one or the away one, which the save stores as the slot’s parity: the even slots are the home ones. That is checked against the fixture calendar’s own stored home team on every table whose rows account for exactly one season of it, and it holds on 99.76% to 99.87% of the slots the calendar can settle by itself, against a fifth of a percent were the parity the other way round; the handful that disagree are consistent with rescheduled or neutral-ground meetings. A slot never played carries a side too, since the parity belongs to the slot rather than to what happened in it. It is None only where the build fmsave read the save with has not settled the parity. This is the club’s side and not the ground: Fixture.is_neutral_venue is the field that says whether a match was played away from the home club’s usual ground, and a match at a neutral ground still has a home club and an away club.

Type:

fmsave.models.league_tables.MatchSide | None

opponent_team_id

The opponent’s team id exactly as stored, or None for an unplayed slot.

Type:

int | None

opponent_club_uid

Uid of the club fielding the opponent, or None when the slot is unplayed or no club lists that team.

Type:

int | None

opponent_club_name

Denormalised name of opponent_club_uid, or None when it does not resolve.

Type:

str | None

opponent_club_short_name

Denormalised short name of opponent_club_uid, or None when it does not resolve.

Type:

str | None

goals_for

Goals the club scored, or None for an unplayed slot.

Type:

int | None

goals_against

Goals the club conceded, or None for an unplayed slot.

Type:

int | None

outcome

Whether the club won, drew or lost, or None for an unplayed slot and for a row whose counters name no single result.

Type:

fmsave.models.league_tables.MatchOutcome | None

points

Points the club took from the match, or None for an unplayed slot.

Type:

int | None

class fmsave.LeagueTableRow(position, team_id, club_uid, club_name, club_short_name, team_slot, played, won, drawn, lost, goals_for, goals_against, points, home, away, first_half, second_half, rounds_per_venue, matches, unknown)[source]

One club’s row in one live table, with its splits and its match slots.

The seven top-level counters are the block’s TOTAL aggregate; home, away, first_half and second_half are the other four, in the order the save stores them.

Parameters:
position

The club’s place in the table, counting from one, which is the order the save stores. Read it as the league position and leave the rows alone: a sort by points and goal difference reproduces it on about 92% of tables and disagrees only where the game’s own first tie-break, the results between the clubs level on points, decides an order these fields cannot reconstruct.

Type:

int

team_id

The club’s first-team id exactly as stored.

Type:

int

club_uid

Uid of the club fielding that team, or None when no club lists it.

Type:

int | None

club_name

Denormalised name of club_uid, or None when the team does not resolve.

Type:

str | None

club_short_name

Denormalised short name of club_uid, or None when the team does not resolve.

Type:

str | None

team_slot

The team’s slot in its club’s team list, 0 for the first entry, or None when the team does not resolve (unconfirmed).

Type:

int | None

played

Matches played in total.

Type:

int

won

Matches won in total.

Type:

int

drawn

Matches drawn in total.

Type:

int

lost

Matches lost in total.

Type:

int

goals_for

Goals scored in total.

Type:

int

goals_against

Goals conceded in total.

Type:

int

points

Points taken in total.

Type:

int

home

The club’s home record (every field unconfirmed).

Type:

fmsave.models.league_tables.LeagueTableSplit

away

The club’s away record (every field unconfirmed).

Type:

fmsave.models.league_tables.LeagueTableSplit

first_half

The club’s record in the first half of the season (every field unconfirmed).

Type:

fmsave.models.league_tables.LeagueTableSplit

second_half

The club’s record in the second half of the season (every field unconfirmed).

Type:

fmsave.models.league_tables.LeagueTableSplit

rounds_per_venue

How many rounds the group plays at each venue, so the row holds twice this many match slots (unconfirmed).

Type:

int

matches

One entry per match slot, in stored order, including slots never played.

Type:

tuple[fmsave.models.league_tables.LeagueTableMatch, …]

unknown

The 19 bytes stored in front of the block, one int per byte. The first of them is the row’s own place in its table, which is what tells one table from the next and is already reported as position; it is kept here so the blob ships whole. The other 18 are undecoded, and they are the only fields in which the save’s several copies of a block differ (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.TransferWindow(start_day, start_month, start_season_year_offset, end_day, end_month, end_season_year_offset, unknown)[source]

One transfer window: when it opens and closes, relative to the season start.

The window carries no name. The save’s rules database does hold description strings, but none of them belongs to a window record: on the saves measured only six windows in fifty-four have a description ending anywhere near the record, and the rest sit hundreds of thousands of bytes away. Attaching the nearest one would put an unrelated label on a window, so no label ships and the dates are the whole of what a window says.

A save holds more rows than it has distinct windows. On the saves measured the reader returns 54 rows of which 22 are distinct: 17 of those appear twice and 5 appear four times. Rows that repeat are equal in every field this record carries, so a caller who wants one row per distinct window can drop the repeats and lose nothing it is able to see. A record carries a mapping and so is not hashable, which rules out a set; equality works as usual, so keep the first of each instead:

distinct_windows: list[TransferWindow] = []
for window in career_save.transfer_windows():
    if window not in distinct_windows:
        distinct_windows.append(window)

Why the rules database repeats a window is not established. What was measured is that a repeat carries no key of its own that fmsave drops: of the 32 repeated records, 26 repeat the whole underlying record exactly, tag for tag and value for value, including the id each of its two date sub-lists carries, and the other 6 differ only in a description and two unidentified tags. Nothing inside a window record tells one copy from another, so whatever separates them, if anything does, sits outside the record and this reader does not read it. In particular nothing measured here makes them per-competition or per-nation copies; that remains a guess. The count is the anchored walk’s own, not a decode that counts a record twice, and every save measured gives the same 54 rows and the same 22 distinct windows.

Parameters:
start_day

Day of the month the window opens, 1 to 31.

Type:

int

start_month

Month the window opens, 1 to 12.

Type:

int

start_season_year_offset

Which season year the window opens in: 0 is the season’s own start year and 1 the calendar year after it. There is no calendar date without a season, so this is an offset and never a date.

Type:

int

end_day

Day of the month the window closes, 1 to 31.

Type:

int

end_month

Month the window closes, 1 to 12.

Type:

int

end_season_year_offset

Which season year the window closes in, as start_season_year_offset.

Type:

int

unknown

Numeric fields with no known meaning. “close_time” is the value stored against the window’s closing-time tag, which looks like an hour and minute on a 24-hour clock but is not named, because the game’s own Rules screen shows no time of day to pin it to. “window_type” is a small code the record carries on about two windows in five and no displayed label explains (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.CompetitionRules(kind, competition_id, competition_name, fixtures_per_club, promotion_places, playoff_places, relegation_places, prize_money, rounds, unknown)[source]

One competition-rules block: promotion and relegation, prize money and the calendar.

The block stores no competition. Its competition comes from where the save keeps it. No field inside a block names one: every u32 in the 256 bytes before the marker and the 256 bytes after the block’s end was tested against the competition ids the stage table holds, over 200 blocks of every save measured, and no offset named a known competition on even 90% of blocks while varying from block to block. What does work is position. The span alternates rules blocks and league-table blocks, and the run of table blocks stored after a block is the table that block’s rules govern: on the saves measured about seven blocks in ten have such a run, nearly all of those runs are exactly one league_tables() table’s set of clubs, and nearly all of those tables carry a voted competition. A run matching two tables at once names neither.

That link is corroborated, and is no stronger than the vote behind it. 0.74 to 0.78 of a linked block’s dated rounds fall on a date its competition plays a fixture on, against 0.55 to 0.58 when each block is linked to the run stored before it instead, and about 0.13 against a competition drawn at random for each block. On the one division whose Rules screen was read in game, the linked block holds 20 clubs, 38 rounds and 3 relegation places and its last round is the date that screen gives as the season’s end. The competition it hands over is itself a vote on the fixture calendar rather than a stored value, so these two fields are exactly as strong as LeagueTable.competition_id: unconfirmed.

The other field meanings are weakly sourced. They come from checks against one division’s Rules screen in another tool, never re-verified here. The link above does now reach the one division that screen describes, and the values of that block match it, but a shape that matches a screen is corroboration rather than a field read back, so every field here is unconfirmed.

A block’s club count and its points deduction for administration are not read: no fixed position in the block carries either, so no field ships for them rather than a field that is empty on every row. 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 (a home-grown minimum, a maximum squad size, a salary cap and a wage-bill percentage) are not read. They live in tagged groups that nothing readable ties to a competition – seven routes were measured, including the positional one this record uses, and a group is not stored beside a table – and whose content is database content, identical on every save of one installed database. So fmsave ships no field for them rather than a field whose meaning rests on another tool’s guess. Adding them later adds columns and renames nothing.

Transfer windows are their own table: see Save.transfer_windows().

Parameters:
kind

Which structure the row was read from. Every row is PREAMBLE, since the tagged rules groups are not read (unconfirmed).

Type:

fmsave.models.rules.RulesBlockKind

competition_id

The competition of the league table the save stores right after this block, in the stage id space; None where that run is not exactly one table with a competition of its own, which is 32% to 45% of rows. Not a stored link: a position in the span, resolved through a vote on the fixture calendar (unconfirmed).

Type:

int | None

competition_name

Denormalised name of competition_id, which is None unless a name map is supplied, because the save stores no competition names (unconfirmed).

Type:

str | None

fixtures_per_club

How many matches each club plays, which is the number of rounds the block holds; None when no round decoded (unconfirmed).

Type:

int | None

promotion_places

How many clubs are promoted, or None when the block’s two copies of the promotion quad differ (unconfirmed).

Type:

int | None

playoff_places

How many clubs enter a promotion play-off, or None as promotion_places (unconfirmed).

Type:

int | None

relegation_places

How many clubs are relegated, or None as promotion_places (unconfirmed).

Type:

int | None

prize_money

Prize money by finishing position, in the save’s base currency, in finishing order. Empty on most blocks: only about one block in eight carries a prize list at all (unconfirmed).

Type:

tuple[int, …]

rounds

The competition’s calendar, in stored order (unconfirmed).

Type:

tuple[fmsave.models.rules.RulesRound, …]

unknown

Numeric fields with no known meaning. “promotion_byte2” is the third byte of the promotion quad. “tie_break_00” to “tie_break_15” are the tie-break codes the block lists, in order, and are absent past the end of that list; no displayed label names any of them, so they ship as raw numbers (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.RulesRound(number, date, match_count, unknown)[source]

One round of a competition’s calendar, as the rules block stores it.

Parameters:
number

The round’s number, counting from one. A round the save does not number stores a value that reads here as 256 rather than as a guess (unconfirmed).

Type:

int

date

The date the round is played on, or None when the stored date does not decode (unconfirmed).

Type:

datetime.date | None

match_count

How many matches the round holds (unconfirmed).

Type:

int

unknown

Numeric fields with no known meaning. “kind” and “b5” are the two unidentified bytes the round record carries (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.PlayerMatchStats(player_uid, player_name, date, competition_id, opponent_team_id, opponent_club_uid, opponent_club_name, opponent_club_short_name, opponent_team_slot, has_stats, position, minutes, left_at_minute, goals, assists, rating, passes_attempted, passes_completed, stats_in_range, unknown)[source]

One player’s record of one match the save still holds.

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 per-competition total summed from these rows is therefore short without saying so, and 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.

competition_id belongs to the stage id space, the same space stages(), fixtures(), the league tables and Suspension.competition_id use, so all of them join.

Parameters:
  • player_uid (int)

  • player_name (str | None)

  • date (date)

  • competition_id (int)

  • opponent_team_id (int)

  • opponent_club_uid (int | None)

  • opponent_club_name (str | None)

  • opponent_club_short_name (str | None)

  • opponent_team_slot (int | None)

  • has_stats (bool)

  • position (CodedValue[MatchPosition] | None)

  • minutes (int | None)

  • left_at_minute (int | None)

  • goals (int | None)

  • assists (int | None)

  • rating (float | None)

  • passes_attempted (int | None)

  • passes_completed (int | None)

  • stats_in_range (bool)

  • unknown (Mapping[str, int])

player_uid

Uid of the player the record belongs to, which is the player whose object holds it (unconfirmed).

Type:

int

player_name

Denormalised name of player_uid (unconfirmed).

Type:

str | None

date

The date the match was played.

Type:

datetime.date

competition_id

Id of the competition, in the stage id space (not the id space a suspension’s competition id belongs to). It is stored on the record itself rather than reached through a stage.

Type:

int

opponent_team_id

The opposing side’s first-team id, exactly as stored. It is a team id and never a club index.

Type:

int

opponent_club_uid

Uid of the club fielding the opposing team, or None when no club lists that team.

Type:

int | None

opponent_club_name

Denormalised name of opponent_club_uid, or None when the team does not resolve.

Type:

str | None

opponent_club_short_name

Denormalised short name of opponent_club_uid, or None when the team does not resolve.

Type:

str | None

opponent_team_slot

The opposing team’s slot in its club’s team list, 0 for the first entry, or None when the team does not resolve (unconfirmed).

Type:

int | None

has_stats

Whether the save still holds this match’s statistics. A save that has dropped them keeps the date, the competition and the opponent and nothing else, so every field below that can be None is None when this is false (unconfirmed).

Type:

bool

position

The position played, as a coded value over the mask the record stores. Only the goalkeeper bit is named; every other mask keeps its raw value and reads as UNKNOWN. None when the save holds no statistics for the match.

Type:

fmsave.models.common.CodedValue[fmsave.models.matches.MatchPosition] | None

minutes

Minutes played, or None when the save holds no statistics for the match. No value the game displays has been matched to this number; what says it is minutes played is its shape. On the saves measured about half the matches with statistics hold exactly 90, over 0.993 hold 90 or less, none holds more than 130, and the rest are spread over the values below 90 rather than clustered (unconfirmed).

Type:

int | None

left_at_minute

The minute the player left the pitch, whether he was substituted or sent off. None when he was on it at the final whistle, which the save stores as a zero, and None as well when the save holds no statistics for the match; has_stats tells those two apart. It is never 0, so a filter such as left_at_minute < 10 selects the early departures and nobody else (unconfirmed).

Type:

int | None

goals

Goals scored, or None when the save holds no statistics for the match.

Type:

int | None

assists

Assists, or None when the save holds no statistics for the match. This is a hypothesis: no displayed label has been matched to this byte, and the count shown on a player’s season statistics screen is what would confirm it (unconfirmed).

Type:

int | None

rating

The match rating, which is the stored number divided by ten and so exact to one decimal. None when the save holds no statistics for the match, and None as well when the game rated nobody in it, which the save stores as a zero; has_stats tells those two apart, and the stored zero is kept as unknown[“rating_raw”] so nothing is dropped. A rating is never 0.0, so an average over this column is an average of the ratings the game actually gave (unconfirmed).

Type:

float | None

passes_attempted

Passes attempted, or None when the save holds no statistics for the match (unconfirmed).

Type:

int | None

passes_completed

Passes completed, or None when the save holds no statistics for the match (unconfirmed).

Type:

int | None

stats_in_range

fmsave’s own sanity flag, not a value the save stores: true when the save holds this match’s statistics and its minutes, rating and goals are all inside the bounds a match can reach. A false flag never blanks a value; every number is kept exactly as stored so a caller can see what the save holds (unconfirmed).

Type:

bool

unknown

Numeric fields with no known meaning. “role_code” moves with the position played but its meaning is not established, and it is absent from the mapping for a match the save holds no statistics for; “tag” is stored beside the date and is always read; “rating_raw” is the stored rating, kept only where it is zero, which is the one value rating itself cannot carry (unconfirmed).

Type:

collections.abc.Mapping[str, int]

Injuries

injury_types() and injuries().

class fmsave.InjuryType(id, name, unknown)[source]

One named injury the game can hand out.

Parameters:
id

The code the save stores on an injury of this type (unconfirmed).

Type:

int

name

The game’s own name for the injury, in the language the save was written in (unconfirmed).

Type:

str

unknown

Numeric fields with no known meaning: a flag byte, and a second id in an id space of its own (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.InjuryRecord(kind, player_uid, player_name, date, team_id, club_uid, club_name, team_slot, type_id, type_name, cause, severity, unknown)[source]

One injury the save remembers, of either kind.

Parameters:
kind

Which store the row came from: HISTORY for an injury that happened, TYPED for a row carrying an injury type.

Type:

fmsave.models.injuries.InjuryRecordKind

player_uid

Uid of the person the row is stored against, or None when the save no longer keeps that person as a player, which is about one HISTORY row in twenty (unconfirmed).

Type:

int | None

player_name

Denormalised display name of player_uid; None wherever player_uid is (unconfirmed).

Type:

str | None

date

When the injury happened, on a HISTORY row. On a TYPED row it is the day the player is expected back rather than the day he was hurt, which is why it is often ahead of the in-game date; it is None when the save stores no date at all.

Type:

datetime.date | None

team_id

Id of the team the person was registered with at the time, on a HISTORY row; None on every TYPED row, which carries no team.

Type:

int | None

club_uid

Uid of the club that fields team_id, which for a team another club controls is the controlling club; None when no club lists that team and on every TYPED row.

Type:

int | None

club_name

Denormalised full name of club_uid; None wherever club_uid is.

Type:

str | None

team_slot

The team’s slot in club_uid’s team list, counting that club’s own teams first and then the teams it controls; None wherever club_uid is (unconfirmed).

Type:

int | None

type_id

The injury’s type code, on a TYPED row; None on every HISTORY row, which carries no type.

Type:

int | None

type_name

Denormalised name of type_id from Save.injury_types, None for the codes that table has no entry for and on every row of a save with no per-match file (unconfirmed).

Type:

str | None

cause

Whether the injury happened in training or in a match, on a HISTORY row; None on every TYPED row. Both stored codes carry the cause a screen displayed for them, except on an illness row, which displays its own vocabulary.

Type:

fmsave.models.common.CodedValue[fmsave.models.injuries.InjuryCause] | None

severity

How bad the injury was, on a HISTORY row; None on every TYPED row. All four stored codes carry the severity a screen displayed for them, except on an illness row, which displays its own vocabulary.

Type:

fmsave.models.common.CodedValue[fmsave.models.injuries.InjurySeverity] | None

unknown

Numeric fields with no known meaning, each present only on the kind of row that carries it: r0 a byte that is 1 on every row of every save, hi7_date the time-slot bits of the row’s date word, and, on a TYPED row, r11 and r12 (unconfirmed).

Type:

collections.abc.Mapping[str, int]

Managed-club-only

managed_clubs(), tactics(), set_pieces(), training() and mentoring(): rows here exist only for the club the save’s human manager runs.

class fmsave.ManagedClub(club_uid, club_name, club_short_name, manager_name, manager_staff_uid)[source]

A club run by one of the save’s human managers.

The link from a human manager to a club is proven only on saves with a single human manager. On a save with several human managers, only the first one may be listed. A manager with no current club, for example one between jobs, has no row.

Parameters:
  • club_uid (int)

  • club_name (str)

  • club_short_name (str)

  • manager_name (str | None)

  • manager_staff_uid (int | None)

club_uid

Uid of the managed club (unconfirmed).

Type:

int

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str

club_short_name

Denormalised short name of club_uid (unconfirmed).

Type:

str

manager_name

The human manager’s name as the save summary stores it, or None when the summary does not name the manager next to the club (unconfirmed).

Type:

str | None

manager_staff_uid

Uid of the human manager’s own record, which is a Staff.uid and joins to the staff table; None when it cannot be identified exactly. manager_name is this person’s name (unconfirmed).

Type:

int | None

class fmsave.Tactic(club_uid, club_name, team_id, team_slot, index, name, style_name, mentality, slots, unknown)[source]

One stored copy of one tactic, belonging to one team.

Parameters:
club_uid

Uid of the club the team plays for, None for a team no club lists (unconfirmed).

Type:

int | None

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str | None

team_id

The team the copy belongs to, as the save stores it (unconfirmed).

Type:

int

team_slot

The team’s place in that club’s own team list, 0 for its first team; None when the team resolves to no club (unconfirmed).

Type:

int | None

index

The copy’s place among the team’s own tactics, counting from zero in stored order. Nothing stored says which of them is selected (unconfirmed).

Type:

int

name

The tactic’s name as the manager typed it (unconfirmed).

Type:

str

style_name

The tactical style shown with the tactic, as stored text. No screen has confirmed that this label is the style (unconfirmed).

Type:

str

mentality

The mentality the tactic is set to; the seven codes the game offers each carry the word it shows for them, and any other code reads UNKNOWN.

Type:

fmsave.models.common.CodedValue[fmsave.models.tactics.Mentality]

slots

The eleven positions, in stored order.

Type:

tuple[fmsave.models.tactics.TacticSlot, …]

unknown

The 19 team-instruction bytes, one int per byte, and the tactic’s four-byte style code as one little-endian int. Byte 2 of the instructions is the mentality and is shipped as mentality as well; bytes 0, 3, 4, 5, 7, 17 and 18 are the same on every record measured, and bytes 11 to 15 look bit-packed rather than numeric (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.TacticSlot(number, in_possession_positions, in_possession_mask, in_possession_setting_count, in_possession_settings, out_of_possession_positions, out_of_possession_mask, out_of_possession_setting_count, out_of_possession_settings, unknown)[source]

One position of a tactic, in possession and out of possession.

A tactic holds eleven of these in stored order. The mask is the field the positions come from: its low bits are positions and its two high bits flag which of a pair of central or wide slots is on which side, so the flags are in *_mask but never in *_positions.

Parameters:
number

The slot’s place in the tactic, 0 for the first.

Type:

int

in_possession_positions

The positions the in-possession mask sets, in ascending bit number; every label is UNKNOWN and the raw value is the bit number.

Type:

tuple[fmsave.models.common.CodedValue[fmsave.models.tactics.TacticPosition], …]

in_possession_mask

The whole in-possession mask, column flags included.

Type:

int

in_possession_setting_count

How many setting units the in-possession block holds (unconfirmed).

Type:

int

in_possession_settings

Those units, in stored order (unconfirmed).

Type:

tuple[fmsave.models.tactics.TacticSettingUnit, …]

out_of_possession_positions

The same for the out-of-possession block, which differs from the in-possession one on some slots of some saves.

Type:

tuple[fmsave.models.common.CodedValue[fmsave.models.tactics.TacticPosition], …]

out_of_possession_mask

The whole out-of-possession mask.

Type:

int

out_of_possession_setting_count

How many setting units that block holds (unconfirmed).

Type:

int

out_of_possession_settings

Those units, in stored order (unconfirmed).

Type:

tuple[fmsave.models.tactics.TacticSettingUnit, …]

unknown

The two bit fields each block carries besides its units, and the index byte the out-of-possession block carries. The role bits hold exactly one bit and are most likely the position’s role, and the index bytes of a record are a permutation of 0 to 10 which is the identity on all but one record measured (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.TacticSettingUnit(head_byte, first_bits, second_bits_low, second_bits_middle, second_bits_high)[source]

One 24-byte setting unit of a tactic slot, as two bit fields.

A slot holds between none and about twenty of these, and they are what the player instructions of that position most likely are, but the save stores no name for either field, so both ship as the numbers they are. Every field is unconfirmed.

Parameters:
  • head_byte (int)

  • first_bits (int)

  • second_bits_low (int)

  • second_bits_middle (int)

  • second_bits_high (int)

head_byte

The byte after the unit’s lead bytes, which is 0 on most units and 2 or 10 on the rest (unconfirmed).

Type:

int

first_bits

The unit’s seven-byte bit field as one little-endian int; between none and four of its bits are set (unconfirmed).

Type:

int

second_bits_low

Bits 0 to 31 of the unit’s twelve-byte bit field (unconfirmed).

Type:

int

second_bits_middle

Bits 32 to 63 of that field (unconfirmed).

Type:

int

second_bits_high

Bits 64 to 95 of that field. The field is wider than 64 bits, so it ships as three words rather than one int no column type could hold; one or two of its 96 bits are set, always above bit 32 (unconfirmed).

Type:

int

class fmsave.SetPieceRoutine(club_uid, club_name, team_id, team_slot, slot, name)[source]

One set-piece routine slot of one team, named or empty.

Parameters:
  • club_uid (int | None)

  • club_name (str | None)

  • team_id (int)

  • team_slot (int | None)

  • slot (int)

  • name (str | None)

club_uid

Uid of the club the team plays for, None for a team no club lists (unconfirmed).

Type:

int | None

club_name

Denormalised full name of club_uid (unconfirmed).

Type:

str | None

team_id

The team the slot belongs to, as the save stores it (unconfirmed).

Type:

int

team_slot

The team’s place in that club’s own team list, 0 for its first team; None when the team resolves to no club (unconfirmed).

Type:

int | None

slot

The routine’s place in the team’s twenty slots, counting from zero in stored order. Which set-piece situation a slot is for is not stored as text, so the slot number is all there is to go on (unconfirmed).

Type:

int

name

The routine’s name as the manager typed it, None for a slot with no routine, which is half the slots of a first team and all twenty of every other team.

Type:

str | None

class fmsave.TeamTraining(team_id, club_uid, club_name, team_slot, weeks, schedule_library)[source]

One team’s training calendar, with the library of schedules the save holds beside it.

Parameters:
team_id

The team whose calendar this is, as the save stores it.

Type:

int

club_uid

Uid of the club that fields team_id, None for a team no club lists. For a team the managed club fields at a club it controls, this is the managed club.

Type:

int | None

club_name

Denormalised full name of club_uid.

Type:

str | None

team_slot

The team’s place in that club’s own team list, 0 for its first team; None when the team resolves to no club. Which side each slot is, such as a reserve or a youth team, is not known (unconfirmed).

Type:

int | None

weeks

Every week of the calendar, in stored order, running from before the save’s date to well after it.

Type:

tuple[fmsave.models.training.TrainingWeek, …]

schedule_library

The schedules the manager has saved, in stored order. It is one library per save rather than per team, so every row carries the same one, and it holds only saved schedules: the game’s own built-in ones are not in it, although a week may well name one. The save may store a schedule more than once: most saves measured hold twice as many entries as the Schedules screen lists, the same schedules over under two runs of ids, so count the distinct (folder, name) pairs rather than the entries to compare with a screen (unconfirmed).

Type:

tuple[fmsave.models.training.TrainingSchedule, …]

class fmsave.TrainingSchedule(folder, name, unknown)[source]

One schedule the save keeps in the manager’s own library of saved schedules.

Parameters:
folder

The folder the schedule is filed under (unconfirmed).

Type:

str

name

The schedule’s name (unconfirmed).

Type:

str

unknown

Numeric fields with no known meaning: the id the save stores with the schedule, which is not the key a week’s name is matched on. Where the save stores the same schedule twice, the copies differ only in this id (unconfirmed).

Type:

collections.abc.Mapping[str, int]

class fmsave.TrainingWeek(week_start, schedule_name)[source]

One week of a team’s training calendar.

Parameters:
  • week_start (date | None)

  • schedule_name (str)

week_start

The day the week starts, None when the stored date does not decode.

Type:

datetime.date | None

schedule_name

Name of the schedule the week runs, exactly as the save holds it. A schedule is a user’s own file, so this is whatever it was called.

Type:

str

class fmsave.MentoringGroup(team_id, club_uid, club_name, team_slot, group_number, label, member_uids, member_names)[source]

One mentoring group of one team of the managed club.

Parameters:
  • team_id (int)

  • club_uid (int | None)

  • club_name (str | None)

  • team_slot (int | None)

  • group_number (int)

  • label (str)

  • member_uids (tuple[int | None, ...])

  • member_names (tuple[str | None, ...])

team_id

The team the group belongs to, as the save stores it.

Type:

int

club_uid

Uid of the club that fields team_id, None for a team no club lists.

Type:

int | None

club_name

Denormalised full name of club_uid.

Type:

str | None

team_slot

The team’s place in that club’s own team list, 0 for its first team; None when the team resolves to no club (unconfirmed).

Type:

int | None

group_number

The number the save stores with the group, which runs from one up within a team (unconfirmed).

Type:

int

label

The group’s label as the save stores it. Every group of every save measured carries the game’s default wording with its number, and whether renaming a group in game changes the stored text has not been checked.

Type:

str

member_uids

Uid of each member, in stored order, None for a member whose stored selector names no player record.

Type:

tuple[int | None, …]

member_names

Denormalised name of each entry of member_uids, aligned with it and None wherever that uid is.

Type:

tuple[str | None, …]