Custom Data Packs
A Data Pack is a single JSON file that replaces the game's default fictional league with your own conferences, teams, and players. Import one from a local file or a hosted URL on the Data Packs screen in the main menu. This page documents every field the app reads and the validation rules enforced on import — the exact schema, straight from the app's own code.
1. Quick Start
The smallest valid pack is a JSON object with pack_name, version, conferences, teams, and players that satisfies these minimums:
- at least 1 conference
- at least 4 teams
- at least 1 player
- every team has at least 12 skaters and 2 goalies
JSON keys are snake_case. All IDs are UUID strings (standard 8-4-4-4-12 hex format). They just need to be unique within the pack and used consistently to link records together — a team's conference_id points at a conference's id, and a player's team_id points at a team's id.
2. Top-Level Structure
| Key | Type | Required | Notes |
|---|---|---|---|
pack_name | String | Yes | Display name shown in the Data Packs list. Also seeds a stable ID for the pack itself (same name → same ID across installs/re-imports). |
version | String | Yes | Free-form version label (e.g. "1.0"). Not validated against a format. |
start_year | Int | No | The NCAA season the pack represents, as the year the season starts (e.g. 2025 for the 2025-26 season). A new game built from this pack begins in that season instead of the live calendar year. Omit to default to the real-world current season. |
conferences | Array | Yes | At least 1. |
teams | Array | Yes | At least 4 across the whole pack. |
players | Array | Yes | At least 1 total; each team needs 12+ skaters and 2+ goalies. |
3. Conferences
| Key | Type | Required | Notes |
|---|---|---|---|
id | UUID | Yes | Unique within the pack. |
name | String | Yes | Display name. |
exposure_rating | Int | Yes | 0–100. National TV reach / recruiting prestige — feeds recruiting exposure and NIL market visibility. |
abbreviation | String | No | Short code (e.g. "AHA"). Derived from name if omitted — see §9. |
region | String | No | One of northeast, midAtlantic, midwest, upperMidwest, greatLakes, atlanticSouth. Places the conference geographically for recruiting proximity and bracket siting. An unknown/omitted region is hashed deterministically so custom conferences still get a distinct recruiting territory. |
is_independent | Bool | No | See §7 Independents. Inferred from the name/abbreviation if omitted. |
tournament_preset | String | No | Name of a built-in tournament format — see §10. Ignored if tournament is also present. |
tournament | Object | No | Full inline tournament spec — see §10. Takes priority over tournament_preset. |
4. Teams
| Key | Type | Required | Notes |
|---|---|---|---|
id | UUID | Yes | Unique within the pack. Also used as team_id on player records. |
conference_id | UUID | Yes | Must match a conference's id in the same pack. |
name | String | Yes | Full program name. |
mascot | String | Yes | Nickname (e.g. "Force", "Eagles"). |
primary_color / secondary_color | String (hex) | Yes | #RRGGBB hex colors, used for jerseys, logos, and UI accents. |
prestige | Int | Yes | 0–100. Drives recruiting pull, NIL collective size, and revenue-share cap. |
legacy_score | Int | Yes | 0–100. Historical program strength — feeds donor/legacy financial mechanics. |
facilities_rating | Int | Yes | 0–100. Facility quality tier — mapped internally to a facilities level. |
home_rink_width | Int | No | Raw rink width in feet. Bucketed to the nearest tier: < 90 → Standard (85 ft), 90–97 → Wide (92 ft), ≥ 98 → Olympic (100 ft). Omit for Standard. |
abbreviation | String | No | Short code. Derived from name if omitted — see §9. |
logo_url | String (URL) | No | Remote crest image URL, fetched once and cached. Omit to fall back to an initials/color avatar. |
prestige × 15,000 + 150,000 and starting NIL collective balance is prestige × 3,000 + 30,000 — both derived automatically. There's no way to set them directly.5. Players
| Key | Type | Required | Notes |
|---|---|---|---|
id | UUID | Yes | Unique within the pack. |
team_id | UUID | Yes | Must match a team's id in the same pack. |
first_name / last_name | String | Yes | |
position | String | Yes | One of C, LW, RW, LD, RD, G. |
year | String | Yes | One of Freshman, Sophomore, Junior, Senior. There is no "Graduate" year in the schema — author a grad player as a Senior; extra eligibility is granted only through in-game mechanics. |
age | Int | Yes | 16–25. |
archetype | String | Yes | Must match the player's position group — forwards (C/LW/RW) take sniper, playmaker, powerForward, twoWayForward, or grinder; defensemen (LD/RD) take offensiveDefenseman, defensiveDefenseman, or twoWayDefenseman; goalies (G) take butterfly, hybrid, or acrobatic. A mismatch isn't rejected by the validator but reads oddly on the profile. |
overall | Int | Yes | 40–99. Current skill rating. |
potential | Int | Yes | 40–99. Development ceiling; should be ≥ overall (not enforced). |
attributes | Object | Yes | Six sub-ratings — skating, shooting, passing, physical, defending, goaltending — each 1–99. All six are required even for a position that doesn't use one of them (a forward still needs a low goaltending value like 1–10). |
jersey | Int | No | 1–98. Outside that range, or omitted, is treated as unset — the app auto-assigns a collision-free number. |
height_inches / weight_pounds | Int | No | |
birth_date | String | No | ISO yyyy-MM-dd. |
hometown | String | No | "City, ST" or "City, Country". Drives the profile map pin and recruiting-region assignment when present; procedurally generated when absent. |
last_team | String | No | Junior/feeder club label shown on the profile (e.g. "Muskegon (USHL)"). |
stats | Object | No | Prior-season real stat line — see §6. |
6. The stats object — one prior season only
A pack can only carry a single prior-season stat line per player, and it's optional. When present, it's surfaced on the player's profile as their one season of pre-import history and seeds their career totals — it does not create multiple season rows.
| Key | Applies to | Notes |
|---|---|---|
games_played | Both | |
goals, assists, plus_minus, penalty_minutes, shots_on_goal, power_play_goals, shorthanded_goals, game_winning_goals | Skaters | Left at 0 for goalies. |
wins, losses, ties, saves, goals_allowed, shutouts, minutes_played | Goalies | Left at 0 for skaters. |
team_name | Both (optional) | Attributes this stat line to a different team than the player's current team_id — use this for a transferred player whose real prior season was played elsewhere. |
Every field defaults to 0 and every field is present on every player's stat object regardless of position. The season label shown on the profile is derived from the pack's start_year: with "start_year": 2025, a player's stats line is dated the 2024-25 season.
7. Independent Conferences
An independent grouping (real-world example: Alaska, Alaska Anchorage, Lindenwood, Long Island, Stonehill) schedules games and is ranked in the national index like any other conference, and crowns its own postseason champion, but grants no automatic NCAA tournament bid — its teams can only reach the tournament via an at-large bid.
Mark a conference as independent with "is_independent": true. If omitted, the app still infers independence when the conference's name contains "independent" (any case) or its resolved abbreviation is exactly "IND" — a custom-named independent grouping should set the flag explicitly to avoid surprises. Regardless of tournament shape, an independent conference can never grant an automatic bid — is_independent always wins over any grants_auto_bid you set.
Independent conferences default to a 5-team, top-3-bye, neutral-site single-elimination bracket with a consolation round unless overridden. The engine currently schedules an independent grouping the same way it schedules a real conference (games are drawn from within the group) — expect intra-group scheduling rather than bespoke non-conference scheduling.
8. Team Composition & Roster Rules
| Rule | Value | Enforced how |
|---|---|---|
| Minimum skaters per team | 12 | Import fails validation below this. |
| Minimum goalies per team | 2 | Import fails validation below this. |
| Positional depth floor | C: 4 · LW: 4 · RW: 4 · LD: 3 · RD: 3 · G: 2 (20 total) | Not a hard import requirement — a thinner roster is silently backfilled with generated walk-ons so the lineup screen never shows blank slots. |
| Practical roster size | Generated teams target 34 players (20F/11D/3G); the app allows 22–38. | Not something a pack must hit exactly — a thin pack gets backfilled, an over-38 pack isn't rejected. |
In short: the validator only guarantees a team is viable to field a lineup (12 skaters + 2 goalies). Hitting the positional minimums exactly and aiming for a realistic ~34-man roster is a quality bar for your pack, not something the importer blocks on.
9. Abbreviation Derivation
If a team or conference omits abbreviation, the app derives one from name:
- Single-word names take their first three letters, uppercased:
"Harvard"→HAR. - Multi-word names (splitting on spaces and hyphens) take the leading initial of each word (up to 4 words), uppercased:
"Penn State"→PS,"St. Cloud State"→SCS,"Minnesota-Duluth"→MD.
Supply an explicit abbreviation any time the derived one would be ambiguous (two teams deriving to the same code) or just isn't the real-world code you want (e.g. "BC" for Boston College).
10. Conference Tournament Format
By default, a conference's postseason format falls back to a generic bracket sized to its team count. To model a real conference's actual format, author it one of two ways — inline wins over a named preset; either wins over the default.
Valid preset keys: big_ten, ccha, nchc, ecac, hockey_east, atlantic_hockey, uchc.
An inline spec looks like:
"tournament": {
"field_size": 8,
"bye_count": 0,
"grants_auto_bid": true,
"rounds": [
{ "label": "Quarterfinals", "wins_required": 2, "venue": "higher_seed_hosts", "seeding": "bracket_advance" },
{ "label": "Semifinals", "wins_required": 1, "venue": "neutral_site", "seeding": "reseed_by_standing" },
{ "label": "Championship", "wins_required": 1, "venue": "neutral_site", "seeding": "reseed_by_standing" }
]
}
| Key | Type | Required | Default |
|---|---|---|---|
field_size | Int | No | null/omitted = every conference team qualifies. |
bye_count | Int | No | 0 |
grants_auto_bid | Bool | No | true (forced to false if the conference is independent). |
rounds | Array | Yes | Ordered first round → final. |
rounds[].label | String | No | Auto-derived from bracket position ("Championship", "Semifinals", "Quarterfinals", "First Round", "Opening Round", or "Round N"). |
rounds[].wins_required | Int | No | 1 (single game); 2 = best-of-three. Clamped to 1–4 on import regardless of what you author. |
rounds[].venue | String | No | "higher_seed_hosts" (default) or "neutral_site". |
rounds[].seeding | String | No | "bracket_advance" (fixed bracket, default) or "reseed_by_standing". |
Whatever shape you author is validated against the conference's actual team count on import: if it doesn't cleanly fit, the round count is regenerated for the real field while preserving each round's authored flavor as closely as possible. A pack can never produce a broken bracket, even with a wrong team count.
11. Referential Integrity & Uniqueness
The importer runs these checks, in order, and reports every failure at once so you can fix a pack top-to-bottom in one pass:
- Non-empty.
pack_nameandversionnon-blank; ≥ 1 conference, ≥ 4 teams total, ≥ 1 player. - Referential integrity. Every team's
conference_idmatches a conferenceid; every player'steam_idmatches a teamid. - Value ranges — see the tables above for each field.
- Enum membership.
position,archetype, andyearmust each match an exact listed value. - Roster minimums. Every team needs ≥ 1 player, ≥ 12 skaters, ≥ 2 goalies.
- UUID uniqueness. No
idmay repeat across the combined pool of conferences, teams, and players.
A pack that fails validation is never partially imported — it's all-or-nothing. Malformed JSON that doesn't even reach the validator (a missing key, wrong type, corrupted syntax) is reported separately, naming the specific key/path Swift's decoder choked on.
On-disk storage is keyed by the pack's stable ID (derived from pack_name), not by a hand-slugged filename — two differently-named packs can never collide, and re-importing a pack with the same name always updates the same file rather than creating a duplicate.
12. Importing a Pack
From the Data Packs screen (main menu → Data Packs):
- Import from file — pick any
.jsonfile via the system file picker. - Import from URL — paste an
http://orhttps://link to a hosted JSON file.
Either path decodes the JSON, runs the full validation pass above, and — only if it passes — saves a local copy and adds it to your pack list. You can then mark any imported pack as the default, which pre-selects it the next time you start a new game.