FM27: staff attributes are in the ability block (block_40 and three preference slots)

Open
#68 6 comments 0 reactions 0 assignees View on GitHub

Maintainers usually reply within 1 day

Nobody has claimed this yet.

Assessment

Difficulty
4/5
Estimated time
3-5 days
Newbie friendliness
52/100
Issue type
Feature
Clarity
Mostly clear
Activity status
Active
Tech stack
python
Domain
data

Research direction

Start with the staff reader’s _read_ability_block and the Staff.attributes and StaffAttributes definitions mentioned in the issue. Check how block_40 values and preference_slot_4, preference_slot_12, and preference_slot_13 are currently read and exposed, then review the staff reader tests. Done means verified attributes appear on non-human staff with the reported scales, while unconfirmed values remain unset.

Written by the indexing model from the issue text.

Description

No save files or personal details
  • I have not posted a save file or a link to one, and removed personal details
What went wrong

This is missing data rather than a wrong value. Staff.attributes holds only adaptability, and the StaffAttributes docstring says the other ratings haven't been found. On one FM27 career, I found them in bytes fmsave already reads in _read_ability_block: the 26 block_40 values, which it range-checks but doesn't ship, and three of the unnamed preference slots.

I got this only by comparing my own saves with the game's screens. In the table, block_40[n] is counted from layout.block_40_offset, and the preference_slot_n values are already in Staff.unknown.

Staff profile attribute Where it is Scale
Goalkeeping block_40[1] 1 to 100
Judging Player Ability block_40[2] 1 to 100
Judging Player Potential block_40[3] 1 to 100
People Management block_40[4] 1 to 100
Motivating block_40[5] 1 to 100
Physiotherapy block_40[6] 1 to 100
Tactical Knowledge block_40[7] 1 to 100
Attacking block_40[8] 1 to 100
Defending block_40[9] 1 to 100
Fitness block_40[10] 1 to 100
Possession block_40[11] 1 to 100
Technical block_40[12] 1 to 100
Tactical block_40[13] 1 to 100
Analysing Data block_40[18] 1 to 100
Sports Science block_40[21] 1 to 100
Negotiating block_40[23] 1 to 100
Judging Staff Ability block_40[24] 1 to 100
Set Pieces block_40[25] 1 to 100
Authority preference_slot_4 1 to 20
Working With Youngsters preference_slot_12 1 to 20
Determination preference_slot_13 1 to 100

I don't know what block_40 positions 0, 14, 15, 16, 17, 19, 20 and 22 hold. Positions 0, 14, 17, 19 and 20 have only about 22 distinct values across all staff, as if stored in steps of 5.

How I checked it

  • Three staff profiles. The game shows words rather than numbers. Dividing the 1 to 100 values by 5 and rounding, 52 labels across the three profiles all fit these bands:

    Value Label
    1 to 4 Unsuited
    5 to 6 Reasonable
    7 to 9 Competent
    10 to 11 Average
    12 to 14 Good
    15 and 16 Very Good

    The bands are only my fit to those labels, and no value above 16 was on those screens.

  • Averages by job, across about 7,800 staff. Physios average 66 on block_40[6] and most other jobs about 15. Goalkeeping coaches average 58 on block_40[1] and most others about 13. Analysts average 62 on block_40[18] and most others about 15 to 30. Sports scientists average 66 on block_40[21] and most others about 15.

  • Known specialists. Two well-known real set-piece coaches read 100 and 90 on block_40[25], where coaches average 23.

  • Stars on the Set Coach Assignments screen. A weighted average of these attributes matches 8 star ratings from that screen. Seven were used to choose the weights, and the eighth I predicted before reading it from the game. On a 1 to 20 scale, with 20 as five stars and rounded to half stars, I used Attacking, Defending or Possession 3, Technical or Tactical 2 and Determination 3; Fitness or Goalkeeping 4 and Determination 1. Other weights fit too, so this is not the game's formula. It does support Determination, because the rating needs it.

How sure each part is

  • Authority rests on the three profiles alone. I found it by scanning the bytes around the block for a value that fit all three, so that match could be chance.
  • Determination came from the same scan, with the star ratings as extra support.
  • Set Pieces and the star weights have no direct check beyond what's above.
  • This is one career on one build, and I haven't looked at FM26 or at the human manager, whose object fmsave reads differently.

