Skip to content

Commit

Permalink
Merge pull request #695 from dtinth/custom-folder
Browse files Browse the repository at this point in the history
  • Loading branch information
release-train[bot] committed Oct 9, 2021
2 parents fa5bc07 + c3e0989 commit 9993fe9
Show file tree
Hide file tree
Showing 20 changed files with 1,014 additions and 47 deletions.
5 changes: 5 additions & 0 deletions bemuse/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,9 @@
"@types/invariant": "^2.2.30",
"@types/minimatch": "^3.0.3",
"@types/react": "^16.9.2",
"@types/react-dom": "16",
"@types/webpack-env": "^1.14.0",
"@types/wicg-file-system-access": "^2020.9.4",
"@typescript-eslint/parser": "^2.0.0",
"autoprefixer": "^9.1.5",
"body-parser": "^1.18.3",
Expand Down Expand Up @@ -115,6 +117,7 @@
"debug": "^3.2.5",
"emotion": "^9.2.12",
"fastclick": "^1.0.6",
"idb-keyval": "^6.0.2",
"immutable": "^3.8.2",
"impure": "^1.0.0",
"invariant": "^2.2.4",
Expand All @@ -130,6 +133,7 @@
"mobx": "^5.13.1",
"mobx-react-lite": "^1.4.1",
"once": "^1.3.1",
"p-memoize": "4",
"pixi.js": "^4.1.0",
"power-assert": "^1.6.1",
"prop-types": "^15.6.2",
Expand All @@ -140,6 +144,7 @@
"react-fa": "^5.0.0",
"react-fns": "^1.4.0",
"react-hot-loader": "^4.12.14",
"react-query": "^3.25.1",
"react-redux": "^5.0.7",
"react-toggled": "^1.2.7",
"recompose": "^0.26.0",
Expand Down
3 changes: 2 additions & 1 deletion bemuse/src/app/game-launcher.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,12 +11,13 @@ import { getGrade } from 'bemuse/rules/grade'
import { isTitleDisplayMode } from 'bemuse/devtools/query-flags'
import { resolve as resolveUrl } from 'url'
import { unmuteAudio } from 'bemuse/sampling-master'
import { Song, Chart } from 'bemuse/collection-model/types'

import * as Analytics from './analytics'
import * as Options from './entities/Options'
import ResultScene from './ui/ResultScene'
import createAutoVelocity from './interactors/createAutoVelocity'
import { Song, Chart, StoredOptions } from './types'
import { StoredOptions } from './types'
import { LoadSpec } from 'bemuse/game/loaders/game-loader'
import Player from 'bemuse/game/player'
import PlayerState from 'bemuse/game/state/player-state'
Expand Down
13 changes: 13 additions & 0 deletions bemuse/src/app/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,10 @@ import {
getTimeSynchroServer,
} from './query-flags'
import { isBrowserSupported } from './browser-support'
import {
getSongsFromCustomFolders,
getDefaultCustomFolderContext,
} from 'bemuse/custom-folder'

/* eslint import/no-webpack-loader-syntax: off */
export const runIO = createRun({
Expand Down Expand Up @@ -57,6 +61,15 @@ function bootUp() {
text: getInitialGrepString(),
})
run(OptionsIO.loadInitialOptions())

getSongsFromCustomFolders(getDefaultCustomFolderContext()).then(songs => {
if (songs.length > 0) {
store.dispatch({
type: ReduxState.CUSTOM_SONGS_LOADED,
songs,
})
}
})
})
}

Expand Down
2 changes: 1 addition & 1 deletion bemuse/src/app/io/ioContext.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import createCollectionLoader from '../interactors/createCollectionLoader'
import findMatchingSong from '../interactors/findMatchingSong'
import store from '../redux/instance'
import { getInitiallySelectedSong } from '../query-flags'
import { loadSongFromResources } from '../song-loader'
import { loadSongFromResources } from '../../custom-song-loader'

