Skip to content

Fix stadium, staff, league-table and FM27 reading bugs for 0.8.1 - #50

Merged
rhiever merged 17 commits into
mainfrom
fix/0.8.1
Oct 31, 2026
Merged

rhiever merged 17 commits into
mainfrom
fix/0.8.1

Conversation

@rhiever

@rhiever rhiever commented Oct 31, 2026

Copy link
Copy Markdown
Owner

Summary

Bug fixes for 0.8.1, and the version bump.

Checked on FM26 and FM27 saves, against in-game screens where we have them, and with the local corpus tests.

Thanks @jeohist for the reports.

Checklist

  • I added or updated tests for this change.
  • This change holds no real data: no save files or fragments, screenshots, or real names, uids or values from any save.
  • I derived format knowledge only from my own saves and the game UI (no decompilation, leaked or non-disclosure material, or code from unlicensed parsers).
  • uv run pytest, uv run ruff check ., uv run ruff format --check . and uv run pyright pass.

🤖 Generated with Claude Code

rhiever and others added 17 commits October 30, 2026 20:40
The locator finds the table by ordinals 1, 2 and 3, so the row before that
head, ordinal 0, was never read and every fixture storing it had no ground.
Take that row when it reads as a plain row with ordinal 0, the uid doubled,
a zero byte, a pitch length inside the checked range and a uid the walk did
not already read; otherwise the table starts at ordinal 1 as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A ground's built and rebuilt dates went through the shared game-date decode,
whose 1901 floor turned every ground built in the 1800s into no date. Decode
them with a stadium-local floor of 1800, below the oldest year any measured
row stores, keep day 1 of 1900 as the no-date value, and leave the shared
decode unchanged for other readers.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Stadium.all_seater_capacity holds a ground's total capacity, and the seated
figure the facilities screen shows in brackets is the word returned so far
only as unknown["u17"]. Add Stadium.seated_capacity for it, keep the unknown
key with the same value, and correct the docstrings of the total, the
expansion and capacity fields and the pitch dimensions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A new manager's personality stores a zero in one slot, so the marked search
for name blocks never reached his block and his staff row had no name. When
that search finds nothing for the human manager, his already bounded window is
searched once more with personality bytes accepted from a layout floor. Only
the FM27 layout sets the floor; FM26 and every other row read as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
On an FM26 day-one save the human manager's profile also stores a zero
personality byte, so the name search skipped his block and, with no end at
his closing header, took the next person's name, birth date and
personality. The FM26 layout now sets the personality floor, and under the
floor the human's window ends at his closing header. Only the human row
changes; other staff windows are as before.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Under the personality floor the human manager's name-block window ends at
his closing header, so his contract records are now looked for up to the
same header. A record with his selector in the next object is not his,
and his window can no longer come out empty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every club plays friendlies, so the friendly competition covers every member
of a division as fully as the division does, and the lower id won the tie.
A tie now goes to the competition holding the most calendar meetings between
two members of the table; remaining ties keep the lower id.

On five FM26 saves this changes competition_id on 11% to 31% of tables, and
the rules that link to those tables follow. Over the changed tables on one
save, calendar meetings between members in the voted competition rise from
226 to 12,101.

The rules round-date floor is re-tuned from 0.65 to 0.87. On the four saves
it applies to, linked blocks now score 0.947 to 0.987 and blocks linked to the
run before them score 0.657 to 0.790; under the old vote the linked share was
0.767 to 0.901, which the new floor also fails on three of them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The stored count runs above goals and shots on target on some rows of
every FM26 career checked, so it does not count goals everywhere. FM26
now withholds it as FM27 does: the field and CSV column stay, with None.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
experimental_span() capped frames at 8 MiB and the span at 64 MiB, so
early-season FM27 saves whose span frames had outgrown 8 MiB were
reported as corrupt. Default Fm27SpanLimits to the 512 MiB total and
256 MiB per-frame caps that limit_game_index already applies, from one
shared definition. The integration test now passes its own small limit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The span pass now defaults to 128 MiB a frame and 256 MiB in all,
derived from the largest frames and spans measured on FM27 and FM26
saves with headroom, instead of reusing the 512 MiB container budget.
Frames are copied into one buffer rather than kept as chunks and
joined, so the pass holds the span once instead of twice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
FM27 Facilities screens show 0 as None, 3 and 4 as Fairly Basic and 16
as Excellent. Add a NONE member for 0 and map the other three to the
existing words, so every code from 0 to 20 now has a label. Member
values are unchanged. The docstrings say which codes FM27 screens named
and that no FM26 screen has shown them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
One FM27 club's sponsorship screen showed kind 1 as Main Kit, 3 and 23
as Stadium, 4 as General, 8 as Away Kit, 9 as Additional Kit Sponsor
and 11 as Back of Shirt. The FM27 sponsor layout names those codes;
every other code, and every FM26 code, stays UNKNOWN.

Document that the screen's Duration is total_value / annual_value
rather than the span between the dates, and that an unexpired row can
sit outside the displayed sponsorship income.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Adding the kind label lookup moved the cache decorator off
_sponsor_row_struct, so every club rebuilt its row struct. Cache it
again and test that both lookups are built once per layout.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
A summary build number past the u32 range leaked struct.error from
fmsave.open, and a leading-zero major such as 027 opened as FM27 while
every reader looked up FM26 layouts, bypassing FM27's fail-closed check.
Both are now skipped as non-versions, so the save is unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Every read reopens the file, so a save opened by a relative path raised
SaveChangedError once the working folder changed. The index now keeps
the absolute path.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@rhiever
rhiever merged commit 44c06f3 into main Oct 31, 2026
12 checks passed
@rhiever
rhiever deleted the fix/0.8.1 branch October 31, 2026 03:59
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant