Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(sounds): Add sound variants and resource pack support! #258

Merged
merged 2 commits into from
Jan 29, 2025
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions rsbuild.config.ts
Original file line number Diff line number Diff line change
@@ -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' })
2 changes: 1 addition & 1 deletion scripts/downloadSoundsMap.mjs
Original file line number Diff line number Diff line change
@@ -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')
92 changes: 65 additions & 27 deletions scripts/prepareSounds.mjs
Original file line number Diff line number Diff line change
@@ -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 changes: 109 additions & 0 deletions scripts/uploadSoundFiles.ts
Original file line number Diff line number Diff line change
@@ -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: "[email protected]"
};

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 changes: 67 additions & 0 deletions scripts/uploadSounds.ts
Original file line number Diff line number Diff line change
@@ -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: "[email protected]"
},
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);
});
60 changes: 57 additions & 3 deletions src/basicSounds.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
import { subscribeKey } from 'valtio/utils'
import { options } from './optionsStorage'
import { isCypress } from './standaloneUtils'
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 changes: 1 addition & 1 deletion src/connect.ts
Original file line number Diff line number Diff line change
@@ -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 = {
2 changes: 1 addition & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
@@ -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'
1 change: 1 addition & 0 deletions src/optionsStorage.ts
Original file line number Diff line number Diff line change
@@ -25,6 +25,7 @@ const defaultOptions = {
chatOpacityOpened: 100,
messagesLimit: 200,
volume: 50,
enableMusic: false,
// fov: 70,
fov: 75,
guiScale: 3,
2 changes: 1 addition & 1 deletion src/react/SoundMuffler.tsx
Original file line number Diff line number Diff line change
@@ -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'
9 changes: 5 additions & 4 deletions src/resourcePack.ts
Original file line number Diff line number Diff line change
@@ -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')
207 changes: 85 additions & 122 deletions src/soundSystem.ts → src/sounds/botSoundSystem.ts
Original file line number Diff line number Diff line change
@@ -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'

const globalObject = window as {
allSoundsMap?: Record<string, Record<string, string>>,
allSoundsVersionedMap?: Record<string, string[]>,
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'

let soundMap: SoundMap | undefined

const updateResourcePack = async () => {
if (!soundMap) return
soundMap.activeResourcePackBasePath = await getActiveResourcepackBasePath() ?? undefined
}

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) {
return
}

const allSoundsMajor = versionsMapToMajor(allSoundsMap)
const soundsMap = allSoundsMajor[versionToMajor(bot.version)] ?? Object.values(allSoundsMajor)[0]
let musicInterval: ReturnType<typeof setInterval> | null = null

if (!soundsMap || !miscUiState.gameLoaded || !loadedData.sounds) {
subscribeKey(miscUiState, 'gameLoaded', async () => {
if (!miscUiState.gameLoaded || !loadedData.sounds) {
stopMusicSystem()
soundMap?.quit()
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,102 +54,90 @@ 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) {
lastPlayedSounds.lastClientPlayed.shift()
}
}
}

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)) {
// movement happening
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 changes: 33 additions & 0 deletions src/sounds/musicSystem.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 347 additions & 0 deletions src/sounds/soundsMap.ts
Original file line number Diff line number Diff line change
@@ -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 changes: 8 additions & 0 deletions src/sounds/testSounds.ts
Original file line number Diff line number Diff line change
@@ -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'))

Unchanged files with check annotations Beta

viewer.addChunksBatchWaitTime = 0
viewer.world.blockstatesModels = blockstatesModels
viewer.entities.setDebugMode('basic')
viewer.setVersion(this.version)

Check warning on line 194 in prismarine-viewer/examples/baseScene.ts

GitHub Actions / build-and-deploy

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
viewer.entities.onSkinUpdate = () => {
viewer.render()
}
})
if (onlyCurrent) {
viewer.render()
onWorldUpdate()

Check warning on line 216 in prismarine-viewer/examples/scenes/main.ts

GitHub Actions / build-and-deploy

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
} else {
// will be called on every render update
viewer.world.renderUpdateEmitter.addListener('update', onWorldUpdate)
zip.file(`${blockName}.png`, dataUrl.split(',')[1], { base64: true })
if (onlyCurrent) {
end()

Check warning on line 251 in prismarine-viewer/examples/scenes/main.ts

GitHub Actions / build-and-deploy

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
} else {
nextBlock()

Check warning on line 253 in prismarine-viewer/examples/scenes/main.ts

GitHub Actions / build-and-deploy

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
}
const nextBlock = async () => {
if (blockName) {
updateBlock()
} else {
end()

Check warning on line 267 in prismarine-viewer/examples/scenes/main.ts

GitHub Actions / build-and-deploy

Promises must be awaited, end with a call to .catch, end with a call to .then with a rejection handler or be explicitly marked as ignored with the `void` operator
}
}
}
return getGeneralEntitiesMetadata(entity) as any
}
function addArmorModel (entityMesh: THREE.Object3D, slotType: string, item: Item, layer = 1, overlay = false) {

Check warning on line 779 in prismarine-viewer/viewer/lib/entities.ts

GitHub Actions / build-and-deploy

Function 'addArmorModel' has too many parameters (5). Maximum allowed is 4
if (!item) {
removeArmorModel(entityMesh, slotType)
return
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
}
function addCube(attr, boneId, bone, cube, texWidth = 64, texHeight = 64, mirror = false) {

Check warning on line 97 in prismarine-viewer/viewer/lib/entity/EntityMesh.js

GitHub Actions / build-and-deploy

Function 'addCube' has too many parameters (7). Maximum allowed is 4
const cubeRotation = new THREE.Euler(0, 0, 0)
if (cube.rotation) {
cubeRotation.x = -cube.rotation[0] * Math.PI / 180
return group
}
function setUVs (

Check warning on line 48 in prismarine-viewer/viewer/lib/hand.ts

GitHub Actions / build-and-deploy

Function 'setUVs' has too many parameters (8). Maximum allowed is 4
box: THREE.BoxGeometry,
u: number,
v: number,
uvAttr.needsUpdate = true
}
function setSkinUVs (box: THREE.BoxGeometry, u: number, v: number, width: number, height: number, depth: number): void {

Check warning on line 94 in prismarine-viewer/viewer/lib/hand.ts

GitHub Actions / build-and-deploy

Function 'setSkinUVs' has too many parameters (6). Maximum allowed is 4
setUVs(box, u, v, width, height, depth, 64, 64)
}
}))
}
function renderLiquid (world: World, cursor: Vec3, texture: any | undefined, type: number, biome: string, water: boolean, attr: Record<string, any>) {

Check warning on line 131 in prismarine-viewer/viewer/lib/mesher/models.ts

GitHub Actions / build-and-deploy

Function 'renderLiquid' has too many parameters (7). Maximum allowed is 4
const heights: number[] = []
for (let z = -1; z <= 1; z++) {
for (let x = -1; x <= 1; x++) {