feat: automated most of operator config detection

This commit is contained in:
Haoyu Xu
2025-05-06 14:04:17 +08:00
parent ea7947d900
commit f3f84068da
89 changed files with 667 additions and 849 deletions

View File

@@ -194,7 +194,7 @@ ling: !include config/ling.yaml
nearl: !include config/nearl.yaml nearl: !include config/nearl.yaml
nian: !include config/nian.yaml nian: !include config/nian.yaml
nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml
phatom_focus: !include config/phatom_focus.yaml phantom_focus: !include config/phantom_focus.yaml
rosmontis: !include config/rosmontis.yaml rosmontis: !include config/rosmontis.yaml
skadi: !include config/skadi.yaml skadi: !include config/skadi.yaml
skadi_sublimation: !include config/skadi_sublimation.yaml skadi_sublimation: !include config/skadi_sublimation.yaml

View File

@@ -242,8 +242,12 @@
"@aklive2d/libs": "workspace:*", "@aklive2d/libs": "workspace:*",
"@aklive2d/official-info": "workspace:*", "@aklive2d/official-info": "workspace:*",
"@aklive2d/prettier-config": "workspace:*", "@aklive2d/prettier-config": "workspace:*",
"unidecode": "^1.1.0",
"yaml": "^2.7.0", "yaml": "^2.7.0",
}, },
"devDependencies": {
"@types/unidecode": "^1.1.0",
},
"peerDependencies": { "peerDependencies": {
"globals": ">=16.0.0", "globals": ">=16.0.0",
"typescript": ">=5.8.2", "typescript": ">=5.8.2",
@@ -685,6 +689,8 @@
"@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="], "@types/tough-cookie": ["@types/tough-cookie@4.0.5", "", {}, "sha512-/Ad8+nIOV7Rl++6f1BdKxFSMgmoqEoYbHRpPcx3JEfv8VRsQe9Z4mCXeJBzxs7mbHY/XOZZuXlRNfhpVPbs6ZA=="],
"@types/unidecode": ["@types/unidecode@1.1.0", "", {}, "sha512-NTIsFsTe9WRek39/8DDj7KiQ0nU33DHMrKwNHcD1rKlUvn4N0Rc4Di8q/Xavs8bsDZmBa4MMtQA8+HNgwfxC/A=="],
"@types/yauzl-promise": ["@types/yauzl-promise@4.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-qYEC3rJwqiJpdQ9b+bPNeuSY0c3JUM8vIuDy08qfuVN7xHm3ZDsHn2kGphUIB0ruEXrPGNXZ64nMUcu4fDjViQ=="], "@types/yauzl-promise": ["@types/yauzl-promise@4.0.1", "", { "dependencies": { "@types/node": "*" } }, "sha512-qYEC3rJwqiJpdQ9b+bPNeuSY0c3JUM8vIuDy08qfuVN7xHm3ZDsHn2kGphUIB0ruEXrPGNXZ64nMUcu4fDjViQ=="],
"@types/yazl": ["@types/yazl@2.4.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-/ifFjQtcKaoZOjl5NNCQRR0fAKafB3Foxd7J/WvFPTMea46zekapcR30uzkwIkKAAuq5T6d0dkwz754RFH27hg=="], "@types/yazl": ["@types/yazl@2.4.6", "", { "dependencies": { "@types/node": "*" } }, "sha512-/ifFjQtcKaoZOjl5NNCQRR0fAKafB3Foxd7J/WvFPTMea46zekapcR30uzkwIkKAAuq5T6d0dkwz754RFH27hg=="],
@@ -1535,6 +1541,8 @@
"undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="],
"unidecode": ["unidecode@1.1.0", "", {}, "sha512-GIp57N6DVVJi8dpeIU6/leJGdv7W65ZSXFLFiNmxvexXkc0nXdqUvhA/qL9KqBKsILxMwg5MnmYNOIDJLb5JVA=="],
"union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="], "union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="],
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="], "universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],

View File

@@ -34,6 +34,8 @@ module:
directory_assets: _directory directory_assets: _directory
MonoBehaviour: MonoBehaviour MonoBehaviour: MonoBehaviour
Texture2D: Texture2D Texture2D: Texture2D
character_table_json: character_table.json
skin_table_json: skin_table.json
title: title:
zh-CN: '明日方舟:' zh-CN: '明日方舟:'
en-US: 'Arknights: ' en-US: 'Arknights: '

View File