Still missing: which coach is assigned to which training area. I haven't looked for it. The training rows have only team_id, club_uid, club_name, team_slot, weeks and schedule_library.

What you expected

These rated attributes on Staff.attributes for non-human staff, leaving any as None or unconfirmed until you've checked them. The 1 to 100 values are probably better shipped as stored, since dividing by 5 is only how they seem to display.

I can email cropped screens of the three profiles and the assignments screen with the matching save, if that helps.

fmsave version

0.9.3

Python version

3.12.10

Operating system

Windows 11

Game build (FM26 or FM27)

27.0.0+2493426 (unverified build, partial coverage)

fmsave validate output

The staff readers in full, plus the status of the others. tactics, set_pieces and selections fail for the reasons in #67, and finances and sponsorships also fail on this save, which is separate from this issue.

{
 "fmsave_version": "0.9.3",
 "python_version": "3.12.10",
 "os": "Windows",
 "game": "FM27",
 "build": "27.0.0+2493426",
 "known_build": false,
 "staff_readers": [
  {
   "reader": "staff",
   "status": "ok",
   "record_count": 7813,
   "gates": [
    {
     "name": "staff_ability_signature",
     "observed": 1.0,
     "minimum": 0.99,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_preference_slots",
     "observed": 0.9998719918074757,
     "minimum": 0.99,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_codes_in_set",
     "observed": 0.9971838197644649,
     "minimum": 0.98,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_block_40_in_range",
     "observed": 0.9998719918074757,
     "minimum": 0.99,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_person_blocks",
     "observed": 0.9998720081914757,
     "minimum": 0.99,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_minimum",
     "observed": 7813,
     "minimum": 1000,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_listed_contracted_here",
     "observed": 0.9430210325047801,
     "minimum": 0.6,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_unowned_tailed_contracts",
     "observed": 0,
     "minimum": null,
     "maximum": 0,
     "passed": true,
     "applied": true
    }
   ],
   "coverage": {
    "uid": 1.0,
    "unique_id": 0.997440163829515,
    "is_human_manager": 1.0,
    "name": 0.9998720081914757,
    "first_name": 0.9998720081914757,
    "last_name": 0.9998720081914757,
    "common_name": 0.06105209266606937,
    "full_name": 0.9998720081914757,
    "legal_name": 0.4198131319595546,
    "birth_date": 0.9998720081914757,
    "age": 0.9998720081914757,
    "nation_id": 0.9998720081914757,
    "club_uid": 1.0,
    "club_name": 1.0,
    "team_id": 0.9970561884039422,
    "team_slot": 0.9970561884039422,
    "job": 0.9970561884039422,
    "job_code": 0.9970561884039422,
    "listed_club_uid": 0.6693971585818508,
    "listed_club_name": 0.6693971585818508,
    "in_club_lists": 1.0,
    "list_indexes": 1.0,
    "has_contract": 1.0,
    "wage": 0.865736592858057,
    "contract_start": 0.865736592858057,
    "contract_end": 0.7970049916805324,
    "ability_current": 0.9998720081914757,
    "ability_potential": 0.9998720081914757,
    "ability_potential_range_code": 0.0,
    "personality_adaptability": 0.9998720081914757,
    "personality_ambition": 0.9998720081914757,
    "personality_loyalty": 0.9998720081914757,
    "personality_pressure": 0.9998720081914757,
    "personality_professionalism": 0.9998720081914757,
    "personality_sportsmanship": 0.9998720081914757,
    "personality_temperament": 0.9998720081914757,
    "personality_controversy": 0.9998720081914757,
    "attributes_adaptability": 0.9998720081914757,
    "preferences_attacking": 0.9998720081914757,
    "preferences_business": 0.9998720081914757,
    "preferences_directness": 0.9998720081914757,
    "preferences_interference": 0.9998720081914757,
    "preferences_patience": 0.9998720081914757,
    "preferences_trigger_press": 0.9998720081914757,
    "preferences_resources": 0.9998720081914757,
    "preferences_buying_players": 0.9998720081914757,
    "preferences_mind_games": 0.9998720081914757,
    "preferences_flexibility": 0.9998720081914757,
    "preferences_hardness_of_training": 0.9998720081914757,
    "preferences_squad_rotation": 0.9998720081914757,
    "preferences_tempo": 0.9998720081914757,
    "preferences_width": 0.9998720081914757,
    "unknown_entry_count": 0.9998720081914757,
    "unknown_r4": 0.9998720081914757,
    "unknown_r5": 0.9998720081914757,
    "unknown_r6": 0.9998720081914757,
    "unknown_r7": 0.9998720081914757,
    "unknown_r8": 0.9998720081914757,
    "unknown_r9": 0.9998720081914757,
    "unknown_r10": 0.9998720081914757,
    "unknown_r11": 0.9998720081914757,
    "unknown_r12": 0.9998720081914757,
    "unknown_preference_slot_2": 0.9998720081914757,
    "unknown_preference_slot_4": 0.9998720081914757,
    "unknown_preference_slot_5": 0.9998720081914757,
    "unknown_preference_slot_7": 0.9998720081914757,
    "unknown_preference_slot_8": 0.9998720081914757,
    "unknown_preference_slot_12": 0.9998720081914757,
    "unknown_preference_slot_13": 0.9998720081914757,
    "unknown_preference_slot_16": 0.9998720081914757,
    "unknown_preference_slot_17": 0.9998720081914757,
    "unknown_preference_slot_18": 0.9998720081914757,
    "unknown_preference_slot_19": 0.9998720081914757,
    "unknown_preference_slot_20": 0.9998720081914757,
    "unknown_contract_e36": 0.865736592858057,
    "unknown_contract_e37": 0.865736592858057,
    "unknown_contract_e38": 0.865736592858057,
    "unknown_contract_e39": 0.865736592858057,
    "unknown_contract_type": 0.8380903622168181
   },
   "anomalies": {
    "untailed_hits": 114283,
    "club_record_hits": 22239,
    "ambiguous_headers": 0,
    "unlocated_persons": 0,
    "unresolved_contract_teams": 342,
    "repeat_contracts": 0,
    "merged_affiliate_pairs": 72,
    "human_manager_missing": 0,
    "non_staff_tailed_hits": 0,
    "offer_records": 114
   }
  },
  {
   "reader": "staff_lists",
   "status": "ok",
   "record_count": 1599,
   "gates": [
    {
     "name": "staff_lists_fit",
     "observed": 1.0,
     "minimum": 1.0,
     "maximum": null,
     "passed": true,
     "applied": true
    },
    {
     "name": "staff_list_ids_are_staff",
     "observed": 0.9998087954110899,
     "minimum": 0.99,
     "maximum": null,
     "passed": true,
     "applied": true
    }
   ],
   "coverage": {
    "club_uid": 1.0,
    "club_name": 1.0,
    "list_index": 1.0,
    "staff_uids": 1.0,
    "staff_names": 1.0
   },
   "anomalies": {
    "player_values_in_lists": 6,
    "unset_list_values": 0
   }
  }
 ],
 "other_reader_status": {
  "clubs": "ok",
  "players": "ok",
  "contracts": "ok",
  "suspensions": "ok",
  "managed_clubs": "ok",
  "stages": "ok",
  "competitions": "ok",
  "nations": "ok",
  "fixtures": "ok",
  "league_tables": "ok",
  "transfer_windows": "ok",
  "competition_rules": "ok",
  "player_match_stats": "ok",
  "player_season_stats": "ok",
  "player_season_history": "ok",
  "stadiums": "ok",
  "finances": "failed",
  "sponsorships": "failed",
  "affiliates": "ok",
  "job_vacancies": "ok",
  "injury_types": "ok",
  "injuries": "ok",
  "training": "ok",
  "mentoring": "ok",
  "tactics": "failed",
  "set_pieces": "failed",
  "selections": "failed",
  "facilities": "ok"
 }
}
Dominant language
Python
Stars
28
Forks
8
Avg merge
42m
Merged PRs (30d)
33

Getting set up

First steps

  1. Read the whole issue, then the project's contributing guide.
  2. Comment on the issue to say you are picking it up — it saves two people doing the same work.
  3. Fork the repository and make your change on a branch.
  4. Open a pull request that references the issue number.

More from rhiever/fmsave

All issues in rhiever/fmsave

Similar issues

More Python issues

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.