Skip to content

Latest commit

 

History

History
350 lines (287 loc) · 19.1 KB

INITTEAM_FORMAT.md

File metadata and controls

350 lines (287 loc) · 19.1 KB

Starting leagues and squads

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.

Confirmed from the game code

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

PLRRSRC_INITTEAMDATA.TBB

Three tables. Their line sizes (4, 4, 2) are the element sizes, not the record sizes.

Table 0: leagues and divisions (1,248 bytes)

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.

Table 1: last season's order (7,168 bytes)

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.

Table 2: years since last held (112 bytes)

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.

Swapping clubs

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)/2 pairings 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 build changes 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_LIST row; 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: their PLTEAMID records 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.

OTEAMMEMBER.TBB

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=15 he 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-tutorial disc 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).

Names

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.

Club records (PLRESOURCESIM.PAC entry 3)

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.

INITNATIDATA.TBB: nations

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.

STADIUM_DATA.TBB: stadiums

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.

MAPTEAM_LIST.TBB

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=90

setnation 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.

Still unknown

  • 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, 0x0d and 0x0f.
  • What reads MAPTEAM_LIST.TBB, and what its flag is for.

Checking the claims

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 GlobalMsgSetup

Writing

initteam.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:41

Shirt 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.