@@ -42,6 +42,8 @@ export type Config = {
directory_assets: string directory_assets: string
MonoBehaviour: string MonoBehaviour: string
Texture2D: string Texture2D: string
character_table_json: string
skin_table_json: string
title: { title: {
'zh-CN': string 'zh-CN': string
'en-US': string 'en-US': string

View File

@@ -3,6 +3,11 @@ import path from 'node:path'
import yauzl from 'yauzl-promise' import yauzl from 'yauzl-promise'
import yazl from 'yazl' import yazl from 'yazl'
type ReadOpts = {
encoding?: BufferEncoding
useAsPrefix?: boolean
}
export async function write( export async function write(
content: string | NodeJS.ArrayBufferView, content: string | NodeJS.ArrayBufferView,
filePath: string filePath: string
@@ -21,14 +26,32 @@ export function writeSync(
export async function read( export async function read(
filePath: string, filePath: string,
encoding: BufferEncoding = 'utf8' opts: ReadOpts = { encoding: 'utf8', useAsPrefix: false }
) { ) {
return await fsP.readFile(filePath, { encoding, flag: 'r' }) return await fsP.readFile(filePath, { ...opts, flag: 'r' })
} }
export function readSync(filePath: string, encoding: BufferEncoding = 'utf8') { export function readSync(
filePath: string,
opts: ReadOpts = { encoding: 'utf8', useAsPrefix: false }
) {
const useAsPrefix = opts.useAsPrefix
delete opts.useAsPrefix
if (useAsPrefix) {
const parent = path.dirname(filePath)
const filename = path.parse(filePath).name
const ext = path.parse(filePath).ext
const file = readdirSync(parent).find(
(e) => e.startsWith(filename) && e.endsWith(ext)
)
if (!file) return null
return fs.readFileSync(path.join(parent, file), {
...opts,
flag: 'r',
})
}
if (exists(filePath)) { if (exists(filePath)) {
return fs.readFileSync(filePath, { encoding, flag: 'r' }) return fs.readFileSync(filePath, { ...opts, flag: 'r' })
} }
return null return null
} }

View File

@@ -17,7 +17,7 @@ export function read(
return data return data
}, },
} }
const file = fs.readFileSync(file_dir, 'utf8') const file = fs.readFileSync(file_dir, { encoding: 'utf8' })
return parse(file, { return parse(file, {
customTags: [include, ...customTags], customTags: [include, ...customTags],
} as SchemaOptions) } as SchemaOptions)

View File

@@ -46,3 +46,21 @@ export interface OperatorConfig extends OfficialInfoOperatorConfig {
export type OfficialInfoMapping = { export type OfficialInfoMapping = {
[id: string]: OperatorConfig [id: string]: OperatorConfig
} }
export type OfficialInfoV2 = {
length: number
dates: string[]
info: OfficialInfoOperatorConfigV2[]
}
export type OfficialInfoOperatorConfigV2 = {
operatorName: string
skinName: {
'zh-CN': string
'en-US': string
}
type: 'operator' | 'skin'
link: string
id: number
date: string
}

View File

@@ -1 +1,2 @@
data data
auto_update

View File

@@ -5,7 +5,7 @@ ling: !include config/ling.yaml
nearl: !include config/nearl.yaml nearl: !include config/nearl.yaml
nian: !include config/nian.yaml nian: !include config/nian.yaml
nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml nian_unfettered_freedom: !include config/nian_unfettered_freedom.yaml
phatom_focus: !include config/phatom_focus.yaml phantom_focus: !include config/phantom_focus.yaml
rosmontis: !include config/rosmontis.yaml rosmontis: !include config/rosmontis.yaml
skadi: !include config/skadi.yaml skadi: !include config/skadi.yaml
skadi_sublimation: !include config/skadi_sublimation.yaml skadi_sublimation: !include config/skadi_sublimation.yaml
@@ -18,7 +18,7 @@ lee_trust_your_eyes: !include config/lee_trust_your_eyes.yaml
texas_the_omertosa: !include config/texas_the_omertosa.yaml texas_the_omertosa: !include config/texas_the_omertosa.yaml
nearl_relight: !include config/nearl_relight.yaml nearl_relight: !include config/nearl_relight.yaml
rosmontis_become_anew: !include config/rosmontis_become_anew.yaml rosmontis_become_anew: !include config/rosmontis_become_anew.yaml
passager_dream_in_a_moment: !include config/passager_dream_in_a_moment.yaml passenger_dream_in_a_moment: !include config/passenger_dream_in_a_moment.yaml
mizuki_summer_feast: !include config/mizuki_summer_feast.yaml mizuki_summer_feast: !include config/mizuki_summer_feast.yaml
chongyue: !include config/chongyue.yaml chongyue: !include config/chongyue.yaml
ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml ling_it_does_wash_the_strings: !include config/ling_it_does_wash_the_strings.yaml

View File

@@ -1,12 +1,3 @@
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: codename:
zh-CN: 假日威龙陈 zh-CN: 假日威龙陈
en-US: Ch'en/Chen the Holungday en-US: Ch'en/Chen the Holungday
use_json: false

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 寰宇独奏 · 阿米娅 zh-CN: 寰宇独奏 · 阿米娅
en-US: Solo Around The World / Amiya en-US: Solo Around The World / Amiya
use_json: false
official_id: '202412473' official_id: '202412473'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_332_archet_sale#14
logo: logo_Laterano
fallback_name: char_332_archet_sale#14
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 至虔者荣光 · 空弦 zh-CN: 至虔者荣光 · 空弦
en-US: Glory of the Devout / Archetto en-US: Glory of the Devout / Archetto
use_json: false
official_id: '202504953' official_id: '202504953'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 假日威龙陈 zh-CN: 假日威龙陈
en-US: Ch'en/Chen the Holungday en-US: Ch'en/Chen the Holungday
use_json: false
official_id: '20220345' official_id: '20220345'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 万重山 · 假日威龙陈 zh-CN: 万重山 · 假日威龙陈
en-US: Ten Thousand Mountains / Ch'en/Chen the Holungday en-US: Ten Thousand Mountains / Ch'en/Chen the Holungday
use_json: false
official_id: '202304659' official_id: '202304659'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 重岳 zh-CN: 重岳
en-US: Chongyue en-US: Chongyue
use_json: false
official_id: '202301606' official_id: '202301606'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 何处栖 · 重岳 zh-CN: 何处栖 · 重岳
en-US: Alighting / Chongyue en-US: Alighting / Chongyue
use_json: false
official_id: '202401812' official_id: '202401812'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 全能演员 · 重岳 zh-CN: 全能演员 · 重岳
en-US: All-Round Actor / Chongyue en-US: All-Round Actor / Chongyue
use_json: false
official_id: '202412452' official_id: '202412452'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 暗月的影子 · 锏 zh-CN: 暗月的影子 · 锏
en-US: The Shadow Of The Dark Moon / Degenbrecher en-US: The Shadow Of The Dark Moon / Degenbrecher
use_json: false
official_id: '202410426' official_id: '202410426'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: zh-CN:
en-US: Dusk en-US: Dusk
use_json: false
official_id: '202203263' official_id: '202203263'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 染尘烟 · 夕 zh-CN: 染尘烟 · 夕
en-US: Everything is a Miracle / Dusk en-US: Everything is a Miracle / Dusk
use_json: false
official_id: '20220321' official_id: '20220321'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 众志归一 · 圣约送葬人 zh-CN: 众志归一 · 圣约送葬人
en-US: Allmind as one / Executor the Ex Foedere en-US: Allmind as one / Executor the Ex Foedere
use_json: false
official_id: '202410488' official_id: '202410488'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1041_angel2
logo: logo_penguin
fallback_name: char_1041_angel2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 新约能天使 zh-CN: 新约能天使
en-US: Exusiai the New Covenant en-US: Exusiai the New Covenant
use_json: false
official_id: '202504941' official_id: '202504941'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 纯烬艾雅法拉 zh-CN: 纯烬艾雅法拉
en-US: Eyjafjalla the Hvít Aska en-US: Eyjafjalla the Hvít Aska
use_json: false
official_id: '202307865' official_id: '202307865'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 远行前的野餐 · 纯烬艾雅法拉 zh-CN: 远行前的野餐 · 纯烬艾雅法拉
en-US: A Picnic Before A Long Trip / Eyjafjalla the Hvít Aska en-US: A Picnic Before A Long Trip / Eyjafjalla the Hvít Aska
use_json: true
official_id: '202407072' official_id: '202407072'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 嘉维尔 zh-CN: 嘉维尔
en-US: Gavial the Invincible en-US: Gavial the Invincible
use_json: false
official_id: '202208258' official_id: '202208258'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 悠然假日 HD26 · 百炼嘉维尔 zh-CN: 悠然假日 HD26 · 百炼嘉维尔
en-US: Holiday HD26 / Gavial the Invincible en-US: Holiday HD26 / Gavial the Invincible
use_json: false
official_id: '202307886' official_id: '202307886'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 夏卉 FA394 · 澄闪 zh-CN: 夏卉 FA394 · 澄闪
en-US: Summer Flowers FA394 / Goldenglow en-US: Summer Flowers FA394 / Goldenglow
use_json: false
official_id: '202307824' official_id: '202307824'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_4087_ines_ambienceSynesthesia
logo: logo_babel
fallback_name: char_4087_ines_ambienceSynesthesia#5
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 蝶舞华章 · 伊内丝 zh-CN: 蝶舞华章 · 伊内丝
en-US: Melodic Flutter / Ines en-US: Melodic Flutter / Ines
use_json: false
official_id: "202504968" official_id: "202504968"

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 燃烧天穹下 · 伊内丝 zh-CN: 燃烧天穹下 · 伊内丝
en-US: Under the Flaming Dome / Ines en-US: Under the Flaming Dome / Ines
use_json: false
official_id: '202404087' official_id: '202404087'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 残余 · 凯尔希 zh-CN: 残余 · 凯尔希
en-US: Remnant / Kal'tsit en-US: Remnant / Kal'tsit/Kaltsit
use_json: false
official_id: '202304833' official_id: '202304833'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_124_kroos_sale#14
logo: logo_reserve1
fallback_name: char_124_kroos_sale#14
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 星月漂流记 · 克洛丝 zh-CN: 星月漂流记 · 克洛丝
en-US: Moonlit Voyage / Kroos en-US: Moonlit Voyage / Kroos
use_json: false
official_id: '202504992' official_id: '202504992'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 荒芜拉普兰德 zh-CN: 荒芜拉普兰德
en-US: Lappland the Decadenza en-US: Lappland the Decadenza
use_json: false
official_id: '202410440' official_id: '202410440'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 手到牌来 · 老鲤 zh-CN: 手到牌来 · 老鲤
en-US: Trust Your Eyes / Lee en-US: Trust Your Eyes / Lee
use_json: false
official_id: '202210279' official_id: '202210279'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 列瑶台 · 林 zh-CN: 列瑶台 · 林
en-US: Heavenly Mirage / Lin en-US: Heavenly Mirage / Lin
use_json: false
official_id: '202401034' official_id: '202401034'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: zh-CN:
en-US: Ling en-US: Ling
use_json: false
official_id: '20220383' official_id: '20220383'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 濯缨 · 令 zh-CN: 濯缨 · 令
en-US: It Does Wash the Strings / Ling en-US: It Does Wash the Strings / Ling
use_json: false
official_id: '202301647' official_id: '202301647'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 崖高梦远 · 令 zh-CN: 崖高梦远 · 令
en-US: Towering is Cliff of Nostalgia en-US: Towering is Cliff of Nostalgia / Ling
use_json: false
official_id: '202308807' official_id: '202308807'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_4133_logos_ambienceSynesthesia#6
logo: logo_elite
fallback_name: char_4133_logos_ambienceSynesthesia#6
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 辉煌的静谧 · 逻各斯 zh-CN: 辉煌的静谧 · 逻各斯
en-US: Radiant Serenity / Logos en-US: Radiant Serenity / Logos
use_json: false
official_id: "202504989" official_id: "202504989"

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 夏日餮宴 · 水月 zh-CN: 夏日餮宴 · 水月
en-US: Summer Feast / Mizuki en-US: Summer Feast / Mizuki
use_json: false
official_id: '202211685' official_id: '202211685'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 缪尔赛思 zh-CN: 缪尔赛思
en-US: Muelsyse en-US: Muelsyse
use_json: false
official_id: '202304611' official_id: '202304611'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_249_mlyss_ambienceSynesthesia#6
logo: logo_rhine
fallback_name: char_249_mlyss_ambienceSynesthesia#6
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 漫步于黄金之梦 · 缪尔赛思 zh-CN: 漫步于黄金之梦 · 缪尔赛思
en-US: Golden Reverie / Muelsyse en-US: Golden Reverie / Muelsyse
use_json: false
official_id: "202504900" official_id: "202504900"

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 新枝 · 缪尔赛思 zh-CN: 新枝 · 缪尔赛思
en-US: Young Branch / Muelsyse en-US: Young Branch / Muelsyse
use_json: false
official_id: '202404090' official_id: '202404090'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 远路 · 玛恩纳 zh-CN: 远路 · 玛恩纳
en-US: W Dali / Młynar en-US: W Dali / Młynar
use_json: false
official_id: '202310850' official_id: '202310850'

View File

@@ -1,13 +1,4 @@
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: codename:
zh-CN: 耀骑士临光 zh-CN: 耀骑士临光
en-US: Nearl the Radiant Knight en-US: Nearl the Radiant Knight
use_json: false
official_id: '20220304' official_id: '20220304'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1014_nearl2_epoque#17
logo: logo_kazimierz
fallback_name: char_1014_nearl2_epoque#17
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 复现荣光 · 耀骑士临光 zh-CN: 复现荣光 · 耀骑士临光
en-US: Relight / Nearl en-US: Relight / Nearl the Radiant Knight
use_json: false
official_id: '202210623' official_id: '202210623'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2014_nian
logo: logo_sui
fallback_name: char_2014_nian_2
viewport_left: 2
viewport_right: 2
viewport_top: 3
viewport_bottom: 5
invert_filter: true
codename: codename:
zh-CN: zh-CN:
en-US: Nian en-US: Nian
use_json: false
official_id: '202203231' official_id: '202203231'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2014_nian_cfa#1
logo: logo_sui
fallback_name: char_2014_nian_cfa#1
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 霹雳导演 · 年 zh-CN: 霹雳导演 · 年
en-US: Thunderbolt Director / Nian en-US: Thunderbolt Director / Nian
use_json: false
official_id: '202412491' official_id: '202412491'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2014_nian_nian#4
logo: logo_sui
fallback_name: char_2014_nian_nian#4
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 乐逍遥 · 年 zh-CN: 乐逍遥 · 年
en-US: Unfettered Freedom / Nian en-US: Unfettered Freedom / Nian
use_json: false
official_id: '20220362' official_id: '20220362'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_179_cgbird_sightseer#1
logo: logo_followers
fallback_name: char_179_cgbird_sightseer#1
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 流辉 · 夜莺 zh-CN: 流辉 · 夜莺
en-US: Iakhu of Flows / Nightingale en-US: Iakhu of Flows / Nightingale
use_json: false
official_id: '202407435' official_id: '202407435'

View File

@@ -1,13 +0,0 @@
filename: dyn_illust_char_472_pasngr_epoque#17
logo: logo_sargon
fallback_name: char_472_pasngr_epoque#17
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename:
zh-CN: 今昔须臾之梦 · 异客
en-US: Dream in a Moment / Passager
use_json: false
official_id: '202210664'

View File

@@ -0,0 +1,4 @@
codename:
zh-CN: 今昔须臾之梦 · 异客
en-US: Dream in a Moment / Passenger
official_id: '202210664'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_4058_pepe
logo: logo_sargon
fallback_name: char_4058_pepe_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 佩佩 zh-CN: 佩佩
en-US: Pepe en-US: Pepe
use_json: true
official_id: '202407013' official_id: '202407013'

View File

@@ -0,0 +1,4 @@
codename:
zh-CN: 焦点 · 傀影
en-US: Focus / Phantom
official_id: '202203222'

View File

@@ -1,13 +0,0 @@
filename: dyn_illust_char_250_phatom_sale#4
logo: logo_victoria
fallback_name: char_250_phatom_sale#4
viewport_left: 0
viewport_right: 0
viewport_top: 5
viewport_bottom: 1
invert_filter: true
codename:
zh-CN: 焦点 · 傀影
en-US: Focus / Phatom
use_json: false
official_id: '202203222'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_4055_bgsnow_wild#7
logo: logo_rhodes_override
fallback_name: char_4055_bgsnow_wild#7
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: false
codename: codename:
zh-CN: 字句中的雪原 · 鸿雪 zh-CN: 字句中的雪原 · 鸿雪
en-US: Snowy Plains in Words / Позёмка en-US: Snowy Plains in Words / Позёмка/Pozemka
use_json: false
official_id: '202302698' official_id: '202302698'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1020_reed2_epoque#30
logo: logo_dublinn
fallback_name: char_1020_reed2_epoque#30
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 博物 · 焰影苇草 zh-CN: 博物 · 焰影苇草
en-US: Curator / Reed The Flame Shadow en-US: Curator / Reed The Flame Shadow
use_json: false
official_id: '202401871' official_id: '202401871'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1020_reed2_summer#17
logo: logo_dublinn
fallback_name: char_1020_reed2_summer#17
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 夏卉 FA075 · 焰影苇草 zh-CN: 夏卉 FA075 · 焰影苇草
en-US: Summer Flowers FA075 / Reed The Flame Shadow en-US: Summer Flowers FA075 / Reed The Flame Shadow
use_json: true
official_id: '202407051' official_id: '202407051'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_391_rosmon
logo: logo_elite
fallback_name: char_391_rosmon_2
viewport_left: 0
viewport_right: -14
viewport_top: -38
viewport_bottom: -1
invert_filter: true
codename: codename:
zh-CN: 迷迭香 zh-CN: 迷迭香
en-US: Rosmontis en-US: Rosmontis
use_json: false
official_id: '20220378' official_id: '20220378'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_391_rosmon_epoque#17
logo: logo_elite
fallback_name: char_391_rosmon_epoque#17
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 拥抱新生 · 迷迭香 zh-CN: 拥抱新生 · 迷迭香
en-US: Become Anew / Rosmontis en-US: Become Anew / Rosmontis
use_json: false
official_id: '202210632' official_id: '202210632'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2025_shu
logo: logo_sui
fallback_name: char_2025_shu_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: zh-CN:
en-US: Shu en-US: Shu
use_json: false
official_id: '202401025' official_id: '202401025'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2025_shu_nian#11
logo: logo_sui
fallback_name: char_2025_shu_nian#11
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 春日宴 · 黍 zh-CN: 春日宴 · 黍
en-US: Spring Feast / Shu en-US: Spring Feast / Shu
use_json: false
official_id: '202501936' official_id: '202501936'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_172_svrash_ambienceSynesthesia#4
logo: logo_kjerag
fallback_name: char_172_svrash_ambienceSynesthesia#4
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 不融冰 · 银灰 zh-CN: 不融冰 · 银灰
en-US: Never-Melting Ice / SilverAsh en-US: Never-Melting Ice / SilverAsh
use_json: false
official_id: '202404066' official_id: '202404066'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1012_skadi2
logo: logo_egir
fallback_name: char_1012_skadi2_2
viewport_left: -5
viewport_right: -10
viewport_top: 0
viewport_bottom: -12
invert_filter: true
codename: codename:
zh-CN: 浊心斯卡蒂 zh-CN: 浊心斯卡蒂
en-US: Skadi the Corrupting Heart en-US: Skadi the Corrupting Heart
use_json: false
official_id: '20220396' official_id: '20220396'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1012_skadi2_boc#4
logo: logo_egir
fallback_name: char_1012_skadi2_boc#4
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 升华 · 浊心斯卡蒂 zh-CN: 升华 · 浊心斯卡蒂
en-US: Sublimation / Skadi the Corrupting Heart en-US: Sublimation / Skadi the Corrupting Heart
use_json: false
official_id: '202204205' official_id: '202204205'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1012_skadi2_iteration#2
logo: logo_egir
fallback_name: char_1012_skadi2_iteration#2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 红女爵 · 浊心斯卡蒂 zh-CN: 红女爵 · 浊心斯卡蒂
en-US: Red Countess / Skadi the Corrupting Heart en-US: Red Countess / Skadi the Corrupting Heart
use_json: false
official_id: '202404008' official_id: '202404008'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1023_ghost2
logo: logo_abyssal
fallback_name: char_1023_ghost2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 归溟幽灵鲨 zh-CN: 归溟幽灵鲨
en-US: Specter the Unchained en-US: Specter the Unchained
use_json: false
official_id: '202204284' official_id: '202204284'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1023_ghost2_boc#6
logo: logo_abyssal
fallback_name: char_1023_ghost2_boc#6
viewport_left: -1
viewport_right: 1
viewport_top: 0
viewport_bottom: 1
invert_filter: true
codename: codename:
zh-CN: 生而为一 · 归溟幽灵鲨 zh-CN: 生而为一 · 归溟幽灵鲨
en-US: Born as One / Specter the Unchained en-US: Born as One / Specter the Unchained
use_json: false
official_id: '202304670' official_id: '202304670'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_350_surtr_summer#9
logo: logo_rhodes_override
fallback_name: char_350_surtr_summer#9
viewport_left: 0
viewport_right: 6
viewport_top: 1
viewport_bottom: 0
invert_filter: false
codename: codename:
zh-CN: 缤纷奇境 CW03 · 史尔特尔 zh-CN: 缤纷奇境 CW03 · 史尔特尔
en-US: Colorful Wonderland CW03 / Surtr en-US: Colorful Wonderland CW03 / Surtr
use_json: false
official_id: '202208297' official_id: '202208297'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1028_texas2
logo: logo_penguin
fallback_name: char_1028_texas2_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 缄默德克萨斯 zh-CN: 缄默德克萨斯
en-US: Texas the Omertosa en-US: Texas the Omertosa
use_json: false
official_id: '202210210' official_id: '202210210'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1028_texas2_epoque#36
logo: logo_penguin
fallback_name: char_1028_texas2_epoque#36
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 幽兰秘辛 · 缄默德克萨斯 zh-CN: 幽兰秘辛 · 缄默德克萨斯
en-US: Il Segreto Della Notte / Texas the Omertosa en-US: Il Segreto Della Notte / Texas the Omertosa
use_json: false
official_id: '202410409' official_id: '202410409'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1028_texas2_iteration#1
logo: logo_penguin
fallback_name: char_1028_texas2_iteration#1
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 破翼者 · 缄默德克萨斯 zh-CN: 破翼者 · 缄默德克萨斯
en-US: Wingbreaker / Texas the Omertosa en-US: Wingbreaker / Texas the Omertosa
use_json: false
official_id: '202310899' official_id: '202310899'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_245_cello
logo: logo_Laterano
fallback_name: char_245_cello_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 塑心 zh-CN: 塑心
en-US: Virtuosa en-US: Virtuosa
use_json: false
official_id: '202310848' official_id: '202310848'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_245_cello_sale#12
logo: logo_Laterano
fallback_name: char_245_cello_sale#12
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 无我唯识 · 塑心 zh-CN: 无我唯识 · 塑心
en-US: Diversity Oneness / Virtuosa en-US: Diversity Oneness / Virtuosa
use_json: false
official_id: '202410467' official_id: '202410467'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_113_cqbw
logo: logo_babel
fallback_name: char_113_cqbw_2
viewport_left: 3
viewport_right: -3
viewport_top: 0
viewport_bottom: 1
invert_filter: true
codename: codename:
zh-CN: W zh-CN: W
en-US: W en-US: W
use_json: false
official_id: '20220319' official_id: '20220319'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_113_cqbw_epoque#7
logo: logo_babel
fallback_name: char_113_cqbw_epoque#7
viewport_left: 0
viewport_right: 0
viewport_top: 1
viewport_bottom: -4
invert_filter: true
codename: codename:
zh-CN: 恍惚 · W zh-CN: 恍惚 · W
en-US: Wonder / W en-US: Wonder / W
use_json: false
official_id: '202206246' official_id: '202206246'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1035_wisdel
logo: logo_babel
fallback_name: char_1035_wisdel_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 维什戴尔 zh-CN: 维什戴尔
en-US: Wisadel en-US: Wisadel
use_json: false
official_id: '202404049' official_id: '202404049'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_1035_wisdel_sale#14
logo: logo_babel
fallback_name: char_1035_wisdel_sale#14
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 超新星 · 维什戴尔 zh-CN: 超新星 · 维什戴尔
en-US: Supernova / Wisadel en-US: Supernova / Wisadel
use_json: false
official_id: '202504974' official_id: '202504974'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_2026_yu
logo: logo_sui
fallback_name: char_2026_yu_2
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: zh-CN:
en-US: Yu en-US: Yu
use_json: false
official_id: '202501414' official_id: '202501414'

View File

@@ -1,13 +1,4 @@
filename: dyn_illust_char_4121_zuole_nian#11
logo: logo_yan
fallback_name: char_4121_zuole_nian#11
viewport_left: 0
viewport_right: 0
viewport_top: 0
viewport_bottom: 0
invert_filter: true
codename: codename:
zh-CN: 少年游 · 左乐 zh-CN: 少年游 · 左乐
en-US: Youthful Journey / Zuo Le en-US: Youthful Journey / Zuo Le
use_json: false
official_id: '202501927' official_id: '202501927'

View File

@@ -1,9 +1,24 @@
import path from 'node:path' import path from 'node:path'
import { stringify } from 'yaml'
import { yaml, file, alphaComposite } from '@aklive2d/libs' import { yaml, file, alphaComposite } from '@aklive2d/libs'
import config from '@aklive2d/config' import config from '@aklive2d/config'
import { mapping as officialInfoMapping } from '@aklive2d/official-info' import { mapping as officialInfoMapping } from '@aklive2d/official-info'
import type { Config, PortraitHub, PortraitJson, AssetsJson } from './types.ts' import type {
Config,
AssetsJson,
CharacterTableJson,
SkinTableJson,
} from './types.ts'
import {
findLogo,
findSkinEntry,
findSkel,
getActualFilename,
} from './libs/utils.ts'
export const AUTO_UPDATE_FOLDER = path.resolve(
import.meta.dirname,
config.dir_name.auto_update
)
export const CONFIG_PATH = path.resolve( export const CONFIG_PATH = path.resolve(
import.meta.dirname, import.meta.dirname,
@@ -14,78 +29,16 @@ export const OPERATOR_SOURCE_FOLDER = path.resolve(
import.meta.dirname, import.meta.dirname,
config.dir_name.data config.dir_name.data
) )
const DIST_DIR = path.join(import.meta.dirname, config.dir_name.dist) export const DIST_DIR = path.join(import.meta.dirname, config.dir_name.dist)
export const CONFIG_FOLDER = path.resolve(
const getVoiceFolders = (name: string) => { import.meta.dirname,
return config.dir_name.voice.sub.map((sub) => config.module.operator.config
path.join( )
OPERATOR_SOURCE_FOLDER,
name,
config.dir_name.voice.main,
sub.name
)
)
}
const getExtractedFolder = (name: string) => {
return path.join(OPERATOR_SOURCE_FOLDER, name, config.dir_name.extracted)
}
const getConfigFolder = () => {
return path.join(import.meta.dirname, config.module.operator.config)
}
const getDistFolder = (name: string) => {
return path.join(DIST_DIR, config.module.operator.operator, name)
}
export const has = (name: string) => { export const has = (name: string) => {
return Object.keys(operators).includes(name) return Object.keys(operators).includes(name)
} }
const generateMapping = () => {
if (officialInfoMapping) {
for (const [operatorName, operator] of Object.entries(CONFIG)) {
const operatorInfo = officialInfoMapping[operator.official_id]
// add title
operator.title = `${config.module.operator.title['en-US']}${operator.codename['en-US']} - ${config.module.operator.title['zh-CN']}${operator.codename['zh-CN']}`
// add type
operator.type = operatorInfo.type
// add link
operator.link = operatorName
// id
operator.id = getOperatorId(operator.filename).replace(
/^(char_)(\d+)(_.+)$/g,
'$2'
)
operator.date = operatorInfo.date
}
}
return CONFIG
}
const copyVoices = (name: string) => {
file.symlinkAll(
path.join(OPERATOR_SOURCE_FOLDER, name, config.dir_name.voice.main),
path.join(DIST_DIR, config.dir_name.voice.main, name)
)
}
const copyLogos = () => {
file.symlink(
path.join(OPERATOR_SOURCE_FOLDER, config.module.operator.logos_assets),
path.join(DIST_DIR, config.module.operator.logos)
)
}
const operators = generateMapping()
export default operators
export function getOperatorId(name: string, matcher = '$2$3$4') { export function getOperatorId(name: string, matcher = '$2$3$4') {
return name.replace(/^(.*)(char_[\d]+)(_[A-Za-z0-9]+)(|_.*)$/g, matcher) return name.replace(/^(.*)(char_[\d]+)(_[A-Za-z0-9]+)(|_.*)$/g, matcher)
} }
@@ -94,125 +47,28 @@ export const getOperatorAlternativeId = (id: string) => {
return getOperatorId(id, '$2$3') return getOperatorId(id, '$2$3')
} }
export const build = async (namesToBuild: string[]) => {
const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild
console.log('Generating assets for', names.length, 'operators')
for (const name of names) {
await generateAssets(name)
copyVoices(name)
}
copyLogos()
}
const generateAssets = async (name: string) => {
const extractedDir = getExtractedFolder(name)
const outDir = getDistFolder(name)
file.rmdir(outDir)
file.mkdir(outDir)
const fallback_name = operators[name].fallback_name
const fallbackFilename = `${fallback_name}.png`
const alphaCompositeFilename = `${path.parse(fallbackFilename).name}[alpha].png`
if (file.exists(path.join(extractedDir, alphaCompositeFilename))) {
const fallbackBuffer = await alphaComposite.process(
fallbackFilename,
alphaCompositeFilename,
extractedDir
)
file.writeSync(
fallbackBuffer,
path.join(getDistFolder(name), fallbackFilename)
)
} else {
await file.copy(
path.join(extractedDir, fallbackFilename),
path.join(getDistFolder(name), fallbackFilename)
)
}
// generate portrait
const portraitDir = path.join(
OPERATOR_SOURCE_FOLDER,
config.module.operator.portraits
)
const portraitHubContent = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
'portrait_hub.json'
)
)
if (!portraitHubContent) throw new Error('portrait_hub.json not found')
const portraitHub: PortraitHub = JSON.parse(portraitHubContent)
const fallback_name_lowerCase = fallback_name.toLowerCase()
const portraitItem = portraitHub._sprites.find(
(item) => item.name.toLowerCase() === fallback_name_lowerCase
)
if (!portraitItem) throw new Error(`portrait ${fallback_name} not found`)
const portraitAtlas = portraitItem.atlas
const portraitJsonText = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
`portraits#${portraitAtlas}.json`
)
)
if (!portraitJsonText)
throw new Error(`portrait ${fallback_name} json not found`)
const portraitJson: PortraitJson = JSON.parse(portraitJsonText)
const item = portraitJson._sprites.find(
(item) => item.name.toLowerCase() === fallback_name_lowerCase
)
if (!item) throw new Error(`portrait ${fallback_name} not found`)
const rect = {
...item.rect,
rotate: item.rotate,
}
const protraitFilename = `portraits#${portraitAtlas}.png`
const portraitBuffer = await alphaComposite.process(
protraitFilename,
`${path.parse(protraitFilename).name}a.png`,
path.join(portraitDir, config.module.operator.Texture2D)
)
const croppedBuffer = await alphaComposite.crop(portraitBuffer, rect)
file.writeSync(
croppedBuffer,
path.join(getDistFolder(name), `${fallback_name}_portrait.png`)
)
await generateAssetsJson(
operators[name].filename,
extractedDir,
getDistFolder(name),
{
useJSON: operators[name].use_json,
}
)
}
export const generateAssetsJson = async ( export const generateAssetsJson = async (
filename: string, filename: string,
extractedDir: string, extractedDir: string,
targetDir: string, targetDir: string,
_opts: { _opts: {
useJSON?: boolean
useSymLink?: boolean useSymLink?: boolean
} = { } = {
useJSON: false,
useSymLink: true, useSymLink: true,
} }
) => { ) => {
const assetsJson: AssetsJson = [] const assetsJson: AssetsJson = []
let skelFilename /*
if (_opts.useJSON) { * Special Cases:
skelFilename = `${filename}.json` * - ines_melodic_flutter
} else { */
skelFilename = `${filename}.skel` filename = getActualFilename(filename, extractedDir)
}
const skelFilename = findSkel(filename, extractedDir)
const atlasFilename = `${filename}.atlas` const atlasFilename = `${filename}.atlas`
const atlasPath = path.join(extractedDir, atlasFilename) const atlasPath = path.join(extractedDir, atlasFilename)
let atlas = await file.read(atlasPath) let atlas = file.readSync(atlasPath) as string
const matches = atlas.match(new RegExp(/(.*).png/g)) const matches = atlas.match(new RegExp(/(.*).png/g))
if (!matches) if (!matches)
throw new Error(`No matches found in atlas file ${atlasFilename}`) throw new Error(`No matches found in atlas file ${atlasFilename}`)
@@ -258,31 +114,82 @@ export const generateAssetsJson = async (
}) })
} }
export const init = (name: string, id: string) => { const characterTable = (() => {
const voiceFolders = getVoiceFolders(name) const character_table_json = path.resolve(
const extractedFolder = getExtractedFolder(name) AUTO_UPDATE_FOLDER,
const operatorConfigFolder = getConfigFolder() config.module.operator.character_table_json
const foldersToCreate = [extractedFolder, ...voiceFolders]
const template = yaml.read(
path.resolve(operatorConfigFolder, config.module.operator.template_yaml)
) )
foldersToCreate.forEach((dir) => { const t = file.readSync(character_table_json, {
file.mkdir(dir) useAsPrefix: true,
}) }) as string
const currentOpertor = officialInfoMapping[id] if (!t) throw new Error('character_table.json not found')
if (currentOpertor === undefined) { return JSON.parse(t) as CharacterTableJson
throw new Error('Invalid operator id') })()
export const skinTable = (() => {
const skinTable = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.operator.skin_table_json
)
const t = file.readSync(skinTable, {
useAsPrefix: true,
}) as string
if (!t) throw new Error('skin_table.json not found')
return JSON.parse(t) as SkinTableJson
})()
const generateMapping = () => {
if (officialInfoMapping) {
for (const [operatorName, operator] of Object.entries(CONFIG)) {
const operatorInfo = officialInfoMapping[operator.official_id]
const type = operatorInfo.type
const name =
type === 'skin'
? operatorInfo.codename['zh-CN'].split(' · ')[0]
: operatorInfo.codename['en-US']
const skinEntry = findSkinEntry(skinTable, name, type)
operator.filename = skinEntry.dynIllustId.replace(/_2$/, '')
operator.fallback_name =
type === 'skin'
? skinEntry.skinId.replace(/@/, '_')
: `${skinEntry.charId}_2`
// add title
operator.title = `${config.module.operator.title['en-US']}${operator.codename['en-US']} - ${config.module.operator.title['zh-CN']}${operator.codename['zh-CN']}`
// add type
operator.type = operatorInfo.type
// add link
operator.link = operatorName
// add default viewport
operator.viewport_left = 0
operator.viewport_right = 0
operator.viewport_top = 0
operator.viewport_bottom = 0
operator.voice_id = skinEntry.voiceId
const logo = findLogo(characterTable, skinEntry.charId)
operator.logo = logo.logo
operator.invert_filter = logo.invert_filter
operator.color =
skinEntry.displaySkin.colorList.find((e) => e !== '') || '#000'
// id
operator.id = getOperatorId(operator.filename).replace(
/^(char_)(\d+)(_.+)$/g,
'$2'
)
operator.date = operatorInfo.date
}
} }
template.official_id = currentOpertor.id
template.codename = currentOpertor.codename
file.writeSync( return CONFIG
stringify(template),
path.resolve(operatorConfigFolder, `${name}.yaml`)
)
file.appendSync(
`${name}: !include ${config.module.operator.config}/${name}.yaml\n`,
CONFIG_PATH
)
} }
const operators = generateMapping()
export default operators

