ModuleByExtension(extension)

      gameTrees = fileFormatModule.parseFile(filename, evt => {
        if (evt.progress - lastProgress < 0.1) return
        this.window.setProgressBar(evt.progress)
        lastProgress = evt.progress
      })

      if (gameTrees.length == 0) throw true
    } catch (err) {
      dialog.showMessageBox(t('This file is unreadable.'), 'warning')
      success = false
    }

    if (success) {
      await this.loadGameTrees(gameTrees, {
        suppressAskForSave: true,
        clearHistory
      })

      this.setState({representedFilename: filename})
      this.fileHash = this.generateFileHash()

      if (setting.get('game.goto_end_after_loading')) {
        this.goToEnd()
      }
    }

    this.setBusy(false)
  }

  async loadContent(content, extension, options = {}) {
    this.setBusy(true)

    let t = i18n.context('sabaki.file')
    let gameTrees = []
    let success = true
    let lastProgress = -1

    try {
      let fileFormatModule = fileformats.getModuleByExtension(extension)

      gameTrees = fileFormatModule.parse(content, evt => {
        if (evt.progress - lastProgress < 0.1) return
        this.window.setProgressBar(evt.progress)
        lastProgress = evt.progress
      })

      if (gameTrees.length == 0) throw true
    } catch (err) {
      dialog.showMessageBox(t('This file is unreadable.'), 'warning')
      success = false
    }

    if (success) {
      await this.loadGameTrees(gameTrees, options)
    }

    this.setBusy(false)
  }

  async loadGameTrees(
    gameTrees,
    {suppressAskForSave = false, clearHistory = true} = {}
  ) {
    if (!suppressAskForSave &&!this.askForSave()) return

    this.setBusy(true)
    if (this.state.openDrawer!== 'gamechooser') this.closeDrawer()
    this.setMode('play')

    await helper.wait(setting.get('app.loadgame_delay'))

    if (gameTrees.length > 0) {
      this.setState({
        representedFilename: null,
        gameIndex: 0,
        gameTrees,
        gameCurrents: gameTrees.map(_ => ({})),
        boardTransformation: ''
      })

      let [firstTree] = gameTrees
      this.setCurrentTreePosition(firstTree, firstTree.root.id, {
        clearCache: true
      })

      this.treeHash = this.generateTreeHash()
      this.fileHash = this.generateFileHash()

      if (clearHistory) this.clearHistory()
    }

    this.setBusy(false)
    this.window.setProgressBar(-1)
    this.events.emit('fileLoad')

    if (gameTrees.length > 1) {
      await helper.wait(setting.get('gamechooser.show_delay'))
      this.openDrawer('gamechooser')
    }
  }

  saveFile(filename = null, confirmExtension = true) {
    let t = i18n.context('sabaki.file')

    if (!filename || (confirmExtension && extname(filename)!== '.sgf')) {
      let cancel = false
      let result = dialog.showSaveDialog({
        filters: [
          fileformats.sgf.meta,
          {name: t('All Files'), extensions: ['*']}
        ]
      })

      if (result) this.saveFile(result, false)
      cancel =!result

      return!cancel
    }

    this.setBusy(true)
    fs.writeFileSync(filename, this.getSGF())

    this.setBusy(false)
    this.setState({representedFilename: filename})

    this.treeHash = this.generateTreeHash()
    this.fileHash = this.generateFileHash()

    return true
  }

  getSGF() {
    let {gameTrees} = this.state

    gameTrees = gameTrees.map(tree =>
      tree.mutate(draft => {
        draft.updateProperty(draft.root.id, 'AP', [
          `${this.appName}:${this.version}`
        ])
        draft.updateProperty(draft.root.id, 'CA', ['UTF-8'])
      })
    )

    this.setState({gameTrees})
    this.recordHistory()

    return sgf.stringify(
      gameTrees.map(tree => tree.root),
      {
        linebreak: setting.get('sgf.format_code')? helper.linebreak : ''
      }
    )
  }

  getBoardAscii() {
    let {boardTransformation} = this.state
    let tree = this.state.gameTrees[this.state.gameIndex]
    let board = gametree.getBoard(tree, this.state.treePosition)
    let signMap = gobantransformer.transformMap(
      board.signMap,
      boardTransformation
    )
    let markerMap = gobantransformer.transformMap(
      board.markers,
      boardTransformation
    )
    let lines = board.lines.map(l =>
      gobantransformer.transformLine(
        l,
        boardTransformation,
        board.width,
        board.height
      )
    )

    let height = signMap.length
    let width = height === 0? 0 : signMap[0].length
    let result = []
    let lb = helper.linebreak

    let getIndexFromVertex = ([x, y]) => {
      let rowLength = 4 + width * 2
      return rowLength + rowLength * y + 1 + x * 2 + 1
    }

    // Make empty board

    result.push('+')
    for (let x = 0; x < width; x++) result.push('-', '-')
    result.push('-', '+', lb)

    for (let y = 0; y < height; y++) {
      result.push('|')
      for (let x = 0; x < width; x++) result.push(' ', '.')
      result.push(' ', '|', lb)
    }

    result.push('+')
    for (let x = 0; x < width; x++) result.push('-', '-')
    result.push('-', '+', lb)

    for (let vertex of board.getHandicapPlacement(9)) {
      result[getIndexFromVertex(vertex)] = ','
    }

    // Place markers & stones

    let data = {
      plain: ['O', null, 'X'],
      circle: ['W', 'C', 'B'],
      square: ['@', 'S', '#'],
      triangle: ['Q', 'T', 'Y'],
      cross: ['P', 'M', 'Z'],
      label: ['O', null, 'X']
    }

    for (let x = 0; x < width; x++) {
      for (let y = 0; y < height; y++) {
        let i = getIndexFromVertex([x, y])
        let s = signMap[y][x]

        if (!markerMap[y][x] ||!(markerMap[y][x].type in data)) {
          if (s!== 0) result[i] = data.plain[s + 1]
        } else {
          let {type, label} = markerMap[y][x]

          if (type!== 'label' || s!== 0) {
            result[i] = data[type][s + 1]
          } else if (
            s === 0 &&
            label.length === 1 &&
            isNaN(parseFloat(label))
          ) {
            result[i] = label.toLowerCase()
          }
        }
      }
    }

    result = result.join('')

    // Add lines & arrows

    for (let {v1, v2, type} of lines) {
      result += `{${type === 'arrow'? 'AR' : 'LN'} ${board.stringifyVertex(
        v1
      )} ${board.stringifyVertex(v2)}}${lb}`
    