# Web Research Notes — CFB27 Dynasty Save Internals & Adjacent Tooling

Compiled: 2026-07-14. Scope: madden-franchise upstream, the four reference repos, the "Recruit Overhaul 27" (RO27) tool, community save-structure knowledge, and other open-source CFB27 editors.

**Verification legend**
- **[CODE]** — backed by code or machine-extracted data I actually fetched (strongest).
- **[2SRC]** — corroborated by two independent sources.
- **[1SRC]** — single documented source (usually the brooksg357 docs); detailed/mechanistic but not independently corroborated. Treat as high-quality UNVERIFIED.
- **UNVERIFIED** — inference or hearsay; do not build on without local testing.

The single most valuable discovery of this session: **github.com/brooksg357-a11y/cfb27-dynasty-modding** — a full reverse-engineering knowledge base for CFB27 dynasty saves (format docs + toolchain), started 2026-07-02, actively updated. Its `docs/recruiting.md` alone answers most of our design questions. Second most valuable: **gpellis87/cfb27-table-explorer**'s `data/table-map.json` (1.4 MB, downloaded to scratchpad), which contains machine-extracted schemas (names, types, enum type names, example values) for ~1,295 tables from a real CFB27 dynasty save.

---

## 1. madden-franchise (upstream library)

- Repo: https://github.com/bep713/madden-franchise — "JS API for reading and writing Madden franchise files". npm: https://www.npmjs.com/package/madden-franchise
- **[CODE]** README (fetched from raw master): supports Madden 19–27 and **"PC Dynasty file saves from College Football 27+ are supported as well"**.
- **[CODE]** v4.0.0+ is **ESM-only**; CommonJS consumers need **v3.8.0 or older**. (RO27 and brooksg357 vendor **4.2.x**.)
- **[CODE]** Options (exact semantics from README):
  - `schemaOverride: { major: int, minor: int, gameYear: int, path: string }`
  - `schemaDirectory` — extra schema search dirs beyond bundled ones
  - `autoParse: true/false [default: true]`
  - `saveOnChange: true/false [default: false]` — auto-save on any field change (do NOT enable for our tool; we want explicit transactional writes)
  - `gameYearOverride` — for FTC files