View File

@@ -0,0 +1,117 @@
import path from 'node:path'
import config from '@aklive2d/config'
import { file, alphaComposite } from '@aklive2d/libs'
import operators, {
DIST_DIR,
OPERATOR_SOURCE_FOLDER,
generateAssetsJson,
} from '../index.ts'
import { getExtractedFolder, getDistFolder } from './utils.ts'
import type { PortraitHub, PortraitJson } from '../types.ts'
export const build = async (namesToBuild: string[]) => {
const names = !namesToBuild.length ? Object.keys(operators) : namesToBuild
console.log('Generating assets for', names.length, 'operators')
for (const name of names) {
await generateAssets(name)
copyVoices(name)
}
copyLogos()
}
const copyVoices = (name: string) => {
file.symlinkAll(
path.join(OPERATOR_SOURCE_FOLDER, name, config.dir_name.voice.main),
path.join(DIST_DIR, config.dir_name.voice.main, name)
)
}
const copyLogos = () => {
file.symlink(
path.join(OPERATOR_SOURCE_FOLDER, config.module.operator.logos_assets),
path.join(DIST_DIR, config.module.operator.logos)
)
}
const generateAssets = async (name: string) => {
const extractedDir = getExtractedFolder(name)
const outDir = getDistFolder(name)
file.rmdir(outDir)
file.mkdir(outDir)
const fallback_name = operators[name].fallback_name
const fallbackFilename = `${fallback_name}.png`
const alphaCompositeFilename = `${path.parse(fallbackFilename).name}[alpha].png`
if (file.exists(path.join(extractedDir, alphaCompositeFilename))) {
const fallbackBuffer = await alphaComposite.process(
fallbackFilename,
alphaCompositeFilename,
extractedDir
)
file.writeSync(
fallbackBuffer,
path.join(getDistFolder(name), fallbackFilename)
)
} else {
await file.copy(
path.join(extractedDir, fallbackFilename),
path.join(getDistFolder(name), fallbackFilename)
)
}
// generate portrait
const portraitDir = path.join(
OPERATOR_SOURCE_FOLDER,
config.module.operator.portraits
)
const portraitHubContent = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
'portrait_hub.json'
)
) as string
if (!portraitHubContent) throw new Error('portrait_hub.json not found')
const portraitHub: PortraitHub = JSON.parse(portraitHubContent)
const fallback_name_lowerCase = fallback_name.toLowerCase()
const portraitItem = portraitHub._sprites.find(
(item) => item.name.toLowerCase() === fallback_name_lowerCase
)
if (!portraitItem) throw new Error(`portrait ${fallback_name} not found`)
const portraitAtlas = portraitItem.atlas
const portraitJsonText = file.readSync(
path.join(
portraitDir,
config.module.operator.MonoBehaviour,
`portraits#${portraitAtlas}.json`
)
) as string
if (!portraitJsonText)
throw new Error(`portrait ${fallback_name} json not found`)
const portraitJson: PortraitJson = JSON.parse(portraitJsonText)
const item = portraitJson._sprites.find(
(item) => item.name.toLowerCase() === fallback_name_lowerCase
)
if (!item) throw new Error(`portrait ${fallback_name} not found`)
const rect = {
...item.rect,
rotate: item.rotate,
}
const protraitFilename = `portraits#${portraitAtlas}.png`
const portraitBuffer = await alphaComposite.process(
protraitFilename,
`${path.parse(protraitFilename).name}a.png`,
path.join(portraitDir, config.module.operator.Texture2D)
)
const croppedBuffer = await alphaComposite.crop(portraitBuffer, rect)
file.writeSync(
croppedBuffer,
path.join(getDistFolder(name), `${fallback_name}_portrait.png`)
)
await generateAssetsJson(
operators[name].filename,
extractedDir,
getDistFolder(name)
)
}

