feat: migrate to turbo (#22)

* 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>
This commit is contained in:
Haoyu Xu
2025-02-22 15:11:30 +08:00
committed by GitHub
parent 17c61ce5d4
commit d6e7bc20d3
352 changed files with 12911 additions and 9411 deletions

1
packages/assets/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,8 @@
dynchars: dynchars
item_to_download:
- homebackground/wrapper
- ui_camp_logo
- charportraits
- voice.*/extra
additional_regex:
- ^(?!(avg))(.*)$

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

12
packages/assets/index.js Normal file
View File

@@ -0,0 +1,12 @@
import path from 'node:path'
import config from '@aklive2d/config'
import { yaml } from '@aklive2d/libs'
export const DIST_DIR = path.resolve(import.meta.dirname, config.dir_name.dist)
export const CONFIG_PATH = path.resolve(
import.meta.dirname,
config.dir_name.config_yaml
)
const selfConfig = yaml.read(CONFIG_PATH)
export default selfConfig

View File

@@ -0,0 +1,49 @@
import path from 'node:path'
import { file } from '@aklive2d/libs'
import config from '@aklive2d/config'
import { DIST_DIR } from '../index.js'
export default async (packageDir) => {
const copyQueue = [
{
fn: file.symlink,
source: path.resolve(
packageDir,
'background',
config.dir_name.dist
),
target: path.resolve(DIST_DIR, config.dir_name.background),
},
{
fn: file.symlink,
source: path.resolve(
packageDir,
'charword-table',
config.dir_name.dist
),
target: path.resolve(DIST_DIR, config.dir_name.charword_table),
},
{
fn: file.symlink,
source: path.resolve(packageDir, 'music', config.dir_name.data),
target: path.resolve(DIST_DIR, config.dir_name.music),
},
{
fn: file.symlinkAll,
source: path.resolve(packageDir, 'operator', config.dir_name.dist),
target: path.resolve(DIST_DIR),
},
{
fn: file.symlink,
source: path.resolve(
packageDir,
'project-json',
config.dir_name.dist
),
target: path.resolve(DIST_DIR, config.dir_name.project_json),
},
]
copyQueue.map(({ fn, source, target }) => {
fn(source, target)
})
}

View File

@@ -0,0 +1,57 @@
import path from 'node:path'
import { file } from '@aklive2d/libs'
import { unzipDownload } from '@aklive2d/downloader'
import { getOperatorId, getOperatorAlternativeId } from '@aklive2d/operator'
import { mapping } from '@aklive2d/music'
import config from '../index.js'
export default async (dataDir) => {
const pidSet = new Set()
const versionRes = await fetch(
'https://ak-conf.hypergryph.com/config/prod/official/Android/version'
)
const version = (await versionRes.json()).resVersion
const lpacksRes = await fetch(
`https://ak.hycdn.cn/assetbundle/official/Android/assets/${version}/hot_update_list.json`
)
const updateList = await lpacksRes.json()
const itemToDownload = new Set(config.item_to_download)
updateList.abInfos.map((item) => {
if (item.name.includes(config.dynchars)) {
const id = getOperatorId(item.name).replace('.ab', '')
itemToDownload.add(id)
itemToDownload.add(getOperatorAlternativeId(id))
}
})
mapping.musicFiles.map((item) => {
if (!file.exists(path.join(item.source, item.filename))) {
const filename = item.filename.replace('.ogg', '')
itemToDownload.add(filename)
}
})
const itemToDownloadRegExp = new RegExp(
`(.*)(${Array.from(itemToDownload).join('|')})(.*)`
)
updateList.abInfos.map((item) => {
if (itemToDownloadRegExp.test(item.name)) {
item.pid && pidSet.add(item.pid)
}
})
const lpacksToDownload = []
pidSet.forEach((item) => {
lpacksToDownload.push({
name: item,
url: `https://ak.hycdn.cn/assetbundle/official/Android/assets/${version}/${item}.dat`,
})
})
const regexs = []
if (config.additional_regex.length > 0) {
for (const item of config.additional_regex) {
regexs.push(new RegExp(item))
}
}
await unzipDownload(lpacksToDownload, dataDir, {
matchRegExps: regexs,
defaultRegex: itemToDownloadRegExp,
})
}

View File

@@ -0,0 +1,21 @@
{
"name": "@aklive2d/assets",
"version": "0.0.0",
"main": "index.js",
"type": "module",
"license": "MIT",
"dependencies": {
"@aklive2d/config": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/libs": "workspace:*",
"@aklive2d/prettier-config": "workspace:*",
"@aklive2d/downloader": "workspace:*",
"@aklive2d/operator": "workspace:*",
"@aklive2d/music": "workspace:*"
},
"scripts": {
"build": "mode=build node runner.js",
"download:game": "mode=download node runner.js",
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

30
packages/assets/runner.js Normal file
View File

@@ -0,0 +1,30 @@
import path from 'node:path'
import fs from 'node:fs'
import { envParser } from '@aklive2d/libs'
import config from '@aklive2d/config'
import build from './libs/build.js'
import download from './libs/download.js'
const packageDir = path.resolve(import.meta.dirname, '..')
const dataDir = path.resolve(import.meta.dirname, config.dir_name.data)
async function main() {
const { mode } = envParser.parse({
mode: {
type: 'string',
short: 'm',
},
})
switch (mode) {
case 'build':
await build(packageDir)
break
case 'download':
await download(dataDir)
break
default:
throw new Error(`Unknown mode: ${mode}`)
}
}
main()

1
packages/background/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

View File

@@ -0,0 +1,84 @@
import path from 'node:path'
import sharp from 'sharp'
import { file } from '@aklive2d/libs'
import config from '@aklive2d/config'
import { mapping as musicMapping } from '@aklive2d/music'
export const BACKGROUND_DIR = path.join(
import.meta.dirname,
config.dir_name.data
)
const DIST_DIR = path.resolve(import.meta.dirname, config.dir_name.dist)
const EXTRACTED_DIR = path.join(BACKGROUND_DIR, config.dir_name.extracted)
const DEFAULT_BACKGROUND_FILE = path.join(
BACKGROUND_DIR,
config.module.background.operator_bg_png
)
const getFiles = () => {
return file.readdirSync(DIST_DIR)
}
export let files = getFiles()
const filesToBuild = file.readdirSync(EXTRACTED_DIR).filter((f) => {
return f.endsWith('.png') && f.includes('_left')
})
export const build = async () => {
const err = []
file.mkdir(DIST_DIR)
await Promise.all(
filesToBuild.map(async (f) => {
const filenamePrefix = path.parse(f).name.replace('_left', '')
await composite(filenamePrefix, '.png')
})
)
await file.copy(
DEFAULT_BACKGROUND_FILE,
path.join(DIST_DIR, config.module.background.operator_bg_png)
)
const { musicFiles, musicFileMapping } = musicMapping
for (const e of musicFiles) {
const musicPath = path.join(e.source, e.filename)
if (!file.exists(musicPath)) {
err.push(`Music file ${e.filename} is not found in music folder.`)
}
}
files = getFiles()
for (const e of Object.keys(musicFileMapping)) {
if (!files.includes(e)) {
err.push(`Background file ${e} is not found in background folder.`)
}
}
return err
}
const composite = async (filenamePrefix, fileExt) => {
const image = sharp(
path.join(EXTRACTED_DIR, `${filenamePrefix}_left${fileExt}`)
)
const metadata = await image.metadata()
image
.resize(2 * metadata.width, metadata.height, {
kernel: sharp.kernel.nearest,
fit: 'contain',
position: 'left top',
background: { r: 255, g: 255, b: 255, alpha: 0 },
})
.composite([
{
input: path.join(
EXTRACTED_DIR,
`${filenamePrefix}_right${fileExt}`
),
top: 0,
left: metadata.width,
},
])
.toFile(path.join(DIST_DIR, `${filenamePrefix}${fileExt}`))
}

View File

@@ -0,0 +1,19 @@
{
"name": "@aklive2d/background",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"sharp": "^0.33.5",
"@aklive2d/libs": "workspace:*",
"@aklive2d/config": "workspace:*",
"@aklive2d/music": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*"
},
"scripts": {
"build": "mode=build node runner.js",
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

View File

@@ -0,0 +1,22 @@
import { build } from './index.js'
import { envParser, error } from '@aklive2d/libs'
async function main() {
let err
const { mode } = envParser.parse({
mode: {
type: 'string',
short: 'm',
},
})
switch (mode) {
case 'build':
err = await build()
break
default:
throw new Error(`Unknown mode: ${mode}`)
}
error.handle(err)
}
main()

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

View File

@@ -0,0 +1,267 @@
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
)
}

View File

@@ -0,0 +1,20 @@
{
"name": "@aklive2d/charword-table",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"@aklive2d/libs": "workspace:*",
"@aklive2d/config": "workspace:*",
"@aklive2d/downloader": "workspace:*",
"@aklive2d/operator": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*"
},
"scripts": {
"update": "mode=update node runner.js",
"build": "mode=build node runner.js",
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

View File

@@ -0,0 +1,31 @@
import { build, update } from './index.js'
import { envParser, error } from '@aklive2d/libs'
async function main() {
let err = []
const { mode, name } = envParser.parse({
mode: {
type: 'string',
short: 'm',
},
name: {
type: 'string',
short: 'n',
multiple: true,
default: [],
},
})
switch (mode) {
case 'build':
err = await build(name)
break
case 'update':
await update()
break
default:
throw new Error(`Unknown mode: ${mode}`)
}
error.handle(err)
}
main()

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,96 @@
site_id: aklive2d
akassets:
project_name: akassets
url: https://migrate-turbo.akassets.pages.dev
insight:
id: aklive2d
url: https://insight.halyul.dev/on-demand.js
module:
background:
background: background
operator_bg_png: operator_bg.png
charword_table:
charword_table_json: charword_table.json
music:
music_table_json: music_table.json
display_meta_table_json: display_meta_table.json
audio_data_json: audio_data.json
official_info:
official_info_json: official_info.json
operator:
config: config
template_yaml: _template.yaml
config_yaml: config.yaml
portraits: _portraits
logos_assets: _logos
directory_assets: _directory
MonoBehaviour: MonoBehaviour
Texture2D: Texture2D
assets_json: assets.json
title:
zh-CN: '明日方舟:'
en-US: 'Arknights: '
project_json:
project_json: project.json
preview_jpg: preview.jpg
template_yaml: project_json.yaml
wrangler:
index_json: index.json
dir_name:
config_yaml: config.yaml
assets: assets
data: data
dist: dist
extracted: extracted
auto_update: auto_update
operator: operator
background: background
music: music
logos: logos
public: public
charword_table: charword_table
project_json: project_json
config_json: config.json
voice:
main: voice
sub:
- name: jp
lang: JP
lookup_region: zh_CN
- name: cn
lang: CN_MANDARIN
lookup_region: zh_CN
- name: en
lang: EN
lookup_region: en_US
- name: kr
lang: KR
lookup_region: ko_KR
- name: custom
lang: CUSTOM
lookup_region: zh_CN
share:
title:
zh-CN: '明日方舟:'
en-US: 'Arknights: '
directory:
assets_dir: _assets
title: AKLive2D
voice: jp/CN_037.ogg
error:
files:
- key: build_char_128_plosis_epoque#3
paddings:
left: -120
right: 150
top: 10
bottom: 0
- key: build_char_128_plosis
paddings:
left: -90
right: 100
top: 10
bottom: 0
voice:
file: CN_034.ogg
target: error.ogg

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

4
packages/config/index.js Normal file
View File

@@ -0,0 +1,4 @@
import path from 'node:path'
import { yaml } from '@aklive2d/libs'
export default yaml.read(path.resolve(import.meta.dirname, 'config.yaml'))

View File

@@ -0,0 +1,15 @@
{
"name": "@aklive2d/config",
"version": "0.0.0",
"main": "index.js",
"type": "module",
"license": "MIT",
"dependencies": {
"@aklive2d/libs": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*"
},
"scripts": {
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

View File

@@ -0,0 +1,112 @@
import path from 'node:path'
import fs from 'node:fs'
import { Buffer } from 'node:buffer'
import { pipeline } from 'node:stream/promises'
import pThrottle from 'p-throttle'
import { file as fileLib } from '@aklive2d/libs'
export const githubDownload = async (history_url, raw_url, filepath) => {
const historyResponse = await fetch(history_url)
const historyData = await historyResponse.json()
const lastCommit = historyData[0]
const lastCommitDate = new Date(lastCommit.commit.committer.date)
const ext = path.extname(filepath)
const basename = path.basename(filepath).replace(ext, '')
filepath = path.join(
path.dirname(filepath),
`${basename}_${lastCommitDate.getTime()}${ext}`
)
const dirpath = path.dirname(filepath)
console.log(`Last ${basename} commit date: ${lastCommitDate.getTime()}`)
if (fileLib.exists(filepath)) {
console.log(`${basename} is the latest version.`)
return JSON.parse(fileLib.readSync(filepath))
}
const response = await fetch(raw_url)
const data = await response.json()
fileLib.writeSync(JSON.stringify(data), filepath)
console.log(`${basename} is updated.`)
// remove old file
const files = fileLib.readdirSync(path.join(dirpath))
for (const file of files) {
if (file.startsWith(basename) && file !== path.basename(filepath)) {
fileLib.rm(path.join(dirpath, file))
}
}
return data
}
export const unzipDownload = async (
filesToDownload,
targetDir,
opts = {
defaultRegex: null,
matchRegExps: [],
}
) => {
let retry = filesToDownload
const throttle = pThrottle({
limit: 3,
interval: 1000,
})
while (retry.length > 0) {
const newRetry = []
await Promise.all(
retry.map(
throttle(async (item) => {
const name = item.name
console.log(`Downloading ${name}`)
const zip = await fetch(item.url)
.then((resp) => {
const status = resp.status
if (status !== 200)
throw new Error(`Status Code: ${status}`)
return resp.arrayBuffer()
})
.then((arrayBuffer) => {
return fileLib.unzip.fromBuffer(
Buffer.from(arrayBuffer)
)
})
.catch((err) => {
console.error(err)
})
try {
for await (const entry of zip) {
if (
opts.defaultRegex &&
!opts.defaultRegex.test(entry.filename)
) {
continue
}
if (opts.matchRegExps.length > 0) {
let shallContinue = false
for (const regex of opts.matchRegExps) {
if (!regex.test(entry.filename)) {
shallContinue = true
break
}
}
if (shallContinue) continue
}
const filePath = path.join(
targetDir,
entry.filename
)
fileLib.mkdir(path.dirname(filePath))
const readStream = await entry.openReadStream()
const writeStream = fs.createWriteStream(filePath)
await pipeline(readStream, writeStream)
console.log(`Finish Writing to ${entry.filename}`)
}
} finally {
await zip.close()
}
})
)
)
retry = newRetry
}
}

View File

@@ -0,0 +1,16 @@
{
"name": "@aklive2d/downloader",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/libs": "workspace:*",
"@aklive2d/prettier-config": "workspace:*",
"p-throttle": "^7.0.0"
},
"scripts": {
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

View File

@@ -0,0 +1,23 @@
import js from '@eslint/js'
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import globals from 'globals'
export default [
{ ignores: ['dist'] },
{
files: ['**/*.{js,jsx}'],
languageOptions: {
ecmaVersion: 2020,
globals: globals.browser,
parserOptions: {
ecmaVersion: 'latest',
ecmaFeatures: { jsx: true },
sourceType: 'module',
},
},
rules: {
...js.configs.recommended.rules,
},
},
eslintPluginPrettierRecommended,
]

View File

@@ -0,0 +1,17 @@
{
"name": "@aklive2d/eslint-config",
"version": "0.0.0",
"main": "index.js",
"type": "module",
"license": "MIT",
"dependencies": {
"@eslint/js": "^9.19.0",
"eslint": "^9.19.0",
"eslint-config-prettier": "^10.0.1",
"eslint-plugin-prettier": "^5.2.3",
"globals": "^15.14.0"
},
"publishConfig": {
"access": "public"
}
}

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

13
packages/libs/index.js Normal file
View File

@@ -0,0 +1,13 @@
import * as file from './libs/file.js'
import * as yaml from './libs/yaml.js'
import * as env from './libs/env.js'
import * as error from './libs/error.js'
import * as alphaComposite from './libs/alpha_composite.js'
import * as envParser from './libs/env_parser.js'
export { file }
export { yaml }
export { env }
export { error }
export { alphaComposite }
export { envParser }

View File

@@ -0,0 +1,48 @@
import sharp from 'sharp'
import path from 'path'
export const process = async (filename, maskFilename, extractedDir) => {
const image = sharp(path.join(extractedDir, filename)).removeAlpha()
const imageMeta = await image.metadata()
const imageBuffer = await image.toBuffer()
const mask = await sharp(path.join(extractedDir, maskFilename))
.extractChannel('blue')
.resize(imageMeta.width, imageMeta.height)
.toBuffer()
return sharp(imageBuffer).joinChannel(mask).toBuffer()
}
export const crop = async (buffer, rect) => {
const left = rect.y
const top = rect.x
const width = rect.h
const height = rect.w
const rotate = rect.rotate === 0 ? -90 : 0
const newImage = await sharp(buffer)
.rotate(90)
.extract({ left: left, top: top, width: width, height: height })
.resize(width, height)
.extract({ left: 0, top: 0, width: width, height: height })
.toBuffer()
return await sharp(newImage).rotate(rotate).toBuffer()
}
export const toBuffer = async (filename, extractedDir) => {
const file = path.join(extractedDir, filename)
const { data, info } = await sharp(file)
.raw()
.toBuffer({ resolveWithObject: true })
const { width, height, channels } = info
const pixelArray = new Uint8ClampedArray(data.buffer)
for (let i = 0; i < pixelArray.length; i += 4) {
let alpha = pixelArray[i + 3] / 255
pixelArray[i + 0] = pixelArray[i + 0] * alpha
pixelArray[i + 1] = pixelArray[i + 1] * alpha
pixelArray[i + 2] = pixelArray[i + 2] * alpha
}
return await sharp(pixelArray, { raw: { width, height, channels } })
.png()
.toBuffer()
}

View File

@@ -0,0 +1,7 @@
export function generate(values) {
return values
.map((value) => {
return `VITE_${value.key.toUpperCase()}=${value.value}`
})
.join('\n')
}

View File

@@ -0,0 +1,43 @@
import process from 'node:process'
export const parse = (args) => {
const envVars = process.env
const argKeys = Object.keys(args)
const values = {}
argKeys.map((key) => {
let noInput = false
let value,
type = args[key].type || 'string',
defaultVal = args[key].default,
multiple = args[key].multiple || false,
short = args[key].short
value = envVars[key] || envVars[short]
if (!value) noInput = true
value = noInput ? defaultVal : value
if (noInput) {
values[key] = value
} else {
value = multiple ? value.split(',') : value
if (multiple) {
values[key] = []
value.map((item) => {
values[key].push(typeCast(type, item))
})
} else {
values[key] = typeCast(type, value)
}
}
})
return values
}
const typeCast = (type, value) => {
switch (type) {
case 'number':
return Number(value)
case 'boolean':
return Boolean(value)
default:
return value
}
}

View File

@@ -0,0 +1,6 @@
export const handle = (err) => {
if (err.length > 0) {
const str = `${err.length} error${err.length > 1 ? 's were' : ' was'} found:\n${err.join('\n')}`
throw new Error(str)
}
}

166
packages/libs/libs/file.js Normal file
View File

@@ -0,0 +1,166 @@
import fs, { promises as fsP } from 'fs'
import path from 'path'
import yauzl from 'yauzl-promise'
import yazl from 'yazl'
export async function write(content, filePath) {
mkdir(path.dirname(filePath))
return await fsP.writeFile(filePath, content, { flag: 'w' })
}
export function writeSync(content, filePath) {
mkdir(path.dirname(filePath))
return fs.writeFileSync(filePath, content, { flag: 'w' })
}
export async function read(filePath, encoding = 'utf8') {
return await fsP.readFile(filePath, encoding, { flag: 'r' })
}
export function readSync(filePath, encoding = 'utf8') {
if (exists(filePath)) {
return fs.readFileSync(filePath, encoding, { flag: 'r' })
}
return null
}
export function exists(filePath) {
return fs.existsSync(filePath)
}
export function rmdir(dir) {
if (exists(dir)) {
fs.rmSync(dir, { recursive: true })
}
}
export function rm(dir) {
if (exists(dir)) {
fs.rmSync(dir, { recursive: true })
}
}
export function mkdir(dir) {
if (!exists(dir)) {
fs.mkdirSync(dir, { recursive: true })
}
}
export async function copy(
sourcePath,
targetPath,
mode = fs.constants.COPYFILE_FICLONE
) {
if (!exists(sourcePath)) {
console.warn(`Source file ${sourcePath} does not exist.`)
return
}
mkdir(path.dirname(targetPath))
return await fsP.copyFile(sourcePath, targetPath, mode)
}
export async function copyDir(
sourcePath,
targetPath,
mode = fs.constants.COPYFILE_FICLONE
) {
if (!exists(sourcePath)) {
console.warn(`Source file ${sourcePath} does not exist.`)
return
}
mkdir(targetPath)
return await fsP.cp(sourcePath, targetPath, { recursive: true, mode })
}
export function appendSync(content, filePath) {
return fs.appendFileSync(filePath, content, 'utf8')
}
export function readdirSync(dir) {
if (!exists(dir)) {
console.warn(`Source ${dir} does not exist.`)
return []
}
return fs.readdirSync(dir)
}
export function fileTypeSync(dir) {
if (!exists(dir)) {
console.warn(`Source ${dir} does not exist.`)
return null
}
return fs.statSync(dir).isDirectory() ? 'dir' : 'file'
}
export const symlink = (source, target) => {
if (!exists(source)) {
console.warn(`Source ${source} does not exist.`)
return
}
if (exists(target)) {
fs.unlinkSync(target)
}
mkdir(path.dirname(target))
const relative = path.relative(path.dirname(target), source)
fs.symlinkSync(relative, target)
}
export const symlinkAll = (source, target) => {
const files = readdirSync(source)
files.map((file) => {
symlink(path.join(source, file), path.join(target, file))
})
}
export const mv = (source, target) => {
if (!exists(source)) {
console.warn(`Source file ${source} does not exist.`)
return
}
if (exists(target)) {
rmdir(target)
}
mkdir(target)
fs.renameSync(source, target)
}
export const cpSync = (
source,
target,
opts = {
dereference: false,
}
) => {
if (!exists(source)) {
console.warn(`Source file ${source} does not exist.`)
return
}
if (fs.statSync(source).isFile()) {
mkdir(path.dirname(target))
} else {
mkdir(target)
}
fs.cpSync(source, target, {
recursive: true,
dereference: opts.dereference,
})
}
export const relative = (source, target) => {
if (!exists(source)) {
console.warn(`Source file ${source} does not exist.`)
return
}
return path.relative(source, target)
}
export const size = (source) => {
if (!exists(source)) {
console.warn(`Source file ${source} does not exist.`)
return
}
return fs.statSync(source).size
}
export const unzip = yauzl
export const zip = yazl

View File

@@ -0,0 +1,19 @@
import fs from 'fs'
import path from 'path'
import { parse } from 'yaml'
export function read(file_dir, customTags = []) {
const include = {
identify: (value) => value.startsWith('!include'),
tag: '!include',
resolve(str) {
const dir = path.resolve(path.dirname(file_dir), str)
const data = read(dir)
return data
},
}
const file = fs.readFileSync(file_dir, 'utf8')
return parse(file, {
customTags: [include, ...customTags],
})
}

View File

@@ -0,0 +1,18 @@
{
"name": "@aklive2d/libs",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*",
"sharp": "^0.33.5",
"yaml": "^2.7.0",
"yauzl-promise": "^4.0.0",
"yazl": "^3.3.1"
},
"scripts": {
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

1
packages/music/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
[{"id":"bg_rhodes_day","intro":"Audio/Sound_Beta_2/Music/beta1_180603/m_sys_void_intro","loop":"Audio/Sound_Beta_2/Music/beta1_180603/m_sys_void_loop"},{"id":"bg_rhodes_night","intro":"Audio/Sound_Beta_2/Music/beta2_180603/m_sys_tech_intro","loop":"Audio/Sound_Beta_2/Music/beta2_180603/m_sys_tech_loop"},{"id":"bg_main_victoria_1","intro":"Audio/Sound_Beta_2/Music/AVG/m_avg_ghosthunter_intro","loop":"Audio/Sound_Beta_2/Music/AVG/m_avg_ghosthunter_loop"},{"id":"bg_siesta_1","intro":"Audio/Sound_Beta_2/Music/obt/m_sys_ddd_intro","loop":"Audio/Sound_Beta_2/Music/obt/m_sys_ddd_loop"},{"id":"bg_kazimierz_1","intro":"Audio/Sound_Beta_2/Music/beta3_181101/m_dia_street_intro","loop":"Audio/Sound_Beta_2/Music/beta3_181101/m_dia_street_loop"},{"id":"bg_ursus_1","intro":"Audio/Sound_Beta_2/Music/static/m_avg/m_avg_loneliness_intro","loop":"Audio/Sound_Beta_2/Music/static/m_avg/m_avg_loneliness_loop"},{"id":"bg_yan_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/act15side/m_sys_bitw_loop"},{"id":"bg_iberia_1","intro":"Audio/Sound_Beta_2/Music/act18d3d0/m_sys_act18d3d0_intro","loop":"Audio/Sound_Beta_2/Music/act18d3d0/m_sys_act18d3d0_loop"},{"id":"bg_anniversary_1","intro":"Audio/Sound_Beta_2/Music/beta2_180603/m_dia_nightoflongmen_intro","loop":"Audio/Sound_Beta_2/Music/beta2_180603/m_dia_nightoflongmen_loop"},{"id":"bg_rogue_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/rogue_1/m_avg_rglk1secretevent_loop"},{"id":"bg_rogue_2","intro":null,"loop":"Audio/Sound_Beta_2/Music/rogue_2/m_sys_rglk2DLC_loop"},{"id":"bg_laterano_1","intro":"Audio/Sound_Beta_2/Music/act16side/m_sys_act16side_intro","loop":"Audio/Sound_Beta_2/Music/act16side/m_sys_act16side_loop"},{"id":"bg_rhine_1","intro":"Audio/Sound_Beta_2/Music/act19side/m_sys_act19side_intro","loop":"Audio/Sound_Beta_2/Music/act19side/m_sys_act19side_loop"},{"id":"bg_kalts_1","intro":"Audio/Sound_Beta_2/Music/act18d0d0/m_sys_act18d0d0_intro","loop":"Audio/Sound_Beta_2/Music/act18d0d0/m_sys_act18d0d0_loop"},{"id":"bg_rogue_3","intro":"Audio/Sound_Beta_2/Music/rogue_3/m_bat_rglk3DLC_intro","loop":"Audio/Sound_Beta_2/Music/rogue_3/m_bat_rglk3DLC_loop"},{"id":"bg_rainbowsix_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/act32side/m_act32side_sys_loop"},{"id":"bg_rhodes_flower_1","intro":"Audio/Sound_Beta_2/Music/act16mini/m_sys_act16mini_intro","loop":"Audio/Sound_Beta_2/Music/act16mini/m_sys_act16mini_loop"},{"id":"bg_sanrio_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/act27side/m_avg_SiestaCity"},{"id":"bg_sandboxv2_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/sandbox_1/m_sys_sandbox_1_map_loop"},{"id":"bg_dungeon_1","intro":null,"loop":"Audio/Sound_Beta_2/Music/act36side/m_sys_act36side_loop"},{"id":"bg_sui_1","intro":"Audio/Sound_Beta_2/Music/act31side/m_act31side_sys_intro","loop":"Audio/Sound_Beta_2/Music/act31side/m_act31side_sys_loop"},{"id":"bg_rogue_4","intro":null,"loop":"Audio/Sound_Beta_2/Music/act17mini/m_sys_act17mini_loop"},{"id":"operator_bg","intro":"m_sys_void_intro","loop":"m_sys_void_loop"}]

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

117
packages/music/index.js Normal file
View File

@@ -0,0 +1,117 @@
import path from 'path'
import { file } from '@aklive2d/libs'
import { githubDownload } from '@aklive2d/downloader'
import config from '@aklive2d/config'
const AUTO_UPDATE_FOLDER = path.resolve(
import.meta.dirname,
config.dir_name.auto_update
)
export const DATA_DIR = path.resolve(import.meta.dirname, config.dir_name.data)
const MUSIC_TABLE_JSON = path.join(
AUTO_UPDATE_FOLDER,
config.module.music.music_table_json
)
const download = async () => {
const display_meta_table_json = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.music.display_meta_table_json
)
const audio_data_json = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.music.audio_data_json
)
const metaTable = await githubDownload(
`https://api.github.com/repos/Kengxxiao/ArknightsGameData/commits?path=zh_CN/gamedata/excel/display_meta_table.json`,
`https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/display_meta_table.json`,
display_meta_table_json
)
const audioDataTable = await githubDownload(
`https://api.github.com/repos/Kengxxiao/ArknightsGameData/commits?path=zh_CN/gamedata/excel/audio_data.json`,
`https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/audio_data.json`,
audio_data_json
)
return {
metaTable,
audioDataTable,
}
}
const generateMapping = () => {
const musicFolder = DATA_DIR
const musicTable = JSON.parse(file.readSync(MUSIC_TABLE_JSON))
const musicFileMapping = {}
const musicFiles = []
if (!musicTable) return
for (const item of musicTable) {
const key = `${item.id}.png`
musicFileMapping[key] = {}
if (item.intro) {
const filename = `${item.intro.split('/').pop()}.ogg`
musicFileMapping[key].intro = filename
musicFiles.push({
filename,
source: musicFolder,
})
} else {
musicFileMapping[key].intro = null
}
if (item.loop) {
const filename = `${item.loop.split('/').pop()}.ogg`
musicFileMapping[key].loop = filename
musicFiles.push({
filename,
source: musicFolder,
})
} else {
musicFileMapping[key].loop = null
}
}
return {
musicFiles,
musicFileMapping,
}
}
export const mapping = generateMapping()
export const update = async () => {
const { metaTable, audioDataTable } = await download()
const musicTable = audioDataTable.musics
const musicBank = audioDataTable.bgmBanks
const musicBankAlias = audioDataTable.bankAlias
const musicData = metaTable.homeBackgroundData.homeBgDataList.reduce(
(acc, cur) => {
acc.push({
id: cur.bgId,
musicId: cur.bgMusicId,
})
return acc
},
[]
)
const list = []
for (const item of musicData) {
let bankName = musicTable.find((el) => item.musicId === el.id).bank
if (typeof musicBankAlias[bankName] !== 'undefined') {
bankName = musicBankAlias[bankName]
}
const obj = musicBank.find((el) => bankName === el.name)
list.push({
id: item.id,
intro: obj.intro,
loop: obj.loop,
})
}
list.push({
id: 'operator_bg',
intro: 'm_sys_void_intro',
loop: 'm_sys_void_loop',
})
file.writeSync(JSON.stringify(list, null), MUSIC_TABLE_JSON)
}

View File

@@ -0,0 +1,18 @@
{
"name": "@aklive2d/music",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"@aklive2d/libs": "workspace:*",
"@aklive2d/config": "workspace:*",
"@aklive2d/downloader": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*"
},
"scripts": {
"update": "mode=update node runner.js",
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

20
packages/music/runner.js Normal file
View File

@@ -0,0 +1,20 @@
import { envParser } from '@aklive2d/libs'
import { update } from './index.js'
async function main() {
const { mode } = envParser.parse({
mode: {
type: 'string',
short: 'm',
},
})
switch (mode) {
case 'update':
await update()
break
default:
throw new Error(`Unknown mode: ${mode}`)
}
}
main()

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,635 @@
{
"length": 60,
"dates": [
"2025-01-01",
"2024-12-09",
"2024-12-01",
"2024-10-01",
"2024-07-01",
"2024-05-01",
"2024-04-01",
"2024-02-29",
"2024-02-28",
"2024-01-01",
"2023-11-01",
"2023-08-01",
"2023-07-01",
"2023-04-01",
"2023-02-01",
"2023-01-01",
"2022-12-15",
"2022-11-01",
"2022-10-01",
"2022-08-01",
"2022-05-01",
"2022-04-30",
"2022-01-01",
"2021-11-01",
"2021-08-01",
"2021-05-01",
"2021-02-01",
"2020-11-01",
"2020-05-01",
"2020-01-01"
],
"2025-01-01": [
{
"codename": {
"zh-CN": "少年游 · 左乐",
"en-US": "Youthful Journey"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202501927.html",
"id": "202501927"
},
{
"codename": {
"zh-CN": "春日宴 · 黍",
"en-US": "Spring Feast"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202501936.html",
"id": "202501936"
},
{
"codename": {
"zh-CN": "余",
"en-US": "yu"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202501414.html",
"id": "202501414"
}
],
"2024-12-09": [
{
"codename": {
"zh-CN": "霹雳导演 · 年",
"en-US": "Thunderbolt Director"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202412491.html",
"id": "202412491"
}
],
"2024-12-01": [
{
"codename": {
"zh-CN": "寰宇独奏 · 阿米娅",
"en-US": "Solo Around The World"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202412473.html",
"id": "202412473"
},
{
"codename": {
"zh-CN": "全能演员 · 重岳",
"en-US": "All-Round Actor"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202412452.html",
"id": "202412452"
}
],
"2024-10-01": [
{
"codename": {
"zh-CN": "荒芜拉普兰德",
"en-US": "Lappland the Decadenza"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202410440.html",
"id": "202410440"
},
{
"codename": {
"zh-CN": "幽兰秘辛 · 缄默德克萨斯",
"en-US": "Il Segreto Della Notte"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202410409.html",
"id": "202410409"
},
{
"codename": {
"zh-CN": "众志归一 · 圣约送葬人",
"en-US": "Allmind as one"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202410488.html",
"id": "202410488"
},
{
"codename": {
"zh-CN": "无我唯识 · 塑心",
"en-US": "Diversity Oneness"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202410467.html",
"id": "202410467"
},
{
"codename": {
"zh-CN": "暗月的影子 · 锏",
"en-US": "The Shadow Of The Dark Moon"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202410426.html",
"id": "202410426"
}
],
"2024-07-01": [
{
"codename": {
"zh-CN": "流辉 · 夜莺",
"en-US": "Iakhu of Flows"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202407435.html",
"id": "202407435"
},
{
"codename": {
"zh-CN": "佩佩",
"en-US": "pepe"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202407013.html",
"id": "202407013"
},
{
"codename": {
"zh-CN": "远行前的野餐 · 纯烬艾雅法拉",
"en-US": "A Picnic Before A Long Trip"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202407072.html",
"id": "202407072"
},
{
"codename": {
"zh-CN": "夏卉 FA075 · 焰影苇草",
"en-US": "Summer Flowers FA075"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202407051.html",
"id": "202407051"
}
],
"2024-05-01": [
{
"codename": {
"zh-CN": "维什戴尔",
"en-US": "WISADEL"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202404049.html",
"id": "202404049"
},
{
"codename": {
"zh-CN": "新枝 · 缪尔赛思",
"en-US": "Muelsyse"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202404090.html",
"id": "202404090"
},
{
"codename": {
"zh-CN": "红女爵 · 浊心斯卡蒂",
"en-US": "Red Countess"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202404008.html",
"id": "202404008"
},
{
"codename": {
"zh-CN": "燃烧天穹下 · 伊内丝",
"en-US": "INES"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202404087.html",
"id": "202404087"
}
],
"2024-04-01": [
{
"codename": {
"zh-CN": "不融冰 · 银灰",
"en-US": "Never-melting Ice"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202404066.html",
"id": "202404066"
}
],
"2024-02-29": [
{
"codename": {
"zh-CN": "黍",
"en-US": "shu"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202401025.html",
"id": "202401025"
},
{
"codename": {
"zh-CN": "列瑶台 · 林",
"en-US": "Heavenly Mirage"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202401034.html",
"id": "202401034"
}
],
"2024-02-28": [
{
"codename": {
"zh-CN": "何处栖 · 重岳",
"en-US": "Alighting"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202401812.html",
"id": "202401812"
}
],
"2024-01-01": [
{
"codename": {
"zh-CN": "博物 · 焰影苇草",
"en-US": "Curator"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202401871.html",
"id": "202401871"
}
],
"2023-11-01": [
{
"codename": {
"zh-CN": "塑心",
"en-US": "Virtuosa"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202310848.html",
"id": "202310848"
},
{
"codename": {
"zh-CN": "远路 · 玛恩纳",
"en-US": "W Dali"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202310850.html",
"id": "202310850"
},
{
"codename": {
"zh-CN": "破翼者 · 缄默德克萨斯",
"en-US": "Wingbreaker"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202310899.html",
"id": "202310899"
}
],
"2023-08-01": [
{
"codename": {
"zh-CN": "崖高梦远 · 令",
"en-US": "Towering is cliff of nostalgia"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202308807.html",
"id": "202308807"
}
],
"2023-07-01": [
{
"codename": {
"zh-CN": "悠然假日 HD26 · 百炼嘉维尔",
"en-US": "Gavial the Invincible HD26"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202307886.html",
"id": "202307886"
},
{
"codename": {
"zh-CN": "纯烬艾雅法拉",
"en-US": "Eyjafjalla the Hvít Aska"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202307865.html",
"id": "202307865"
},
{
"codename": {
"zh-CN": "夏卉 FA394 · 澄闪",
"en-US": "Summer Flowers FA394"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202307824.html",
"id": "202307824"
}
],
"2023-04-01": [
{
"codename": {
"zh-CN": "残余 · 凯尔希",
"en-US": "Remnant"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202304833.html",
"id": "202304833"
},
{
"codename": {
"zh-CN": "缪尔赛思",
"en-US": "Muelsyse"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202304611.html",
"id": "202304611"
},
{
"codename": {
"zh-CN": "生而为一 · 归溟幽灵鲨",
"en-US": "Born as One"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202304670.html",
"id": "202304670"
},
{
"codename": {
"zh-CN": "万重山 · 假日威龙陈",
"en-US": "Ten Thousand Mountains"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202304659.html",
"id": "202304659"
}
],
"2023-02-01": [
{
"codename": {
"zh-CN": "字句中的雪原 · 鸿雪",
"en-US": "Snowy Plains in Words"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202302698.html",
"id": "202302698"
}
],
"2023-01-01": [
{
"codename": {
"zh-CN": "重岳",
"en-US": "Chongyue"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202301606.html",
"id": "202301606"
},
{
"codename": {
"zh-CN": "濯缨 · 令",
"en-US": "It Does Wash the Strings"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202301647.html",
"id": "202301647"
}
],
"2022-12-15": [
{
"codename": {
"zh-CN": "夏日餮宴 · 水月",
"en-US": "Summer Feast"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202211685.html",
"id": "202211685"
}
],
"2022-11-01": [
{
"codename": {
"zh-CN": "缄默德克萨斯",
"en-US": "Texas the Omertosa"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202210210.html",
"id": "202210210"
},
{
"codename": {
"zh-CN": "今昔须臾之梦 · 异客",
"en-US": "Dream in a Moment"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202210664.html",
"id": "202210664"
},
{
"codename": {
"zh-CN": "复现荣光 · 耀骑士临光",
"en-US": "Relight"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202210623.html",
"id": "202210623"
},
{
"codename": {
"zh-CN": "拥抱新生 · 迷迭香",
"en-US": "Become Anew"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202210632.html",
"id": "202210632"
}
],
"2022-10-01": [
{
"codename": {
"zh-CN": "手到牌来 · 老鲤",
"en-US": "Trust Your Eyes"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202210279.html",
"id": "202210279"
}
],
"2022-08-01": [
{
"codename": {
"zh-CN": "百炼嘉维尔 ",
"en-US": "Gavial the Invincible"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202208258.html",
"id": "202208258"
},
{
"codename": {
"zh-CN": "缤纷奇境 CW03 · 史尔特尔",
"en-US": "Colorful Wonderland CW03"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202208297.html",
"id": "202208297"
}
],
"2022-05-01": [
{
"codename": {
"zh-CN": "归溟幽灵鲨",
"en-US": "SPECTER THE UNCHAINED"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202204284.html",
"id": "202204284"
},
{
"codename": {
"zh-CN": "升华 · 浊心斯卡蒂",
"en-US": "SUBLIMATION"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202204205.html",
"id": "202204205"
}
],
"2022-04-30": [
{
"codename": {
"zh-CN": "焦点 · 傀影",
"en-US": "FOCUS"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202203222.html",
"id": "202203222"
}
],
"2022-01-01": [
{
"codename": {
"zh-CN": "令",
"en-US": "LING"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220383.html",
"id": "20220383"
},
{
"codename": {
"zh-CN": "染尘烟 · 夕",
"en-US": "EVERYTHING IS A MIRACLE"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220321.html",
"id": "20220321"
}
],
"2021-11-01": [
{
"codename": {
"zh-CN": "耀骑士临光",
"en-US": "NEARL THE RADIANT KNIGHT"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220304.html",
"id": "20220304"
}
],
"2021-08-01": [
{
"codename": {
"zh-CN": "假日威龙陈",
"en-US": "Ch'en the Holungday"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220345.html",
"id": "20220345"
}
],
"2021-05-01": [
{
"codename": {
"zh-CN": "浊心斯卡蒂",
"en-US": "SKADI THE CORRUPTING HEART"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220396.html",
"id": "20220396"
}
],
"2021-02-01": [
{
"codename": {
"zh-CN": "夕",
"en-US": "DUSK"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202203263.html",
"id": "202203263"
},
{
"codename": {
"zh-CN": "乐逍遥 · 年",
"en-US": "UNFETTERED FREEDOM"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220362.html",
"id": "20220362"
}
],
"2020-11-01": [
{
"codename": {
"zh-CN": "迷迭香",
"en-US": "ROSMONTIS"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220378.html",
"id": "20220378"
},
{
"codename": {
"zh-CN": "恍惚 · W",
"en-US": "Wonder"
},
"type": "skin",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202206246.html",
"id": "202206246"
}
],
"2020-05-01": [
{
"codename": {
"zh-CN": "W",
"en-US": "W"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/20220319.html",
"id": "20220319"
}
],
"2020-01-01": [
{
"codename": {
"zh-CN": "年",
"en-US": "NIAN"
},
"type": "operator",
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202203231.html",
"id": "202203231"
}
]
}

View File

@@ -0,0 +1,3 @@
import baseConfig from '@aklive2d/eslint-config'
/** @type {import('eslint').Config} */
export default [...baseConfig]

View File

@@ -0,0 +1,101 @@
import jsdom from 'jsdom'
import path from 'path'
import { file } from '@aklive2d/libs'
import config from '@aklive2d/config'
const AUTO_UPDATE_FOLDER = path.resolve(
import.meta.dirname,
config.dir_name.auto_update
)
const OFFICIAL_INFO_JSON = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.official_info.official_info_json
)
export const update = async () => {
const f = await fetch('https://ak.hypergryph.com/archive/dynamicCompile/')
const html_text = await f.text()
const dom = new jsdom.JSDOM(html_text)
const scripts = dom.window.document.body.querySelectorAll('script')
let data
scripts.forEach((e) => {
if (e.textContent.includes('干员晋升')) {
data = JSON.parse(
e.textContent
.replace('self.__next_f.push([1,"c:', '')
.replace('\\n"])', '')
.replaceAll('\\', '')
)
}
})
const rows = data[0][3].initialData
const dict = {
length: rows.length,
dates: [],
}
let current_displayTime = rows[0].displayTime
let current_block = []
for (const row of rows) {
const displayTime = row.displayTime
if (displayTime !== current_displayTime) {
dict[current_displayTime] = current_block
dict.dates.push(current_displayTime)
current_displayTime = row.displayTime
current_block = []
}
current_block.push(get_row(row))
}
dict[current_displayTime] = current_block
dict.dates.push(current_displayTime)
file.writeSync(JSON.stringify(dict, null, 4), OFFICIAL_INFO_JSON)
}
const get_row = (row) => {
const type = row.type
let codename_zhCN, item_type
switch (type) {
case 0:
codename_zhCN = row.charName
item_type = 'operator'
break
case 1:
codename_zhCN = row.suitName + ' · ' + row.charName
item_type = 'skin'
break
default:
throw 'unknown type'
}
return {
codename: {
'zh-CN': codename_zhCN,
'en-US': row.codename,
},
type: item_type,
link: `https://ak.hypergryph.com/archive/dynamicCompile/${row.cid}.html`,
id: row.cid,
}
}
const generateMapping = () => {
const mapping = {}
const data = JSON.parse(file.readSync(OFFICIAL_INFO_JSON))
if (!data) return
Object.keys(data).forEach((key) => {
if (typeof data[key] === 'object') {
data[key].forEach((operator) => {
mapping[operator.id] = {
date: key,
...operator,
}
})
}
})
return mapping
}
export const mapping = generateMapping()

View File

@@ -0,0 +1,18 @@
{
"name": "@aklive2d/official-info",
"private": true,
"version": "0.0.0",
"main": "index.js",
"type": "module",
"dependencies": {
"jsdom": "^26.0.0",
"@aklive2d/libs": "workspace:*",
"@aklive2d/config": "workspace:*",
"@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*"
},
"scripts": {
"update": "mode=update node runner.js",
"lint": "eslint \"*.js\" \"**/*.js\" && prettier --check ."
}
}

View File

@@ -0,0 +1,11 @@
import baseConfig from '@aklive2d/prettier-config'
/**
* @type {import("prettier").Config}
*/
const config = {
...baseConfig,
semi: false,
}
export default config

View File

@@ -0,0 +1,20 @@
import { envParser } from '@aklive2d/libs'
import { update } from './index.js'
async function main() {
const { mode } = envParser.parse({
mode: {
type: 'string',
short: 'm',
},
})
switch (mode) {
case 'update':
await update()
break
default:
throw new Error(`Unknown mode: ${mode}`)
}
}
main()

1
packages/operator/.gitignore vendored Normal file
View File

@@ -0,0 +1 @@
data

View File

@@ -0,0 +1,3 @@
dist
data
auto_update

View File

@@ -0,0 +1,60 @@
chen: !include config/chen.yaml
dusk: !include config/dusk.yaml
dusk_everything_is_a_miracle: !include config/dusk_everything_is_a_miracle.yaml
ling: !include config/ling.yaml
nearl: !include config/nearl.yaml
nian: !include config/nian.yaml
nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml
phatom_focus: !include config/phatom_focus.yaml
rosmontis: !include config/rosmontis.yaml
skadi: !include config/skadi.yaml
skadi_sublimation: !include config/skadi_sublimation.yaml
w: !include config/w.yaml
w_wonder: !include config/w_wonder.yaml
specter: !include config/specter.yaml
gavial: !include config/gavial.yaml
surtr_colorful_wonderland: !include config/surtr_colorful_wonderland.yaml
lee_trust_your_eyes: !include config/lee_trust_your_eyes.yaml
texas_the_omertosa: !include config/texas_the_omertosa.yaml
nearl_relight: !include config/nearl_relight.yaml
rosmontis_become_anew: !include config/rosmontis_become_anew.yaml
passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
chongyue: !include config/chongyue.yaml
ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml
pozemka_snowy_plains_in_words: !include config/pozemka_snowy_plains_in_words.yaml
chen_ten_thousand_mountains: !include config/chen_ten_thousand_mountains.yaml
specter_born_as_one: !include config/specter_born_as_one.yaml
muelsyse: !include config/muelsyse.yaml
kaltsit_remnant: !include config/kaltsit_remnant.yaml
eyjafjalla_the_hvit_aska: !include config/eyjafjalla_the_hvit_aska.yaml
goldenglow_summer_flowers_fa394: !include config/goldenglow_summer_flowers_fa394.yaml
gavial_the_invincible_holiday_hd26: !include config/gavial_the_invincible_holiday_hd26.yaml
ling_towering_is_cliff_of_nostalgia: !include config/ling_towering_is_cliff_of_nostalgia.yaml
virtuosa: !include config/virtuosa.yaml
texas_the_omertosa_wingbreaker: !include config/texas_the_omertosa_wingbreaker.yaml
mwynar_w_dali: !include config/mwynar_w_dali.yaml
reed_the_frame_shadow_curator: !include config/reed_the_frame_shadow_curator.yaml
shu: !include config/shu.yaml
lin_heavenly_mirage: !include config/lin_heavenly_mirage.yaml
chongyue_alighting: !include config/chongyue_alighting.yaml
wisadel: !include config/wisadel.yaml
muelsyse_young_branch: !include config/muelsyse_young_branch.yaml
skadi_the_corrupting_heart_red_countess: !include config/skadi_the_corrupting_heart_red_countess.yaml
ines_under_the_flaming_dome: !include config/ines_under_the_flaming_dome.yaml
silverash_never_melting_ice: !include config/silverash_never_melting_ice.yaml
reed_the_frame_shadow_summer_flower: !include config/reed_the_frame_shadow_summer_flower.yaml
eyjafjalla_the_hvit_aska_a_picnic_before_a_long_trip: !include config/eyjafjalla_the_hvit_aska_a_picnic_before_a_long_trip.yaml
pepe: !include config/pepe.yaml
nightingale_iakhu_of_flows: !include config/nightingale_iakhu_of_flows.yaml
degenbrecher_the_shadow_of_dark_moon: !include config/degenbrecher_the_shadow_of_dark_moon.yaml
lappland_the_decadenza: !include config/lappland_the_decadenza.yaml
texas_the_omertosa_il_se_de_no: !include config/texas_the_omertosa_il_se_de_no.yaml
executor_the_ex_foedere_allmind_as_one: !include config/executor_the_ex_foedere_allmind_as_one.yaml
virtuosa_diversity_oneness: !include config/virtuosa_diversity_oneness.yaml
chongyue_allround_actor: !include config/chongyue_allround_actor.yaml
nian_thunderbolt_director: !include config/nian_thunderbolt_director.yaml
amiya_solo_around_the_world: !include config/amiya_solo_around_the_world.yaml
zuole_youthful_journey: !include config/zuole_youthful_journey.yaml
shu_spring_feast: !include config/shu_spring_feast.yaml
yu: !include config/yu.yaml

View File

@@ -0,0 +1,12 @@
filename: dyn_illust_char_1013_chen2
logo: logo_rhodes_override
fallback_name: char_1013_chen2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 假日威龙陈
en-US: Ch'en/Chen the Holungday
use_json: false

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1037_amiya3_sale#13
logo: logo_rhodes_override
fallback_name: char_1037_amiya3_sale#13
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: false
codename:
zh-CN: 寰宇独奏 · 阿米娅
en-US: Solo Around The World / Amiya
use_json: false
official_id: '202412473'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1013_chen2
logo: logo_rhodes_override
fallback_name: char_1013_chen2_2
viewport_left: 0
viewport_right: 0
viewport_top: 1
viewport_bottom: 1
invert_filter: false
codename:
zh-CN: 假日威龙陈
en-US: Ch'en/Chen the Holungday
use_json: false
official_id: '20220345'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1013_chen2_boc#6
logo: logo_rhodes_override
fallback_name: char_1013_chen2_boc#6
viewport_left: -1
viewport_right: 1
viewport_top: 2
viewport_bottom: -1
invert_filter: false
codename:
zh-CN: 万重山 · 假日威龙陈
en-US: Ten Thousand Mountains / Ch'en/Chen the Holungday
use_json: false
official_id: '202304659'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2024_chyue
logo: logo_sui
fallback_name: char_2024_chyue_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 重岳
en-US: Chongyue
use_json: false
official_id: '202301606'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2024_chyue_nian#10
logo: logo_sui
fallback_name: char_2024_chyue_nian#10
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 何处栖 · 重岳
en-US: Alighting / Chongyue
use_json: false
official_id: '202401812'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2024_chyue_cfa#1
logo: logo_sui
fallback_name: char_2024_chyue_cfa#1
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 全能演员 · 重岳
en-US: All-Round Actor / Chongyue
use_json: false
official_id: '202412452'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_4116_blkkgt_witch#5
logo: logo_kjerag
fallback_name: char_4116_blkkgt_witch#5
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 暗月的影子 · 锏
en-US: The Shadow Of The Dark Moon / Degenbrecher
use_json: false
official_id: '202410426'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2015_dusk
logo: logo_sui
fallback_name: char_2015_dusk_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN:
en-US: Dusk
use_json: false
official_id: '202203263'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2015_dusk_nian#7
logo: logo_sui
fallback_name: char_2015_dusk_nian#7
viewport_left: 10
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 染尘烟 · 夕
en-US: Everything is a Miracle / Dusk
use_json: false
official_id: '20220321'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1032_excu2_sale#12
logo: logo_Laterano
fallback_name: char_1032_excu2_sale#12
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 众志归一 · 圣约送葬人
en-US: Allmind as one / Executor the Ex Foedere
use_json: false
official_id: '202410488'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1016_agoat2
logo: logo_Leithanien
fallback_name: char_1016_agoat2_2
viewport_left: 1
viewport_right: 0
viewport_top: 5
viewport_bottom: 5
invert_filter: true
codename:
zh-CN: 纯烬艾雅法拉
en-US: Eyjafjalla the Hvít Aska
use_json: false
official_id: '202307865'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1016_agoat2_epoque#34
logo: logo_Leithanien
fallback_name: char_1016_agoat2_epoque#34
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 远行前的野餐 · 纯烬艾雅法拉
en-US: A Picnic Before A Long Trip / Eyjafjalla the Hvít Aska
use_json: true
official_id: '202407072'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1026_gvial2
logo: logo_rhodes_override
fallback_name: char_1026_gvial2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: false
codename:
zh-CN: 百练嘉维尔
en-US: Gavial the Invincible
use_json: false
official_id: '202208258'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1026_gvial2_summer#12
logo: logo_rhodes_override
fallback_name: char_1026_gvial2_summer#12
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: false
codename:
zh-CN: 悠然假日 HD26 · 百炼嘉维尔
en-US: Holiday HD26 / Gavial the Invincible
use_json: false
official_id: '202307886'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_377_gdglow_summer#12
logo: logo_victoria
fallback_name: char_377_gdglow_summer#12
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 夏卉 FA394 · 澄闪
en-US: Summer Flowers FA394 / Goldenglow
use_json: false
official_id: '202307824'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_4087_ines_boc#8
logo: logo_babel
fallback_name: char_4087_ines_boc#8
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 燃烧天穹下 · 伊内丝
en-US: Under the Flaming Dome / Ines
use_json: false
official_id: '202404087'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_003_kalts_boc#6
logo: logo_rhodes_override
fallback_name: char_003_kalts_boc#6
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: false
codename:
zh-CN: 残余 · 凯尔希
en-US: Remnant / Kal'tsit
use_json: false
official_id: '202304833'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1038_whitw2
logo: logo_chiave
fallback_name: char_1038_whitw2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 荒芜拉普兰德
en-US: Lappland the Decadenza
use_json: false
official_id: '202410440'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_322_lmlee_witch#3
logo: logo_lee
fallback_name: char_322_lmlee_witch#3
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 手到牌来 · 老鲤
en-US: Trust Your Eyes / Lee
use_json: false
official_id: '202210279'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_4080_lin_nian#10
logo: logo_lungmen
fallback_name: char_4080_lin_nian#10
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 列瑶台 · 林
en-US: Heavenly Mirage / Lin
use_json: false
official_id: '202401034'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2023_ling
logo: logo_sui
fallback_name: char_2023_ling_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN:
en-US: Ling
use_json: false
official_id: '20220383'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2023_ling_nian#9
logo: logo_sui
fallback_name: char_2023_ling_nian#9
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 濯缨 · 令
en-US: It Does Wash the Strings / Ling
use_json: false
official_id: '202301647'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_2023_ling_ncg#1
logo: logo_sui
fallback_name: char_2023_ling_ncg#1
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 崖高梦远 · 令
en-US: Towering is Cliff of Nostalgia
use_json: false
official_id: '202308807'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_437_mizuki_sale#7
logo: logo_higashi
fallback_name: char_437_mizuki_sale#7
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 夏日餮宴 · 水月
en-US: Summer Feast / Mizuki
use_json: false
official_id: '202211685'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_249_mlyss
logo: logo_rhine
fallback_name: char_249_mlyss_2
viewport_left: 0
viewport_right: 0
viewport_top: 3
viewport_bottom: 2
invert_filter: true
codename:
zh-CN: 缪尔赛思
en-US: Muelsyse
use_json: false
official_id: '202304611'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_249_mlyss_boc#8
logo: logo_rhine
fallback_name: char_249_mlyss_boc#8
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 新枝 · 缪尔赛思
en-US: Young Branch / Muelsyse
use_json: false
official_id: '202404090'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_4064_mlynar_epoque#28
logo: logo_kazimierz
fallback_name: char_4064_mlynar_epoque#28
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 远路 · 玛恩纳
en-US: W Dali / Młynar
use_json: false
official_id: '202310850'

View File

@@ -0,0 +1,13 @@
filename: dyn_illust_char_1014_nearl2
logo: logo_kazimierz
fallback_name: char_1014_nearl2_2
viewport_left: 2
viewport_right: 3
viewport_top: 10
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 耀骑士临光
en-US: Nearl the Radiant Knight
use_json: false
official_id: '20220304'

Some files were not shown because too many files have changed in this diff Show More