* feat: migrate top turbo * ci: ci test * fix: fix codeql issues * feat: ci test * chore: lint * chore: misc changes * feat: rename vite helpers * feat: use fetch to handle assets * feat: update directory * feat: fetch charword table * feat: migrate download game data and detect missing voice files * feat: symlink relative path * feat: finish wrangler upload * feat: migrate wrangler download * feat: finish * chore: auto update * ci: update ci * ci: update ci --------- Co-authored-by: Halyul <Halyul@users.noreply.github.com>
268 lines
9.7 KiB
JavaScript
268 lines
9.7 KiB
JavaScript
import path from 'node:path'
|
|
import { file } from '@aklive2d/libs'
|
|
import { githubDownload } from '@aklive2d/downloader'
|
|
import config from '@aklive2d/config'
|
|
import operators, {
|
|
getOperatorId,
|
|
getOperatorAlternativeId,
|
|
OPERATOR_SOURCE_FOLDER,
|
|
} from '@aklive2d/operator'
|
|
|
|
// zh_TW uses an older version of charword_table.json
|
|
// zh_TW is removed
|
|
const REGIONS = ['zh_CN', 'en_US', 'ja_JP', 'ko_KR']
|
|
const REGION_URLS = {
|
|
zh_CN: 'Kengxxiao/ArknightsGameData',
|
|
en_US: 'Kengxxiao/ArknightsGameData_YoStar',
|
|
ja_JP: 'Kengxxiao/ArknightsGameData_YoStar',
|
|
ko_KR: 'Kengxxiao/ArknightsGameData_YoStar',
|
|
}
|
|
const DEFAULT_REGION = REGIONS[0]
|
|
export const defaultRegion = DEFAULT_REGION.replace('_', '-')
|
|
const NICKNAME = {
|
|
zh_CN: '博士',
|
|
en_US: 'Doctor',
|
|
ja_JP: 'ドクター',
|
|
ko_KR: '박사',
|
|
zh_TW: '博士',
|
|
}
|
|
|
|
const OPERATOR_IDS = Object.values(operators).map((operator) => {
|
|
return getOperatorId(operator.filename)
|
|
})
|
|
const AUTO_UPDATE_FOLDER = path.resolve(
|
|
import.meta.dirname,
|
|
config.dir_name.auto_update
|
|
)
|
|
const CHARWORD_TABLE_FILE = path.resolve(
|
|
AUTO_UPDATE_FOLDER,
|
|
config.module.charword_table.charword_table_json
|
|
)
|
|
const CHARWORD_TABLE = JSON.parse(file.readSync(CHARWORD_TABLE_FILE)) || {}
|
|
const DIST_DIR = path.resolve(import.meta.dirname, config.dir_name.dist)
|
|
|
|
export const lookup = (operatorName) => {
|
|
const operatorId = getOperatorId(operators[operatorName].filename)
|
|
const operatorBlock = CHARWORD_TABLE[operatorId]
|
|
return operatorBlock.ref
|
|
? CHARWORD_TABLE[operatorBlock.alternativeId]
|
|
: operatorBlock
|
|
}
|
|
|
|
const getDistDir = (name) => {
|
|
return path.join(
|
|
DIST_DIR,
|
|
name,
|
|
config.module.charword_table.charword_table_json
|
|
)
|
|
}
|
|
|
|
export const getLangs = (name, voiceJson = null) => {
|
|
voiceJson = voiceJson
|
|
? voiceJson
|
|
: JSON.parse(file.readSync(getDistDir(name)))
|
|
const voiceLangs = Object.keys(voiceJson.voiceLangs['zh-CN'])
|
|
const subtitleLangs = Object.keys(voiceJson.subtitleLangs)
|
|
return { voiceLangs, subtitleLangs }
|
|
}
|
|
|
|
export const build = async (namesToBuild) => {
|
|
const err = []
|
|
const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild
|
|
console.log('Generating charword_table for', names.length, 'operators')
|
|
await updateFn(true)
|
|
for (const name of names) {
|
|
const charwordTableLookup = lookup(name)
|
|
const voiceJson = {}
|
|
voiceJson.voiceLangs = {}
|
|
voiceJson.subtitleLangs = {}
|
|
const subtitleInfo = Object.keys(charwordTableLookup.info)
|
|
let voiceList = {}
|
|
subtitleInfo.forEach((item) => {
|
|
if (Object.keys(charwordTableLookup.info[item]).length > 0) {
|
|
const key = item.replace('_', '-')
|
|
voiceJson.subtitleLangs[key] = {}
|
|
for (const [id, subtitles] of Object.entries(
|
|
charwordTableLookup.voice[item]
|
|
)) {
|
|
const match = id.replace(/(.+?)([A-Z]\w+)/, '$2')
|
|
if (match === id) {
|
|
voiceJson.subtitleLangs[key].default = subtitles
|
|
voiceList[key] = Object.keys(subtitles)
|
|
} else {
|
|
voiceJson.subtitleLangs[key][match] = subtitles
|
|
}
|
|
}
|
|
voiceJson.voiceLangs[key] = {}
|
|
Object.values(charwordTableLookup.info[item]).forEach(
|
|
(item) => {
|
|
voiceJson.voiceLangs[key] = {
|
|
...voiceJson.voiceLangs[key],
|
|
...item,
|
|
}
|
|
}
|
|
)
|
|
}
|
|
})
|
|
let voiceLangs = []
|
|
try {
|
|
voiceLangs = getLangs(name, voiceJson).voiceLangs
|
|
|
|
file.writeSync(JSON.stringify(voiceJson), getDistDir(name))
|
|
} catch (e) {
|
|
console.log(`charword_table is not available`, e)
|
|
}
|
|
|
|
// check whether voice files has been added
|
|
const customVoiceName = voiceLangs.filter(
|
|
(i) => !config.dir_name.voice.sub.map((e) => e.lang).includes(i)
|
|
)[0]
|
|
const voiceLangMapping = config.dir_name.voice.sub
|
|
.filter((e) => {
|
|
return (
|
|
voiceLangs.includes(e.lang) ||
|
|
(e.lang === 'CUSTOM' &&
|
|
typeof customVoiceName !== 'undefined')
|
|
)
|
|
})
|
|
.map((e) => {
|
|
return {
|
|
name: e.name,
|
|
lang: e.lang === 'CUSTOM' ? customVoiceName : e.lang,
|
|
lookup_region: e.lookup_region.replace('_', '-'),
|
|
}
|
|
})
|
|
for (const voiceSubFolderMapping of voiceLangMapping) {
|
|
const voiceSubFolder = path.join(
|
|
OPERATOR_SOURCE_FOLDER,
|
|
name,
|
|
config.dir_name.voice.main,
|
|
voiceSubFolderMapping.name
|
|
)
|
|
const voiceFileList = file.readdirSync(voiceSubFolder)
|
|
voiceList[voiceSubFolderMapping.lookup_region].map((item) => {
|
|
if (!voiceFileList.includes(`${item}.ogg`))
|
|
err.push(
|
|
`Voice folder ${voiceSubFolderMapping.name} for ${name} is missing ${item}.ogg`
|
|
)
|
|
})
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
export const update = async () => {
|
|
await updateFn()
|
|
}
|
|
|
|
const updateFn = async (isLocalOnly = false) => {
|
|
const regionObject = REGIONS.reduce(
|
|
(acc, cur) => ({ ...acc, [cur]: {} }),
|
|
{}
|
|
)
|
|
OPERATOR_IDS.forEach((id) => {
|
|
CHARWORD_TABLE[id] = {
|
|
alternativeId: getOperatorAlternativeId(id),
|
|
voice: structuredClone(regionObject),
|
|
info: structuredClone(regionObject),
|
|
}
|
|
})
|
|
await load(DEFAULT_REGION, isLocalOnly)
|
|
await Promise.all(
|
|
REGIONS.slice(1).map(async (region) => {
|
|
await load(region, isLocalOnly)
|
|
})
|
|
)
|
|
}
|
|
|
|
const load = async (region, isLocalOnly = false) => {
|
|
const basename = `charword_table_${region}`
|
|
const filename = file
|
|
.readdirSync(AUTO_UPDATE_FOLDER)
|
|
.filter((item) => item.startsWith(`charword_table_${region}`))[0]
|
|
const localFilePath = path.join(AUTO_UPDATE_FOLDER, filename)
|
|
const data = isLocalOnly
|
|
? JSON.parse(file.readSync(localFilePath))
|
|
: await download(
|
|
region,
|
|
path.join(path.dirname(localFilePath), `${basename}.json`)
|
|
)
|
|
|
|
// put voice actor info into charword_table
|
|
for (const [id, element] of Object.entries(CHARWORD_TABLE)) {
|
|
let operatorId = id
|
|
let useAlternativeId = false
|
|
if (typeof data.voiceLangDict[operatorId] === 'undefined') {
|
|
operatorId = element.alternativeId
|
|
useAlternativeId = true
|
|
}
|
|
if (region === DEFAULT_REGION) {
|
|
element.infile = OPERATOR_IDS.includes(operatorId)
|
|
element.ref = useAlternativeId && element.infile
|
|
}
|
|
// not available in other region
|
|
if (typeof data.voiceLangDict[operatorId] === 'undefined') {
|
|
console.log(
|
|
`Voice actor info of ${id} is not available in ${region}.`
|
|
)
|
|
continue
|
|
}
|
|
|
|
if (element.infile && useAlternativeId) {
|
|
// if using alternative id and infile is true, means data can be
|
|
// refered inside the file
|
|
// if infile is false, useAlternativeId is always true
|
|
// if useAlternativeId is false, infile is always true
|
|
// | case | infile | useAlternativeId | Note |
|
|
// | ------------------- | ------ | ---------------- | --------------- |
|
|
// | lee_trust_your_eyes | false | true | skin only |
|
|
// | nearl_relight | true | true | skin, operator, no voice |
|
|
// | nearl | true | false | operator only |
|
|
// | w_fugue | true | false | skin, operator, voice |
|
|
continue
|
|
}
|
|
Object.values(data.voiceLangDict[operatorId].dict).forEach((item) => {
|
|
if (typeof element.info[region][item.wordkey] === 'undefined') {
|
|
element.info[region][item.wordkey] = {}
|
|
}
|
|
element.info[region][item.wordkey][item.voiceLangType] = [
|
|
...(typeof item.cvName === 'string'
|
|
? [item.cvName]
|
|
: item.cvName),
|
|
]
|
|
})
|
|
}
|
|
|
|
// put voice lines into charword_table
|
|
Object.values(data.charWords).forEach((item) => {
|
|
const operatorInfo = Object.values(CHARWORD_TABLE).filter(
|
|
(element) => element.info[region][item.wordKey]
|
|
)
|
|
if (operatorInfo.length > 0) {
|
|
for (const operator of operatorInfo) {
|
|
if (
|
|
typeof operator.voice[region][item.wordKey] === 'undefined'
|
|
) {
|
|
operator.voice[region][item.wordKey] = {}
|
|
}
|
|
operator.voice[region][item.wordKey][item.voiceId] = {
|
|
title: item.voiceTitle,
|
|
text: item.voiceText.replace(
|
|
/{@nickname}/g,
|
|
NICKNAME[region]
|
|
),
|
|
}
|
|
}
|
|
}
|
|
})
|
|
}
|
|
|
|
const download = async (region, targetFilePath) => {
|
|
return await githubDownload(
|
|
`https://api.github.com/repos/${REGION_URLS[region]}/commits?path=${region}/gamedata/excel/charword_table.json`,
|
|
`https://raw.githubusercontent.com/${REGION_URLS[region]}/master/${region}/gamedata/excel/charword_table.json`,
|
|
targetFilePath
|
|
)
|
|
}
|