feat(sounds): Add sound variants and resource pack support! (#258)
feat: add in-game music support! Enable it with `options.enableMusic = true`
This commit is contained in:
parent
a628f64d37
commit
df442338f8
15 changed files with 784 additions and 160 deletions
|
|
@ -103,6 +103,9 @@ const appConfig = defineConfig({
|
|||
configJson.defaultProxy = ':8080'
|
||||
}
|
||||
fs.writeFileSync('./dist/config.json', JSON.stringify({ ...configJson, ...configLocalJson }), 'utf8')
|
||||
if (fs.existsSync('./generated/sounds.js')) {
|
||||
fs.copyFileSync('./generated/sounds.js', './dist/sounds.js')
|
||||
}
|
||||
// childProcess.execSync('./scripts/prepareSounds.mjs', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genMcDataTypes.ts', { stdio: 'inherit' })
|
||||
// childProcess.execSync('tsx ./scripts/genPixelartTypes.ts', { stdio: 'inherit' })
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import fs from 'fs'
|
||||
|
||||
const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds.js'
|
||||
const url = 'https://github.com/zardoy/minecraft-web-client/raw/sounds-generated/sounds-v2.js'
|
||||
const savePath = 'dist/sounds.js'
|
||||
fetch(url).then(res => res.text()).then(data => {
|
||||
fs.writeFileSync(savePath, data, 'utf8')
|
||||
|
|
|
|||
|
|
@ -10,26 +10,31 @@ import { build } from 'esbuild'
|
|||
|
||||
const __dirname = path.dirname(fileURLToPath(new URL(import.meta.url)))
|
||||
|
||||
const targetedVersions = ['1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
|
||||
const targetedVersions = ['1.21.1', '1.20.6', '1.20.1', '1.19.2', '1.18.2', '1.17.1', '1.16.5', '1.15.2', '1.14.4', '1.13.2', '1.12.2', '1.11.2', '1.10.2', '1.9.4', '1.8.9']
|
||||
|
||||
/** @type {{name, size, hash}[]} */
|
||||
let prevSounds = null
|
||||
|
||||
const burgerDataUrl = (version) => `https://raw.githubusercontent.com/Pokechu22/Burger/gh-pages/${version}.json`
|
||||
const burgerDataPath = './generated/burger.json'
|
||||
const EXISTING_CACHE_PATH = './generated/existing-sounds-cache.json'
|
||||
|
||||
// const perVersionData: Record<string, { removed: string[],
|
||||
|
||||
const soundsPathVersionsRemap = {}
|
||||
|
||||
const downloadAllSounds = async () => {
|
||||
const downloadAllSoundsAndCreateMap = async () => {
|
||||
let existingSoundsCache = {}
|
||||
try {
|
||||
existingSoundsCache = JSON.parse(await fs.promises.readFile(EXISTING_CACHE_PATH, 'utf8'))
|
||||
} catch (err) {}
|
||||
const { versions } = await getVersionList()
|
||||
const lastVersion = versions.filter(version => !version.id.includes('w'))[0]
|
||||
// if (lastVersion.id !== targetedVersions[0]) throw new Error('last version is not the same as targetedVersions[0], update')
|
||||
for (const targetedVersion of targetedVersions) {
|
||||
const versionData = versions.find(x => x.id === targetedVersion)
|
||||
if (!versionData) throw new Error('no version data for ' + targetedVersion)
|
||||
console.log('Getting assets for version', targetedVersion)
|
||||
for (const version of targetedVersions) {
|
||||
const versionData = versions.find(x => x.id === version)
|
||||
if (!versionData) throw new Error('no version data for ' + version)
|
||||
console.log('Getting assets for version', version)
|
||||
const { assetIndex } = await fetch(versionData.url).then((r) => r.json())
|
||||
/** @type {{objects: {[a: string]: { size, hash }}}} */
|
||||
const index = await fetch(assetIndex.url).then((r) => r.json())
|
||||
|
|
@ -45,26 +50,30 @@ const downloadAllSounds = async () => {
|
|||
const changedSize = soundAssets.filter(x => prevSoundNames.has(x.name) && prevSounds.find(y => y.name === x.name).size !== x.size)
|
||||
console.log('changed size', changedSize.map(x => ({ name: x.name, prev: prevSounds.find(y => y.name === x.name).size, curr: x.size })))
|
||||
if (addedSounds.length || changedSize.length) {
|
||||
soundsPathVersionsRemap[targetedVersion] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
|
||||
soundsPathVersionsRemap[version] = [...addedSounds, ...changedSize].map(x => x.name.replace('minecraft/sounds/', '').replace('.ogg', ''))
|
||||
}
|
||||
if (addedSounds.length) {
|
||||
console.log('downloading new sounds for version', targetedVersion)
|
||||
downloadSounds(addedSounds, targetedVersion + '/')
|
||||
console.log('downloading new sounds for version', version)
|
||||
downloadSounds(version, addedSounds, version + '/')
|
||||
}
|
||||
if (changedSize.length) {
|
||||
console.log('downloading changed sounds for version', targetedVersion)
|
||||
downloadSounds(changedSize, targetedVersion + '/')
|
||||
console.log('downloading changed sounds for version', version)
|
||||
downloadSounds(version, changedSize, version + '/')
|
||||
}
|
||||
} else {
|
||||
console.log('downloading sounds for version', targetedVersion)
|
||||
downloadSounds(soundAssets)
|
||||
console.log('downloading sounds for version', version)
|
||||
downloadSounds(version, soundAssets)
|
||||
}
|
||||
prevSounds = soundAssets
|
||||
}
|
||||
async function downloadSound({ name, hash, size }, namePath, log) {
|
||||
const cached =
|
||||
!!namePath.replace('.ogg', '.mp3').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds) ||
|
||||
!!namePath.replace('.ogg', '.ogg').split('/').reduce((acc, cur) => acc?.[cur], existingSoundsCache.sounds)
|
||||
const savePath = path.resolve(`generated/sounds/${namePath}`)
|
||||
if (fs.existsSync(savePath)) {
|
||||
if (cached || fs.existsSync(savePath)) {
|
||||
// console.log('skipped', name)
|
||||
existingSoundsCache.sounds[namePath] = true
|
||||
return
|
||||
}
|
||||
log()
|
||||
|
|
@ -86,7 +95,12 @@ const downloadAllSounds = async () => {
|
|||
}
|
||||
writer.close()
|
||||
}
|
||||
async function downloadSounds(assets, addPath = '') {
|
||||
async function downloadSounds(version, assets, addPath = '') {
|
||||
if (addPath && existingSoundsCache.sounds[version]) {
|
||||
console.log('using existing sounds for version', version)
|
||||
return
|
||||
}
|
||||
console.log(version, 'have to download', assets.length, 'sounds')
|
||||
for (let i = 0; i < assets.length; i += 5) {
|
||||
await Promise.all(assets.slice(i, i + 5).map((asset, j) => downloadSound(asset, `${addPath}${asset.name}`, () => {
|
||||
console.log('downloading', addPath, asset.name, i + j, '/', assets.length)
|
||||
|
|
@ -95,6 +109,7 @@ const downloadAllSounds = async () => {
|
|||
}
|
||||
|
||||
fs.writeFileSync('./generated/soundsPathVersionsRemap.json', JSON.stringify(soundsPathVersionsRemap), 'utf8')
|
||||
fs.writeFileSync(EXISTING_CACHE_PATH, JSON.stringify(existingSoundsCache), 'utf8')
|
||||
}
|
||||
|
||||
const lightpackOverrideSounds = {
|
||||
|
|
@ -106,7 +121,8 @@ const lightpackOverrideSounds = {
|
|||
// this is not done yet, will be used to select only sounds for bundle (most important ones)
|
||||
const isSoundWhitelisted = (name) => name.startsWith('random/') || name.startsWith('note/') || name.endsWith('/say1') || name.endsWith('/death') || (name.startsWith('mob/') && name.endsWith('/step1')) || name.endsWith('/swoop1') || /* name.endsWith('/break1') || */ name.endsWith('dig/stone1')
|
||||
|
||||
const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // will be ffmpeg-static
|
||||
// const ffmpeg = 'C:/Users/Vitaly/Documents/LosslessCut-win-x64/resources/ffmpeg.exe' // can be ffmpeg-static
|
||||
const ffmpegExec = 'ffmpeg'
|
||||
const maintainBitrate = true
|
||||
|
||||
const scanFilesDeep = async (root, onOggFile) => {
|
||||
|
|
@ -127,7 +143,7 @@ const convertSounds = async () => {
|
|||
})
|
||||
|
||||
const convertSound = async (i) => {
|
||||
const proc = promisify(exec)(`${ffmpeg} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
|
||||
const proc = promisify(exec)(`${ffmpegExec} -i "${toConvert[i]}" -y -codec:a libmp3lame ${maintainBitrate ? '-qscale:a 2' : ''} "${toConvert[i].replace('.ogg', '.mp3')}"`)
|
||||
// pipe stdout to the console
|
||||
proc.child.stdout.pipe(process.stdout)
|
||||
await proc
|
||||
|
|
@ -147,8 +163,8 @@ const getSoundsMap = (burgerData) => {
|
|||
}
|
||||
|
||||
const writeSoundsMap = async () => {
|
||||
// const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
|
||||
// fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
|
||||
const burgerData = await fetch(burgerDataUrl(targetedVersions[0])).then((r) => r.json())
|
||||
fs.writeFileSync(burgerDataPath, JSON.stringify(burgerData[0].sounds), 'utf8')
|
||||
|
||||
const allSoundsMapOutput = {}
|
||||
let prevMap
|
||||
|
|
@ -174,16 +190,22 @@ const writeSoundsMap = async () => {
|
|||
// const includeSound = isSoundWhitelisted(firstName)
|
||||
// if (!includeSound) continue
|
||||
const mostUsedSound = sounds.sort((a, b) => b.weight - a.weight)[0]
|
||||
const targetSound = sounds[0]
|
||||
// outputMap[id] = { subtitle, sounds: mostUsedSound }
|
||||
// outputMap[id] = { subtitle, sounds }
|
||||
const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
|
||||
// const soundFilePath = `generated/sounds/minecraft/sounds/${targetSound.name}.mp3`
|
||||
// if (!fs.existsSync(soundFilePath)) {
|
||||
// console.warn('no sound file', targetSound.name)
|
||||
// continue
|
||||
// }
|
||||
let outputUseSoundLine = []
|
||||
const minWeight = sounds.reduce((acc, cur) => cur.weight ? Math.min(acc, cur.weight) : acc, sounds[0].weight ?? 1)
|
||||
if (isNaN(minWeight)) debugger
|
||||
for (const sound of sounds) {
|
||||
if (sound.weight && isNaN(sound.weight)) debugger
|
||||
outputUseSoundLine.push(`${sound.volume ?? 1};${sound.name};${sound.weight ?? minWeight}`)
|
||||
}
|
||||
const key = `${id};${name}`
|
||||
outputIdMap[key] = `${targetSound.volume ?? 1};${targetSound.name}`
|
||||
outputIdMap[key] = outputUseSoundLine.join(',')
|
||||
if (prevMap && prevMap[key]) {
|
||||
keysStats.same++
|
||||
} else {
|
||||
|
|
@ -221,7 +243,7 @@ const makeSoundsBundle = async () => {
|
|||
|
||||
const allSoundsMeta = {
|
||||
format: 'mp3',
|
||||
baseUrl: 'https://raw.githubusercontent.com/zardoy/minecraft-web-client/sounds-generated/sounds/'
|
||||
baseUrl: `https://raw.githubusercontent.com/${process.env.REPO_SLUG}/sounds/sounds/`
|
||||
}
|
||||
|
||||
await build({
|
||||
|
|
@ -235,9 +257,25 @@ const makeSoundsBundle = async () => {
|
|||
},
|
||||
metafile: true,
|
||||
})
|
||||
// copy also to generated/sounds.js
|
||||
fs.copyFileSync('./dist/sounds.js', './generated/sounds.js')
|
||||
}
|
||||
|
||||
// downloadAllSounds()
|
||||
// convertSounds()
|
||||
// writeSoundsMap()
|
||||
// makeSoundsBundle()
|
||||
const action = process.argv[2]
|
||||
if (action) {
|
||||
const execFn = {
|
||||
download: downloadAllSoundsAndCreateMap,
|
||||
convert: convertSounds,
|
||||
write: writeSoundsMap,
|
||||
bundle: makeSoundsBundle,
|
||||
}[action]
|
||||
|
||||
if (execFn) {
|
||||
execFn()
|
||||
}
|
||||
} else {
|
||||
// downloadAllSoundsAndCreateMap()
|
||||
// convertSounds()
|
||||
// writeSoundsMap()
|
||||
makeSoundsBundle()
|
||||
}
|
||||
|
|
|
|||
109
scripts/uploadSoundFiles.ts
Normal file
109
scripts/uploadSoundFiles.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
import fetch from 'node-fetch';
|
||||
import * as fs from 'fs';
|
||||
import * as path from 'path';
|
||||
import { glob } from 'glob';
|
||||
|
||||
// Git details
|
||||
const REPO_SLUG = process.env.REPO_SLUG;
|
||||
const owner = REPO_SLUG.split('/')[0];
|
||||
const repo = REPO_SLUG.split('/')[1];
|
||||
const branch = "sounds";
|
||||
|
||||
// GitHub token for authentication
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
// GitHub API endpoint
|
||||
const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
async function getShaForExistingFile(repoFilePath: string): Promise<string | null> {
|
||||
const url = `${baseUrl}/${repoFilePath}?ref=${branch}`;
|
||||
const response = await fetch(url, { headers });
|
||||
if (response.status === 404) {
|
||||
return null; // File does not exist
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
async function uploadFiles() {
|
||||
const commitMessage = "Upload multiple files via script";
|
||||
const committer = {
|
||||
name: "GitHub",
|
||||
email: "noreply@github.com"
|
||||
};
|
||||
|
||||
const filesToUpload = glob.sync("generated/sounds/**/*.mp3").map(localPath => {
|
||||
const repoPath = localPath.replace(/^generated\//, '');
|
||||
return { localPath, repoPath };
|
||||
});
|
||||
|
||||
const files = await Promise.all(filesToUpload.map(async file => {
|
||||
const content = fs.readFileSync(file.localPath, 'base64');
|
||||
const sha = await getShaForExistingFile(file.repoPath);
|
||||
return {
|
||||
path: file.repoPath,
|
||||
mode: "100644",
|
||||
type: "blob",
|
||||
sha: sha || undefined,
|
||||
content: content
|
||||
};
|
||||
}));
|
||||
|
||||
const treeResponse = await fetch(`${baseUrl}/git/trees`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
base_tree: null,
|
||||
tree: files
|
||||
})
|
||||
});
|
||||
|
||||
if (!treeResponse.ok) {
|
||||
throw new Error(`Failed to create tree: ${treeResponse.statusText}`);
|
||||
}
|
||||
|
||||
const treeData = await treeResponse.json();
|
||||
|
||||
const commitResponse = await fetch(`${baseUrl}/git/commits`, {
|
||||
method: 'POST',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
message: commitMessage,
|
||||
tree: treeData.sha,
|
||||
parents: [branch],
|
||||
committer: committer
|
||||
})
|
||||
});
|
||||
|
||||
if (!commitResponse.ok) {
|
||||
throw new Error(`Failed to create commit: ${commitResponse.statusText}`);
|
||||
}
|
||||
|
||||
const commitData = await commitResponse.json();
|
||||
|
||||
const updateRefResponse = await fetch(`${baseUrl}/git/refs/heads/${branch}`, {
|
||||
method: 'PATCH',
|
||||
headers: headers,
|
||||
body: JSON.stringify({
|
||||
sha: commitData.sha
|
||||
})
|
||||
});
|
||||
|
||||
if (!updateRefResponse.ok) {
|
||||
throw new Error(`Failed to update ref: ${updateRefResponse.statusText}`);
|
||||
}
|
||||
|
||||
console.log("Files uploaded successfully");
|
||||
}
|
||||
|
||||
uploadFiles().catch(error => {
|
||||
console.error("Error uploading files:", error);
|
||||
});
|
||||
67
scripts/uploadSounds.ts
Normal file
67
scripts/uploadSounds.ts
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
import fs from 'fs'
|
||||
|
||||
// GitHub details
|
||||
const owner = "zardoy";
|
||||
const repo = "minecraft-web-client";
|
||||
const branch = "sounds-generated";
|
||||
const filePath = "dist/sounds.js"; // Local file path
|
||||
const repoFilePath = "sounds-v2.js"; // Path in the repo
|
||||
|
||||
// GitHub token for authentication
|
||||
const token = process.env.GITHUB_TOKEN;
|
||||
|
||||
// GitHub API endpoint
|
||||
const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${repoFilePath}`;
|
||||
|
||||
const headers = {
|
||||
Authorization: `token ${token}`,
|
||||
'Content-Type': 'application/json'
|
||||
};
|
||||
|
||||
async function getShaForExistingFile(): Promise<string | null> {
|
||||
const url = `${baseUrl}?ref=${branch}`;
|
||||
const response = await fetch(url, { headers });
|
||||
if (response.status === 404) {
|
||||
return null; // File does not exist
|
||||
}
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to fetch ${url}: ${response.statusText}`);
|
||||
}
|
||||
const data = await response.json();
|
||||
return data.sha;
|
||||
}
|
||||
|
||||
async function uploadFile() {
|
||||
const content = fs.readFileSync(filePath, 'utf8');
|
||||
const base64Content = Buffer.from(content).toString('base64');
|
||||
const sha = await getShaForExistingFile();
|
||||
console.log('got sha')
|
||||
|
||||
const body = {
|
||||
message: "Update sounds.js",
|
||||
content: base64Content,
|
||||
branch: branch,
|
||||
committer: {
|
||||
name: "GitHub",
|
||||
email: "noreply@github.com"
|
||||
},
|
||||
sha: sha || undefined
|
||||
};
|
||||
|
||||
const response = await fetch(baseUrl, {
|
||||
method: 'PUT',
|
||||
headers: headers,
|
||||
body: JSON.stringify(body)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
throw new Error(`Failed to upload file: ${response.statusText}`);
|
||||
}
|
||||
|
||||
const responseData = await response.json();
|
||||
console.log("File uploaded successfully:", responseData);
|
||||
}
|
||||
|
||||
uploadFile().catch(error => {
|
||||
console.error("Error uploading file:", error);
|
||||
});
|
||||
|
|
@ -1,3 +1,4 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { options } from './optionsStorage'
|
||||
import { isCypress } from './standaloneUtils'
|
||||
import { reportWarningOnce } from './utils'
|
||||
|
|
@ -5,9 +6,14 @@ import { reportWarningOnce } from './utils'
|
|||
let audioContext: AudioContext
|
||||
const sounds: Record<string, any> = {}
|
||||
|
||||
// Track currently playing sounds and their gain nodes
|
||||
const activeSounds: Array<{ source: AudioBufferSourceNode; gainNode: GainNode; volumeMultiplier: number }> = []
|
||||
window.activeSounds = activeSounds
|
||||
|
||||
// load as many resources on page load as possible instead on demand as user can disable internet connection after he thinks the page is loaded
|
||||
const loadingSounds = [] as string[]
|
||||
const convertedSounds = [] as string[]
|
||||
|
||||
export async function loadSound (path: string, contents = path) {
|
||||
if (loadingSounds.includes(path)) return true
|
||||
loadingSounds.push(path)
|
||||
|
|
@ -24,15 +30,15 @@ export async function loadSound (path: string, contents = path) {
|
|||
loadingSounds.splice(loadingSounds.indexOf(path), 1)
|
||||
}
|
||||
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1) => {
|
||||
export const loadOrPlaySound = async (url, soundVolume = 1, loadTimeout = 500) => {
|
||||
const soundBuffer = sounds[url]
|
||||
if (!soundBuffer) {
|
||||
const start = Date.now()
|
||||
const cancelled = await loadSound(url)
|
||||
if (cancelled || Date.now() - start > 500) return
|
||||
if (cancelled || Date.now() - start > loadTimeout) return
|
||||
}
|
||||
|
||||
await playSound(url)
|
||||
return playSound(url, soundVolume)
|
||||
}
|
||||
|
||||
export async function playSound (url, soundVolume = 1) {
|
||||
|
|
@ -49,6 +55,7 @@ export async function playSound (url, soundVolume = 1) {
|
|||
|
||||
for (const [soundName, sound] of Object.entries(sounds)) {
|
||||
if (convertedSounds.includes(soundName)) continue
|
||||
// eslint-disable-next-line no-await-in-loop
|
||||
sounds[soundName] = await audioContext.decodeAudioData(sound)
|
||||
convertedSounds.push(soundName)
|
||||
}
|
||||
|
|
@ -66,4 +73,51 @@ export async function playSound (url, soundVolume = 1) {
|
|||
gainNode.connect(audioContext.destination)
|
||||
gainNode.gain.value = volume
|
||||
source.start(0)
|
||||
|
||||
// Add to active sounds
|
||||
activeSounds.push({ source, gainNode, volumeMultiplier: soundVolume })
|
||||
|
||||
const callbacks = [] as Array<() => void>
|
||||
source.onended = () => {
|
||||
// Remove from active sounds when finished
|
||||
const index = activeSounds.findIndex(s => s.source === source)
|
||||
if (index !== -1) activeSounds.splice(index, 1)
|
||||
|
||||
for (const callback of callbacks) {
|
||||
callback()
|
||||
}
|
||||
callbacks.length = 0
|
||||
}
|
||||
|
||||
return {
|
||||
onEnded (callback: () => void) {
|
||||
callbacks.push(callback)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
export function stopAllSounds () {
|
||||
for (const { source } of activeSounds) {
|
||||
try {
|
||||
source.stop()
|
||||
} catch (err) {
|
||||
console.warn('Failed to stop sound:', err)
|
||||
}
|
||||
}
|
||||
activeSounds.length = 0
|
||||
}
|
||||
|
||||
export function changeVolumeOfCurrentlyPlayingSounds (newVolume: number) {
|
||||
const normalizedVolume = newVolume / 100
|
||||
for (const { gainNode, volumeMultiplier } of activeSounds) {
|
||||
try {
|
||||
gainNode.gain.value = normalizedVolume * volumeMultiplier
|
||||
} catch (err) {
|
||||
console.warn('Failed to change sound volume:', err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
subscribeKey(options, 'volume', () => {
|
||||
changeVolumeOfCurrentlyPlayingSounds(options.volume)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ import { versionsByMinecraftVersion } from 'minecraft-data'
|
|||
import minecraftInitialDataJson from '../generated/minecraft-initial-data.json'
|
||||
import { AuthenticatedAccount } from './react/ServersListProvider'
|
||||
import { setLoadingScreenStatus } from './utils'
|
||||
import { downloadSoundsIfNeeded } from './soundSystem'
|
||||
import { downloadSoundsIfNeeded } from './sounds/botSoundSystem'
|
||||
import { miscUiState } from './globalState'
|
||||
|
||||
export type ConnectOptions = {
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@ import { fsState } from './loadSave'
|
|||
import { watchFov } from './rendererUtils'
|
||||
import { loadInMemorySave } from './react/SingleplayerProvider'
|
||||
|
||||
import { downloadSoundsIfNeeded } from './soundSystem'
|
||||
import { downloadSoundsIfNeeded } from './sounds/botSoundSystem'
|
||||
import { ua } from './react/utils'
|
||||
import { handleMovementStickDelta, joystickPointer } from './react/TouchAreasControls'
|
||||
import { possiblyHandleStateVariable } from './googledrive'
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ const defaultOptions = {
|
|||
chatOpacityOpened: 100,
|
||||
messagesLimit: 200,
|
||||
volume: 50,
|
||||
enableMusic: false,
|
||||
// fov: 70,
|
||||
fov: 75,
|
||||
guiScale: 3,
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { useState } from 'react'
|
||||
import { useSnapshot } from 'valtio'
|
||||
import { hideCurrentModal } from '../globalState'
|
||||
import { lastPlayedSounds } from '../soundSystem'
|
||||
import { lastPlayedSounds } from '../sounds/botSoundSystem'
|
||||
import { options } from '../optionsStorage'
|
||||
import Button from './Button'
|
||||
import Screen from './Screen'
|
||||
|
|
|
|||
|
|
@ -157,7 +157,7 @@ const getSizeFromImage = async (filePath: string) => {
|
|||
return probeImg.width
|
||||
}
|
||||
|
||||
export const getActiveTexturepackBasePath = async () => {
|
||||
export const getActiveResourcepackBasePath = async () => {
|
||||
if (await existsAsync('/resourcepack/pack.mcmeta')) {
|
||||
return '/resourcepack'
|
||||
}
|
||||
|
|
@ -198,7 +198,7 @@ const getFilesMapFromDir = async (dir: string) => {
|
|||
}
|
||||
|
||||
export const getResourcepackTiles = async (type: 'blocks' | 'items', existingTextures: string[]) => {
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
if (!basePath) return
|
||||
let firstTextureSize: number | undefined
|
||||
const namespaces = await fs.promises.readdir(join(basePath, 'assets'))
|
||||
|
|
@ -282,7 +282,7 @@ const prepareBlockstatesAndModels = async () => {
|
|||
viewer.world.customBlockStates = {}
|
||||
viewer.world.customModels = {}
|
||||
const usedTextures = new Set<string>()
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
if (!basePath) return
|
||||
if (appStatusState.status) {
|
||||
setLoadingScreenStatus('Reading resource pack blockstates and models')
|
||||
|
|
@ -361,6 +361,7 @@ export const onAppLoad = () => {
|
|||
customEvents.on('mineflayerBotCreated', () => {
|
||||
// todo also handle resourcePack
|
||||
const handleResourcePackRequest = async (packet) => {
|
||||
console.log('Received resource pack request', packet)
|
||||
if (options.serverResourcePacks === 'never') return
|
||||
const promptMessagePacket = ('promptMessage' in packet && packet.promptMessage) ? packet.promptMessage : undefined
|
||||
const promptMessageText = promptMessagePacket ? '' : 'Do you want to use server resource pack?'
|
||||
|
|
@ -397,7 +398,7 @@ export const onAppLoad = () => {
|
|||
}
|
||||
|
||||
const updateAllReplacableTextures = async () => {
|
||||
const basePath = await getActiveTexturepackBasePath()
|
||||
const basePath = await getActiveResourcepackBasePath()
|
||||
const setCustomCss = async (path: string | null, varName: string, repeat = 1) => {
|
||||
if (path && await existsAsync(path)) {
|
||||
const contents = await fs.promises.readFile(path, 'base64')
|
||||
|
|
|
|||
|
|
@ -1,50 +1,51 @@
|
|||
import { subscribeKey } from 'valtio/utils'
|
||||
import { Vec3 } from 'vec3'
|
||||
import { versionToMajor, versionToNumber, versionsMapToMajor } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import { versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
import { loadScript } from 'prismarine-viewer/viewer/lib/utils'
|
||||
import type { Block } from 'prismarine-block'
|
||||
import { miscUiState } from './globalState'
|
||||
import { options } from './optionsStorage'
|
||||
import { loadOrPlaySound } from './basicSounds'
|
||||
import { showNotification } from './react/NotificationProvider'
|
||||
import { subscribeKey } from 'valtio/utils'
|
||||
import { miscUiState } from '../globalState'
|
||||
import { options } from '../optionsStorage'
|
||||
import { loadOrPlaySound } from '../basicSounds'
|
||||
import { getActiveResourcepackBasePath, resourcePackState } from '../resourcePack'
|
||||
import { createSoundMap, SoundMap } from './soundsMap'
|
||||
import { musicSystem } from './musicSystem'
|
||||
|
||||
const globalObject = window as {
|
||||
allSoundsMap?: Record<string, Record<string, string>>,
|
||||
allSoundsVersionedMap?: Record<string, string[]>,
|
||||
let soundMap: SoundMap | undefined
|
||||
|
||||
const updateResourcePack = async () => {
|
||||
if (!soundMap) return
|
||||
soundMap.activeResourcePackBasePath = await getActiveResourcepackBasePath() ?? undefined
|
||||
}
|
||||
|
||||
let musicInterval: ReturnType<typeof setInterval> | null = null
|
||||
|
||||
subscribeKey(miscUiState, 'gameLoaded', async () => {
|
||||
if (!miscUiState.gameLoaded) return
|
||||
const soundsLegacyMap = window.allSoundsVersionedMap as Record<string, string[]>
|
||||
const { allSoundsMap } = globalObject
|
||||
const allSoundsMeta = window.allSoundsMeta as { format: string, baseUrl: string }
|
||||
if (!allSoundsMap) {
|
||||
if (!miscUiState.gameLoaded || !loadedData.sounds) {
|
||||
stopMusicSystem()
|
||||
soundMap?.quit()
|
||||
return
|
||||
}
|
||||
|
||||
const allSoundsMajor = versionsMapToMajor(allSoundsMap)
|
||||
const soundsMap = allSoundsMajor[versionToMajor(bot.version)] ?? Object.values(allSoundsMajor)[0]
|
||||
|
||||
if (!soundsMap || !miscUiState.gameLoaded || !loadedData.sounds) {
|
||||
return
|
||||
}
|
||||
|
||||
// const soundsPerId = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [+id.split(';')[0], sound]))
|
||||
const soundsPerName = Object.fromEntries(Object.entries(soundsMap).map(([id, sound]) => [id.split(';')[1], sound]))
|
||||
console.log(`Loading sounds for version ${bot.version}. Resourcepack state: ${JSON.stringify(resourcePackState)}`)
|
||||
soundMap = createSoundMap(bot.version) ?? undefined
|
||||
if (!soundMap) return
|
||||
void updateResourcePack()
|
||||
startMusicSystem()
|
||||
|
||||
const playGeneralSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
|
||||
if (!options.volume) return
|
||||
const soundStaticData = soundsPerName[soundKey]?.split(';')
|
||||
if (!soundStaticData) return
|
||||
const soundVolume = +soundStaticData[0]!
|
||||
const soundPath = soundStaticData[1]!
|
||||
const versionedSound = getVersionedSound(bot.version, soundPath, Object.entries(soundsLegacyMap))
|
||||
// todo test versionedSound
|
||||
const url = allSoundsMeta.baseUrl.replace(/\/$/, '') + (versionedSound ? `/${versionedSound}` : '') + '/minecraft/sounds/' + soundPath + '.' + allSoundsMeta.format
|
||||
const isMuted = options.mutedSounds.includes(soundKey) || options.mutedSounds.includes(soundPath) || options.volume === 0
|
||||
if (!options.volume || !soundMap) return
|
||||
const soundData = await soundMap.getSoundUrl(soundKey, volume)
|
||||
if (!soundData) return
|
||||
|
||||
const isMuted = options.mutedSounds.includes(soundKey) || options.volume === 0
|
||||
if (position) {
|
||||
if (!isMuted) {
|
||||
viewer.playSound(position, url, soundVolume * Math.max(Math.min(volume, 1), 0) * (options.volume / 100), Math.max(Math.min(pitch ?? 1, 2), 0.5))
|
||||
viewer.playSound(
|
||||
position,
|
||||
soundData.url,
|
||||
soundData.volume * (options.volume / 100),
|
||||
Math.max(Math.min(pitch ?? 1, 2), 0.5)
|
||||
)
|
||||
}
|
||||
if (getDistance(bot.entity.position, position) < 4 * 16) {
|
||||
lastPlayedSounds.lastServerPlayed[soundKey] ??= { count: 0, last: 0 }
|
||||
|
|
@ -53,7 +54,7 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
} else {
|
||||
if (!isMuted) {
|
||||
await loadOrPlaySound(url, volume)
|
||||
await loadOrPlaySound(soundData.url, volume)
|
||||
}
|
||||
lastPlayedSounds.lastClientPlayed.push(soundKey)
|
||||
if (lastPlayedSounds.lastClientPlayed.length > 10) {
|
||||
|
|
@ -61,84 +62,72 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
const musicStartCheck = async (force = false) => {
|
||||
if (!soundMap) return
|
||||
// 20% chance to start music
|
||||
if (Math.random() > 0.2 && !force && !options.enableMusic) return
|
||||
|
||||
const musicKeys = ['music.game']
|
||||
if (bot.game.gameMode === 'creative') {
|
||||
musicKeys.push('music.creative')
|
||||
}
|
||||
const randomMusicKey = musicKeys[Math.floor(Math.random() * musicKeys.length)]
|
||||
const soundData = await soundMap.getSoundUrl(randomMusicKey)
|
||||
if (!soundData) return
|
||||
await musicSystem.playMusic(soundData.url, soundData.volume)
|
||||
}
|
||||
|
||||
function startMusicSystem () {
|
||||
if (musicInterval) return
|
||||
musicInterval = setInterval(() => {
|
||||
void musicStartCheck()
|
||||
}, 10_000)
|
||||
}
|
||||
|
||||
window.forceStartMusic = () => {
|
||||
void musicStartCheck(true)
|
||||
}
|
||||
|
||||
|
||||
function stopMusicSystem () {
|
||||
if (musicInterval) {
|
||||
clearInterval(musicInterval)
|
||||
musicInterval = null
|
||||
}
|
||||
}
|
||||
|
||||
const playHardcodedSound = async (soundKey: string, position?: Vec3, volume = 1, pitch?: number) => {
|
||||
await playGeneralSound(soundKey, position, volume, pitch)
|
||||
}
|
||||
|
||||
bot.on('soundEffectHeard', async (soundId, position, volume, pitch) => {
|
||||
await playHardcodedSound(soundId, position, volume, pitch)
|
||||
})
|
||||
|
||||
bot.on('hardcodedSoundEffectHeard', async (soundIdNum, soundCategory, position, volume, pitch) => {
|
||||
const fixOffset = versionToNumber('1.20.4') === versionToNumber(bot.version) ? -1 : 0
|
||||
const soundKey = loadedData.sounds[soundIdNum + fixOffset]?.name
|
||||
if (soundKey === undefined) return
|
||||
await playGeneralSound(soundKey, position, volume, pitch)
|
||||
})
|
||||
// workaround as mineflayer doesn't support soundEvent
|
||||
|
||||
bot._client.on('sound_effect', async (packet) => {
|
||||
const soundResource = packet['soundEvent']?.resource as string | undefined
|
||||
if (packet.soundId !== 0 || !soundResource) return
|
||||
const pos = new Vec3(packet.x / 8, packet.y / 8, packet.z / 8)
|
||||
await playHardcodedSound(soundResource.replace('minecraft:', ''), pos, packet.volume, packet.pitch)
|
||||
})
|
||||
|
||||
bot.on('entityHurt', async (entity) => {
|
||||
if (entity.id === bot.entity.id) {
|
||||
await playHardcodedSound('entity.player.hurt')
|
||||
}
|
||||
})
|
||||
|
||||
const useBlockSound = (blockName: string, category: string, fallback: string) => {
|
||||
blockName = {
|
||||
// todo somehow generated, not full
|
||||
grass_block: 'grass',
|
||||
tall_grass: 'grass',
|
||||
fern: 'grass',
|
||||
large_fern: 'grass',
|
||||
dead_bush: 'grass',
|
||||
seagrass: 'grass',
|
||||
tall_seagrass: 'grass',
|
||||
kelp: 'grass',
|
||||
kelp_plant: 'grass',
|
||||
sugar_cane: 'grass',
|
||||
bamboo: 'grass',
|
||||
vine: 'grass',
|
||||
nether_sprouts: 'grass',
|
||||
nether_wart: 'grass',
|
||||
twisting_vines: 'grass',
|
||||
weeping_vines: 'grass',
|
||||
|
||||
cobblestone: 'stone',
|
||||
stone_bricks: 'stone',
|
||||
mossy_stone_bricks: 'stone',
|
||||
cracked_stone_bricks: 'stone',
|
||||
chiseled_stone_bricks: 'stone',
|
||||
stone_brick_slab: 'stone',
|
||||
stone_brick_stairs: 'stone',
|
||||
stone_brick_wall: 'stone',
|
||||
polished_granite: 'stone',
|
||||
}[blockName] ?? blockName
|
||||
const key = 'block.' + blockName + '.' + category
|
||||
return soundsPerName[key] ? key : fallback
|
||||
}
|
||||
|
||||
const getStepSound = (blockUnder: Block) => {
|
||||
// const soundsMap = globalObject.allSoundsMap?.[bot.version]
|
||||
// if (!soundsMap) return
|
||||
// let soundResult = 'block.stone.step'
|
||||
// for (const x of Object.keys(soundsMap).map(n => n.split(';')[1])) {
|
||||
// const match = /block\.(.+)\.step/.exec(x)
|
||||
// const block = match?.[1]
|
||||
// if (!block) continue
|
||||
// if (loadedData.blocksByName[block]?.name === blockUnder.name) {
|
||||
// soundResult = x
|
||||
// break
|
||||
// }
|
||||
// }
|
||||
return useBlockSound(blockUnder.name, 'step', 'block.stone.step')
|
||||
}
|
||||
|
||||
let lastStepSound = 0
|
||||
const movementHappening = async () => {
|
||||
if (!bot.player) return // no info yet
|
||||
if (!bot.player || !soundMap) return // no info yet
|
||||
const VELOCITY_THRESHOLD = 0.1
|
||||
const { x, z, y } = bot.player.entity.velocity
|
||||
if (bot.entity.onGround && Math.abs(x) < VELOCITY_THRESHOLD && (Math.abs(z) > VELOCITY_THRESHOLD || Math.abs(y) > VELOCITY_THRESHOLD)) {
|
||||
|
|
@ -146,9 +135,9 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
if (Date.now() - lastStepSound > 300) {
|
||||
const blockUnder = bot.world.getBlock(bot.entity.position.offset(0, -1, 0))
|
||||
if (blockUnder) {
|
||||
const stepSound = getStepSound(blockUnder)
|
||||
const stepSound = soundMap.getStepSound(blockUnder.name)
|
||||
if (stepSound) {
|
||||
await playHardcodedSound(stepSound, undefined, 0.6)// todo not sure why 0.6
|
||||
await playHardcodedSound(stepSound, undefined, 0.6)
|
||||
lastStepSound = Date.now()
|
||||
}
|
||||
}
|
||||
|
|
@ -157,8 +146,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
const playBlockBreak = async (blockName: string, position?: Vec3) => {
|
||||
const sound = useBlockSound(blockName, 'break', 'block.stone.break')
|
||||
|
||||
if (!soundMap) return
|
||||
const sound = soundMap.getBreakSound(blockName)
|
||||
await playHardcodedSound(sound, position, 0.6, 1)
|
||||
}
|
||||
|
||||
|
|
@ -200,8 +189,8 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
if (effectId === 1010) {
|
||||
console.log('play record', data)
|
||||
}
|
||||
// todo add support for all current world events
|
||||
})
|
||||
|
||||
let diggingBlock: Block | null = null
|
||||
customEvents.on('digStart', () => {
|
||||
diggingBlock = bot.blockAtCursor(5)
|
||||
|
|
@ -214,40 +203,14 @@ subscribeKey(miscUiState, 'gameLoaded', async () => {
|
|||
}
|
||||
|
||||
registerEvents()
|
||||
|
||||
// 1.20+ soundEffectHeard is broken atm
|
||||
// bot._client.on('packet', (data, { name }, buffer) => {
|
||||
// if (name === 'sound_effect') {
|
||||
// console.log(data, buffer)
|
||||
// }
|
||||
// })
|
||||
})
|
||||
|
||||
// todo
|
||||
// const music = {
|
||||
// activated: false,
|
||||
// playing: '',
|
||||
// activate () {
|
||||
// const gameMusic = Object.entries(globalObject.allSoundsMap?.[bot.version] ?? {}).find(([id, sound]) => sound.includes('music.game'))
|
||||
// if (!gameMusic) return
|
||||
// const soundPath = gameMusic[0].split(';')[1]
|
||||
// const next = () => {}
|
||||
// }
|
||||
// }
|
||||
|
||||
const getVersionedSound = (version: string, item: string, itemsMapSortedEntries: Array<[string, string[]]>) => {
|
||||
const verNumber = versionToNumber(version)
|
||||
for (const [itemsVer, items] of itemsMapSortedEntries) {
|
||||
// 1.18 < 1.18.1
|
||||
// 1.13 < 1.13.2
|
||||
if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
|
||||
return itemsVer
|
||||
}
|
||||
}
|
||||
}
|
||||
subscribeKey(resourcePackState, 'resourcePackInstalled', async () => {
|
||||
await updateResourcePack()
|
||||
})
|
||||
|
||||
export const downloadSoundsIfNeeded = async () => {
|
||||
if (!globalObject.allSoundsMap) {
|
||||
if (!window.allSoundsMap) {
|
||||
try {
|
||||
await loadScript('./sounds.js')
|
||||
} catch (err) {
|
||||
33
src/sounds/musicSystem.ts
Normal file
33
src/sounds/musicSystem.ts
Normal file
|
|
@ -0,0 +1,33 @@
|
|||
import { loadOrPlaySound } from '../basicSounds'
|
||||
import { options } from '../optionsStorage'
|
||||
|
||||
class MusicSystem {
|
||||
private currentMusic: string | null = null
|
||||
|
||||
async playMusic (url: string, musicVolume = 1) {
|
||||
if (!options.enableMusic || this.currentMusic) return
|
||||
|
||||
try {
|
||||
const { onEnded } = await loadOrPlaySound(url, 0.5 * musicVolume, 5000) ?? {}
|
||||
|
||||
if (!onEnded) return
|
||||
|
||||
this.currentMusic = url
|
||||
|
||||
onEnded(() => {
|
||||
this.currentMusic = null
|
||||
})
|
||||
} catch (err) {
|
||||
console.warn('Failed to play music:', err)
|
||||
this.currentMusic = null
|
||||
}
|
||||
}
|
||||
|
||||
stopMusic () {
|
||||
if (this.currentMusic) {
|
||||
this.currentMusic = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const musicSystem = new MusicSystem()
|
||||
347
src/sounds/soundsMap.ts
Normal file
347
src/sounds/soundsMap.ts
Normal file
|
|
@ -0,0 +1,347 @@
|
|||
import fs from 'fs'
|
||||
import path from 'path'
|
||||
import { versionsMapToMajor, versionToMajor, versionToNumber } from 'prismarine-viewer/viewer/prepare/utils'
|
||||
|
||||
import { stopAllSounds } from '../basicSounds'
|
||||
import { musicSystem } from './musicSystem'
|
||||
|
||||
interface SoundMeta {
|
||||
format: string
|
||||
baseUrl: string
|
||||
}
|
||||
|
||||
interface SoundData {
|
||||
volume: number
|
||||
path: string
|
||||
}
|
||||
|
||||
interface SoundMapData {
|
||||
allSoundsMap: Record<string, Record<string, string>>
|
||||
soundsLegacyMap: Record<string, string[]>
|
||||
soundsMeta: SoundMeta
|
||||
}
|
||||
|
||||
interface BlockSoundMap {
|
||||
[blockName: string]: string
|
||||
}
|
||||
|
||||
interface SoundEntry {
|
||||
file: string
|
||||
weight: number
|
||||
volume: number
|
||||
}
|
||||
|
||||
export class SoundMap {
|
||||
private readonly soundsPerName: Record<string, SoundEntry[]>
|
||||
private readonly existingResourcePackPaths: Set<string>
|
||||
public activeResourcePackBasePath: string | undefined
|
||||
|
||||
constructor (
|
||||
private readonly soundData: SoundMapData,
|
||||
private readonly version: string
|
||||
) {
|
||||
const allSoundsMajor = versionsMapToMajor(soundData.allSoundsMap)
|
||||
const soundsMap = allSoundsMajor[versionToMajor(version)] ?? Object.values(allSoundsMajor)[0]
|
||||
this.soundsPerName = Object.fromEntries(
|
||||
Object.entries(soundsMap).map(([id, soundsStr]) => {
|
||||
const sounds = soundsStr.split(',').map(s => {
|
||||
const [volume, name, weight] = s.split(';')
|
||||
if (isNaN(Number(volume))) throw new Error('volume is not a number')
|
||||
if (isNaN(Number(weight))) {
|
||||
// debugger
|
||||
throw new TypeError('weight is not a number')
|
||||
}
|
||||
return {
|
||||
file: name,
|
||||
weight: Number(weight),
|
||||
volume: Number(volume)
|
||||
}
|
||||
})
|
||||
return [id.split(';')[1], sounds]
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
async updateExistingResourcePackPaths () {
|
||||
if (!this.activeResourcePackBasePath) return
|
||||
// todo support sounds.js from resource pack
|
||||
const soundsBasePath = path.join(this.activeResourcePackBasePath, 'assets/minecraft/sounds')
|
||||
// scan recursively for sounds files
|
||||
const scan = async (dir: string) => {
|
||||
const entries = await fs.promises.readdir(dir, { withFileTypes: true })
|
||||
for (const entry of entries) {
|
||||
const entryPath = path.join(dir, entry.name)
|
||||
if (entry.isDirectory()) {
|
||||
await scan(entryPath)
|
||||
} else if (entry.isFile() && entry.name.endsWith('.ogg')) {
|
||||
const relativePath = path.relative(soundsBasePath, entryPath)
|
||||
this.existingResourcePackPaths.add(relativePath)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await scan(soundsBasePath)
|
||||
}
|
||||
|
||||
async getSoundUrl (soundKey: string, volume = 1): Promise<{ url: string; volume: number } | undefined> {
|
||||
const sounds = this.soundsPerName[soundKey]
|
||||
if (!sounds?.length) return undefined
|
||||
|
||||
// Pick a random sound based on weights
|
||||
const totalWeight = sounds.reduce((sum, s) => sum + s.weight, 0)
|
||||
let random = Math.random() * totalWeight
|
||||
const sound = sounds.find(s => {
|
||||
random -= s.weight
|
||||
return random <= 0
|
||||
}) ?? sounds[0]
|
||||
|
||||
const versionedSound = this.getVersionedSound(sound.file)
|
||||
|
||||
let url = this.soundData.soundsMeta.baseUrl.replace(/\/$/, '') +
|
||||
(versionedSound ? `/${versionedSound}` : '') +
|
||||
'/minecraft/sounds/' +
|
||||
sound.file +
|
||||
'.' +
|
||||
this.soundData.soundsMeta.format
|
||||
|
||||
// Try loading from resource pack file first
|
||||
if (this.activeResourcePackBasePath) {
|
||||
const tryFormat = async (format: string) => {
|
||||
try {
|
||||
const resourcePackPath = path.join(this.activeResourcePackBasePath!, `/assets/minecraft/sounds/${sound.file}.${format}`)
|
||||
const fileData = await fs.promises.readFile(resourcePackPath)
|
||||
url = `data:audio/${format};base64,${fileData.toString('base64')}`
|
||||
return true
|
||||
} catch (err) {
|
||||
}
|
||||
}
|
||||
const success = await tryFormat(this.soundData.soundsMeta.format)
|
||||
if (!success && this.soundData.soundsMeta.format !== 'ogg') {
|
||||
await tryFormat('ogg')
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
url,
|
||||
volume: sound.volume * Math.max(Math.min(volume, 1), 0)
|
||||
}
|
||||
}
|
||||
|
||||
private getVersionedSound (item: string): string | undefined {
|
||||
const verNumber = versionToNumber(this.version)
|
||||
const entries = Object.entries(this.soundData.soundsLegacyMap)
|
||||
for (const [itemsVer, items] of entries) {
|
||||
if (items.includes(item) && verNumber <= versionToNumber(itemsVer)) {
|
||||
return itemsVer
|
||||
}
|
||||
}
|
||||
return undefined
|
||||
}
|
||||
|
||||
getBlockSound (blockName: string, category: string, fallback: string): string {
|
||||
const mappedName = blockSoundAliases[blockName] ?? blockName
|
||||
const key = `block.${mappedName}.${category}`
|
||||
return this.soundsPerName[key] ? key : fallback
|
||||
}
|
||||
|
||||
getStepSound (blockName: string): string {
|
||||
return this.getBlockSound(blockName, 'step', 'block.stone.step')
|
||||
}
|
||||
|
||||
getBreakSound (blockName: string): string {
|
||||
return this.getBlockSound(blockName, 'break', 'block.stone.break')
|
||||
}
|
||||
|
||||
quit () {
|
||||
musicSystem.stopMusic()
|
||||
stopAllSounds()
|
||||
}
|
||||
}
|
||||
|
||||
export function createSoundMap (version: string): SoundMap | null {
|
||||
const globalObject = window as {
|
||||
allSoundsMap?: Record<string, Record<string, string>>,
|
||||
allSoundsVersionedMap?: Record<string, string[]>,
|
||||
allSoundsMeta?: { format: string, baseUrl: string }
|
||||
}
|
||||
if (!globalObject.allSoundsMap) return null
|
||||
return new SoundMap({
|
||||
allSoundsMap: globalObject.allSoundsMap,
|
||||
soundsLegacyMap: globalObject.allSoundsVersionedMap ?? {},
|
||||
soundsMeta: globalObject.allSoundsMeta!
|
||||
}, version)
|
||||
}
|
||||
|
||||
// Block name mappings for sound effects
|
||||
const blockSoundAliases: BlockSoundMap = {
|
||||
// Grass-like blocks
|
||||
grass_block: 'grass',
|
||||
tall_grass: 'grass',
|
||||
fern: 'grass',
|
||||
large_fern: 'grass',
|
||||
dead_bush: 'grass',
|
||||
seagrass: 'grass',
|
||||
tall_seagrass: 'grass',
|
||||
kelp: 'grass',
|
||||
kelp_plant: 'grass',
|
||||
sugar_cane: 'grass',
|
||||
bamboo: 'grass',
|
||||
vine: 'grass',
|
||||
nether_sprouts: 'grass',
|
||||
nether_wart: 'grass',
|
||||
twisting_vines: 'grass',
|
||||
weeping_vines: 'grass',
|
||||
sweet_berry_bush: 'grass',
|
||||
glow_lichen: 'grass',
|
||||
moss_carpet: 'grass',
|
||||
moss_block: 'grass',
|
||||
hanging_roots: 'grass',
|
||||
spore_blossom: 'grass',
|
||||
small_dripleaf: 'grass',
|
||||
big_dripleaf: 'grass',
|
||||
flowering_azalea: 'grass',
|
||||
azalea: 'grass',
|
||||
azalea_leaves: 'grass',
|
||||
flowering_azalea_leaves: 'grass',
|
||||
|
||||
// Stone-like blocks
|
||||
cobblestone: 'stone',
|
||||
stone_bricks: 'stone',
|
||||
mossy_stone_bricks: 'stone',
|
||||
cracked_stone_bricks: 'stone',
|
||||
chiseled_stone_bricks: 'stone',
|
||||
stone_brick_slab: 'stone',
|
||||
stone_brick_stairs: 'stone',
|
||||
stone_brick_wall: 'stone',
|
||||
polished_granite: 'stone',
|
||||
granite: 'stone',
|
||||
andesite: 'stone',
|
||||
diorite: 'stone',
|
||||
polished_andesite: 'stone',
|
||||
polished_diorite: 'stone',
|
||||
deepslate: 'deepslate',
|
||||
cobbled_deepslate: 'deepslate',
|
||||
polished_deepslate: 'deepslate',
|
||||
deepslate_bricks: 'deepslate_bricks',
|
||||
deepslate_tiles: 'deepslate_tiles',
|
||||
calcite: 'stone',
|
||||
tuff: 'stone',
|
||||
smooth_stone: 'stone',
|
||||
smooth_sandstone: 'stone',
|
||||
smooth_quartz: 'stone',
|
||||
smooth_red_sandstone: 'stone',
|
||||
|
||||
// Wood-like blocks
|
||||
oak_planks: 'wood',
|
||||
spruce_planks: 'wood',
|
||||
birch_planks: 'wood',
|
||||
jungle_planks: 'wood',
|
||||
acacia_planks: 'wood',
|
||||
dark_oak_planks: 'wood',
|
||||
crimson_planks: 'wood',
|
||||
warped_planks: 'wood',
|
||||
oak_log: 'wood',
|
||||
spruce_log: 'wood',
|
||||
birch_log: 'wood',
|
||||
jungle_log: 'wood',
|
||||
acacia_log: 'wood',
|
||||
dark_oak_log: 'wood',
|
||||
crimson_stem: 'stem',
|
||||
warped_stem: 'stem',
|
||||
|
||||
// Metal blocks
|
||||
iron_block: 'metal',
|
||||
gold_block: 'metal',
|
||||
copper_block: 'copper',
|
||||
exposed_copper: 'copper',
|
||||
weathered_copper: 'copper',
|
||||
oxidized_copper: 'copper',
|
||||
netherite_block: 'netherite_block',
|
||||
ancient_debris: 'ancient_debris',
|
||||
lodestone: 'lodestone',
|
||||
chain: 'chain',
|
||||
anvil: 'anvil',
|
||||
chipped_anvil: 'anvil',
|
||||
damaged_anvil: 'anvil',
|
||||
|
||||
// Glass blocks
|
||||
glass: 'glass',
|
||||
glass_pane: 'glass',
|
||||
white_stained_glass: 'glass',
|
||||
orange_stained_glass: 'glass',
|
||||
magenta_stained_glass: 'glass',
|
||||
light_blue_stained_glass: 'glass',
|
||||
yellow_stained_glass: 'glass',
|
||||
lime_stained_glass: 'glass',
|
||||
pink_stained_glass: 'glass',
|
||||
gray_stained_glass: 'glass',
|
||||
light_gray_stained_glass: 'glass',
|
||||
cyan_stained_glass: 'glass',
|
||||
purple_stained_glass: 'glass',
|
||||
blue_stained_glass: 'glass',
|
||||
brown_stained_glass: 'glass',
|
||||
green_stained_glass: 'glass',
|
||||
red_stained_glass: 'glass',
|
||||
black_stained_glass: 'glass',
|
||||
tinted_glass: 'glass',
|
||||
|
||||
// Wool blocks
|
||||
white_wool: 'wool',
|
||||
orange_wool: 'wool',
|
||||
magenta_wool: 'wool',
|
||||
light_blue_wool: 'wool',
|
||||
yellow_wool: 'wool',
|
||||
lime_wool: 'wool',
|
||||
pink_wool: 'wool',
|
||||
gray_wool: 'wool',
|
||||
light_gray_wool: 'wool',
|
||||
cyan_wool: 'wool',
|
||||
purple_wool: 'wool',
|
||||
blue_wool: 'wool',
|
||||
brown_wool: 'wool',
|
||||
green_wool: 'wool',
|
||||
red_wool: 'wool',
|
||||
black_wool: 'wool',
|
||||
|
||||
// Nether blocks
|
||||
netherrack: 'netherrack',
|
||||
nether_bricks: 'nether_bricks',
|
||||
red_nether_bricks: 'nether_bricks',
|
||||
nether_wart_block: 'wart_block',
|
||||
warped_wart_block: 'wart_block',
|
||||
soul_sand: 'soul_sand',
|
||||
soul_soil: 'soul_soil',
|
||||
basalt: 'basalt',
|
||||
polished_basalt: 'basalt',
|
||||
blackstone: 'gilded_blackstone',
|
||||
gilded_blackstone: 'gilded_blackstone',
|
||||
|
||||
// Amethyst blocks
|
||||
amethyst_block: 'amethyst_block',
|
||||
amethyst_cluster: 'amethyst_cluster',
|
||||
large_amethyst_bud: 'large_amethyst_bud',
|
||||
medium_amethyst_bud: 'medium_amethyst_bud',
|
||||
small_amethyst_bud: 'small_amethyst_bud',
|
||||
|
||||
// Miscellaneous
|
||||
sand: 'sand',
|
||||
red_sand: 'sand',
|
||||
gravel: 'gravel',
|
||||
snow: 'snow',
|
||||
snow_block: 'snow',
|
||||
powder_snow: 'powder_snow',
|
||||
ice: 'glass',
|
||||
packed_ice: 'glass',
|
||||
blue_ice: 'glass',
|
||||
slime_block: 'slime_block',
|
||||
honey_block: 'honey_block',
|
||||
scaffolding: 'scaffolding',
|
||||
ladder: 'ladder',
|
||||
lantern: 'lantern',
|
||||
soul_lantern: 'lantern',
|
||||
pointed_dripstone: 'pointed_dripstone',
|
||||
dripstone_block: 'dripstone_block',
|
||||
rooted_dirt: 'rooted_dirt',
|
||||
sculk_sensor: 'sculk_sensor',
|
||||
shroomlight: 'shroomlight'
|
||||
}
|
||||
8
src/sounds/testSounds.ts
Normal file
8
src/sounds/testSounds.ts
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
import { createSoundMap } from './soundsMap'
|
||||
|
||||
//@ts-expect-error
|
||||
globalThis.window = {}
|
||||
require('../../generated/sounds.js')
|
||||
|
||||
const soundMap = createSoundMap('1.20.1')
|
||||
console.log(soundMap?.getSoundUrl('ambient.cave'))
|
||||
Loading…
Add table
Add a link
Reference in a new issue