- **[CODE]** `save(output)`: "will re-pack and save the file. If output is omitted, it will overwrite the currently opened file."
- **[CODE]** Table lookup: `getTableByUniqueId(id)` is documented as the "**(Best way to find a table)**" because "These ids do not change between game years and schema versions". `getTableByName(name)` returns only the *first* match; `getAllTablesByName(name)` returns all. (Our established pick-largest-recordCapacity heuristic remains a good safety net.)
- **[CODE]** Empty records form a **linked list (freelist)**: "the first 4 bytes of a record contain the next empty record index while the rest of the record bytes are set to 00"; table header keeps `nextRecordToUse`. Manipulate with caution.
- **[CODE]** Reference helpers: `getReferencedRecord(ref)` and `getReferenceToRecord(tableId, rowNum)` (reverse-reference lookup — useful for "who points at this coach/recruit"). Also `getReferenceDataByKey()` for `{tableId, rowNumber}` decoding without manual bit math (used by Aball1495's tool as `.referenceData`).
- Usage pattern from README:
  ```javascript
  import Franchise from 'madden-franchise';
  let franchise = await Franchise.create(path, options);
  let table = franchise.getTableByName('Player');
  await table.readRecords(['FirstName', 'LastName']);
  table.records[0].FirstName = 'John';
  await franchise.save();
  ```
- Related: https://github.com/bep713/madden-franchise-editor (Electron GUI, Madden-focused; releases at /releases). Madden-centric; no CFB-specific docs found there.

### 1a. CRITICAL OPEN ISSUE — does `file.save()` corrupt CFB27 saves?

- **[1SRC]** brooksg357 docs state, as a "standing invariant": **"Never call madden-franchise `file.save()` on a CFB 27 save — corruption."** Mechanism (from `docs/save-format.md`): the library's `postPackFile` recompresses chunk 1 with Node zlib, which produces a **larger** stream than EA's (measured: Node zlib 5,441,483 B vs EA 5,401,081 B) and **overflows chunk-1's slot, silently clobbering the head of chunk 2 (CharacterVisuals)**. There is no pointer to chunk 2 and no checksum, so the overwrite is silent.
- Their fix (`saveCollegeSave()`): recompress with **libdeflate level 12** (produces 4,769,145 B — "632 KB smaller than EA's, always fits"), patch the u32le size at offset 0x4A, restore zero padding, keep chunk 2 byte-identical, and **hard-fail if the stream exceeds slot capacity**. Result "byte-identical to the original except the stream region and the size field". Their writer also "refuses in-place overwrites — always specify a new output path".
- **Tension**: all four of our reference tools + RO27 + cfb-offline use stock `madden-franchise` `save()` and their users report working saves. The RO27 teardown phrases the risk conditionally: write "uses stock madden-franchise with no overrun protection; **if** the recompressed FrTk stream outgrows chunk-1's slot, it silently clobbers the head of chunk 2." So stock `save()` plausibly works while edits keep the recompressed stream small enough, and corrupts only past the threshold — a **latent, data-dependent risk**, worst for saves that grow (e.g., ours will add rows/edit many records).
- **ACTION for our tool**: test locally on a scratch copy: save with stock library, then verify chunk-2 region integrity (locate first non-zero byte after `0x52 + u32@0x4A`; compare bytes with original). If it ever moves/changes, adopt the libdeflate re-wrap approach. UNVERIFIED until we test.

### 1b. Schema versioning notes

- Established (local): reference tools use **C27_468_2.gz** (major 468, minor 2, gameYear 27).
- **[1SRC]** brooksg357's save header analysis says the FBCHUNKS header carries **schema major/minor 809/0** and their own extracted schema is `CFB27_809_0.gz` (extracted from Frostbite CAS via `extract-schema.py` + `generate-schema.js`). The 468_2 vs 809_0 discrepancy is unexplained — possibly different version namespaces (madden-franchise schema file version vs FrTk DB schema version), or different game patch levels. OPEN QUESTION: check the FBCHUNKS header bytes of a real save locally and see what `file.schemaList`/header report.
- **[CODE]** Library works "with partial or no schemas" but that "can lead to unexpected results in-game and very likely will cause crashes" (README).

---

## 2. The four reference repos (GitHub state as of 2026-07-14)

Checked via GitHub API **[CODE]**: 

| Repo | Stars | Forks | Issues | Default branch | Last push |
|---|---|---|---|---|---|
| https://github.com/KivJoy/CFB27-Coaching-Carousel | 0 | 0 | none | main | 2026-07-12 |
| https://github.com/Aball1495/CFB27-Dynamic-Pipeline-Tool | 0 | 0 | none | main | 2026-07-13 |
| https://github.com/jhusting/force-commit-recruits | 2 | 1 | 1 closed ("Save PIcker") | master | 2026-07-13 |
| https://github.com/ArtVsTheWorld/CFB27-Jersey-ReNumber-Tool | 0 | 0 | none | main | 2026-07-13 |

No discussions/wiki content found on any of them. Everything documented below comes from their READMEs (fetched).

### KivJoy/CFB27-Coaching-Carousel
- Desktop app (88% JS / 9% CSS / 3% HTML) to edit **coaching carousel job offers and coach movements**: view job openings & candidates, adjust "Coach Interest, Team Interest", replace coaches, filter by team/position/conference/prestige.
- README mentions loading conference data from **"Conference and TeamSlots tables"** (note: in the real schema, `TeamSlots` is a `Team[]` array field on `Conference` — see §7). Unassigned teams display as "Independents".
- Save must be edited during **"Weeks 14–16 in-season, or offseason weeks"**; back up first; close CFB27 while editing. Exact field names are NOT in the README — mine the local clone's source for those.

### Aball1495/CFB27-Dynamic-Pipeline-Tool
- Recomputes each school's top-10 recruiting pipelines each preseason. README documents exact tables **[2SRC — matches table-map.json ids]**:
  - `Team` (id **6334**), `SchoolPipelineInfluence[]` list (id **5919**), `SchoolPipelineInfluence` (id **4306**), `Player` (id **4244**), `Coach` (id **4173**).
  - Resolves refs via `.referenceData` (`tableId`, `rowNumber`) — no manual bit manipulation.
  - `Coach.IsUserControlled` identifies the user-controlled team.
  - Team table: **143 rows total, 5 placeholders** (blank `DisplayName`, `TeamIndex === 255`) → 138 real FBS teams.
- Writes a **new save copy** (original untouched). Changes appear only in recruiting tabs/boards in-game, not team-select screens.
- Default weights: roster 0.35 / recruit quality 0.35 / coach influence 0.20 / geography 0.10; decay factor 0.75 vs prior season; coach influence ramp-up default 3 seasons for new hires (HC/OC/DC toggleable — directly relevant prior art for our "coach change affects recruiting" logic).

### jhusting/force-commit-recruits
- Forces 0-NIL commits of offer-less recruits to needy schools. **Must run on "Week 4 of the transfer portal period of offseason"** (README emphasizes twice).
- Auto-backup before modification; `--dry-run`, `--verbose` flags; interactive save picker (the closed issue was about this).
- Skips recruits on the user's board that have any offer ("send them a 0 NIL offer" to protect them). README warns some forced commits may **decommit afterward** or be dropped by AI — consistent with brooksg357's finding that CPU boards are re-curated weekly (§6).
- README names no tables — mine the local clone for its table/field usage.

### ArtVsTheWorld/CFB27-Jersey-ReNumber-Tool
- Jersey-number-only editor; position-based numbering rules; duplicates resolved. Skips NIL players (`IsNIL` — real-likeness preservation). OL/K/P intentionally excluded.
- Gotcha: long backup filenames can fail to load in-game — "manually rename the backup to something shorter" (relevant to our backup naming scheme!).

---

## 3. RO27 — "Recruit Overhaul 27" (Fang's Recruit Overhaul)

- **What it is**: community Electron app (portable Win x64), **v0.1.0-beta.1, July 2026** — batch randomizer for the whole recruit class (names, portraits, skin tone, rating variance, gem/bust, height/weight). Made by "Fang" (long-running community brand: "Fangs Recruit Overhaul" existed for NCAA 14 Revamped — see YouTube "Fangs Recruit Overhaul V3.0 Tutorial - NCAA 14 Revamped").
- **No public GitHub repo found.** Distribution appears to be via community channels (YouTube tutorials + presumably Discord). UNVERIFIED distribution channel. Tutorials:
  - https://www.youtube.com/watch?v=AKIVm_D5_ts ("CFB 27 DESPERATELY Needed This: Fang's Recruit Overhaul Tutorial")
  - https://www.youtube.com/watch?v=l0NaLbP2hGY ("College Football 27 Recruiting Just Got a COMPLETE Overhaul")
- **[CODE]** Local artifact read (user's saves dir): `Fangs-Settings.ro27-settings.json` — `"schema": "recruit-overhaul-27-settings-config"`, `"app": "Recruit Overhaul 27"`, settings version 8. Modules: `globalRating, diamondInTheRough, blueChip, gemBust, starStrength, heightVariance, weightVariance, nameSuffixes`, plus `projectPlayersSettings`, `hideGemsBustsSettings`, `skinToneSettings` (per-position light/medium/dark %). Its `globalRatingVariance` block enumerates the **exact Player-table rating field names** (52): `AccelerationRating, AgilityRating, AwarenessRating, BCVisionRating, BlockSheddingRating, BreakSackRating, BreakTackleRating, CarryingRating, CatchInTrafficRating, CatchingRating, ChangeOfDirectionRating, ConfidenceRating, DeepRouteRunningRating, FinesseMovesRating, HitPowerRating, ImpactBlockingRating, InjuryRating, JukeMoveRating, JumpingRating, KickAccuracyRating, KickPowerRating, KickReturnRating, LeadBlockRating, LongSnapRating, ManCoverageRating, MediumRouteRunningRating, PassBlockFinesseRating, PassBlockPowerRating, PassBlockRating, PlayActionRating, PlayRecognitionRating, PowerMovesRating, PressRating, PursuitRating, ReleaseRating, RunBlockFinesseRating, RunBlockPowerRating, RunBlockRating, ShortRouteRunningRating, SpectacularCatchRating, SpeedRating, SpinMoveRating, StaminaRating, StiffArmRating, StrengthRating, TackleRating, ThrowAccuracyDeepRating, ThrowAccuracyMidRating, ThrowAccuracyRating, ThrowAccuracyShortRating, ThrowOnTheRunRating, ThrowPowerRating, ThrowUnderPressureRating, ToughnessRating, TruckingRating, ZoneCoverageRating`. Positions keyed as `QB HB FB WR TE LT LG C RG RT LE RE DT LOLB MLB ROLB CB FS SS K P ATH`.
- **[1SRC]** Teardown (`docs/ro27-teardown.md` in brooksg357 repo, verified 2026-07-09):
  - Vendors **madden-franchise 4.2.0**; loads `DYNASTY*` saves via child-process bridge; exports/imports **eight JSON tables**.
  - Writes touch: **`Recruit_1873209313`** (core recruit rows), **`UserRecruitTarget_3987156317`**, `ProspectTargetSchool` variants, portraits / `GenericHeadAssetName` / skin tone, `QualityModifier`. (The `_NNNNNNNNN` suffixes are almost certainly the madden-franchise **uniqueId**s: `Recruit` → **1873209313**, `UserRecruitTarget` → **3987156317** — usable with `getTableByUniqueId`. UNVERIFIED until checked against our save.)
  - **Read-only** on recruiting board / influence / offers; no new-recruit generation; does not touch CharacterVisuals when editing skin tone.
  - Save read hack: `zlib.inflateSync(data.slice(0x52))` (works because chunk-1 zlib starts at 0x52). Write: stock madden-franchise, **no overrun protection** (see §1a).
  - Backup behavior: **`RecruitOverhaulBackups/<name>.backup-<ts>`** with user confirmation — matches the dir in the user's saves folder.
  - RNG: plain `Math.random()`, non-seeded, non-reproducible. OVR model: archetype-weighted delta formulas across 67 archetypes.

---

## 4. Save file binary format (chunk level) — from brooksg357 `docs/save-format.md` **[1SRC]**

- Root container: **FBCHUNKS**.
  - Header 0x00–0x51: magic `"FBCHUNKS"`, version u16le=1, timestamp, DB name `"College-27-RL1-9039126"`, schema major/minor (**809/0**).
  - **Chunk 1** starts at **0x52**: zlib-compressed **FrTk** database; compressed size stored at **0x4A (u32le)**. Zero padding (growth buffer) follows.
  - **Chunk 2** (reference save: absolute offset 0x52D426): uncompressed FrTk-style table holding **CharacterVisuals** — tableId 4222, fields `Overflow` + `RawData` (maxLength 375). `RawData` blobs are fixed **377-byte "table3" slots**: 2 LE bytes of zstd frame length + zstd frame + zero padding. zstd dictionary id `0x65FC508B` (resolved from `cas_42.cas`).
  - **No checksum anywhere; no pointer to chunk 2** — it sits at a fixed absolute offset after the zlib stream.
- FrTk DB (decompressed chunk 1): magic `FrTk`, **big-endian header**, table count 2,269 (reference save), schema major 809, table markers `SPBF`, `BSFT`, `ASTO`, `SPEX`.
- References: **`(tableId << 17) | rowIndex`** in 32-bit fields; tableIds ≥ 4096 are valid refs; zero = null. (Consistent with our established 15-bit/17-bit split.)
- **table2 string pool**: Player strings live in fixed **138-byte slots** (16,500 × 138 = 2,277,000 B), null-terminated subfields at: +0 FirstName (17 B), +17 GenericHeadAssetName (33 B), +50 LastName (21 B), +71 AssetName (41 B), +112 HomeTown (26 B).
- **Dynasty autosaves drop the plaintext chunk-2 `"CharacterVisuals"` marker** — locate chunk 2 by stream-end detection (first non-zero byte after `0x52 + u32@0x4A`), not marker scan. RTG autosaves report **5 chunks** and need separate handling.
- Row allocation: `openCollegeSave(path, { autoUnempty: true })` fills at `nextRecordToUse` and repairs the freelist chain (their extension; stock library has manual empty-record APIs).

---

## 5. brooksg357-a11y/cfb27-dynasty-modding — the community knowledge base

- Upstream: https://github.com/brooksg357-a11y/cfb27-dynasty-modding (fork: https://github.com/eric-levinson/cfb27-dynasty-modding). Started 2026-07-02. Docs index (`docs/MAP.md`, fetched): `save-format.md`, `player-table.md`, `recruiting.md`, `toolchain.md`, `character-visuals.md`, `engine-modding.md`, `ro27-teardown.md`, `external-tooling-reference.md`, `open-questions.md`, `manifesto.md`, recruit identity docs, plans/, research-log.md (3,700 lines).
- Claims (their MAP): parses **2,269 tables**; Player table **16,500 rows × 282 fields** fully mapped ("51 ratings + ~25 more fields, verified"); writes game-accepted saves; real EA schema mode via `{ useSchema: true }`.
- Their toolchain (Node ≥ 24.6 for native zstd; Python 3.11 + libdeflate): `openCollegeSave` / `saveCollegeSave` in `franchise-lab/college-franchise.js`; usage:
  ```js
  const file = await openCollegeSave(path, { useSchema: true });
  const player = file.tables.find(t => t.name === 'Player');
  await player.readRecords();
  rec.fieldsArray.find(f => f.key === 'SpeedRating').value = 93;
  saveCollegeSave(file, output); // chunk-2-safe writer
  ```
- Their per-field write-governance model is worth copying: `writable` (verified + read-back tested) / `research` (blocked) / `preserve` / `unsafe`.

### 5a. Recruiting tables (from `docs/recruiting.md`) **[1SRC unless noted]**

- **`Recruit` (table 4269)**: `NationalRank`, `PositionRank`, `StateRank`, `Class` (RecruitingClass), `RecruitStage`, `RecruitStageAdvance`, `CommitScore`, `QualityModifier`, `ProductionGrade`, `TotalScholarshipOffers`, `TopSchoolsList` (ProspectTargetSchool[] ×10), `Player` (packed ref → Player). "Recruit editing spans both the Recruit row and the linked Player row." **[2SRC — full field list confirmed by table-map.json, §7]**
- **`RecruitingBoard` (table 4251, 138 rows — one per FBS team)**: `RecruitingHoursTotal / RecruitingHoursAssigned / RecruitingHoursProcessed` (0–4095), `Recruits: RecruitTarget[]` (max 35). **Board row ≠ Team row** — 108 of 138 misaligned; always resolve via `Team.RecruitingBoard` ref. **[2SRC field names via table-map.json]**
- **`RecruitTarget` (table 4288, cap 4,870)** = CPU teams' per-recruit pursuit rows; **`UserRecruitTarget` (table 4168, cap 1,120)** = user team's (superset: adds `IsFavorite`, `RecruitingFeedback`, `ImmediateRecruitingFeedback`). Shared fields: `ProspectInfluenceTotal`, `ProspectInfluenceTotalLastWeek`, `ProspectInfluenceDelta`, `ProspectHoursSpentCurrent`, `CommittedWeekNumber`, weekly action bools (`SearchSocialMedia`, `ContactHighSchoolCoaches`, `ContactFriendsAndFamily`, `SendTheHouse`, `VisitRecruitsSchool`), `ActivePitches: ActiveRecruitingPitch[]`, `SwayPitch`, `ScheduledVisit: ActiveVisitInfo`, `ScholarshipStatus`, `CurrentNILOffer`, `CurrentScholarshipBonus`, `NILExpectation`, `OriginalNILExpectation`, `UnlockedIntelBitfield`, `Recruit` (ref). **[2SRC via table-map.json]**
- **`ProspectTargetSchool` (tables 5840/5841; list table 5842)**: `{ TeamId:int, TeamInfluence:int }` pairs — the recruit's top-10 schools, **sorted descending by influence**. **UI reads physical slot order, not influence order — after any edit you must re-permute the array to influence-desc order.** (Directly relevant to our "keep old school in top list with a small edge" feature. Note it stores `TeamId` (int), not a Team ref.) **[2SRC via table-map.json]**
- **`ProspectInteraction` (table 4260, cap 3,700; global list table 4972)**: user-team scouting state `{ Recruit, Team, TimesScouted, UnlockedIntelBitfield, IsDevTraitUnlocked, HasOfferedScholarship, IsVisitScheduled, VisitActivityType, VisitWeekNumber, VisitWeekType }`. Created lazily; absence doesn't block offers but blocks visit scheduling. Appending: write ref one slot past `arraySize` auto-bumps size. **[2SRC via table-map.json]**
- **`SchoolOffer` (table 4108, cap 138)**: `{ AdjustedOVR, EstimatedPlayerDepth, HasOffer, InterestLevel, OfferInterestLevel, OfferType: RecruitOfferType, Team }` — per-team offer/interest snapshot (likely UI-facing). **[CODE via table-map.json]**
- **Recruiting stage enum** (`RecruitStage`): `Top10=0, Top5=1, Top3=2, Battle=3, SoftCommitted=4 (verbal), HardCommitted=5, Signed=6`. Stage-advance enum (`RecruitStageAdvance`): `None=0, Advance=1, Decommit=2, InstantCommit=3, Invalid=5`. **A `Decommit` value exists in the engine's own stage-advance enum — our decommit mechanic may be expressible natively.**
- Stage thresholds: leader's `TeamInfluence` as % of `CommitScore` → Top5 at **35%**, Top3 at **75%**, Commit at **100%**; battle triggers when two schools within **10%** of commit threshold; battle adds **+25%** effective commit score; hard commit adds **+50%**.
- **Write-survival semantics (game-verified by them 2026-07-07)** — crucial for our tool design:
  - `RecruitTarget.ProspectInfluenceTotal` (team-side) is the **MASTER store**; weekly advance re-derives the recruit's TopSchoolsList entry from it. List entries with no backing target row persist untouched.
  - **Sticky (one-shot) writes**: `Recruit.RecruitStage` (ratchet — never recomputed downward), `Recruit.CommitScore` (lower to ≤ leader influence ⇒ HardCommitted next advance, rival influence zeroed), Player identity/ratings.
  - **Transient (re-apply weekly)**: board membership, `RecruitTarget` rows, `ProspectInfluenceTotal`, `ActivePitches`, `ScheduledVisit`, `ScholarshipStatus`. **CPU re-curates its board every weekly advance** and drops injected 0-influence targets ⇒ durable orchestration needs per-week enforcement by an external app (ours!). Their recommended write-set is idempotent, meant to be re-applied each week.
  - Force-commit recipe: make target team the influence leader, then set `Recruit.CommitScore ≤ leader influence` → HardCommitted on next advance.
  - Gotcha: `ActiveRecruitingPitch[]` array table (5790) can be **exhausted** (4,830/4,830 in their 2028 save) — new RecruitTarget rows can't always get their own pitch arrays; recycle instead.
- **Recruit motivations** (`RecruitingMotivationType`, 14 values): `0 AcademicPrestige, 1 AthleticFacilities, 2 BrandExposure, 3 CampusLifestyle, 4 ChampionshipContender, 5 CoachPrestige, 6 CoachStability, 7 ConferencePrestige, 8 PlayingStyle, 9 PlayingTime, 10 ProPotential, 11 ProgramTradition, 12 ProximityToHome, 13 StadiumAtmosphere`. (`CoachPrestige`/`CoachStability` are exactly the levers for a coach-departure consequence system.) Dealbreaker fails when school's letter grade in that motivation drops below **B−**. `RecruitingDealbreaker` (Player Field_243 in generic mode) packs the enum into the **first 4 bits** (remaining 28 bits preserved): `newValue = value.toString(2).padStart(4,'0') + existingBits.slice(4)`. Only 8 motivations can be dealbreakers (excluded: AcademicPrestige, AthleticFacilities, CoachStability, PlayingStyle, ProgramTradition, StadiumAtmosphere).
- Pitches: 20 pitches, each mapping to a motivation triplet; `ActiveRecruitingPitch = { Pitch: RecruitingPitchType, Intensity: RecruitingActionIntensity }`; intensity `SoftSell=0, HardSell=1, Sway=2`.
- Action hour costs / base influence: Search Social Media 5/4; Contact HS Coaches 10/8; Contact Friends & Family 25/20; Send the House / Visit School 50/40; Soft Sell 20/20; Hard Sell 40/40; Sway 30/15; Offer Scholarship 5/0 (+5/week after); Schedule Visit 40/0; Scouting 10/0. Hours are a weekly flow (refill 250–1000 by prestige × season-phase splines; 50-hour/recruit/week cap); NIL/program points are a seasonal stock (`Team.ProgramPointBudget`, `Team.NILProgramPointsSpent`).
- Visits: `ActiveVisitInfo = { Activity: VisitActivityType, WeekNumber, WeekType }`; 14 activities (AttendLecture, TeamWorkout, PodcastInterview, CampusTour, AttendTeamMeeting, OneOnOneCoaching, TeamDinner, TrophyTour, AttendPractice, AttendPositionMeeting, MeetAlumni, TeamHistory, FamilyVisit, Tailgate); `MaxRecruitVisitsPerWeek = 4`; visit influence tuned by `VisitTunables` (game stakes + win margin modifiers).
- Scouting: max 5 scouts per recruit (`MaxTimesScouted`); the 14-bit `UnlockedIntelBitfield` maps to the 14 motivations; **reveal display driven by `ProspectInteraction.TimesScouted`, not the bitfield**; `IsDevTraitUnlocked` separate bool; coach ability `Recruiting_BoostChance_DevTraitUnlock` (0–100%) can unlock on first scout.
- `RecruitingTunables` values (CPU AI): `AIAggressivenessSlider 1.15`, `AINILAggressivenessSlider 2.25`, `CPU_AI_MaxPitchCount 2`, `CPU_AI_SpendScoutingWeeklyMaxRecruitsToScout 35`, `MaxRecruitingBoardTargets 35`, `MaxTeamScholarshipOffers 35`, `MinimumNILToOfferPercentage 0.8`, `MaxFiveStarsInClass 32`, `RecruitsToGenerateMin/Max 4100` (observed 4,100 + 1 narrative = 4,101).
- Reference-save population: 3,751 HS-likely, 349 JUCO-likely, 0 transfers (preseason), 3,500 invalid/unused rows (Recruit capacity 7,600).
- Recruit-linked Player fields (authoritative on Player): identity (`FirstName`, `LastName`, `PLYR_ASSETNAME`, `GenericHeadAssetName`), `Position`, `IronManPosition`, `PlayerType` (archetype), `Height`, `Weight`, `Age`, `OverallRating`, `ProspectStarRating` (FIVE_STAR…ONE_STAR), `HomePipeline`, `IdealRecruitingPitch`, `RecruitingDealbreaker`, `Motivation1/2/3`, `SchoolYear`, `TraitDevelopment`, `BaseNILValue`, `CurrentNILCompensation`, `IsNIL`.

### 5b. Player table (from `docs/player-table.md`) **[1SRC unless noted]**

- 16,500 records × 282 generic fields (~244 all-zero placeholders; filter `Overall > 0`), **ordered alphabetically by last name then first name**. Generic Field_94–290 names sort alphabetically by EA schema name (case-insensitive) — that's how they mapped unknowns.
- Key generic-mode encodings: Field 3 Position enum `0 QB, 1 HB, 2 FB, 3 WR, 4 TE, 5 LT, 6 LG, 7 C, 8 RG, 9 RT, 10 LEDG, 11 REDG, 12 DT, 13 LOLB, 14 MLB, 15 ROLB, 16 CB, 17 FS, 18 SS, 19 K, 20 P`; Field 123 Age; Field 154 Height (inches); Field 285 **Weight stored as pounds − 160** (floor at 160 lb — 311 real players clamp there; EA behavior, not a bug) **[2SRC — also in external-tooling ref]**; Field 222 HomeState (alphabetical US index, 0=Alabama…50=International; **DC encodes as 19/Maryland**); Field 156 HomePipeline (0=Alabama…42=International; e.g. 3 BigApple, 15 MetroAtlanta, 35 SouthernCalifornia); Field 168 JerseyNumber; Field 282 TraitDevelopment (1=Impact/Star, 3=Elite/XFactor; real-schema enum shows values like `College_Impact`).
- **`OverallRating` is calculated, not authoritative** (stored 80 vs displayed 84 example) — don't treat stored OVR as truth.
- Position and `PlayerType` (archetype) are separate — change both together.
- Mental abilities: fields 185–187 identity (MentalAbilities enum), 188–190 tier (0 None…4 Platinum). Physical abilities: fields 203–207 tier-only; identity derived from archetype.

---

## 6. Coach / carousel / season-flow tables — machine-extracted schemas **[CODE]**

Source: `data/table-map.json` from https://github.com/gpellis87/cfb27-table-explorer (downloaded raw, 1,295 table entries extracted from a real CFB27 dynasty save; local copy in scratchpad + extracted `key-tables.json`). These are real observed schemas with example values. Table ids are the ids observed in that save AND (where overlapping) match Aball1495's README ids exactly, so ids appear stable per game version — but keep resolving by name/uniqueId anyway.

### `Coach` — ids [4173, 6110], 497/632 rows populated, 146 named fields. Highlights:
- Identity/employment: `FirstName`, `LastName`, `Name` ("P. Longo"), `Age`, `Position: CoachPosition` (e.g. `HeadCoach`), `PrevPosition: CoachPosition` (e.g. `Invalid_`), **`TeamIndex:int`** (e.g. 132; **255 = unemployed/none**, UNVERIFIED but consistent with Team placeholder convention), **`PrevTeamIndex:int`** (e.g. 255), `SeasonsWithTeam`, `IsUserControlled`, `IsCreated`, `IsLegend`, `AlmaMater:int`, `YearsCoaching`.
- Contract: `ContractLength`, `ContractSalary`, `ContractYearsRemaining`, `ContractStatus: StaffPersonContractStatus` (e.g. `First_Active`), `Probation:bool`, `NumContractOffers`, `EarnedContractPoints_ThisYear/_LastYear/_TwoYearsAgo`, `ContractYearSummaries: ContractYearSummary[]`, `CurrentContractExpectation`/`ContractExpectationProgress: ContractExpectations` (e.g. `Win4Games`).
- Job security (fire risk): **`CurrentJobSecurityPercentage`** (e.g. 100), `CurrentJobSecurityPercentageRank`, **`CurrentJobSecurityStatus: JobSecurityStatus`** (e.g. `Safe`), `SeasonStartJobSecurityStatus`.
- Carousel-history flags (legacy Madden-style names): **`COACH_FIREREPORTED:bool`**, **`COACH_RESIGNREPORTED:bool`**, **`COACH_LASTTEAMFIRED:int`**, **`COACH_LASTTEAMRESIGNED:int`**, `COACH_LASTCONTRACTTEAM:int`, `COACH_RETIREYRSLEFT:int`, `COACH_CONSECTEAMCONTRACTS:int`, `COACH_WASPLAYER:bool`.
- Prestige/progression: `CoachPrestige: LetterGrade` (e.g. `D`), `CoachPrestigeScore:int` (e.g. 210), `Level` (e.g. 20), `ExperiencePoints`, `CoachPoints`, `LegacyScore`, `AwardPoints`, `DominantArchetype: CoachTalentArcheType` (e.g. `SchemeGuru`), `SpecialtyType: CoachSpecialtyType` (`Offense`), `ActiveTalentTree` ref.
- Scheme/personality: `OffensivePlaybook`, `DefensivePlaybook`, `OffensiveScheme`, `DefensiveScheme`, `Personality`, `TeamBuilding`, `CoachBackstory`, `PrimaryPipeline: Pipeline` (e.g. `BigApple` — coach pipeline, used by Dynamic Pipeline Tool), `TraitExpertScout`, various `COACH_*` tendency ints, appearance fields (`Portrait`, `CharacterVisuals` ref, `Height`, `Weight`, `HomeState: StateName`, `HomeTown`).
- Stats: `CareerStats: CareerCoachStats` ref, `SeasonStats: SeasonCoachStats`, streak/points fields.

### `Team` — ids [5292, 5294–5297, 6039–6041, 6334], 143/143 rows, ~380 fields. Carousel/recruiting-relevant:
- Staff refs: **`HeadCoach: Coach`**, **`OffensiveCoordinator: Coach`**, **`DefensiveCoordinator: Coach`**, `SpecialTeamsCoach: Coach`, plus `HeadScout`, `HeadTrainer`, `GeneralManager`, `StaffPersonBlacklist: StaffPerson[]`.
- Coordinator/HC hiring: `IsHiringBonusAvailableHC/OC/DC:bool`, `DesiredPrimArchetype/Sec/Tert: CoachTalentArcheType`, `DesiredSpecialtyType`, `DesiresAlumni:bool`, `AllowsTripleOptionCoaches:bool`, `UserCoachExpressedInterestCount:int`.
- Contract goals: `HCContractGoal1..3` + `HCContractGoal1..3Status`, same for `OC…`, `DC…` (`CoachContractGoalStatus` e.g. `InProgress`), `AccumulatedCoachContractGoalsPoints`, `ExpectedContractPoints_ThisYear/_LastYear/_TwoYearsAgo`.
- Recruiting: **`RecruitingBoard: RecruitingBoard`** ref, **`CommittedPlayers: Player[]`**, `SchoolPipelineInfluenceList: SchoolPipelineInfluence[]` (×10), `PipelineInitialInfluence`, `LastWeekCommittedRecruits`, `TopClassRank`, `TopClassConferenceRank`, `RecruitProgramPointsSpent`, `NILProgramPointsSpent`, `ProgramPointBudget`, `RemainingProgramPoints`, program-point grade fields (`ProgramPointsBudgetGrade` etc. — LetterGrade, i.e. the motivation grades recruits check), `CoachTalentEffects: CoachTalentEffects` ref.
- Transfers: **`LastSeasonTransfersLost:int`**, **`LastSeasonTransfersSigned:int`**.
- Identity/prestige: `DisplayName`, `LongName`, `NickName`, `ShortName`, `AssetName`, `TeamIndex:int` (join key — see gotcha below), **`TeamPrestige:int`** (e.g. 5), `TeamPrestigeBias`, `PrestigeRank`, `TeamRank`, polls (`MediaPoll_*`, `CoachesPoll_*`, `CFPPoll_*`), `Rival1TeamRef/2/3`, `Rivalries`, `TEAM_TYPE: TeamType` (e.g. `Current`), plus large `TEAM_*` cosmetic block.
- **Gotcha [2SRC]** (table-explorer README + brooksg357 MAP): **`TeamIndex` is NOT the row index into the Team table** — join by matching each team's own `TeamIndex` field (they verified across all 143 teams' head coaches). brooksg357 keeps a `team-index-map.json`. Coach.TeamIndex → Team must go through this join.
- Placeholder rows: 5 of 143 have blank `DisplayName` / `TeamIndex == 255` **[2SRC]**.

### `SeasonInfo` — id 4141, singleton. **The season-calendar/stage authority:**
- `CurrentStage: SeasonStage` (example `PreSeason`), `CurrentWeek:int`, `CurrentWeekType: SeasonWeekType` (examples seen: `PreSeason`, `RegularSeason`, `OffSeason` — full enum values UNVERIFIED), `CurrentSeasonYear` (e.g. 2026), `CurrentYear` (0-based), `BaseCalendarYear`, **`CurrentOffseasonStage:int`**, **`OffseasonNumStages:int = 9`**, `NumberOffseasonAdvances:int`, `RegularSeasonLastWeekScheduled = 15`, `RegularSeasonWeekConferenceChampionship = 16`, `PostSeasonNumWeeks = 4`, `PreseasonWeekCount = 1`, `MaxYears = 30`.
- Period flags (booleans — exactly what our tool must gate on): **`IsCarouselPeriodActive`**, `IsStaffHiringPeriodActive`, `IsStaffHiringCreateOfferPeriodActive`, `IsStaffHiringEvaluateOfferPeriodActive`, `IsCoachDemandReleasePeriodActive`, **`IsTransferPortalNewlyAvailable`**, **`IsTransferSignPeriodActive`**, `IsRecruitingPeriodActive`, `IsCommittmentPeriodActive` (sic, double-t), `IsSigningPeriodActive`, `IsScholarshipPeriodActive`, `IsVisitingPeriodActive`, `IsPitchingPeriodActive`, `IsScoutingPeriodActive`, `IsPlayerDemandReleasePeriodActive`, `IsGraduatingSeniorNILExclusionPeriodActive`, `HSRecruitingCurrentMaxTopSchools = 10`, `HSRecruitingNextStopMaxTopSchools`.
- Reached from `Franchise.SeasonInfo` (Franchise table also has `LeagueID:int` — used by brooksg357 as per-dynasty key).

### `JobOpening` — id 4151, capacity 408 (empty during regular season). **The carousel's core record:**
- `Team: Team`, `Position: CoachPosition`, **`PrevCoach: Coach`**, **`SelectedCoach: Coach`**, **`Filled: bool`**, **`Reason: CoachLeaveReason`** (enum — values UNVERIFIED but the type name promises fired/retired/left distinctions), `ContractOfferList: StaffPersonContractOffer[]`, `HighestOfferedProgramPoints`, `FinalContractProgramPoints`, `InterestedUserTeamsList: Team[]`, `IsEmergentJobOpening: bool`. Global list table `JobOpening[]` id 4711.
- **This is likely the richest signal for our coaching-change tracker: after the carousel, each filled opening records team, position, who left, why, and who was hired.** (Whether rows persist after the carousel completes is UNVERIFIED — snapshot during `IsCarouselPeriodActive` to be safe.)

### `StaffPersonContractOffer` — id 4298, capacity 804:
- `Team`, `StaffPerson`, `StaffPersonTeam` (current team of the coach), `ContractPosition: CoachPosition`, `Length`, `OfferedContractProgramPoints`, `ExpectedContractProgramPoints`, `Status: ContractOfferStatus` (e.g. `Pending`), `TeamInterestInStaffPerson:int`, `BaseStaffPersonInterestInOffer:int`, `AdjustedStaffPersonInterestInOffer:int`, `OfferIndex`, `ExperiencePoints`, `ContractExpectationsByYear: enum[]`. (These interest fields are surely what KivJoy's "Coach Interest / Team Interest" edit.)

### `StaffHiringEval` — id 5171, singleton (the carousel engine object):
- State: **`JobOpenings: JobOpening[]`**, `OutstandingStaffPersonOffersList: StaffPersonContractOffer[]`, `StaffMovesRequest` ref.
- Tunables: `ChanceCoachWaits = 25`, `ChanceToIncreaseOffer = 90`, `ChanceToWithdrawOffer = 5`, `PercentageToIncreaseOffer = 15`, salary/interest splines (`SalaryOfferRatioToCoachInterestSpline`, `ExpectedCoachSalaryLevelSpline`, `ExpectedSalaryLegacySpline`).
- Function-typed fields reveal the engine flow: `HandleOffseasonStart`, `HandleStaffHiringWeekAdvance`, `HandleChampionshipWeekStart`, `HandleNationalChampionshipStart`, `HandleRegularBowlWeekStart`, `CreateOfferForAllTeams`, `EvaluateFireStaffForAllTeams`, `EvaluateHireStaffForAllTeams`, `ReleaseStaffWithNoOutstandingOffers`, `SelectOfferToAccept`, `GetCoachInterest`, `GetCoachRating`, `GetIsCoachToBeRehired`, `IssueEnterCoachCarouselRequest`, `ValidateHeadCoach/OffensiveCoordinator/DefensiveCoordinator`.

### Coach retirement — `CoachRetirementEval` id 4451 + `CoachRetirementEvalInfo` id 5945 (singletons):
- Eval: `CoachesReturningThisYear/AfterOneYear/AfterTwoYears/AfterThreeYears: Coach[]` (un-retirement lists!), `EvaluateCoachForRetirement`, `ForceRetirement`, HOF/no-HOF farewell strings per archetype.
- Info (tunables): `RetirementAgeThreshold = 65`, `AlwaysRetireAgeThreshold = 100`, `RetirementModifier = 1.25`, `FreeAgentMod = 5`, `RetirementReturnEnable = false`, splines (`CoachLeavingOddsSpline`, `LegacyScoreRetireSpline`, `LegacyScoreReturnModSpline`, `YearsCoachingModSpline`).

### `CoachManager` — id 4444, singleton: function surface `SignCoach`, `RetireCoach`, `UnRetireCoach`, `AgeCoach`, `ExtendContract`, `DeleteStaffPerson`, `ProgressStaffPersonContract`, `HandleComebackCoaches`, `UpdateCoachContractStatus`.

### `CoachTalentEffects` — id 4264, 138 rows (one per FBS team; referenced by `Team.CoachTalentEffects`):
- Pre-resolved per-team coach-ability effects. Recruiting-relevant: `Recruiting_BoostChance_DevTraitUnlock:int`, `Recruiting_ScoutingBoost_Start:int[8]` (indexed by `CoachTalentPosGroup`: QB=0, RB=1, WR_TE=2, OL=3, DB=4, LB=5, KP_ATH=6, LE_RE_DT=7), `Recruiting_BonusHours:int[8]`, `Recruiting_InfluenceBoost_Start:int[8]`, `Recruiting_Action_InfluenceBoost:int[8]`, `Recruiting_InfluenceBoost_Pipeline:int[8]`, `Recruiting_SwayBoost:int[8]`, `Recruiting_PointsBoost_Visit/CompVisit/SchoolGrade:int[8]`, `Recruiting_BoostCommitChanceOn1st`, `Recruiting_XPBoost_SignedRecruits(HighCaliber)`, `Pipeline_LevelBoost_AlmaMater/Top2/Top5/Bottom5`, transfer-retention effects `PlayersLeaving_LessTransferChance:int[8]`, `PlayersLeaving_InterestBoost_Transfers:int[8]`, `PlayersLeaving_ThresholdDecrease_Dealbreakers:int[8]`, `ProgramPoints_DecreaseRiskOfTransfer`, and `CoachCarousel_IncreaseCoachPoints_NewContract`.

### `Conference` — id 4291, 12 rows: `Name`, `AssetName` (e.g. `FBS_Independents`), `ConferenceEnum: CollegeConferences`, **`TeamSlots: Team[]`** (the field KivJoy reads), `Divisions: Division[]`, championship config, scheduling config.

### Transfer portal plumbing:
- **`AddToTransferPortalEvent`** (id 4345): payload field `Recruit: Recruit` — i.e., **portal entries are rows in the `Recruit` table** (with `Class: RecruitingClass` presumably = `Transfer`; enum value name UNVERIFIED — observed example was `HighSchool`; brooksg357's classifier has a "TransferLikely" bucket keyed off Recruit+Player evidence). `RecruitingEvalTransferAddedToPortalReaction` (4993) consumes it.
- `EncourageTransfersStartEvent` (4330) / `EncourageTransfersEndEvent` (4106) + reactions wired to `CutDayEval` — the "encourage transfers" phase.
- `Team.LastSeasonTransfersLost/Signed` counters.
- SeasonInfo flags `IsTransferPortalNewlyAvailable`, `IsTransferSignPeriodActive`.
- No table named `TransferPortal` exists; the portal is Recruit-table + events + period flags. **[CODE for the schema facts; interpretation UNVERIFIED]**

### `SeasonGame` — ids [5127, 5128, 5129, 6034, 6330], 934/983 rows: `HomeTeam`/`AwayTeam` refs, scores, `SeasonWeek:int`, `SeasonWeekType: SeasonWeekType`, `SeasonGameNum`, `GameStatus`, `BowlGame` ref, weather block, `IsGameOfTheWeek`. (Used by DarrellRichards/cfb-offline.)

### Player (real schema) — id 4244, 16,257/16,500 rows, 288 fields. Recruit-relevant real names **[CODE]**:
`Position: PositionE`, `IronManPosition`, `Age`, `Height`, `Weight` (example 62 — confirms stored-as-offset encoding), `JerseyNum`, `OverallRating`, `ProspectStarRating: ProspectQuality` (e.g. `THREE_STAR`), `RecruitingDealbreaker: RecruitingMotivationType` (e.g. `PlayingStyle`), `Motivation1/2/3: MotivationType`, `IdealRecruitingPitch: RecruitingPitchType`, `HomePipeline: Pipeline` (e.g. `Iowa`), `SchoolYear: SchoolYear` (e.g. `Freshman`), `RedshirtStatus: RedshirtStatus` (e.g. `Eligible`), `TraitDevelopment: TraitDevelopment` (e.g. `College_Impact`), `IsNIL`, `BaseNILValue` (can be negative: −35), `CurrentNILCompensation`, **`AbsoluteTransferChance:int`** (−1 = unset? UNVERIFIED — likely the transfer-out propensity lever), all `*Rating` fields (names match RO27's list exactly), `PT_*` trait bools, portrait/visuals fields.

---

## 7. franchise-mcp-server / cfb-recruiting-mcp (seanpdwyer7)

- Listings: https://glama.ai/mcp/servers/seanpdwyer7/franchise-mcp-server and https://glama.ai/mcp/servers/seanpdwyer7/cfb-recruiting-mcp. The linked GitHub repos (github.com/seanpdwyer7/…) returned **404** on fetch — private, renamed, or deleted. UNVERIFIED availability.
- franchise-mcp-server (from listing): wraps madden-franchise as MCP tools; Madden 19–27 + CFB27+; opens franchise saves and FTC game-data files; bundles a "full CFB 27 schema" auto-applied to college saves ("real field names for team identity, the native Media/Coaches/CFP polls, prestige, and records"); ~2,000+ tables; validation-first editing. Documented **uniqueIds**: `Player = 1612938518` (verified M25/26 + CFB27), `CharacterVisuals = 1429178382` (CFB27). **[1SRC]** — useful candidates for `getTableByUniqueId`.
- cfb-recruiting-mcp (from listing): diagnoses/remediates recruit pools (star-distribution health, unsigned 4–5★ with zero offers, prune 1–2★). Real-save stats quoted: "3,764 high-school recruits; 55% 1–2 star", "66 unsigned 4–5 star recruits with ZERO scholarship offers". Explicit caveat: "The recruiting AI/engine is compiled into the game — not moddable. This tool changes the save-file data the AI works on." Writes timestamped backups.

---

## 8. Other open-source CFB27 tools worth mining later

- **https://github.com/brooksg357-a11y/cfb27-dynasty-modding** — THE knowledge base (format docs + toolchain, recruit generator, progression, appearance, engine modding). Fork: eric-levinson/cfb27-dynasty-modding.
- **https://github.com/gpellis87/cfb27-table-explorer** — static table/record browser; `data/table-map.json` (schemas, saved to scratchpad) + `data/data.json` (records); extraction scripts live in a `cfb27-recruiting-lab` project (`mapTables.ts`, `buildSearchIndex.ts`) — repo for that lab not found publicly (UNVERIFIED). Key finding documented: TeamIndex join semantics.
- **https://github.com/DarrellRichards/cfb-offline** — dynasty overview app (schedule, rankings, stats, recruiting board, NIL/program-point write-back). **Uses the same `C27_468_2.gz` schema at repo root** and madden-franchise; `lib/franchise.js` has shared open/schema helpers; per-domain extractors; web (localhost:3000)/CLI (`npm run extract:all -- "path"`)/Electron. Closest architectural cousin to our tool.
- **https://github.com/eric-levinson/cfb27-lua-hook** — renamed from `cfb27-save-editor` (GitHub API redirects; old save-editor blueprint preserved in brooksg357's `external-tooling-reference.md`). Now: persistent Lua 5.4 runtime injected via MMC startup proxy, named-pipe protocol, `@cfb27/lua-hook` SDK, `cfb27lua` CLI; **live memory writes** with anticheat gates. Their earlier save-editor stack: Python HTTP server owning FBCHUNKS/zlib container + Node madden-franchise emitting only decompressed FrTk via `franchise.strategy.file.generateUnpackedContents(franchise.tables, franchise.unpackedFileContents)` + vanilla JS SPA; deterministic PRNG (`sha256(seed|counter|label)`); preview→validate→apply-to-copy; per-field write gates. Notable finding there **[1SRC]**: "the running game OVERWRITES live table edits to managed fields" — save-file path is the reliable one (validates our architecture).
- **https://github.com/xBandaku/CFB27-RTG-Editor** and **https://github.com/Trixx542/cfb27-rtg-editor** — Road to Glory save editors (RTG saves: 5 chunks, different layout).
- **https://github.com/Velossity/CFB27-MODS** — Frosty/MMC engine-mod workspace (`.fbmod`s), not save editing; references a separate "CFB27 Tracker" project.
- **https://github.com/elrey-430/cfb27-aio-app** — "all-in-one" JS app, no description; unexamined.
- **https://github.com/jwired21/cfb27-dynasty-tournament**, **https://github.com/ndyer28/cfb27-dynasty-wheel** — peripheral utilities.
- **https://chasestubbs.github.io/cfb27dynastyhub/** — CFB27 Dynasty Hub site (community hub; not fetched in depth).
- **https://github.com/bep713/madden-franchise-editor** — general table editor GUI (Madden; may open CFB saves via library support — UNVERIFIED).
- **https://github.com/cfbrevamped/CFBR** — College Football Revamped (NCAA 14), historical home of Fang's original Recruit Overhaul; not CFB27.
- **https://github.com/sportsdataverse/recruitR** — real-world recruiting data package (R); not save editing.

---

## 9. Implications for CoachCarouselRecruitTool (synthesis)

1. **Detecting coaching changes**: diff `Team.HeadCoach/OffensiveCoordinator/DefensiveCoordinator` refs across saves/sessions, AND read `JobOpening` rows (`Team`, `Position`, `PrevCoach`, `SelectedCoach`, `Reason: CoachLeaveReason`, `Filled`) during/after the carousel; supplement with Coach fields `PrevTeamIndex`, `PrevPosition`, `COACH_FIREREPORTED`, `COACH_LASTTEAMFIRED`, `COACH_LASTTEAMRESIGNED`, `ContractStatus`, and retirement lists on `CoachRetirementEval`. Gate on `SeasonInfo.IsCarouselPeriodActive` / `IsStaffHiringPeriodActive`.
2. **Timing our writes**: the "first offseason recruiting stage (transfer portal)" is observable via `SeasonInfo.CurrentStage/CurrentWeekType/CurrentOffseasonStage` + `IsTransferPortalNewlyAvailable`/`IsTransferSignPeriodActive` (and force-commit-recruits' "week 4 of transfer period" precedent shows offseason recruiting weeks advance like weeks).
3. **Decommits**: candidate mechanisms (in likely order of safety): (a) set `Recruit.RecruitStage` back below SoftCommitted + restore `CommitScore` headroom + zero the committed school's `RecruitTarget.ProspectInfluenceTotal`; (b) use `RecruitStageAdvance = Decommit (2)` and let the next advance process it (semantics UNVERIFIED); (c) remove from `Team.CommittedPlayers`. CAUTION: `RecruitStage` is documented as **sticky/ratchet** ("never recomputed downward") — whether a *manual* downward write sticks (or is even honored) is UNVERIFIED and must be game-tested. Also verify `Recruit.RecruitStage >= SoftCommitted ⟺ membership in Team.CommittedPlayers` consistency requirements.
4. **Old school keeps re-recruiting edge**: keep/insert `{TeamId, TeamInfluence}` in the recruit's `TopSchoolsList` and **always re-sort slots to influence-descending** or the UI misrenders; back it with a `RecruitTarget` row (master store) re-applied weekly if we want the CPU to keep pursuing — CPU boards are re-curated every advance.
5. **Follow-the-coach**: choose recruits whose `Motivation1..3`/`RecruitingDealbreaker` include `CoachPrestige`/`CoachStability`, check destination `Team.TeamPrestige` vs old school, scholarship space via board/`MaxTeamScholarshipOffers` (35) and `TotalScholarshipOffers`, then apply the brooksg357 "durable CPU pursuit + force commit" write-set (make new school influence leader; set `CommitScore ≤` leader influence for instant hard-commit on next advance).
6. **Write safety**: prefer writing a new save file (Dynamic-Pipeline/brooksg357 pattern), auto-backup with SHORT filenames (Jersey-tool gotcha), and verify chunk-2 integrity after stock `save()` — or adopt a libdeflate-based chunk-1 re-wrap.

---

## 10. Open questions / follow-ups

1. Does stock madden-franchise `save()` actually corrupt CFB27 saves in practice, or only when the recompressed stream outgrows the slot? Test locally (§1a). What do our four reference tools' successful writes imply about typical slack?
2. Schema version 468.2 (C27_468_2.gz used by reference tools) vs 809.0 (FBCHUNKS header / brooksg357 CAS extraction) — same data, different numbering? Inspect header of a real save.
3. `CoachLeaveReason` enum values (fired / hired away / retired / promoted?) — extract from schema enums locally (the C27 schema gz should contain them) or from `data.json` of table-explorer.
4. Do `JobOpening` rows persist after the carousel resolves (are they wiped on offseason end)? Determines whether we snapshot during carousel or can read after.
5. Exact `SeasonStage` / `SeasonWeekType` / `RecruitingClass` / `ScholarshipStatus` / `StaffPersonContractStatus` / `ContractOfferStatus` enum value lists — extract from schema.
6. RO27's uniqueIds (`Recruit_1873209313`, `UserRecruitTarget_3987156317`) — confirm against our save's table headers; if right, prefer `getTableByUniqueId`.
7. Is `Coach.TeamIndex == 255` the unemployed marker, and how do free-agent coaches appear (rows with TeamIndex 255 + ContractStatus)? 497 populated vs 632 capacity suggests a coach pool.
8. Whether a downward `RecruitStage` write (decommit) is honored in-game — needs a live test save.
9. seanpdwyer7's MCP repos: are they still public anywhere (rename?) — low priority, glama listings suffice.
10. `Player.AbsoluteTransferChance` semantics (transfer-out propensity? −1 default) — relevant if we ever extend to roster players following coaches.

## Source URL index

- https://github.com/bep713/madden-franchise (+ raw README) · https://www.npmjs.com/package/madden-franchise
- https://github.com/bep713/madden-franchise-editor
- https://github.com/KivJoy/CFB27-Coaching-Carousel · https://github.com/Aball1495/CFB27-Dynamic-Pipeline-Tool · https://github.com/jhusting/force-commit-recruits · https://github.com/ArtVsTheWorld/CFB27-Jersey-ReNumber-Tool
- https://github.com/brooksg357-a11y/cfb27-dynasty-modding — docs fetched: MAP.md, recruiting.md, player-table.md, save-format.md, toolchain.md, open-questions.md, ro27-teardown.md, external-tooling-reference.md
- https://github.com/eric-levinson/cfb27-dynasty-modding (fork) · https://github.com/eric-levinson/cfb27-lua-hook (ex cfb27-save-editor)
- https://github.com/gpellis87/cfb27-table-explorer (raw data/table-map.json downloaded)
- https://github.com/DarrellRichards/cfb-offline · https://github.com/xBandaku/CFB27-RTG-Editor · https://github.com/Trixx542/cfb27-rtg-editor · https://github.com/Velossity/CFB27-MODS · https://github.com/elrey-430/cfb27-aio-app · https://github.com/jwired21/cfb27-dynasty-tournament · https://github.com/ndyer28/cfb27-dynasty-wheel
- https://glama.ai/mcp/servers/seanpdwyer7/franchise-mcp-server · https://glama.ai/mcp/servers/seanpdwyer7/cfb-recruiting-mcp
- https://www.youtube.com/watch?v=AKIVm_D5_ts · https://www.youtube.com/watch?v=l0NaLbP2hGY (RO27 tutorials)
- https://chasestubbs.github.io/cfb27dynastyhub/ · https://github.com/cfbrevamped/CFBR · https://github.com/sportsdataverse/recruitR
- Local artifact: `C:/Users/rvanv/OneDrive/Documents/EA SPORTS College Football 27/saves/Fangs-Settings.ro27-settings.json` (read-only)
- Scratchpad copies: `table-map.json`, `key-tables.json` (extracted schemas)
