feat: automated most of operator config detection
This commit is contained in:
@@ -194,7 +194,7 @@ 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
|
||||
phantom_focus: !include config/phantom_focus.yaml
|
||||
rosmontis: !include config/rosmontis.yaml
|
||||
skadi: !include config/skadi.yaml
|
||||
skadi_sublimation: !include config/skadi_sublimation.yaml
|
||||
|
||||
8
bun.lock
8
bun.lock
@@ -242,8 +242,12 @@
|
||||
"@aklive2d/libs": "workspace:*",
|
||||
"@aklive2d/official-info": "workspace:*",
|
||||
"@aklive2d/prettier-config": "workspace:*",
|
||||
"unidecode": "^1.1.0",
|
||||
"yaml": "^2.7.0",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/unidecode": "^1.1.0",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"globals": ">=16.0.0",
|
||||
"typescript": ">=5.8.2",
|
||||
@@ -685,6 +689,8 @@
|
||||
|
||||
"@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/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=="],
|
||||
|
||||
"unidecode": ["unidecode@1.1.0", "", {}, "sha512-GIp57N6DVVJi8dpeIU6/leJGdv7W65ZSXFLFiNmxvexXkc0nXdqUvhA/qL9KqBKsILxMwg5MnmYNOIDJLb5JVA=="],
|
||||
|
||||
"union": ["union@0.5.0", "", { "dependencies": { "qs": "^6.4.0" } }, "sha512-N6uOhuW6zO95P3Mel2I2zMsbsanvvtgn6jVqJv4vbVcz/JN0OkL9suomjQGmWtxJQXOCqUJvquc1sMeNz/IwlA=="],
|
||||
|
||||
"universalify": ["universalify@2.0.1", "", {}, "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw=="],
|
||||
|
||||
@@ -34,6 +34,8 @@ module:
|
||||
directory_assets: _directory
|
||||
MonoBehaviour: MonoBehaviour
|
||||
Texture2D: Texture2D
|
||||
character_table_json: character_table.json
|
||||
skin_table_json: skin_table.json
|
||||
title:
|
||||
zh-CN: '明日方舟:'
|
||||
en-US: 'Arknights: '
|
||||
|
||||
@@ -42,6 +42,8 @@ export type Config = {
|
||||
directory_assets: string
|
||||
MonoBehaviour: string
|
||||
Texture2D: string
|
||||
character_table_json: string
|
||||
skin_table_json: string
|
||||
title: {
|
||||
'zh-CN': string
|
||||
'en-US': string
|
||||
|
||||
@@ -3,6 +3,11 @@ import path from 'node:path'
|
||||
import yauzl from 'yauzl-promise'
|
||||
import yazl from 'yazl'
|
||||
|
||||
type ReadOpts = {
|
||||
encoding?: BufferEncoding
|
||||
useAsPrefix?: boolean
|
||||
}
|
||||
|
||||
export async function write(
|
||||
content: string | NodeJS.ArrayBufferView,
|
||||
filePath: string
|
||||
@@ -21,14 +26,32 @@ export function writeSync(
|
||||
|
||||
export async function read(
|
||||
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)) {
|
||||
return fs.readFileSync(filePath, { encoding, flag: 'r' })
|
||||
return fs.readFileSync(filePath, { ...opts, flag: 'r' })
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -17,7 +17,7 @@ export function read(
|
||||
return data
|
||||
},
|
||||
}
|
||||
const file = fs.readFileSync(file_dir, 'utf8')
|
||||
const file = fs.readFileSync(file_dir, { encoding: 'utf8' })
|
||||
return parse(file, {
|
||||
customTags: [include, ...customTags],
|
||||
} as SchemaOptions)
|
||||
|
||||
@@ -77,7 +77,7 @@
|
||||
{
|
||||
"codename": {
|
||||
"zh-CN": "新约能天使",
|
||||
"en-US": "Exusiai the New Covenant"
|
||||
"en-US": "Exusiai the New Covenant"
|
||||
},
|
||||
"type": "operator",
|
||||
"link": "https://ak.hypergryph.com/archive/dynamicCompile/202504941.html",
|
||||
|
||||
@@ -46,3 +46,21 @@ export interface OperatorConfig extends OfficialInfoOperatorConfig {
|
||||
export type OfficialInfoMapping = {
|
||||
[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
|
||||
}
|
||||
|
||||
1
packages/operator/.gitignore
vendored
1
packages/operator/.gitignore
vendored
@@ -1 +1,2 @@
|
||||
data
|
||||
auto_update
|
||||
|
||||
@@ -5,7 +5,7 @@ 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
|
||||
phantom_focus: !include config/phantom_focus.yaml
|
||||
rosmontis: !include config/rosmontis.yaml
|
||||
skadi: !include config/skadi.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
|
||||
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
|
||||
passenger_dream_in_a_moment: !include config/passenger_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
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 假日威龙陈
|
||||
en-US: Ch'en/Chen the Holungday
|
||||
use_json: false
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 寰宇独奏 · 阿米娅
|
||||
en-US: Solo Around The World / Amiya
|
||||
use_json: false
|
||||
official_id: '202412473'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 至虔者荣光 · 空弦
|
||||
en-US: Glory of the Devout / Archetto
|
||||
use_json: false
|
||||
official_id: '202504953'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 假日威龙陈
|
||||
en-US: Ch'en/Chen the Holungday
|
||||
use_json: false
|
||||
official_id: '20220345'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 万重山 · 假日威龙陈
|
||||
en-US: Ten Thousand Mountains / Ch'en/Chen the Holungday
|
||||
use_json: false
|
||||
official_id: '202304659'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 重岳
|
||||
en-US: Chongyue
|
||||
use_json: false
|
||||
official_id: '202301606'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 何处栖 · 重岳
|
||||
en-US: Alighting / Chongyue
|
||||
use_json: false
|
||||
official_id: '202401812'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 全能演员 · 重岳
|
||||
en-US: All-Round Actor / Chongyue
|
||||
use_json: false
|
||||
official_id: '202412452'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 暗月的影子 · 锏
|
||||
en-US: The Shadow Of The Dark Moon / Degenbrecher
|
||||
use_json: false
|
||||
official_id: '202410426'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 夕
|
||||
en-US: Dusk
|
||||
use_json: false
|
||||
official_id: '202203263'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 染尘烟 · 夕
|
||||
en-US: Everything is a Miracle / Dusk
|
||||
use_json: false
|
||||
official_id: '20220321'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 众志归一 · 圣约送葬人
|
||||
en-US: Allmind as one / Executor the Ex Foedere
|
||||
use_json: false
|
||||
official_id: '202410488'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 新约能天使
|
||||
en-US: Exusiai the New Covenant
|
||||
use_json: false
|
||||
official_id: '202504941'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 纯烬艾雅法拉
|
||||
en-US: Eyjafjalla the Hvít Aska
|
||||
use_json: false
|
||||
official_id: '202307865'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 远行前的野餐 · 纯烬艾雅法拉
|
||||
en-US: A Picnic Before A Long Trip / Eyjafjalla the Hvít Aska
|
||||
use_json: true
|
||||
official_id: '202407072'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 百练嘉维尔
|
||||
zh-CN: 百炼嘉维尔
|
||||
en-US: Gavial the Invincible
|
||||
use_json: false
|
||||
official_id: '202208258'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 悠然假日 HD26 · 百炼嘉维尔
|
||||
en-US: Holiday HD26 / Gavial the Invincible
|
||||
use_json: false
|
||||
official_id: '202307886'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 夏卉 FA394 · 澄闪
|
||||
en-US: Summer Flowers FA394 / Goldenglow
|
||||
use_json: false
|
||||
official_id: '202307824'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 蝶舞华章 · 伊内丝
|
||||
en-US: Melodic Flutter / Ines
|
||||
use_json: false
|
||||
official_id: "202504968"
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 燃烧天穹下 · 伊内丝
|
||||
en-US: Under the Flaming Dome / Ines
|
||||
use_json: false
|
||||
official_id: '202404087'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 残余 · 凯尔希
|
||||
en-US: Remnant / Kal'tsit
|
||||
use_json: false
|
||||
en-US: Remnant / Kal'tsit/Kaltsit
|
||||
official_id: '202304833'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 星月漂流记 · 克洛丝
|
||||
en-US: Moonlit Voyage / Kroos
|
||||
use_json: false
|
||||
official_id: '202504992'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 荒芜拉普兰德
|
||||
en-US: Lappland the Decadenza
|
||||
use_json: false
|
||||
official_id: '202410440'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 手到牌来 · 老鲤
|
||||
en-US: Trust Your Eyes / Lee
|
||||
use_json: false
|
||||
official_id: '202210279'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 列瑶台 · 林
|
||||
en-US: Heavenly Mirage / Lin
|
||||
use_json: false
|
||||
official_id: '202401034'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 令
|
||||
en-US: Ling
|
||||
use_json: false
|
||||
official_id: '20220383'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 濯缨 · 令
|
||||
en-US: It Does Wash the Strings / Ling
|
||||
use_json: false
|
||||
official_id: '202301647'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 崖高梦远 · 令
|
||||
en-US: Towering is Cliff of Nostalgia
|
||||
use_json: false
|
||||
en-US: Towering is Cliff of Nostalgia / Ling
|
||||
official_id: '202308807'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 辉煌的静谧 · 逻各斯
|
||||
en-US: Radiant Serenity / Logos
|
||||
use_json: false
|
||||
official_id: "202504989"
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 夏日餮宴 · 水月
|
||||
en-US: Summer Feast / Mizuki
|
||||
use_json: false
|
||||
official_id: '202211685'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 缪尔赛思
|
||||
en-US: Muelsyse
|
||||
use_json: false
|
||||
official_id: '202304611'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 漫步于黄金之梦 · 缪尔赛思
|
||||
en-US: Golden Reverie / Muelsyse
|
||||
use_json: false
|
||||
official_id: "202504900"
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 新枝 · 缪尔赛思
|
||||
en-US: Young Branch / Muelsyse
|
||||
use_json: false
|
||||
official_id: '202404090'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 远路 · 玛恩纳
|
||||
en-US: W Dali / Młynar
|
||||
use_json: false
|
||||
official_id: '202310850'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 耀骑士临光
|
||||
en-US: Nearl the Radiant Knight
|
||||
use_json: false
|
||||
official_id: '20220304'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 复现荣光 · 耀骑士临光
|
||||
en-US: Relight / Nearl
|
||||
use_json: false
|
||||
en-US: Relight / Nearl the Radiant Knight
|
||||
official_id: '202210623'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 年
|
||||
en-US: Nian
|
||||
use_json: false
|
||||
official_id: '202203231'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 霹雳导演 · 年
|
||||
en-US: Thunderbolt Director / Nian
|
||||
use_json: false
|
||||
official_id: '202412491'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 乐逍遥 · 年
|
||||
en-US: Unfettered Freedom / Nian
|
||||
use_json: false
|
||||
official_id: '20220362'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 流辉 · 夜莺
|
||||
en-US: Iakhu of Flows / Nightingale
|
||||
use_json: false
|
||||
official_id: '202407435'
|
||||
|
||||
@@ -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'
|
||||
@@ -0,0 +1,4 @@
|
||||
codename:
|
||||
zh-CN: 今昔须臾之梦 · 异客
|
||||
en-US: Dream in a Moment / Passenger
|
||||
official_id: '202210664'
|
||||
@@ -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:
|
||||
zh-CN: 佩佩
|
||||
en-US: Pepe
|
||||
use_json: true
|
||||
official_id: '202407013'
|
||||
|
||||
4
packages/operator/config/phantom_focus.yaml
Normal file
4
packages/operator/config/phantom_focus.yaml
Normal file
@@ -0,0 +1,4 @@
|
||||
codename:
|
||||
zh-CN: 焦点 · 傀影
|
||||
en-US: Focus / Phantom
|
||||
official_id: '202203222'
|
||||
@@ -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'
|
||||
@@ -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:
|
||||
zh-CN: 字句中的雪原 · 鸿雪
|
||||
en-US: Snowy Plains in Words / Позёмка
|
||||
use_json: false
|
||||
en-US: Snowy Plains in Words / Позёмка/Pozemka
|
||||
official_id: '202302698'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 博物 · 焰影苇草
|
||||
en-US: Curator / Reed The Flame Shadow
|
||||
use_json: false
|
||||
official_id: '202401871'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 夏卉 FA075 · 焰影苇草
|
||||
en-US: Summer Flowers FA075 / Reed The Flame Shadow
|
||||
use_json: true
|
||||
official_id: '202407051'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 迷迭香
|
||||
en-US: Rosmontis
|
||||
use_json: false
|
||||
official_id: '20220378'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 拥抱新生 · 迷迭香
|
||||
en-US: Become Anew / Rosmontis
|
||||
use_json: false
|
||||
official_id: '202210632'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 黍
|
||||
en-US: Shu
|
||||
use_json: false
|
||||
official_id: '202401025'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 春日宴 · 黍
|
||||
en-US: Spring Feast / Shu
|
||||
use_json: false
|
||||
official_id: '202501936'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 不融冰 · 银灰
|
||||
en-US: Never-Melting Ice / SilverAsh
|
||||
use_json: false
|
||||
official_id: '202404066'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 浊心斯卡蒂
|
||||
en-US: Skadi the Corrupting Heart
|
||||
use_json: false
|
||||
official_id: '20220396'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 升华 · 浊心斯卡蒂
|
||||
en-US: Sublimation / Skadi the Corrupting Heart
|
||||
use_json: false
|
||||
official_id: '202204205'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 红女爵 · 浊心斯卡蒂
|
||||
en-US: Red Countess / Skadi the Corrupting Heart
|
||||
use_json: false
|
||||
official_id: '202404008'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 归溟幽灵鲨
|
||||
en-US: Specter the Unchained
|
||||
use_json: false
|
||||
official_id: '202204284'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 生而为一 · 归溟幽灵鲨
|
||||
en-US: Born as One / Specter the Unchained
|
||||
use_json: false
|
||||
official_id: '202304670'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 缤纷奇境 CW03 · 史尔特尔
|
||||
en-US: Colorful Wonderland CW03 / Surtr
|
||||
use_json: false
|
||||
official_id: '202208297'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 缄默德克萨斯
|
||||
en-US: Texas the Omertosa
|
||||
use_json: false
|
||||
official_id: '202210210'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 幽兰秘辛 · 缄默德克萨斯
|
||||
en-US: Il Segreto Della Notte / Texas the Omertosa
|
||||
use_json: false
|
||||
official_id: '202410409'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 破翼者 · 缄默德克萨斯
|
||||
en-US: Wingbreaker / Texas the Omertosa
|
||||
use_json: false
|
||||
official_id: '202310899'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 塑心
|
||||
en-US: Virtuosa
|
||||
use_json: false
|
||||
official_id: '202310848'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 无我唯识 · 塑心
|
||||
en-US: Diversity Oneness / Virtuosa
|
||||
use_json: false
|
||||
official_id: '202410467'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: W
|
||||
en-US: W
|
||||
use_json: false
|
||||
official_id: '20220319'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 恍惚 · W
|
||||
en-US: Wonder / W
|
||||
use_json: false
|
||||
official_id: '202206246'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 维什戴尔
|
||||
en-US: Wisadel
|
||||
use_json: false
|
||||
official_id: '202404049'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 超新星 · 维什戴尔
|
||||
en-US: Supernova / Wisadel
|
||||
use_json: false
|
||||
official_id: '202504974'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 余
|
||||
en-US: Yu
|
||||
use_json: false
|
||||
official_id: '202501414'
|
||||
|
||||
@@ -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:
|
||||
zh-CN: 少年游 · 左乐
|
||||
en-US: Youthful Journey / Zuo Le
|
||||
use_json: false
|
||||
official_id: '202501927'
|
||||
|
||||
@@ -1,9 +1,24 @@
|
||||
import path from 'node:path'
|
||||
import { stringify } from 'yaml'
|
||||
import { yaml, file, alphaComposite } from '@aklive2d/libs'
|
||||
import config from '@aklive2d/config'
|
||||
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(
|
||||
import.meta.dirname,
|
||||
@@ -14,78 +29,16 @@ export const OPERATOR_SOURCE_FOLDER = path.resolve(
|
||||
import.meta.dirname,
|
||||
config.dir_name.data
|
||||
)
|
||||
const DIST_DIR = path.join(import.meta.dirname, config.dir_name.dist)
|
||||
|
||||
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
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
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 DIST_DIR = path.join(import.meta.dirname, config.dir_name.dist)
|
||||
export const CONFIG_FOLDER = path.resolve(
|
||||
import.meta.dirname,
|
||||
config.module.operator.config
|
||||
)
|
||||
|
||||
export const has = (name: string) => {
|
||||
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') {
|
||||
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')
|
||||
}
|
||||
|
||||
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 (
|
||||
filename: string,
|
||||
extractedDir: string,
|
||||
targetDir: string,
|
||||
_opts: {
|
||||
useJSON?: boolean
|
||||
useSymLink?: boolean
|
||||
} = {
|
||||
useJSON: false,
|
||||
useSymLink: true,
|
||||
}
|
||||
) => {
|
||||
const assetsJson: AssetsJson = []
|
||||
|
||||
let skelFilename
|
||||
if (_opts.useJSON) {
|
||||
skelFilename = `${filename}.json`
|
||||
} else {
|
||||
skelFilename = `${filename}.skel`
|
||||
}
|
||||
/*
|
||||
* Special Cases:
|
||||
* - ines_melodic_flutter
|
||||
*/
|
||||
filename = getActualFilename(filename, extractedDir)
|
||||
|
||||
const skelFilename = findSkel(filename, extractedDir)
|
||||
const atlasFilename = `${filename}.atlas`
|
||||
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))
|
||||
if (!matches)
|
||||
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 voiceFolders = getVoiceFolders(name)
|
||||
const extractedFolder = getExtractedFolder(name)
|
||||
const operatorConfigFolder = getConfigFolder()
|
||||
const foldersToCreate = [extractedFolder, ...voiceFolders]
|
||||
|
||||
const template = yaml.read(
|
||||
path.resolve(operatorConfigFolder, config.module.operator.template_yaml)
|
||||
const characterTable = (() => {
|
||||
const character_table_json = path.resolve(
|
||||
AUTO_UPDATE_FOLDER,
|
||||
config.module.operator.character_table_json
|
||||
)
|
||||
foldersToCreate.forEach((dir) => {
|
||||
file.mkdir(dir)
|
||||
})
|
||||
const currentOpertor = officialInfoMapping[id]
|
||||
if (currentOpertor === undefined) {
|
||||
throw new Error('Invalid operator id')
|
||||
const t = file.readSync(character_table_json, {
|
||||
useAsPrefix: true,
|
||||
}) as string
|
||||
if (!t) throw new Error('character_table.json not found')
|
||||
return JSON.parse(t) as CharacterTableJson
|
||||
})()
|
||||
|
||||
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(
|
||||
stringify(template),
|
||||
path.resolve(operatorConfigFolder, `${name}.yaml`)
|
||||
)
|
||||
file.appendSync(
|
||||
`${name}: !include ${config.module.operator.config}/${name}.yaml\n`,
|
||||
CONFIG_PATH
|
||||
)
|
||||
return CONFIG
|
||||
}
|
||||
|
||||
const operators = generateMapping()
|
||||
|
||||
export default operators
|
||||
|
||||
117
packages/operator/libs/builder.ts
Normal file
117
packages/operator/libs/builder.ts
Normal 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)
|
||||
)
|
||||
}
|
||||
55
packages/operator/libs/initer.ts
Normal file
55
packages/operator/libs/initer.ts
Normal 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
|
||||
)
|
||||
}
|
||||
25
packages/operator/libs/updater.ts
Normal file
25
packages/operator/libs/updater.ts
Normal 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
|
||||
)
|
||||
}
|
||||
204
packages/operator/libs/utils.ts
Normal file
204
packages/operator/libs/utils.ts
Normal 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')
|
||||
}
|
||||
}
|
||||
@@ -5,12 +5,13 @@
|
||||
"main": "index.ts",
|
||||
"type": "module",
|
||||
"dependencies": {
|
||||
"yaml": "^2.7.0",
|
||||
"@aklive2d/libs": "workspace:*",
|
||||
"@aklive2d/config": "workspace:*",
|
||||
"@aklive2d/official-info": "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": {
|
||||
"globals": ">=16.0.0",
|
||||
@@ -18,9 +19,13 @@
|
||||
"typescript": ">=5.8.2"
|
||||
},
|
||||
"scripts": {
|
||||
"update": "mode=update bun runner.ts",
|
||||
"build": "mode=build bun runner.ts",
|
||||
"init": "mode=init bun runner.ts",
|
||||
"lint": "eslint && prettier --check .",
|
||||
"build:cleanup": "rm -rf ./dist ./data"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/unidecode": "^1.1.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
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 = {
|
||||
mode: string
|
||||
@@ -27,6 +29,9 @@ async function main() {
|
||||
case 'build':
|
||||
await build(name)
|
||||
break
|
||||
case 'update':
|
||||
await update()
|
||||
break
|
||||
case 'init':
|
||||
if (!name.length) {
|
||||
throw new Error('Please set the operator name.')
|
||||
|
||||
@@ -1,22 +1,26 @@
|
||||
export type Codename = { 'zh-CN': string; 'en-US': string }
|
||||
|
||||
export type OperatorEntryType = 'operator' | 'skin'
|
||||
|
||||
export interface OperatorConfig {
|
||||
filename: string
|
||||
logo: string
|
||||
fallback_name: string
|
||||
viewport_left: number
|
||||
viewport_left: number // should be default to 0 in the future
|
||||
viewport_right: number
|
||||
viewport_top: number
|
||||
viewport_bottom: number
|
||||
invert_filter: boolean
|
||||
codename: Codename
|
||||
use_json: boolean
|
||||
official_id: string
|
||||
use_json: boolean // can be detected automatically
|
||||
official_id: string // reverse lookup from official_update?
|
||||
title: string
|
||||
type: 'operator' | 'skin'
|
||||
type: OperatorEntryType
|
||||
link: string
|
||||
id: string
|
||||
id: string // used in directory
|
||||
date: string
|
||||
voice_id: string | null
|
||||
color: string
|
||||
}
|
||||
|
||||
export type Config = {
|
||||
@@ -91,3 +95,40 @@ export type AssetsJson = {
|
||||
path?: string
|
||||
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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,11 @@ import operators, {
|
||||
OPERATOR_SOURCE_FOLDER,
|
||||
generateAssetsJson,
|
||||
} from '@aklive2d/operator'
|
||||
import {
|
||||
getExtractedFolder,
|
||||
getActualFilename,
|
||||
findSkel,
|
||||
} from '@aklive2d/operator/libs/utils'
|
||||
import type { OperatorConfig } from '@aklive2d/operator/types'
|
||||
import { DIST_DIR as ASSETS_DIST_DIR } from '@aklive2d/assets'
|
||||
import { file, env } from '@aklive2d/libs'
|
||||
@@ -106,10 +111,14 @@ export const copyShowcaseData = (
|
||||
fn(source, target)
|
||||
}
|
||||
})
|
||||
const filename = getActualFilename(
|
||||
operators[name].filename,
|
||||
getExtractedFolder(name)
|
||||
)
|
||||
const buildConfig = {
|
||||
insight_id: config.insight.id,
|
||||
link: operators[name].link,
|
||||
filename: operators[name].filename.replace(/#/g, '%23'),
|
||||
filename: filename.replace(/#/g, '%23'),
|
||||
logo_filename: operators[name].logo,
|
||||
fallback_filename: operators[name].fallback_name.replace(/#/g, '%23'),
|
||||
viewport_left: operators[name].viewport_left,
|
||||
@@ -126,7 +135,7 @@ export const copyShowcaseData = (
|
||||
voice_folders: config.dir_name.voice,
|
||||
music_folder: config.module.assets.music,
|
||||
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}/`,
|
||||
logo_dir:
|
||||
mode === 'build:directory'
|
||||
@@ -190,6 +199,14 @@ export const copyDirectoryData = async ({
|
||||
Object.values(operators).reduce(
|
||||
(acc, cur) => {
|
||||
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
|
||||
|
||||
curD.workshopId = null
|
||||
@@ -200,7 +217,7 @@ export const copyDirectoryData = async ({
|
||||
cur.link,
|
||||
config.module.project_json.project_json
|
||||
)
|
||||
)
|
||||
) as string
|
||||
if (!text) {
|
||||
console.log(`No workshop id for ${cur.link}!`)
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user