View File

@@ -0,0 +1,55 @@
import path from 'node:path'
import { stringify } from 'yaml'
import { yaml, file } from '@aklive2d/libs'
import config from '@aklive2d/config'
import { mapping as officialInfoMapping } from '@aklive2d/official-info'
import { CONFIG_PATH, skinTable, CONFIG_FOLDER } from '../index.ts'
import {
getVoiceFolders,
getExtractedFolder,
findSkinEntry,
findCodename,
} from './utils.ts'
export const init = (name: string, id: string) => {
const voiceFolders = getVoiceFolders(name)
const extractedFolder = getExtractedFolder(name)
const operatorConfigFolder = CONFIG_FOLDER
const foldersToCreate = [extractedFolder, ...voiceFolders]
const template = yaml.read(
path.resolve(operatorConfigFolder, config.module.operator.template_yaml)
)
foldersToCreate.forEach((dir) => {
file.mkdir(dir)
})
const currentOpertor = officialInfoMapping[id]
if (currentOpertor === undefined) {
throw new Error('Invalid operator id')
}
template.official_id = currentOpertor.id
try {
const entryName =
currentOpertor.type === 'skin'
? currentOpertor.codename['zh-CN'].split(' · ')[0]
: currentOpertor.codename['en-US']
const skinEntry = findSkinEntry(
skinTable,
entryName,
currentOpertor.type
)
template.codename = findCodename(skinEntry, currentOpertor)
} catch (e: unknown) {
console.log(e as string)
template.codename = currentOpertor.codename
}
file.writeSync(
stringify(template),
path.resolve(operatorConfigFolder, `${name}.yaml`)
)
file.appendSync(
`${name}: !include ${config.module.operator.config}/${name}.yaml\n`,
CONFIG_PATH
)
}