// Configure a collection loader, which loads the Bemuse music collection.
const collectionLoader = createCollectionLoader({
Expand Down
2 changes: 2 additions & 0 deletions bemuse/src/app/redux/ReduxState.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ export const COLLECTION_LOADING_ERRORED = 'COLLECTION_LOADING_ERRORED'
export const COLLECTION_LOADED = 'COLLECTION_LOADED'
export const CUSTOM_SONG_LOAD_STARTED = 'CUSTOM_SONG_LOAD_STARTED'
export const CUSTOM_SONG_LOADED = 'CUSTOM_SONG_LOADED'
export const CUSTOM_SONGS_LOADED = 'CUSTOM_SONGS_LOADED'
export const MUSIC_SEARCH_TEXT_TYPED = 'MUSIC_SEARCH_TEXT_TYPED'
export const MUSIC_SEARCH_TEXT_INITIALIZED = 'MUSIC_SEARCH_TEXT_INITIALIZED'
export const MUSIC_SEARCH_DEBOUNCED = 'MUSIC_SEARCH_DEBOUNCED'
Expand Down Expand Up @@ -56,6 +57,7 @@ export const reducer = combineReducers({
}),
customSongs: createReducer([], {
[CUSTOM_SONG_LOADED]: action => state => [action.song],
[CUSTOM_SONGS_LOADED]: action => state => action.songs,
}),
currentCollection: createReducer('', {
[COLLECTION_LOADING_BEGAN]: action => state =>
Expand Down
32 changes: 0 additions & 32 deletions bemuse/src/app/types.ts
Original file line number Diff line number Diff line change
@@ -1,39 +1,7 @@
/* eslint camelcase: off */
/* REASON: These snake case names are used in our JSON files. */

import { IResources } from 'bemuse/resources/types'
import { PlayerOptionsPlacement } from 'bemuse/game/player'

export type Song = {
id: string
resources?: IResources
path: string
tutorial?: boolean
video_url?: string
video_file?: string
video_offset?: number
replaygain?: string
}

export type Chart = {
file: string
bpm: {
init: number
min: number
max: number
median: number
}
info: {
title: string
artist: string
genre: string
subtitles: string[]
subartists: string[]
difficulty: number
level: number
}
}

export type StoredOptions = {
'system.offset.audio-input': string
'player.P1.speed': string
Expand Down
39 changes: 39 additions & 0 deletions bemuse/src/collection-model/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/* eslint camelcase: off */
/* REASON: These snake case names are used in our JSON files. */

import { IResources } from 'bemuse/resources/types'

export type Song = {
id: string
title: string
path: string
tutorial?: boolean
video_url?: string
video_file?: string
video_offset?: number
replaygain?: string
charts: Chart[]

/** Added by Bemuse at runtime */
resources?: IResources
custom?: boolean
}

export type Chart = {
file: string
bpm: {
init: number
min: number
max: number
median: number
}
info: {
title: string
artist: string
genre: string
subtitles: string[]
subartists: string[]
difficulty: number
level: number
}
}
145 changes: 145 additions & 0 deletions bemuse/src/custom-folder/index.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,145 @@
import {
CustomFolderContext,
CustomFolderScanIO,
getCustomFolderState,
scanFolder,
setCustomFolder,
} from '.'
import { CustomFolderState } from './types'

const debugging = false

const mockFolderScanIO: CustomFolderScanIO = {
log: text => {
debugging && console.log('scanFolder: [log]', text)
},
setStatus: text => {
debugging && console.debug('scanFolder: [setStatus]', text)
},
updateState: () => {},
}

class CustomFolderContextMock implements CustomFolderContext {
private map = new Map<string, CustomFolderState>()
async get(key: string) {
return this.map.get(key)
}
async set(key: string, value: CustomFolderState) {
this.map.set(key, value)
}
async del(key: string) {
this.map.delete(key)
}
}

const song1: MockFolder = {
'normal.bms': '#TITLE meow [NORMAL]\n#BPM 90\n#00111:01',
'hyper.bms': '#TITLE meow [HYPER]\n#BPM 90\n#00111:01',
}

const song2: MockFolder = {
'normal.bms': '#TITLE nyan [NORMAL]\n#BPM 90\n#00111:01',
'hyper.bms': '#TITLE nyan [HYPER]\n#BPM 90\n#00111:01',
}

it('allows setting custom folder and loading it in-game', async () => {
const folder: MockFolder = { song1, song2 }
const tester = new CustomFolderTestHarness()
await tester.setFolder(folder)
await tester.checkState(async state => {
expect(state.chartFilesScanned).not.to.equal(true)
})

await tester.scan()
await tester.checkState(async state => {
expect(state.songs).to.have.length(2)
})
})

it('can scan for new songs', async () => {
const folder: MockFolder = { song1 }
const tester = new CustomFolderTestHarness()
await tester.setFolder(folder)
await tester.scan()
await tester.checkState(async state => {
expect(state.songs).to.have.length(1)
})

folder['song2'] = song2
await tester.scan()
await tester.checkState(async state => {
expect(state.songs).to.have.length(2)
})
})

class CustomFolderTestHarness {
private context = new CustomFolderContextMock()

constructor() {
this.context = new CustomFolderContextMock()
}

async setFolder(data: MockFolder) {
await setCustomFolder(
this.context,
createMockFileSystemDirectoryHandle(data)
)
}

async scan() {
await scanFolder(this.context, mockFolderScanIO)
}

async checkState(f: (state: CustomFolderState) => Promise<void>) {
const state = await getCustomFolderState(this.context)
void expect(state).not.to.equal(undefined)
await f(state!)
}
}

type MockFolder = {
[name: string]: MockFolder | string
}

function createMockFileSystemDirectoryHandle(
data: MockFolder
): FileSystemDirectoryHandle {
return {
kind: 'directory',
queryPermission: async () => 'granted',
[Symbol.asyncIterator]: async function*() {
for (const [name, value] of Object.entries(data)) {
if (typeof value === 'string') {
yield [name, createMockFileSystemFileHandle(name, value)] as const
} else {
yield [name, createMockFileSystemDirectoryHandle(value)] as const
}
}
},
getDirectoryHandle: async (name: string) => {
expect(data[name]).to.be.an('object')
return createMockFileSystemDirectoryHandle(data[name] as MockFolder)
},
getFileHandle: async (name: string) => {
expect(data[name]).to.be.a('string')
return createMockFileSystemFileHandle(name, data[name] as string)
},
} as any
}

function createMockFileSystemFileHandle(
name: string,
data: string
): FileSystemFileHandle {
return {
kind: 'file',
queryPermission: async () => 'granted',
getFile: async () => {
const blob = new Blob([data], { type: 'text/plain' })
return Object.assign(blob, {
name,
lastModified: 1,
})
},
} as any
}
Loading

0 comments on commit 9993fe9

Please sign in to comment.