bemusic/bemuse

View on GitHub
bemuse/src/game/loaders/game-loader.ts

Summary

Maintainability
C
1 day
Test Coverage
import Notechart from 'bemuse-notechart'
import NotechartLoader from 'bemuse-notechart/lib/loader'
import Progress from 'bemuse/progress'
import SamplingMaster from 'bemuse/sampling-master'
import keysoundCache from 'bemuse/keysound-cache'
import { ChartInfo } from 'bemuse-types'
import { IResource, IResources } from 'bemuse/resources/types'
import { atomic } from 'bemuse/progress/utils'
import { resolveRelativeResources } from 'bemuse/resources/resolveRelativeResource'

import * as Multitasker from './multitasker'
import GameAudio from '../audio'
import GameController from '../game-controller'
import GameDisplay from '../display'
import SamplesLoader from './samples-loader'
import loadImage from './loadImage'
import Game, { GamePlayerOptionsInput } from '../game'

type Tasks = {
  Scintillator: TODO
  Skin: TODO
  SkinContext: TODO
  Notechart: Notechart
  EyecatchImage: HTMLImageElement
  BackgroundImage: HTMLImageElement
  SamplingMaster: SamplingMaster
  Video: { element: HTMLVideoElement; offset: number } | null
  Game: Game
  GameDisplay: GameDisplay
  Samples: TODO
  GameAudio: GameAudio
  GameController: GameController
}

export type Assets = IResources & {
  progress?: {
    current?: Progress
    all?: Progress
  }
}

export type LoadSpec = {
  assets: Assets
  bms: IResource
  metadata: ChartInfo
  songId?: string
  displayMode?: 'touch3d' | 'normal'
  backImageUrl?: string
  eyecatchImageUrl?: string
  videoUrl?: string
  videoOffset?: number
  options: GamePlayerOptionsInput
}

export function load(spec: LoadSpec) {
  const assets = spec.assets
  const bms = spec.bms
  const songId = spec.songId

  return Multitasker.start<Tasks, GameController>(function (task, run) {
    task('Scintillator', 'Loading game engine', [], function (progress) {
      return atomic(
        progress,
        import(/* webpackChunkName: 'gameEngine' */ 'bemuse/scintillator')
      )
    })

    task(
      'Skin',
      'Loading skin',
      ['Scintillator'],
      function (Scintillator, progress) {
        return Scintillator.load(
          Scintillator.getSkinUrl({
            displayMode: spec.displayMode,
          }),
          progress
        )
      }
    )

    task(
      'SkinContext',
      null,
      ['Scintillator', 'Skin'],
      function (Scintillator, skin) {
        return new Scintillator.Context(skin, { touchEventTarget: window })
      }
    )

    if (assets.progress) {
      if (assets.progress.current) {
        task.bar('Loading package', assets.progress.current)
      }
      if (assets.progress.all) {
        task.bar('Loading song packages', assets.progress.all)
      }
    }

    task('Notechart', 'Loading ' + spec.bms.name, [], async (progress) => {
      const loader = new NotechartLoader()
      const arraybuffer = await bms.read(progress)
      return loader.load(arraybuffer, spec.bms, spec.options.players[0])
    })

    task('EyecatchImage', null, ['Notechart'], function (notechart) {
      if (spec.eyecatchImageUrl) {
        const [base, filename] = resolveRelativeResources(
          assets,
          spec.eyecatchImageUrl
        )
        return loadImage(base, filename)
      }
      return loadImage(assets, notechart.eyecatchImage)
    })

    task('BackgroundImage', null, ['Notechart'], function (notechart) {
      if (spec.backImageUrl) {
        const [base, filename] = resolveRelativeResources(
          assets,
          spec.backImageUrl
        )
        return loadImage(base, filename)
      }
      return loadImage(assets, notechart.backgroundImage)
    })

    const audioLoadProgress = new Progress()
    const audioDecodeProgress = new Progress()

    task.bar('Loading audio', audioLoadProgress)
    task.bar('Decoding audio', audioDecodeProgress)

    task('SamplingMaster', null, [], async () => {
      return new SamplingMaster()
    })

    task(
      'Video',
      spec.videoUrl ? 'Loading video' : null,
      ['Notechart'],
      async function (notechart, progress) {
        if (!spec.videoUrl) return Promise.resolve(null)
        let videoUrl = spec.videoUrl
        if (!videoUrl.includes('://')) {
          // This is a relative URL, we need to load from assets.
          const [base, filename] = resolveRelativeResources(assets, videoUrl)
          const file = await base.file(filename)
          videoUrl = await file.resolveUrl()
        }
        return new Promise((resolve, reject) => {
          const video = document.createElement('video')
          if (!video.canPlayType('video/webm')) return resolve(null)
          video.src = videoUrl
          video.preload = 'auto'
          video.addEventListener('progress', onProgress, true)
          video.addEventListener('canplaythrough', onCanPlayThrough, true)
          video.addEventListener('error', onError, true)
          video.addEventListener('abort', onError, true)
          video.load()

          function onProgress() {
            if (video.buffered && video.buffered.length && video.duration) {
              progress.report(
                video.buffered.end(0) - video.buffered.start(0),
                video.duration
              )
            }
          }
          function finish() {
            video.removeEventListener('progress', onProgress, true)
            video.removeEventListener('canplaythrough', onCanPlayThrough, true)
            video.removeEventListener('error', onError, true)
            video.removeEventListener('abort', onError, true)
          }
          function onCanPlayThrough() {
            finish()
            const n = video.duration || 100
            progress.report(n, n)
            resolve({ element: video, offset: spec.videoOffset! })
          }
          function onError() {
            finish()
            console.warn('Cannot load video... Just skip it!')
            resolve(null)
          }
        })
      }
    )

    task('Game', null, ['Notechart'], async (notechart) => {
      return new Game([notechart], spec.options)
    })

    task(
      'GameDisplay',
      null,
      ['Game', 'SkinContext', 'Video'],
      async (game, context, video) => {
        return new GameDisplay({
          game,
          context,
          backgroundImagePromise: run('BackgroundImage'),
          video,
        })
      }
    )

    task('Samples', null, ['SamplingMaster', 'Game'], function (master, game) {
      keysoundCache.receiveSongId(songId)
      const samplesLoader = new SamplesLoader(assets, master)
      return samplesLoader.loadFiles(
        game.samples,
        audioLoadProgress,
        audioDecodeProgress
      )
    })

    task(
      'GameAudio',
      null,
      ['Game', 'Samples', 'SamplingMaster'],
      async (game, samples, master) => {
        return new GameAudio({ game, samples, master })
      }
    )

    task(
      'GameController',
      null,
      ['Game', 'GameDisplay', 'GameAudio'],
      async (game, display, audio) => {
        return new GameController({ game, display, audio })
      }
    )

    return run('GameController')
  })
}