Two tables in DAT/PARAM set up the clubs at the start of a career:
| File | What it sets up |
|---|---|
PLRRSRC_INITTEAMDATA.TBB |
which clubs start in which league and division, and each competition's order from "last season" |
OTEAMMEMBER.TBB |
the 25-player squad of every computer-controlled club |
Both are plain TBB1 files (TBB_FORMAT.md). Which code
loads them is in PARAM_DIR.md
(ePLRSRC 4 and 3). SRC/initteam.py reads both, and gives clubs their
names from MES.PAC.
| Address | Symbol | What it shows |
|---|---|---|
0x253338 |
PlRrsrc_InitTeamData load callback |
hands tables 0, 1 and 2 to the three readers below |
0x253228 |
table 0 reader | leagues 0–5 (pwkLg_GetLeague). Each gets two divisions (plLg_AddDiv twice), filled from data + i*0xd0 and data + i*0xd0 + 0x68 |
0x2e94c8 |
plLg_EntryTeamSetToDiv |
walks u32 team ids until a 0 or until the division holds 0x1a (26) teams |
0x2531b0 |
table 1 reader | for PLSCHE_COMPE 0–48 (slti 0x31): clears the past record and passes the next 0x80 bytes to pwkRec_SetPastRecordLastTeam |
0x253650, 0x2535f0 |
pwkRec_SetPastRecordLastTeam, …One |
32 u32 team ids, stored as positions 0–31 (slti 0x20) of the competition's record at +0x08 |
0x2532c8 |
table 2 reader | 49 u16 values to pwkRec_GetCompeConventionRecordKeikayear |
0x24a6a0 |
pwkOteam_Init |
teams 3 to 0x1b9 (slti 0x1ba), 25 records of 16 bytes each. Copies +0x0, +0x4, +0x8, +0xC into squad slot bytes +0, +2, +3, +4 |
0x2734c8 |
pwkTeam_SetUnumberOpinfo |
a squad is 25 six-byte PlOpinfo slots. +0 is an s16 player number, with a negative value meaning an empty slot. +3 is the shirt number: 1–99 is kept, anything else is replaced by the player's own preferred number or the first free number from 12 |
0x221480 |
UpdateConyear |
writes +4 (contract years) from +2: under 28 → 3–5 years, under 30 → 2–4, older → 2–3. So +2 is the age |
0x201f20 |
MSG_UTIL::CWildCardFunc::getPlayerName |
builds a PlOpinfo {player, age 16, 0, 0, 0} to look a player up in the player database (plBp_GetBpinfo) |
0x200ea8 |
Msg::GetString |
message type 0x2a (team name) for team t is global category slot 2, record 2000 + t. The base is the u16 at 0x52f7b0. Team 0 is a fixed string, teams 1 and 2 (your club and the rival) are runtime names, and teams 0x21f–0x22e use pwkVs_GetTeamName |
0x112ad0, 0x51ab38 |
CFcEuro_RootModule::GlobalMsgSetup and its table |
global slots 0–9 are message categories 1, 11, 3, 4, 5, 6, 7, 8, 9, 10. So slot 2 is category 3 |
Three tables. Their line sizes (4, 4, 2) are the element sizes, not the record sizes.
6 leagues × 2 divisions × 0x68 bytes. Division d of league l is at
(l*2 + d) * 0x68:
| Offset | Type | Meaning |
|---|---|---|
0x00 |
u32[26] | PlTeam ids, in order. The list ends at the first 0, or after 26 |
Empirical, checked by initteam.py info:
- The divisions hold 20, 24, 20, 20, 18, 18, 20, 22, 20, 22, 18 and 20 clubs: 242 in total.
- Each club appears once. The ids are exactly 3–244.
- No list has non-zero ids after its terminating
0. - By their names the leagues are England, France, Germany, Italy, Spain and the Netherlands, each with a first (division 0) and second division. Nothing in the code names them, so this is empirical.
56 records of 0x80 bytes, one per PLSCHE_COMPE:
| Offset | Type | Meaning |
|---|---|---|
0x00 |
u32[32] | PlTeam id at positions 0–31, 0 for none |
Only records 0–48 are read. Records 38–43 and 48 are empty, as are the
unread 49–55, so 42 of the 49 read records have teams. (--mod third-tier fills records 50–55 and has the game read them,
UID_LIMITS.md.)
For a league the
list is its starting table: record 0 is the English first division in
the same order as table 0. Records 44–47 hold national teams. Knockout
competitions have their entrants. The schedule's LAST_RANK team entries
read these records back through pwkRec_GetCompePastRecord
(SCHEDULE_FORMAT.md), so this table decides who
qualifies for the first season's cups.
56 values (u16 in the reader, all small negative numbers as s16). The
first 49 are read. They are -1 ×45, -2 ×2 and -4 ×2. The -4 and -2
fall on the national-team competitions 44–47, which fits a 4-year or
2-year cycle ("keika year" is roughly "years elapsed"). How the game uses
the sign is not traced.
initteam.py swap <in> <out> <a>:<b> ... exchanges two clubs' ids in
tables 0 and 1. Each club takes the other's place in the starting
divisions and in last season's results. The first season is built from
those results: the league schedules fill their slots from LAST_RANK
records (in England UID 0 keeps ranks 1–17 of competition 0 and takes
the second division's top two and the winner of competition 2, the
3rd–6th promotion playoffs; UIDs 1 and 2 take ranks 18–20 of
competition 0, the playoff losers and the rest of the second division,
SCHEDULE_FORMAT.md),
and the cups take their entrants the same way. So a swapped club starts
in the other's division and inherits its promotion, relegation and cup
places.
What a swap keeps, and why:
- Division sizes. A league schedule has a fixed number of entrant
slots and
n(n−1)/2pairings for it, so a writer can't add a club to a division or take one away without new schedules. A swap changes no size.leaguesize.py buildchanges a first division's size, with new schedules (SCHEDULE_FORMAT.md). - Only league clubs. Both clubs must be in table 0 (teams 3–244).
Clubs from the rest of Europe and beyond (245 on) have squads and club
records but no place in the emblem screens' league lists and no
MAPTEAM_LISTrow; whether one can play in a league isn't tested. - Not touched: table 2,
MAPTEAM_LIST.TBB(its reader isn't known), the club records (city, stadium, manager) and the schedules, none of which names a league club: theirPLTEAMIDrecords name only team 1 and the VS teams.
A club's squad, kit, crest and record stay its own.
Tested in PCSX2. In the editor's Season tab, Chelsea (team 7, 1st of
20 in the English first division last season) was swapped with
Sheffield (41, 24th of 24 in the second division), on a
--skip-tutorial disc. In the 2006–07 season Chelsea played in the
Champions Division (the English second division), at home to the
player's club. A club that finishes low in a second division stays
there: the English second division's own-nation schedule (UID 1) keeps
ranks 7–26 of last season's second division, and the retail game has
no third tier to relegate to (Nexxus's report; --mod third-tier adds one,
UID_LIMITS.md).
Across leagues (Nexxus's report, PCSX2): F.C. Barcelona, swapped into the English Champions Division in the Season tab, has fixtures in that league in 2006–07 (fixture 22, away at the player's Dunstable Utd). Its Information page still shows Spain and Barcelona and world ranking 12. League Ranking is the club's current place in its league table, so it shows "-" in week 1, before any league match has been played.
Empirical: each division's clubs are exactly the clubs of one read
past record, its table last season: records 0, 1, 6, 7, 11, 12, 16, 17,
22, 23, 27 and 28 for the 12 divisions in order (the English second
division is also record 3). Eleven are in the division's order; the
Italian second division's record is in another, so the records hold the
finishing order. InitTeamData.past_record finds it, and the editor's
Season tab shows each division in that order with its swaps.
One table of 175,600 bytes = 439 teams (ids 3–441) × 25 slots × 16 bytes. The line size is 4 because every field was exported as a u32:
| Offset | Type | Slot byte | Meaning |
|---|---|---|---|
0x0 |
u16 (+2 pad) | +0 |
player number (index into the player database) |
0x4 |
u8 (+3 pad) | +2 |
age |
0x8 |
u8 (+3 pad) | +3 |
shirt number |
0xC |
u8 (+3 pad) | +4 |
contract years remaining |
Empirical, checked by initteam.py info:
- Every slot is filled. The player numbers are 0–10,974, each used exactly once.
- Ages are 16–40, and contract lengths are 2–6.
- These are the ages a computer team's players show in game, one year
older. Tested in PCSX2: Terry's 25 shows as 26, and after
initteam.py set 7:1 age=15he shows as 16. The player database's own age field doesn't override them. Also tested through the editor's Clubs tab: Van der Sar (team 13, slot 0) at age 16 and shirt 69 showed 17 and 69 on his detail screen. The extra year may come from the tutorial skip rather than from a new game:TEAM_INIT_DATA's ages show one year older on a--skip-tutorialdisc only (TEAMINIT_FORMAT.md), and whether these tests used the skip isn't recorded. - Shirt numbers are 1–99 and never repeat within a team.
- All padding bytes are zero.
The squads cover more clubs (439) than the leagues (242). Teams 245–441
are clubs from the rest of Europe (Salzburg, Porto, Glasgow City,
...), followed by clubs from South America, Africa, North America, Asia
and Oceania (Flamengo, Cairo, Toronto, Kanagawa, North Shore).
The names go on past 441 to 542 (Australia, New Zealand). Those teams
have no squad in this file.
Extended by --mod third-tier. The table may also hold 18 more
squads after club 441, for clubs 442–459 (457 squads, 182,848 bytes);
initteam.py reads either size. initteam.add_tier_squads builds them
from the database (each club's 25 players from 0x63f7), as the
LMASTER third tier needs
(CLUB_LIMITS.md).
Club names are message category 3, record 2000 + team id, in each
language slot (python SRC/mbb.py dump DAT/MESSAGE/MES.PAC --cat 3).
The game reads them from the copy in PRELOAD/STATIONMES<lang>.PAC, which
stays loaded all game, not from MES.PAC. Tested in PCSX2: with different
names in the two copies, VS mode showed the STATIONMES1 one. An edit
must be patched with --copies (PRELOAD_DIR.md).
Categories 4 and 5 are parallel lists (5 holds the three-letter short
names, CHE, LIV, ...). Category 10 has <name> Stadium, from 11000.
Category 961 is something else: 839 English town names (Reading,
Slough, ...) with short forms in 962, which aren't indexed by team.
Player names aren't in these files. The player number is an id in the
player database, PBDATA_EU.PAC (PBDATA_FORMAT.md),
and initteam.py squads adds each player's name from it. Team 7's squad
starts with players 100–111: Petr Cech, John Terry, Ricardo Carvalho, …
Each player's shirt number here matches their preferred number (+0x2b)
in the database for these examples, but the squad's own number is the
one used.
Confirmed, plOteam_GetDb (0x2165d8). One TBB table of 457 × 24
bytes, record PlTeam − 3 (teams 3–459; later ids read record 0, and
team 2, the rival, uses a runtime record). Readers were found for most
fields by following all 21 callers:
| Offset | Type | Name | Reader | Meaning |
|---|---|---|---|---|
0x00 |
s16 | rank | plOteam_GetManagerClubRank, pwkOteam_GetRank (via pwkOteam_Init2 → +0xa0) |
club rank, 0–31 |
0x02 |
u16 | world_rank | pwkOteam_Init2 → +0xa2, pwkOteam_GetWorldClubRank |
world club rank points, 0–1020 (Chelsea 980) |
0x04 |
s16 | manager | plOteam_GetManagerNoOffset |
manager: database id − 0x6d2e. All 457 differ |
0x06 |
s16 | stadium | PlGiTask::InitStadium, plTeam_GetStadiumNoFromNation |
STADIUM_DATA row (below) |
0x08 |
u8 | foreign | GetCanBelongForeignPlayerNum, CAcquirePlayer::GetSearchNation |
0–7: row of PLRESOURCESIM entry 6, table 1 (2 bytes: foreign players allowed, % chance to search abroad) |
0x09 |
u8 | newface | CAcquirePlayer::IsAcquireNewfacePlayer |
0–3: index into the % table at 0x533ff8 (chance to sign new faces) |
0x0a |
u8 | search_region | CAcquirePlayer::GetSearchNation |
0–31: row of entry 6, table 4 (13 weights, one per scouting region) |
0x0b, 0x0c, 0x0d, 0x0f |
u8 | – | none found | 0x0b always equals 0x0a; 0x0c 1–87; 0x0d 0–4; 0x0f 0–25 in steps of 5 |
0x0e |
u8 | money | plTeam_IsAgreeTransferChangeMoney, 0x248140 |
transfer sums × 1.0 below 60, × 0.95 for 60–79, × 0.9 from 80. 438 clubs have 0, 8 have 70, 11 have 85. Clubs without a record count as 50 |
0x10 |
u16 | city | plTeam_GetCity |
home city |
0x12 |
u16 | list_state | plState_GetEmblemDisplayTeamList |
the state the club is listed under in the emblem screens |
0x14 |
u32 | list_city | plCity_GetEmblemDisplayTeamList |
the city it is listed under (Chelsea: West London). 0 for clubs outside the six leagues |
National teams (460–542) take their manager from PLRESOURCESIM entry 4
instead: 18 bytes per nation, with the s16 at +0 read by
plOteam_GetManagerNoOffset. Their rank is the nation's world rating
÷ 4.
City names are message category 961 and state names category 960
(empirical: city 604 is London, state 17 Greater London, and every
club checked is in its own town). plCity_GetString and
plState_GetString take the category from their caller.
The best clubs by world club rank share the game's few huge grounds: Chelsea and Highbury (Arsenal) are at stadium 96, AC Milan and Inter at 99, Bayern, Dortmund and Leverkusen at 98, and Barcelona and La Boca at 100 (110,000 seats each). The game gives the biggest clubs by reputation these stadiums instead of their real grounds.
Confirmed, the load callback 0x253418, pwkRec_GetUefaNation
(0x253e08) and pwkRec_GetWorldNation (0x253e68). 145 records of
6 bytes, for nations 1–145:
| Offset | Type | Nations | Meaning |
|---|---|---|---|
0x0 |
u8 | 1–52 | UEFA ranking position (Italy 2, England 3, France 4) |
0x2 |
u16 | 1–52 | UEFA coefficient points (Italy 492, England 482) |
0x4 |
u8 | all | world rating (Brazil 125, Argentina 120). ÷ 4 is the national team's rank |
Nations 1–52 are the UEFA members. Their +0/+2 go into 4-byte slots
of save block 5 at 0x4663c, and +4 into bytes at 0x4670c.
Confirmed, plTeam_IsRoofFromID, plTeam_GetStadiumLvFromID and
plTeam_GetCapacityFromID (0x22ae18, 0x22adc8, 0x22ae68). 119
records of 3 bytes: roof (0/1, 52 have one), stadium level (0–4), and
capacity in thousands. GetCapacityFromID adds 500 seats for stadiums
103, 105 and 107. Capacities run from 8,000 to 110,000.
242 × {u16 team, u16 flag}, the league clubs 3–244 in order. Only the
file name is known from the code (SIMPRG.REL's load list at
0x80694). The reader hasn't been found. Empirical: the 116 clubs
with flag 1 are the real 2005/06 top divisions. Compared with the
starting divisions (PLRRSRC_INITTEAMDATA table 0, which has the
2004/05 line-up), the flags move the clubs promoted and relegated in
2005: Sunderland, West Ham and Wigan in, Crystal Palace, Norwich and
Southampton out, and likewise in the other five leagues.
initteam.py teams, nations and stadiums list all of this, info
checks it, and setteam edits a club record into a same-size copy of
PLRESOURCESIM.PAC. setnation and setstadium edit the two tables
above, field by field, into same-size copies:
python SRC/initteam.py setnation DAT/PARAM/INITNATIDATA.TBB out.TBB 3 uefa_rank=1 uefa_points=500 60 world=99
python SRC/initteam.py setstadium DAT/PARAM/STADIUM_DATA.TBB out.TBB 5 roof=1 level=4 capacity=90setnation refuses the UEFA fields for nations 53–145, which the game
doesn't read, and keeps each row's two pad bytes. setstadium takes a
roof of 0 or 1, a level of 0–4 (the range on the disc) and a capacity
in thousands (0–255). roundtrip rebuilds both files through the same
encoders; both equal the originals byte for byte, on both releases.
The editor's Nations & stadiums tab makes the same edits and logs
them as setnation and setstadium commands. Neither edit has been
checked in the game.
- What the game does with table 2's negative values.
- Whether a new game or the tutorial skip adds the year to the squads'
ages (a PCSX2 test in
TESTING.md). - Club-record bytes
0x0b,0x0c,0x0dand0x0f. - What reads
MAPTEAM_LIST.TBB, and what its flag is for.
python SRC/initteam.py info DAT/PARAM # layout checks and counts
python SRC/initteam.py leagues DAT/PARAM # clubs per division, with names
python SRC/initteam.py past DAT/PARAM # table 1 and 2 per competition
python SRC/initteam.py squads DAT/PARAM 7 # one squad
python SRC/initteam.py teams DAT/PARAM 7 # a club record, with names
python SRC/initteam.py nations DAT/PARAM
python SRC/initteam.py stadiums DAT/PARAM
python SRC/initteam.py roundtrip DAT/PARAM # every squad slot and club record re-encodes
python SRC/sles_disasm.py ISO/SLES_541.51 dis plOteam_ pwkOteam_Init2 plTeam_IsAgreeTransferChangeMoney
python SRC/sles_disasm.py ISO/SLES_541.51 dis pwkRec_GetUefaNation pwkRec_GetWorldNation plTeam_GetCapacityFromID
python SRC/sles_disasm.py ISO/SLES_541.51 addr 253228 40
python SRC/sles_disasm.py ISO/SLES_541.51 dis plLg_EntryTeamSetToDiv UpdateConyear pwkTeam_SetUnumberOpinfo
python SRC/sles_disasm.py ISO/SLES_541.51 dis GetString__3MsgQ23Msg5eTYPEUib GlobalMsgSetupinitteam.py edits all three files into same-size copies that
patch_disc.py puts on the disc:
python SRC/initteam.py set DAT/PARAM/OTEAMMEMBER.TBB out/OTEAMMEMBER.TBB 7:1 age=15 # team 7, slot 1: Terry
python SRC/initteam.py setteam DAT/PARAM/PLRESOURCESIM.PAC out/PLRESOURCESIM.PAC 7 stadium=100
python SRC/initteam.py swap DAT/PARAM/PLRRSRC_INITTEAMDATA.TBB out/PLRRSRC_INITTEAMDATA.TBB 7:41Shirt numbers are limited to 1–99, the range pwkTeam_SetUnumberOpinfo
keeps. set, setteam and swap write through the same encoders that
roundtrip checks (all 10,975 squad slots and 457 club records
re-encode, and all three files rebuild byte for byte). Changing a
division's size needs new schedules: leaguesize.py build
(SCHEDULE_FORMAT.md).
The editor's Clubs tab edits the club records and squads, refusing a player already in another squad, a repeated shirt number and a manager who already has a club; its Season tab swaps clubs and changes first division sizes. Both save through these functions.