View File

@@ -0,0 +1,25 @@
import path from 'node:path'
import { githubDownload } from '@aklive2d/downloader'
import config from '@aklive2d/config'
import { AUTO_UPDATE_FOLDER } from '../index.ts'
export const update = async () => {
const character_table_json = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.operator.character_table_json
)
const skin_table_json = path.resolve(
AUTO_UPDATE_FOLDER,
config.module.operator.skin_table_json
)
await githubDownload(
`https://api.github.com/repos/Kengxxiao/ArknightsGameData/commits?path=zh_CN/gamedata/excel/character_table.json`,
`https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/character_table.json`,
character_table_json
)
await githubDownload(
`https://api.github.com/repos/Kengxxiao/ArknightsGameData/commits?path=zh_CN/gamedata/excel/skin_table.json`,
`https://raw.githubusercontent.com/Kengxxiao/ArknightsGameData/master/zh_CN/gamedata/excel/skin_table.json`,
skin_table_json
)
}

View File

@@ -0,0 +1,204 @@
import path from 'node:path'
import unidecode from 'unidecode'
import config from '@aklive2d/config'
import { DIST_DIR, OPERATOR_SOURCE_FOLDER } from '../index.ts'
import { file } from '@aklive2d/libs'
import {
CharacterTableJson,
SkinTableJson,
OperatorEntryType,
SkinTableJsonCharSkinEntry,
Codename,
} from '../types.ts'
import type { OfficialInfoOperatorConfig } from '@aklive2d/official-info/types'
export const getExtractedFolder = (name: string) => {
return path.join(OPERATOR_SOURCE_FOLDER, name, config.dir_name.extracted)
}
export const getDistFolder = (name: string) => {
return path.join(DIST_DIR, config.module.operator.operator, name)
}
export const getVoiceFolders = (name: string) => {
return config.dir_name.voice.sub.map((sub) =>
path.join(
OPERATOR_SOURCE_FOLDER,
name,
config.dir_name.voice.main,
sub.name
)
)
}
export const findLogo = (characterTable: CharacterTableJson, id: string) => {
const SEARCH_ORDER = ['teamId', 'groupId', 'nationId'] as const
let logo: string | null = null
const entry = characterTable[id]
if (!entry) throw new Error(`Character ${id} not found`)
for (const key of SEARCH_ORDER) {
logo = entry[key]
if (logo) break
}
if (logo === null) throw new Error(`Logo not found for character ${id}`)
let invert_filter = true
if (logo === 'rhodes') {
logo = 'rhodes_override'
invert_filter = false
}
logo = `logo_${logo}`
const logoFiles = file
.readdirSync(
path.resolve(
OPERATOR_SOURCE_FOLDER,
config.module.operator.logos_assets
)
)
.map((e) => {
const name = path.parse(e).name
return {
file: name,
name: name.toLowerCase(),
}
})
const logoFile = logoFiles.find((f) => f.name === logo)
if (!logoFile)
throw new Error(`Logo file not found for character ${id}, ${logo}`)
logo = logoFile.file
return {
logo,
invert_filter,
}
}
export const findSkinEntry = (
skinTableJson: SkinTableJson,
name: string,
type: OperatorEntryType
) => {
const OVERWRITE_ENTRIES = {
WISADEL: "Wiš'adel",
} as const
type OverwriteKeys = keyof typeof OVERWRITE_ENTRIES
name = OVERWRITE_ENTRIES[name as OverwriteKeys] ?? name
const dynSkinEntries = Object.values(skinTableJson.charSkins).filter(
(entry) => entry.dynIllustId !== null
)
let entry: SkinTableJsonCharSkinEntry | undefined
if (type === 'operator') {
entry = dynSkinEntries.find(
(e) => e.displaySkin.modelName?.toLowerCase() === name.toLowerCase()
)
} else if (type === 'skin') {
entry = dynSkinEntries.find(
(e) => e.displaySkin.skinName?.toLowerCase() === name.toLowerCase()
)
} else {
throw new Error(`Invalid type: ${type}`)
}
if (!entry) throw new Error(`Skin entry not found for ${name}`)
return entry
}
/**
* Name from Official Info sometimes is incorrect, can only be used as a
* reference
* @param skinEntry
* @param officialInfo
* @returns
*/
export const findCodename = (
skinEntry: SkinTableJsonCharSkinEntry,
officialInfo: OfficialInfoOperatorConfig
) => {
const UPPER_CASE_EXCEPTION_WORDS = [
'the',
'is',
'of',
'and',
'for',
'a',
'an',
'are',
'in',
'as',
]
const codename: Codename = { 'zh-CN': '', 'en-US': '' }
const regexp = /[^(\w) ]/
let modelNameNormalized = skinEntry.displaySkin.modelName
const hasSpecialCharInModelName = regexp.test(
skinEntry.displaySkin.modelName
)
if (hasSpecialCharInModelName) {
modelNameNormalized = unidecode(modelNameNormalized).replace(regexp, '')
const modelNameArray = skinEntry.displaySkin.modelName.split(' ')
const modelNameNormalizedArray = modelNameNormalized.split(' ')
modelNameArray.forEach((word, index) => {
if (word !== modelNameNormalizedArray[index]) {
modelNameNormalizedArray[index] =
`${word}/${modelNameNormalizedArray[index]}`
}
})
modelNameNormalized = modelNameNormalizedArray.join(' ')
}
if (skinEntry.displaySkin.skinName) {
let engSkinName = officialInfo.codename['en-US']
const engkinNameArray = engSkinName.split(' ')
engkinNameArray.forEach((word, index) => {
if (/^[a-zA-Z]+$/.test(word)) {
word = word.toLowerCase()
if (UPPER_CASE_EXCEPTION_WORDS.includes(word)) {
engkinNameArray[index] = word
} else {
engkinNameArray[index] =
word[0].toUpperCase() + word.slice(1)
}
}
})
engSkinName = engkinNameArray.join(' ')
codename['zh-CN'] = officialInfo.codename['zh-CN'].replace(/ +$/, '')
codename['en-US'] = `${engSkinName} / ${modelNameNormalized}`
} else {
codename['zh-CN'] = officialInfo.codename['zh-CN'].replace(/ +$/, '')
codename['en-US'] = modelNameNormalized
}
return codename
}
export const getActualFilename = (filename: string, dir: string) => {
const files = file.readdirSync(dir)
const actualFilename = files.find((e) => {
const name = path.parse(e).name
return filename.startsWith(name) && !name.endsWith('_Start')
})
return actualFilename ? path.parse(actualFilename).name : filename
}
export const findSkel = (name: string, dir: string) => {
const files = file.readdirSync(dir)
const skel = files.find((e) => {
const actualName = path.parse(e)
return (
name.startsWith(actualName.name) &&
!actualName.name.endsWith('_Start') &&
actualName.ext === '.skel'
)
})
const json = files.find((e) => {
const actualName = path.parse(e)
return (
name.startsWith(actualName.name) &&
!actualName.name.endsWith('_Start') &&
actualName.ext === '.json'
)
})
if (skel) {
return skel
} else if (json) {
return json
} else {
throw new Error('No skel or json file found')
}
}

