College Hockey Dynasty
Menu
Reference

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.

Don't want to hand-write JSON? The Data Pack Builder is a free browser tool that builds a valid pack for you — add conferences and teams, auto-generate legal rosters, and download a file ready to import. Nothing you enter leaves your browser.

1. Quick Start

The smallest valid pack is a JSON object with pack_name, version, conferences, teams, and players that satisfies these minimums:

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

KeyTypeRequiredNotes
pack_nameStringYesDisplay name shown in the Data Packs list. Also seeds a stable ID for the pack itself (same name → same ID across installs/re-imports).
versionStringYesFree-form version label (e.g. "1.0"). Not validated against a format.
start_yearIntNoThe 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.
conferencesArrayYesAt least 1.
teamsArrayYesAt least 4 across the whole pack.
playersArrayYesAt least 1 total; each team needs 12+ skaters and 2+ goalies.

3. Conferences

KeyTypeRequiredNotes
idUUIDYesUnique within the pack.
nameStringYesDisplay name.
exposure_ratingIntYes0–100. National TV reach / recruiting prestige — feeds recruiting exposure and NIL market visibility.
abbreviationStringNoShort code (e.g. "AHA"). Derived from name if omitted — see §9.
regionStringNoOne 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_independentBoolNoSee §7 Independents. Inferred from the name/abbreviation if omitted.
tournament_presetStringNoName of a built-in tournament format — see §10. Ignored if tournament is also present.
tournamentObjectNoFull inline tournament spec — see §10. Takes priority over tournament_preset.

4. Teams

KeyTypeRequiredNotes
idUUIDYesUnique within the pack. Also used as team_id on player records.
conference_idUUIDYesMust match a conference's id in the same pack.
nameStringYesFull program name.
mascotStringYesNickname (e.g. "Force", "Eagles").
primary_color / secondary_colorString (hex)Yes#RRGGBB hex colors, used for jerseys, logos, and UI accents.
prestigeIntYes0–100. Drives recruiting pull, NIL collective size, and revenue-share cap.
legacy_scoreIntYes0–100. Historical program strength — feeds donor/legacy financial mechanics.
facilities_ratingIntYes0–100. Facility quality tier — mapped internally to a facilities level.
home_rink_widthIntNoRaw 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.
abbreviationStringNoShort code. Derived from name if omitted — see §9.
logo_urlString (URL)NoRemote crest image URL, fetched once and cached. Omit to fall back to an initials/color avatar.
Starting revenue-share cap is 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

KeyTypeRequiredNotes
idUUIDYesUnique within the pack.
team_idUUIDYesMust match a team's id in the same pack.
first_name / last_nameStringYes
positionStringYesOne of C, LW, RW, LD, RD, G.
yearStringYesOne 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.
ageIntYes16–25.
archetypeStringYesMust 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.
overallIntYes40–99. Current skill rating.
potentialIntYes40–99. Development ceiling; should be ≥ overall (not enforced).
attributesObjectYesSix 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 110).
jerseyIntNo1–98. Outside that range, or omitted, is treated as unset — the app auto-assigns a collision-free number.
height_inches / weight_poundsIntNo
birth_dateStringNoISO yyyy-MM-dd.
hometownStringNo"City, ST" or "City, Country". Drives the profile map pin and recruiting-region assignment when present; procedurally generated when absent.
last_teamStringNoJunior/feeder club label shown on the profile (e.g. "Muskegon (USHL)").
statsObjectNoPrior-season real stat line — see §6.
Data packs do not author coaches or staff. Every coach and support-staff member — yours and every CPU opponent's — is generated by the app at import time from each team's prestige and region. A pack only ever supplies conferences, teams, and players.

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.

KeyApplies toNotes
games_playedBoth
goals, assists, plus_minus, penalty_minutes, shots_on_goal, power_play_goals, shorthanded_goals, game_winning_goalsSkatersLeft at 0 for goalies.
wins, losses, ties, saves, goals_allowed, shutouts, minutes_playedGoaliesLeft at 0 for skaters.
team_nameBoth (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

RuleValueEnforced how
Minimum skaters per team12Import fails validation below this.
Minimum goalies per team2Import fails validation below this.
Positional depth floorC: 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 sizeGenerated 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:

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" }
  ]
}
KeyTypeRequiredDefault
field_sizeIntNonull/omitted = every conference team qualifies.
bye_countIntNo0
grants_auto_bidBoolNotrue (forced to false if the conference is independent).
roundsArrayYesOrdered first round → final.
rounds[].labelStringNoAuto-derived from bracket position ("Championship", "Semifinals", "Quarterfinals", "First Round", "Opening Round", or "Round N").
rounds[].wins_requiredIntNo1 (single game); 2 = best-of-three. Clamped to 1–4 on import regardless of what you author.
rounds[].venueStringNo"higher_seed_hosts" (default) or "neutral_site".
rounds[].seedingStringNo"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:

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):

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.

See Also