fmsave 0.4.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- fmsave-0.4.0/LICENSE +21 -0
- fmsave-0.4.0/PKG-INFO +164 -0
- fmsave-0.4.0/README.md +135 -0
- fmsave-0.4.0/pyproject.toml +102 -0
- fmsave-0.4.0/pyproject.toml.orig +87 -0
- fmsave-0.4.0/src/fmsave/__init__.py +194 -0
- fmsave-0.4.0/src/fmsave/__main__.py +4 -0
- fmsave-0.4.0/src/fmsave/_checks.py +2510 -0
- fmsave-0.4.0/src/fmsave/_container.py +627 -0
- fmsave-0.4.0/src/fmsave/_context.py +385 -0
- fmsave-0.4.0/src/fmsave/_errors.py +52 -0
- fmsave-0.4.0/src/fmsave/_frozen.py +43 -0
- fmsave-0.4.0/src/fmsave/_layouts/__init__.py +2036 -0
- fmsave-0.4.0/src/fmsave/_layouts/fm26_26_3_2.py +1488 -0
- fmsave-0.4.0/src/fmsave/_package.py +8 -0
- fmsave-0.4.0/src/fmsave/_reader_stats.py +729 -0
- fmsave-0.4.0/src/fmsave/_save.py +2305 -0
- fmsave-0.4.0/src/fmsave/_scan.py +195 -0
- fmsave-0.4.0/src/fmsave/_status.py +179 -0
- fmsave-0.4.0/src/fmsave/_version.py +248 -0
- fmsave-0.4.0/src/fmsave/checks.py +38 -0
- fmsave-0.4.0/src/fmsave/cli.py +1288 -0
- fmsave-0.4.0/src/fmsave/export.py +1238 -0
- fmsave-0.4.0/src/fmsave/models/__init__.py +138 -0
- fmsave-0.4.0/src/fmsave/models/affiliates.py +57 -0
- fmsave-0.4.0/src/fmsave/models/clubs.py +90 -0
- fmsave-0.4.0/src/fmsave/models/common.py +72 -0
- fmsave-0.4.0/src/fmsave/models/competitions.py +118 -0
- fmsave-0.4.0/src/fmsave/models/contracts.py +309 -0
- fmsave-0.4.0/src/fmsave/models/facilities.py +109 -0
- fmsave-0.4.0/src/fmsave/models/finances.py +143 -0
- fmsave-0.4.0/src/fmsave/models/fixtures.py +184 -0
- fmsave-0.4.0/src/fmsave/models/injuries.py +180 -0
- fmsave-0.4.0/src/fmsave/models/jobs.py +76 -0
- fmsave-0.4.0/src/fmsave/models/league_tables.py +277 -0
- fmsave-0.4.0/src/fmsave/models/managed.py +45 -0
- fmsave-0.4.0/src/fmsave/models/matches.py +182 -0
- fmsave-0.4.0/src/fmsave/models/meta.py +94 -0
- fmsave-0.4.0/src/fmsave/models/players.py +566 -0
- fmsave-0.4.0/src/fmsave/models/rules.py +251 -0
- fmsave-0.4.0/src/fmsave/models/stadiums.py +131 -0
- fmsave-0.4.0/src/fmsave/models/staff.py +286 -0
- fmsave-0.4.0/src/fmsave/models/suspensions.py +149 -0
- fmsave-0.4.0/src/fmsave/models/tactics.py +256 -0
- fmsave-0.4.0/src/fmsave/models/training.py +153 -0
- fmsave-0.4.0/src/fmsave/name_maps.py +186 -0
- fmsave-0.4.0/src/fmsave/py.typed +0 -0
- fmsave-0.4.0/src/fmsave/readers/__init__.py +1 -0
- fmsave-0.4.0/src/fmsave/readers/_common.py +102 -0
- fmsave-0.4.0/src/fmsave/readers/affiliates.py +121 -0
- fmsave-0.4.0/src/fmsave/readers/clubs.py +618 -0
- fmsave-0.4.0/src/fmsave/readers/competitions.py +281 -0
- fmsave-0.4.0/src/fmsave/readers/contracts.py +1546 -0
- fmsave-0.4.0/src/fmsave/readers/facilities.py +84 -0
- fmsave-0.4.0/src/fmsave/readers/finances.py +460 -0
- fmsave-0.4.0/src/fmsave/readers/fixtures.py +357 -0
- fmsave-0.4.0/src/fmsave/readers/injuries.py +633 -0
- fmsave-0.4.0/src/fmsave/readers/jobs.py +281 -0
- fmsave-0.4.0/src/fmsave/readers/league_tables.py +595 -0
- fmsave-0.4.0/src/fmsave/readers/managed.py +352 -0
- fmsave-0.4.0/src/fmsave/readers/matches.py +601 -0
- fmsave-0.4.0/src/fmsave/readers/names.py +178 -0
- fmsave-0.4.0/src/fmsave/readers/persons.py +642 -0
- fmsave-0.4.0/src/fmsave/readers/player_scan.py +440 -0
- fmsave-0.4.0/src/fmsave/readers/players.py +536 -0
- fmsave-0.4.0/src/fmsave/readers/results.py +425 -0
- fmsave-0.4.0/src/fmsave/readers/rules.py +399 -0
- fmsave-0.4.0/src/fmsave/readers/span.py +1068 -0
- fmsave-0.4.0/src/fmsave/readers/stadiums.py +681 -0
- fmsave-0.4.0/src/fmsave/readers/staff.py +1067 -0
- fmsave-0.4.0/src/fmsave/readers/stages.py +374 -0
- fmsave-0.4.0/src/fmsave/readers/suspensions.py +353 -0
- fmsave-0.4.0/src/fmsave/readers/tactics.py +751 -0
- fmsave-0.4.0/src/fmsave/readers/training.py +511 -0
- fmsave-0.4.0/src/fmsave/table.py +441 -0
fmsave-0.4.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Randal S. Olson
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
fmsave-0.4.0/PKG-INFO
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: fmsave
|
|
3
|
+
Version: 0.4.0
|
|
4
|
+
Summary: Read Football Manager 26 save files into Python records, DataFrames, CSV and JSON.
|
|
5
|
+
Keywords: football-manager,fm26,save,parser,data
|
|
6
|
+
Author: Randal S. Olson
|
|
7
|
+
License-Expression: MIT
|
|
8
|
+
License-File: LICENSE
|
|
9
|
+
Classifier: Development Status :: 3 - Alpha
|
|
10
|
+
Classifier: Intended Audience :: Developers
|
|
11
|
+
Classifier: Intended Audience :: Science/Research
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3 :: Only
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
18
|
+
Classifier: Topic :: Games/Entertainment
|
|
19
|
+
Classifier: Typing :: Typed
|
|
20
|
+
Requires-Dist: backports-zstd>=1.7 ; python_full_version < '3.14'
|
|
21
|
+
Requires-Dist: pandas>=2.2 ; extra == 'pandas'
|
|
22
|
+
Requires-Dist: polars>=1.0 ; extra == 'polars'
|
|
23
|
+
Requires-Python: >=3.12
|
|
24
|
+
Project-URL: Repository, https://github.com/rhiever/fmsave
|
|
25
|
+
Project-URL: Issues, https://github.com/rhiever/fmsave/issues
|
|
26
|
+
Provides-Extra: pandas
|
|
27
|
+
Provides-Extra: polars
|
|
28
|
+
Description-Content-Type: text/markdown
|
|
29
|
+
|
|
30
|
+
# fmsave
|
|
31
|
+
|
|
32
|
+
Read your Football Manager 26 saves from disk into Python records, DataFrames, CSV and JSON.
|
|
33
|
+
|
|
34
|
+
## Install
|
|
35
|
+
|
|
36
|
+
fmsave needs Python 3.12 or newer.
|
|
37
|
+
|
|
38
|
+
```console
|
|
39
|
+
uvx fmsave info path/to/career.fm # try it without installing
|
|
40
|
+
pip install fmsave
|
|
41
|
+
pip install "fmsave[pandas]" # adds Table.to_pandas()
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
## Quickstart
|
|
45
|
+
|
|
46
|
+
```python
|
|
47
|
+
import fmsave
|
|
48
|
+
|
|
49
|
+
with fmsave.open("career.fm") as career_save:
|
|
50
|
+
my_club = career_save.managed_clubs()[0] # empty when you are between jobs
|
|
51
|
+
squad = career_save.players().where(club_uid=my_club.club_uid)
|
|
52
|
+
|
|
53
|
+
for player in squad.sorted_by(lambda player: player.ability.current, reverse=True)[:5]:
|
|
54
|
+
print(player.name, player.age, player.ability.current, player.attributes.finishing)
|
|
55
|
+
# "Alex Example" 24 148 16
|
|
56
|
+
|
|
57
|
+
squad_frame = squad.to_pandas() # needs fmsave[pandas]
|
|
58
|
+
squad.write_csv("squad.csv")
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
A player carries his names, birth date and age, nationality, club, height, positions, all 24
|
|
62
|
+
attributes, the eight personality attributes, current and potential ability, reputation, transfer
|
|
63
|
+
value, condition, traits, his contract and any unserved ban. Records and tables are immutable and
|
|
64
|
+
keep working after the save is closed.
|
|
65
|
+
|
|
66
|
+
Each reader returns a `Table`: `where(...)`, `filter(...)`, `sorted_by(...)`, `find(name=...)`,
|
|
67
|
+
`by_uid(...)`, `by_id(...)` and `coverage`, plus `to_dicts()`, `to_columns()`, `to_pandas()`,
|
|
68
|
+
`to_polars()`, `write_csv(...)`, `write_json(...)` and `write_jsonl(...)`.
|
|
69
|
+
|
|
70
|
+
## What else it reads
|
|
71
|
+
|
|
72
|
+
Twenty-six readers in all, each returning a `Table` of records:
|
|
73
|
+
|
|
74
|
+
- **People** — `players()`, `contracts()`, `suspensions()`, `staff()`, `staff_lists()`
|
|
75
|
+
- **Clubs** — `clubs()`, `managed_clubs()`, `finances()`, `sponsorships()`, `facilities()`,
|
|
76
|
+
`stadiums()`, `affiliates()`, `job_vacancies()`
|
|
77
|
+
- **Competitions** — `stages()`, `competitions()`, `fixtures()`, `league_tables()`,
|
|
78
|
+
`competition_rules()`, `transfer_windows()`, `player_match_stats()`
|
|
79
|
+
- **Injuries** — `injury_types()`, `injuries()`
|
|
80
|
+
- **Your own club's work**, which only the club you manage stores — `training()`, `mentoring()`,
|
|
81
|
+
`tactics()`, `set_pieces()`
|
|
82
|
+
|
|
83
|
+
Every field is either verified against the game or unconfirmed; ask with
|
|
84
|
+
`fmsave.field_status(fmsave.Player, "contract.wage")`. Values come from the save as stored, a value
|
|
85
|
+
fmsave cannot read is `None` rather than a guess, and money is kept in the unit the game stores it
|
|
86
|
+
in, which is not the currency it displays.
|
|
87
|
+
|
|
88
|
+
Each reader also measures what it decoded against loose bounds drawn from a couple of careers. A
|
|
89
|
+
save unlike them can miss one and still be read perfectly well, so a missed bound is a warning and
|
|
90
|
+
you get the table anyway. Pass `strict=True` to `fmsave.open` to have one raise instead.
|
|
91
|
+
|
|
92
|
+
## What it cannot read
|
|
93
|
+
|
|
94
|
+
- **Names the game renders from its own installed database.** Competitions, leagues, nations,
|
|
95
|
+
cities and all but a couple of hundred grounds. fmsave ships none of these and reads nothing from
|
|
96
|
+
your game install. Competitions carry `database_id`, the id every outside name source is keyed
|
|
97
|
+
on, so you can supply your own map — see [Competition names](#competition-names).
|
|
98
|
+
- **Links the save does not store**, such as which competition a rules block belongs to, or which
|
|
99
|
+
ground a club plays at. Worked out from the fixture calendar where possible, left empty where not.
|
|
100
|
+
- **Meanings for numbers the game never displayed**, such as staff job titles or the settings
|
|
101
|
+
inside a tactic. These come back as raw numbers rather than as labels fmsave guessed at.
|
|
102
|
+
- **What the save keeps only in part or not at all** — scores for about three quarters of a
|
|
103
|
+
career's matches, injuries older than about two years, and today's availability, line-ups, staff
|
|
104
|
+
attribute values, card counts, club debt and asking prices.
|
|
105
|
+
|
|
106
|
+
## Competition names
|
|
107
|
+
|
|
108
|
+
```python
|
|
109
|
+
competition_names = fmsave.read_competition_names("competition-names.csv")
|
|
110
|
+
with fmsave.open("career.fm", competition_names=competition_names) as career_save:
|
|
111
|
+
for competition in career_save.competitions():
|
|
112
|
+
print(competition.database_id, competition.name) # 12345 "Example League"
|
|
113
|
+
```
|
|
114
|
+
|
|
115
|
+
The file is UTF-8, two columns, `database_id` then `name`; a leading header row is skipped. Any
|
|
116
|
+
mapping of database id to name works too. Without a map, `name` and every denormalised
|
|
117
|
+
`competition_name` is `None`. About one competition in ten carries no database id and can never be
|
|
118
|
+
named, and one the game created during a career carries an id no outside source holds.
|
|
119
|
+
|
|
120
|
+
## Command line
|
|
121
|
+
|
|
122
|
+
```console
|
|
123
|
+
fmsave info career.fm
|
|
124
|
+
fmsave export career.fm players --managed-club -o squad.csv
|
|
125
|
+
fmsave export career.fm players --nation 7 --columns name,age,club_name,contract_wage
|
|
126
|
+
fmsave validate career.fm --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
`export` writes any of the twenty-six tables as CSV, JSON or JSON Lines, and needs a scope:
|
|
130
|
+
`--club`, `--managed-club`, `--competition`, `--nation` or `--all`. Nested groups flatten to
|
|
131
|
+
`contract_wage`-style columns and a coded value gives a label column plus a `_code` column, so JSON
|
|
132
|
+
is the lossless format. `validate` runs every reader and reports its checks, counts and coverage,
|
|
133
|
+
holding no names, uids or text from the save, so it is safe to paste into an issue.
|
|
134
|
+
|
|
135
|
+
## Where saves live
|
|
136
|
+
|
|
137
|
+
- macOS: `~/Library/Application Support/Sports Interactive/Football Manager 26/games/`
|
|
138
|
+
- Windows: usually `Documents\Sports Interactive\Football Manager 26\games\`
|
|
139
|
+
- Steam Cloud and other stores may keep saves somewhere else.
|
|
140
|
+
|
|
141
|
+
If the game is running, copy the save and read the copy.
|
|
142
|
+
|
|
143
|
+
## Safety
|
|
144
|
+
|
|
145
|
+
fmsave is read-only. It never modifies saves, makes no network connections, and does not read game
|
|
146
|
+
memory. Using it to gain an advantage in shared online careers may break platform or community
|
|
147
|
+
rules.
|
|
148
|
+
|
|
149
|
+
## Support
|
|
150
|
+
|
|
151
|
+
fmsave is a hobby project, maintained on a best-effort basis. Report problems through
|
|
152
|
+
[GitHub issues](https://github.com/rhiever/fmsave/issues), and never attach save files. See
|
|
153
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for how to report a wrong value without sharing real data. To
|
|
154
|
+
request removal of any content, open an issue with the rights request form.
|
|
155
|
+
|
|
156
|
+
## Disclaimer
|
|
157
|
+
|
|
158
|
+
fmsave is an unofficial fan project. It is not affiliated with, endorsed by, or sponsored by Sports
|
|
159
|
+
Interactive or SEGA. Football Manager, Sports Interactive and SEGA are trademarks or registered
|
|
160
|
+
trademarks of their respective owners.
|
|
161
|
+
|
|
162
|
+
## License
|
|
163
|
+
|
|
164
|
+
MIT. See [LICENSE](LICENSE).
|
fmsave-0.4.0/README.md
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
# fmsave
|
|
2
|
+
|
|
3
|
+
Read your Football Manager 26 saves from disk into Python records, DataFrames, CSV and JSON.
|
|
4
|
+
|
|
5
|
+
## Install
|
|
6
|
+
|
|
7
|
+
fmsave needs Python 3.12 or newer.
|
|
8
|
+
|
|
9
|
+
```console
|
|
10
|
+
uvx fmsave info path/to/career.fm # try it without installing
|
|
11
|
+
pip install fmsave
|
|
12
|
+
pip install "fmsave[pandas]" # adds Table.to_pandas()
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## Quickstart
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
import fmsave
|
|
19
|
+
|
|
20
|
+
with fmsave.open("career.fm") as career_save:
|
|
21
|
+
my_club = career_save.managed_clubs()[0] # empty when you are between jobs
|
|
22
|
+
squad = career_save.players().where(club_uid=my_club.club_uid)
|
|
23
|
+
|
|
24
|
+
for player in squad.sorted_by(lambda player: player.ability.current, reverse=True)[:5]:
|
|
25
|
+
print(player.name, player.age, player.ability.current, player.attributes.finishing)
|
|
26
|
+
# "Alex Example" 24 148 16
|
|
27
|
+
|
|
28
|
+
squad_frame = squad.to_pandas() # needs fmsave[pandas]
|
|
29
|
+
squad.write_csv("squad.csv")
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
A player carries his names, birth date and age, nationality, club, height, positions, all 24
|
|
33
|
+
attributes, the eight personality attributes, current and potential ability, reputation, transfer
|
|
34
|
+
value, condition, traits, his contract and any unserved ban. Records and tables are immutable and
|
|
35
|
+
keep working after the save is closed.
|
|
36
|
+
|
|
37
|
+
Each reader returns a `Table`: `where(...)`, `filter(...)`, `sorted_by(...)`, `find(name=...)`,
|
|
38
|
+
`by_uid(...)`, `by_id(...)` and `coverage`, plus `to_dicts()`, `to_columns()`, `to_pandas()`,
|
|
39
|
+
`to_polars()`, `write_csv(...)`, `write_json(...)` and `write_jsonl(...)`.
|
|
40
|
+
|
|
41
|
+
## What else it reads
|
|
42
|
+
|
|
43
|
+
Twenty-six readers in all, each returning a `Table` of records:
|
|
44
|
+
|
|
45
|
+
- **People** — `players()`, `contracts()`, `suspensions()`, `staff()`, `staff_lists()`
|
|
46
|
+
- **Clubs** — `clubs()`, `managed_clubs()`, `finances()`, `sponsorships()`, `facilities()`,
|
|
47
|
+
`stadiums()`, `affiliates()`, `job_vacancies()`
|
|
48
|
+
- **Competitions** — `stages()`, `competitions()`, `fixtures()`, `league_tables()`,
|
|
49
|
+
`competition_rules()`, `transfer_windows()`, `player_match_stats()`
|
|
50
|
+
- **Injuries** — `injury_types()`, `injuries()`
|
|
51
|
+
- **Your own club's work**, which only the club you manage stores — `training()`, `mentoring()`,
|
|
52
|
+
`tactics()`, `set_pieces()`
|
|
53
|
+
|
|
54
|
+
Every field is either verified against the game or unconfirmed; ask with
|
|
55
|
+
`fmsave.field_status(fmsave.Player, "contract.wage")`. Values come from the save as stored, a value
|
|
56
|
+
fmsave cannot read is `None` rather than a guess, and money is kept in the unit the game stores it
|
|
57
|
+
in, which is not the currency it displays.
|
|
58
|
+
|
|
59
|
+
Each reader also measures what it decoded against loose bounds drawn from a couple of careers. A
|
|
60
|
+
save unlike them can miss one and still be read perfectly well, so a missed bound is a warning and
|
|
61
|
+
you get the table anyway. Pass `strict=True` to `fmsave.open` to have one raise instead.
|
|
62
|
+
|
|
63
|
+
## What it cannot read
|
|
64
|
+
|
|
65
|
+
- **Names the game renders from its own installed database.** Competitions, leagues, nations,
|
|
66
|
+
cities and all but a couple of hundred grounds. fmsave ships none of these and reads nothing from
|
|
67
|
+
your game install. Competitions carry `database_id`, the id every outside name source is keyed
|
|
68
|
+
on, so you can supply your own map — see [Competition names](#competition-names).
|
|
69
|
+
- **Links the save does not store**, such as which competition a rules block belongs to, or which
|
|
70
|
+
ground a club plays at. Worked out from the fixture calendar where possible, left empty where not.
|
|
71
|
+
- **Meanings for numbers the game never displayed**, such as staff job titles or the settings
|
|
72
|
+
inside a tactic. These come back as raw numbers rather than as labels fmsave guessed at.
|
|
73
|
+
- **What the save keeps only in part or not at all** — scores for about three quarters of a
|
|
74
|
+
career's matches, injuries older than about two years, and today's availability, line-ups, staff
|
|
75
|
+
attribute values, card counts, club debt and asking prices.
|
|
76
|
+
|
|
77
|
+
## Competition names
|
|
78
|
+
|
|
79
|
+
```python
|
|
80
|
+
competition_names = fmsave.read_competition_names("competition-names.csv")
|
|
81
|
+
with fmsave.open("career.fm", competition_names=competition_names) as career_save:
|
|
82
|
+
for competition in career_save.competitions():
|
|
83
|
+
print(competition.database_id, competition.name) # 12345 "Example League"
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
The file is UTF-8, two columns, `database_id` then `name`; a leading header row is skipped. Any
|
|
87
|
+
mapping of database id to name works too. Without a map, `name` and every denormalised
|
|
88
|
+
`competition_name` is `None`. About one competition in ten carries no database id and can never be
|
|
89
|
+
named, and one the game created during a career carries an id no outside source holds.
|
|
90
|
+
|
|
91
|
+
## Command line
|
|
92
|
+
|
|
93
|
+
```console
|
|
94
|
+
fmsave info career.fm
|
|
95
|
+
fmsave export career.fm players --managed-club -o squad.csv
|
|
96
|
+
fmsave export career.fm players --nation 7 --columns name,age,club_name,contract_wage
|
|
97
|
+
fmsave validate career.fm --json
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
`export` writes any of the twenty-six tables as CSV, JSON or JSON Lines, and needs a scope:
|
|
101
|
+
`--club`, `--managed-club`, `--competition`, `--nation` or `--all`. Nested groups flatten to
|
|
102
|
+
`contract_wage`-style columns and a coded value gives a label column plus a `_code` column, so JSON
|
|
103
|
+
is the lossless format. `validate` runs every reader and reports its checks, counts and coverage,
|
|
104
|
+
holding no names, uids or text from the save, so it is safe to paste into an issue.
|
|
105
|
+
|
|
106
|
+
## Where saves live
|
|
107
|
+
|
|
108
|
+
- macOS: `~/Library/Application Support/Sports Interactive/Football Manager 26/games/`
|
|
109
|
+
- Windows: usually `Documents\Sports Interactive\Football Manager 26\games\`
|
|
110
|
+
- Steam Cloud and other stores may keep saves somewhere else.
|
|
111
|
+
|
|
112
|
+
If the game is running, copy the save and read the copy.
|
|
113
|
+
|
|
114
|
+
## Safety
|
|
115
|
+
|
|
116
|
+
fmsave is read-only. It never modifies saves, makes no network connections, and does not read game
|
|
117
|
+
memory. Using it to gain an advantage in shared online careers may break platform or community
|
|
118
|
+
rules.
|
|
119
|
+
|
|
120
|
+
## Support
|
|
121
|
+
|
|
122
|
+
fmsave is a hobby project, maintained on a best-effort basis. Report problems through
|
|
123
|
+
[GitHub issues](https://github.com/rhiever/fmsave/issues), and never attach save files. See
|
|
124
|
+
[CONTRIBUTING.md](CONTRIBUTING.md) for how to report a wrong value without sharing real data. To
|
|
125
|
+
request removal of any content, open an issue with the rights request form.
|
|
126
|
+
|
|
127
|
+
## Disclaimer
|
|
128
|
+
|
|
129
|
+
fmsave is an unofficial fan project. It is not affiliated with, endorsed by, or sponsored by Sports
|
|
130
|
+
Interactive or SEGA. Football Manager, Sports Interactive and SEGA are trademarks or registered
|
|
131
|
+
trademarks of their respective owners.
|
|
132
|
+
|
|
133
|
+
## License
|
|
134
|
+
|
|
135
|
+
MIT. See [LICENSE](LICENSE).
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fmsave"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Read Football Manager 26 save files into Python records, DataFrames, CSV and JSON."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
keywords = [
|
|
10
|
+
"football-manager",
|
|
11
|
+
"fm26",
|
|
12
|
+
"save",
|
|
13
|
+
"parser",
|
|
14
|
+
"data",
|
|
15
|
+
]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"Intended Audience :: Science/Research",
|
|
20
|
+
"Operating System :: OS Independent",
|
|
21
|
+
"Programming Language :: Python :: 3",
|
|
22
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Programming Language :: Python :: 3.14",
|
|
26
|
+
"Topic :: Games/Entertainment",
|
|
27
|
+
"Typing :: Typed",
|
|
28
|
+
]
|
|
29
|
+
dependencies = ["backports.zstd>=1.7; python_version < '3.14'"]
|
|
30
|
+
|
|
31
|
+
[[project.authors]]
|
|
32
|
+
name = "Randal S. Olson"
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
pandas = ["pandas>=2.2"]
|
|
36
|
+
polars = ["polars>=1.0"]
|
|
37
|
+
|
|
38
|
+
[project.urls]
|
|
39
|
+
Repository = "https://github.com/rhiever/fmsave"
|
|
40
|
+
Issues = "https://github.com/rhiever/fmsave/issues"
|
|
41
|
+
|
|
42
|
+
[project.scripts]
|
|
43
|
+
fmsave = "fmsave.cli:main"
|
|
44
|
+
|
|
45
|
+
[dependency-groups]
|
|
46
|
+
test = [
|
|
47
|
+
"pytest>=9.1",
|
|
48
|
+
"pytest-cov>=7.1",
|
|
49
|
+
"pytest-timeout>=2.4",
|
|
50
|
+
"hypothesis>=6.168",
|
|
51
|
+
"syrupy>=6.1",
|
|
52
|
+
"pandas>=2.2",
|
|
53
|
+
"polars>=1.0",
|
|
54
|
+
]
|
|
55
|
+
dev = [
|
|
56
|
+
{ include-group = "test" },
|
|
57
|
+
"ruff>=0.16.7,<0.17",
|
|
58
|
+
"pyright>=1.1.414",
|
|
59
|
+
]
|
|
60
|
+
|
|
61
|
+
[build-system]
|
|
62
|
+
requires = ["uv_build>=0.12.13,<0.13"]
|
|
63
|
+
build-backend = "uv_build"
|
|
64
|
+
|
|
65
|
+
[tool.uv]
|
|
66
|
+
default-groups = ["dev"]
|
|
67
|
+
|
|
68
|
+
[tool.ruff]
|
|
69
|
+
line-length = 100
|
|
70
|
+
target-version = "py312"
|
|
71
|
+
|
|
72
|
+
[tool.pyright]
|
|
73
|
+
include = [
|
|
74
|
+
"src",
|
|
75
|
+
"tests",
|
|
76
|
+
"scripts",
|
|
77
|
+
]
|
|
78
|
+
typeCheckingMode = "standard"
|
|
79
|
+
strict = [
|
|
80
|
+
"src",
|
|
81
|
+
"scripts",
|
|
82
|
+
]
|
|
83
|
+
|
|
84
|
+
[tool.pytest.ini_options]
|
|
85
|
+
testpaths = ["tests"]
|
|
86
|
+
pythonpath = ["."]
|
|
87
|
+
strict = true
|
|
88
|
+
addopts = ["-ra"]
|
|
89
|
+
timeout = "300"
|
|
90
|
+
markers = [
|
|
91
|
+
"corpus: needs the local private corpus (never runs in CI)",
|
|
92
|
+
"corpus_fast: corpus tier for open, info, players and clubs",
|
|
93
|
+
"corpus_full: corpus tier for every reader",
|
|
94
|
+
]
|
|
95
|
+
|
|
96
|
+
[tool.coverage.run]
|
|
97
|
+
branch = true
|
|
98
|
+
source = ["fmsave"]
|
|
99
|
+
|
|
100
|
+
[tool.coverage.report]
|
|
101
|
+
fail_under = 90
|
|
102
|
+
show_missing = true
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
[project]
|
|
2
|
+
name = "fmsave"
|
|
3
|
+
version = "0.4.0"
|
|
4
|
+
description = "Read Football Manager 26 save files into Python records, DataFrames, CSV and JSON."
|
|
5
|
+
readme = "README.md"
|
|
6
|
+
requires-python = ">=3.12"
|
|
7
|
+
license = "MIT"
|
|
8
|
+
license-files = ["LICENSE"]
|
|
9
|
+
authors = [{ name = "Randal S. Olson" }]
|
|
10
|
+
keywords = ["football-manager", "fm26", "save", "parser", "data"]
|
|
11
|
+
classifiers = [
|
|
12
|
+
"Development Status :: 3 - Alpha",
|
|
13
|
+
"Intended Audience :: Developers",
|
|
14
|
+
"Intended Audience :: Science/Research",
|
|
15
|
+
"Operating System :: OS Independent",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"Programming Language :: Python :: 3 :: Only",
|
|
18
|
+
"Programming Language :: Python :: 3.12",
|
|
19
|
+
"Programming Language :: Python :: 3.13",
|
|
20
|
+
"Programming Language :: Python :: 3.14",
|
|
21
|
+
"Topic :: Games/Entertainment",
|
|
22
|
+
"Typing :: Typed",
|
|
23
|
+
]
|
|
24
|
+
dependencies = ["backports.zstd>=1.7; python_version < '3.14'"]
|
|
25
|
+
|
|
26
|
+
[project.optional-dependencies]
|
|
27
|
+
pandas = ["pandas>=2.2"]
|
|
28
|
+
polars = ["polars>=1.0"]
|
|
29
|
+
|
|
30
|
+
[project.urls]
|
|
31
|
+
Repository = "https://github.com/rhiever/fmsave"
|
|
32
|
+
Issues = "https://github.com/rhiever/fmsave/issues"
|
|
33
|
+
|
|
34
|
+
[project.scripts]
|
|
35
|
+
fmsave = "fmsave.cli:main"
|
|
36
|
+
|
|
37
|
+
[dependency-groups]
|
|
38
|
+
test = [
|
|
39
|
+
"pytest>=9.1",
|
|
40
|
+
"pytest-cov>=7.1",
|
|
41
|
+
"pytest-timeout>=2.4",
|
|
42
|
+
"hypothesis>=6.168",
|
|
43
|
+
"syrupy>=6.1",
|
|
44
|
+
"pandas>=2.2",
|
|
45
|
+
"polars>=1.0",
|
|
46
|
+
]
|
|
47
|
+
dev = [
|
|
48
|
+
{ include-group = "test" },
|
|
49
|
+
"ruff>=0.16.7,<0.17",
|
|
50
|
+
"pyright>=1.1.414",
|
|
51
|
+
]
|
|
52
|
+
|
|
53
|
+
[build-system]
|
|
54
|
+
requires = ["uv_build>=0.12.13,<0.13"]
|
|
55
|
+
build-backend = "uv_build"
|
|
56
|
+
|
|
57
|
+
[tool.uv]
|
|
58
|
+
default-groups = ["dev"]
|
|
59
|
+
|
|
60
|
+
[tool.ruff]
|
|
61
|
+
line-length = 100
|
|
62
|
+
target-version = "py312"
|
|
63
|
+
|
|
64
|
+
[tool.pyright]
|
|
65
|
+
include = ["src", "tests", "scripts"]
|
|
66
|
+
typeCheckingMode = "standard"
|
|
67
|
+
strict = ["src", "scripts"]
|
|
68
|
+
|
|
69
|
+
[tool.pytest.ini_options]
|
|
70
|
+
testpaths = ["tests"]
|
|
71
|
+
pythonpath = ["."]
|
|
72
|
+
strict = true
|
|
73
|
+
addopts = ["-ra"]
|
|
74
|
+
timeout = "300"
|
|
75
|
+
markers = [
|
|
76
|
+
"corpus: needs the local private corpus (never runs in CI)",
|
|
77
|
+
"corpus_fast: corpus tier for open, info, players and clubs",
|
|
78
|
+
"corpus_full: corpus tier for every reader",
|
|
79
|
+
]
|
|
80
|
+
|
|
81
|
+
[tool.coverage.run]
|
|
82
|
+
branch = true
|
|
83
|
+
source = ["fmsave"]
|
|
84
|
+
|
|
85
|
+
[tool.coverage.report]
|
|
86
|
+
fail_under = 90
|
|
87
|
+
show_missing = true
|