View File

@@ -5,12 +5,13 @@
"main": "index.ts", "main": "index.ts",
"type": "module", "type": "module",
"dependencies": { "dependencies": {
"yaml": "^2.7.0",
"@aklive2d/libs": "workspace:*",
"@aklive2d/config": "workspace:*", "@aklive2d/config": "workspace:*",
"@aklive2d/official-info": "workspace:*",
"@aklive2d/eslint-config": "workspace:*", "@aklive2d/eslint-config": "workspace:*",
"@aklive2d/prettier-config": "workspace:*" "@aklive2d/libs": "workspace:*",
"@aklive2d/official-info": "workspace:*",
"@aklive2d/prettier-config": "workspace:*",
"unidecode": "^1.1.0",
"yaml": "^2.7.0"
}, },
"peerDependencies": { "peerDependencies": {
"globals": ">=16.0.0", "globals": ">=16.0.0",
@@ -18,9 +19,13 @@
"typescript": ">=5.8.2" "typescript": ">=5.8.2"
}, },
"scripts": { "scripts": {
"update": "mode=update bun runner.ts",
"build": "mode=build bun runner.ts", "build": "mode=build bun runner.ts",
"init": "mode=init bun runner.ts", "init": "mode=init bun runner.ts",
"lint": "eslint && prettier --check .", "lint": "eslint && prettier --check .",
"build:cleanup": "rm -rf ./dist ./data" "build:cleanup": "rm -rf ./dist ./data"
},
"devDependencies": {
"@types/unidecode": "^1.1.0"
} }
} }

