aboutsummaryrefslogtreecommitdiff
path: root/content-system/registry/registry.ts
blob: 6910cab38ab3122b82077acbfb506c72306466a8 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import type {
  CityContent,
  CityId,
  InstitutionContent,
  InstitutionContentId,
  RegionContent,
  RegionId
} from '@hollowdark/content-system/regions/schema'
import type { ContentManifest } from '@hollowdark/content-system/manifest/schema'
import { loadChunk } from '@hollowdark/content-system/loader/loader'

/**
 * The in-memory content registry. Populated once at session start by
 * walking the manifest and loading every chunk into typed maps. All
 * simulation-time reads go through here — synchronous, typed, fast.
 */
export class ContentRegistry {
  readonly #regions = new Map<RegionId, RegionContent>()
  readonly #cities = new Map<CityId, CityContent>()
  readonly #institutions = new Map<InstitutionContentId, InstitutionContent>()

  get regions(): ReadonlyMap<RegionId, RegionContent> {
    return this.#regions
  }

  get cities(): ReadonlyMap<CityId, CityContent> {
    return this.#cities
  }

  get institutions(): ReadonlyMap<InstitutionContentId, InstitutionContent> {
    return this.#institutions
  }

  addRegion(region: RegionContent): void {
    this.#regions.set(region.id, region)
  }

  addCity(city: CityContent): void {
    this.#cities.set(city.id, city)
  }

  addInstitution(institution: InstitutionContent): void {
    this.#institutions.set(institution.id, institution)
  }

  region(id: RegionId): RegionContent {
    const value = this.#regions.get(id)
    if (value === undefined) throw new Error(`Unknown region: ${id}`)
    return value
  }

  city(id: CityId): CityContent {
    const value = this.#cities.get(id)
    if (value === undefined) throw new Error(`Unknown city: ${id}`)
    return value
  }

  institution(id: InstitutionContentId): InstitutionContent {
    const value = this.#institutions.get(id)
    if (value === undefined) throw new Error(`Unknown institution: ${id}`)
    return value
  }
}

/** Per-chunk callback used to advance a progress bar as loading proceeds. */
export type ChunkProgressCallback = (loaded: number, total: number) => void

/**
 * Populate a fresh registry from the supplied manifest. Fetches every
 * chunk whose id begins with a world-content prefix; ignores unknown
 * prefixes so the registry can grow without breaking older clients.
 * Chunks load in parallel; the callback reports completion order, not
 * start order.
 */
export async function populateFromManifest(
  manifest: ContentManifest,
  baseUrl: string,
  onProgress?: ChunkProgressCallback
): Promise<ContentRegistry> {
  const registry = new ContentRegistry()
  const entries = Object.entries(manifest.chunks)
  const total = entries.length
  let loaded = 0

  await Promise.all(
    entries.map(async ([chunkId]) => {
      const { data } = await loadChunk(chunkId, manifest, baseUrl)
      if (chunkId.startsWith('world/regions/')) {
        registry.addRegion(data as unknown as RegionContent)
      } else if (chunkId.startsWith('world/cities/')) {
        registry.addCity(data as unknown as CityContent)
      } else if (chunkId.startsWith('world/institutions/')) {
        registry.addInstitution(data as unknown as InstitutionContent)
      }
      loaded += 1
      onProgress?.(loaded, total)
    })
  )

  return registry
}