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
Show file tree
Hide file tree
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
Expand Up @@ -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' })
Expand Down
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')
Expand Down
92 changes: 65 additions & 27 deletions scripts/prepareSounds.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand All @@ -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()
Expand All @@ -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)
Expand All @@ -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 = {
Expand All @@ -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) => {
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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 {
Expand Down Expand Up @@ -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({
Expand All @@ -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);
});
Loading
Loading