View File

@@ -1,5 +1,7 @@
import { envParser } from '@aklive2d/libs' import { envParser } from '@aklive2d/libs'
import { build, init } from './index.ts' import { init } from './libs/initer.ts'
import { build } from './libs/builder.ts'
import { update } from './libs/updater.ts'
type Args = { type Args = {
mode: string mode: string
@@ -27,6 +29,9 @@ async function main() {
case 'build': case 'build':
await build(name) await build(name)
break break
case 'update':
await update()
break
case 'init': case 'init':
if (!name.length) { if (!name.length) {
throw new Error('Please set the operator name.') throw new Error('Please set the operator name.')

View File

@@ -1,22 +1,26 @@
export type Codename = { 'zh-CN': string; 'en-US': string } export type Codename = { 'zh-CN': string; 'en-US': string }
export type OperatorEntryType = 'operator' | 'skin'
export interface OperatorConfig { export interface OperatorConfig {
filename: string filename: string
logo: string logo: string
fallback_name: string fallback_name: string
viewport_left: number viewport_left: number // should be default to 0 in the future
viewport_right: number viewport_right: number
viewport_top: number viewport_top: number
viewport_bottom: number viewport_bottom: number
invert_filter: boolean invert_filter: boolean
codename: Codename codename: Codename
use_json: boolean use_json: boolean // can be detected automatically
official_id: string official_id: string // reverse lookup from official_update?
title: string title: string
type: 'operator' | 'skin' type: OperatorEntryType
link: string link: string
id: string id: string // used in directory
date: string date: string
voice_id: string | null
color: string
} }
export type Config = { export type Config = {
@@ -91,3 +95,40 @@ export type AssetsJson = {
path?: string path?: string
content?: string | Buffer<ArrayBufferLike> | Buffer content?: string | Buffer<ArrayBufferLike> | Buffer
}[] }[]
/**
* Minimum type for character_table.json
* Implmented for:
* - findLogo
*/
export type CharacterTableJson = {
[id: string]: {
name: string // operator chinese name
appellation: string // operator english name
nationId: string // class i logo classifier
groupId: string | null // class ii logo classifier
teamId: string | null // class iii logo classifier
}
}
/**
* Minimum type for skin_table.json
* Intended for:
* - replacing OperatorConfig.filename
*/
export type SkinTableJsonCharSkinEntry = {
skinId: string
charId: string
dynIllustId: string // when replacing filename, remove suffix `_2`
voiceId: string | null // if null, use default voice
displaySkin: {
skinName: string | null // if null, not a skin
modelName: string
colorList: string[]
}
}
export type SkinTableJson = {
charSkins: {
[id: string]: SkinTableJsonCharSkinEntry
}
}

View File

@@ -4,6 +4,11 @@ import operators, {
OPERATOR_SOURCE_FOLDER, OPERATOR_SOURCE_FOLDER,
generateAssetsJson, generateAssetsJson,
} from '@aklive2d/operator' } from '@aklive2d/operator'
import {
getExtractedFolder,
getActualFilename,
findSkel,
} from '@aklive2d/operator/libs/utils'
import type { OperatorConfig } from '@aklive2d/operator/types' import type { OperatorConfig } from '@aklive2d/operator/types'
import { DIST_DIR as ASSETS_DIST_DIR } from '@aklive2d/assets' import { DIST_DIR as ASSETS_DIST_DIR } from '@aklive2d/assets'
import { file, env } from '@aklive2d/libs' import { file, env } from '@aklive2d/libs'
@@ -106,10 +111,14 @@ export const copyShowcaseData = (
fn(source, target) fn(source, target)
} }
}) })
const filename = getActualFilename(
operators[name].filename,
getExtractedFolder(name)
)
const buildConfig = { const buildConfig = {
insight_id: config.insight.id, insight_id: config.insight.id,
link: operators[name].link, link: operators[name].link,
filename: operators[name].filename.replace(/#/g, '%23'), filename: filename.replace(/#/g, '%23'),
logo_filename: operators[name].logo, logo_filename: operators[name].logo,
fallback_filename: operators[name].fallback_name.replace(/#/g, '%23'), fallback_filename: operators[name].fallback_name.replace(/#/g, '%23'),
viewport_left: operators[name].viewport_left, viewport_left: operators[name].viewport_left,
@@ -126,7 +135,7 @@ export const copyShowcaseData = (
voice_folders: config.dir_name.voice, voice_folders: config.dir_name.voice,
music_folder: config.module.assets.music, music_folder: config.module.assets.music,
music_mapping: musicMapping.musicFileMapping, music_mapping: musicMapping.musicFileMapping,
use_json: operators[name].use_json, use_json: findSkel(filename, getExtractedFolder(name)).endsWith('json'),
default_assets_dir: `${config.app.showcase.assets}/`, default_assets_dir: `${config.app.showcase.assets}/`,
logo_dir: logo_dir:
mode === 'build:directory' mode === 'build:directory'
@@ -190,6 +199,14 @@ export const copyDirectoryData = async ({
Object.values(operators).reduce( Object.values(operators).reduce(
(acc, cur) => { (acc, cur) => {
const curD = cur as DirectoryOperatorConfig const curD = cur as DirectoryOperatorConfig
curD.filename = getActualFilename(
operators[curD.link].filename,
getExtractedFolder(curD.link)
)
curD.use_json = findSkel(
curD.filename,
getExtractedFolder(curD.link)
).endsWith('json')
const date = curD.date const date = curD.date
curD.workshopId = null curD.workshopId = null
@@ -200,7 +217,7 @@ export const copyDirectoryData = async ({
cur.link, cur.link,
config.module.project_json.project_json config.module.project_json.project_json
) )
) ) as string
if (!text) { if (!text) {
console.log(`No workshop id for ${cur.link}!`) console.log(`No workshop id for ${cur.link}!`)
} else { } else {