refactor: 收口 BangDream 称号资源仓储
This commit is contained in:
parent
fe8542c687
commit
5b0d2f754d
@ -310,6 +310,7 @@ export interface TsuguHook {
|
||||
- 已新增 `models/costume-resource-repository.ts`,把 `Costume` 的服装详情请求和 Live2D 缩略图素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFile` 收口到 provider-backed repository;`costume-resource-repository.spec.ts` 覆盖详情路径、`sdchara.png` 路径和非内存缓存下载调用,本地 `/查卡 472` 图片 smoke 保持演出缩略图正常输出。
|
||||
- 已新增 `models/item-resource-repository.ts`,把 `Item` 的道具缩略图路径从 `bestdoriUrl + downloadFileCache` 收口到 provider-backed repository;`item-resource-repository.spec.ts` 覆盖 material、star 和 common item 三类路径,本地 `/查卡池 259` 图片 smoke 输出非空。
|
||||
- 已新增 `models/cutoff-event-top-repository.ts` 和 `render-blocks/player-ranking-resource-repository.ts`,把前十榜 eventtop 数据请求与排名徽章素材路径从 `bestdoriUrl + callAPIAndCacheResponse/downloadFileCache` 收口到 provider-backed repository;相关 spec 覆盖 eventtop query path、排名徽章路径和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
|
||||
- 已新增 `models/degree-resource-repository.ts`,把 `Degree` 的称号缩略图、称号框、称号图标、动态称号脚本和纹理素材路径从 `bestdoriUrl + downloadFile/downloadFileCache` 收口到 provider-backed repository;`degree-resource-repository.spec.ts` 覆盖旧/新缩略图 fallback、动态称号旧纹理白名单和 provider 调用,本地 `/ycx 10 100 cn` 图片 smoke 输出非空。
|
||||
- 固化:Windows 下不要用反斜杠测试路径直接调用 Jest pattern,容易出现 `Pattern ... - 0 matches`;指定文件测试统一使用 `pnpm exec jest --runInBand --runTestsByPath test/qqbot/plugins/bangDream/tsugu/<file>.spec.ts ...`,路径用正斜杠。
|
||||
|
||||
### Phase 4:搜索 specification 和 matcher
|
||||
|
||||
@ -0,0 +1,154 @@
|
||||
import { bangDreamBestdoriProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/bestdori-provider';
|
||||
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
|
||||
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
|
||||
const LEGACY_ANIMATED_TEXTURE_NAMES = new Set([
|
||||
'ani_degree_bilibili_day1',
|
||||
'ani_degree_bilibili_092701',
|
||||
'ani_degree_bilibili_collabo',
|
||||
'ani_degree_bilibili_6years',
|
||||
]);
|
||||
|
||||
export class DegreeResourceRepository {
|
||||
constructor(
|
||||
private readonly provider: BangDreamDataProvider = bangDreamBestdoriProvider,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* 获取称号缩略图资源路径。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getThumbnailPath(baseImageName: string, server: Server): string {
|
||||
return `/assets/${Server[server]}/thumb/degree_rip/${baseImageName}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取新版称号缩略图兜底资源路径。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getFallbackThumbnailPath(baseImageName: string, server: Server): string {
|
||||
return `/assets/${Server[server]}/thumb/degree_rip/assets-star-forassetbundle-startapp-thumbnail-degree-${baseImageName}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取称号框资源路径。
|
||||
*
|
||||
* @param frameName - 称号框资源名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getFramePath(frameName: string, server: Server): string {
|
||||
return `/assets/${Server[server]}/thumb/degree_rip/${frameName}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取称号图标资源路径。
|
||||
*
|
||||
* @param iconName - 称号图标资源名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getIconPath(iconName: string, server: Server): string {
|
||||
return `/assets/${Server[server]}/thumb/degree_rip/${iconName}.png`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态称号脚本资源路径。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getAnimatedScriptPath(baseImageName: string, server: Server): string {
|
||||
return `/assets/${Server[server]}/${baseImageName}_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-${baseImageName}-${baseImageName}.asset`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取动态称号纹理资源路径。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
getAnimatedTexturePath(baseImageName: string, server: Server): string {
|
||||
const fileName = LEGACY_ANIMATED_TEXTURE_NAMES.has(baseImageName)
|
||||
? `${baseImageName}.png`
|
||||
: `assets-star-forassetbundle-startapp-thumbnail-animedegree-${baseImageName}-${baseImageName}.png`;
|
||||
return `/assets/${Server[server]}/${baseImageName}_rip/${fileName}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载称号缩略图资源,缺失时回退到新版统一路径。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
async getThumbnailBuffer(
|
||||
baseImageName: string,
|
||||
server: Server,
|
||||
): Promise<Buffer> {
|
||||
try {
|
||||
return await this.provider.getAsset(
|
||||
this.getThumbnailPath(baseImageName, server),
|
||||
{ ignoreError: false, memoryCache: false },
|
||||
);
|
||||
} catch {
|
||||
return await this.provider.getAsset(
|
||||
this.getFallbackThumbnailPath(baseImageName, server),
|
||||
{ memoryCache: false },
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载称号框资源。
|
||||
*
|
||||
* @param frameName - 称号框资源名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
async getFrameBuffer(frameName: string, server: Server): Promise<Buffer> {
|
||||
return await this.provider.getAsset(this.getFramePath(frameName, server));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载称号图标资源。
|
||||
*
|
||||
* @param iconName - 称号图标资源名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
async getIconBuffer(iconName: string, server: Server): Promise<Buffer> {
|
||||
return await this.provider.getAsset(this.getIconPath(iconName, server));
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载动态称号脚本资源。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
async getAnimatedScriptBuffer(
|
||||
baseImageName: string,
|
||||
server: Server,
|
||||
): Promise<Buffer> {
|
||||
return await this.provider.getAsset(
|
||||
this.getAnimatedScriptPath(baseImageName, server),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 下载动态称号纹理资源。
|
||||
*
|
||||
* @param baseImageName - 称号基础图片名。
|
||||
* @param server - 目标服务器。
|
||||
*/
|
||||
async getAnimatedTextureBuffer(
|
||||
baseImageName: string,
|
||||
server: Server,
|
||||
): Promise<Buffer> {
|
||||
return await this.provider.getAsset(
|
||||
this.getAnimatedTexturePath(baseImageName, server),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
export const degreeResourceRepository = new DegreeResourceRepository();
|
||||
@ -1,10 +1,8 @@
|
||||
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
import { downloadFileCache } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
|
||||
import { downloadFile } from '@/qqbot/plugins/bangDream/tsugu/data-clients/asset-cache-client';
|
||||
import { Canvas, Image, loadImage } from 'skia-canvas';
|
||||
import mainAPI from '@/qqbot/plugins/bangDream/tsugu/models/main-data-store';
|
||||
import { bestdoriUrl } from '@/qqbot/plugins/bangDream/tsugu/runtime/config';
|
||||
import { readJSONFromBuffer } from './model-utils';
|
||||
import { degreeResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/degree-resource-repository';
|
||||
|
||||
export class Degree {
|
||||
degreeId: number;
|
||||
@ -52,20 +50,11 @@ export class Degree {
|
||||
return degreeImageBuffer;
|
||||
} catch {}
|
||||
}
|
||||
// 资源路径修复
|
||||
try {
|
||||
const degreeImageBuffer = await downloadFile(
|
||||
`${bestdoriUrl}/assets/${Server[server]}/thumb/degree_rip/${this.baseImageName[server]}.png`,
|
||||
false,
|
||||
);
|
||||
return loadImage(degreeImageBuffer);
|
||||
} catch {
|
||||
const degreeImageBuffer = await downloadFile(
|
||||
`${bestdoriUrl}/assets/${Server[server]}/thumb/degree_rip/assets-star-forassetbundle-startapp-thumbnail-degree-${this.baseImageName[server]}.png`,
|
||||
true,
|
||||
);
|
||||
return loadImage(degreeImageBuffer);
|
||||
}
|
||||
const degreeImageBuffer = await degreeResourceRepository.getThumbnailBuffer(
|
||||
this.baseImageName[server],
|
||||
server,
|
||||
);
|
||||
return loadImage(degreeImageBuffer);
|
||||
}
|
||||
/**
|
||||
* 在 Degree 模型中获取称号Frame。
|
||||
@ -80,8 +69,9 @@ export class Degree {
|
||||
return new Canvas(1, 1);
|
||||
}
|
||||
|
||||
const degreeFrameBuffer = await downloadFileCache(
|
||||
`${bestdoriUrl}/assets/${Server[server]}/thumb/degree_rip/${frameName}.png`,
|
||||
const degreeFrameBuffer = await degreeResourceRepository.getFrameBuffer(
|
||||
frameName,
|
||||
server,
|
||||
);
|
||||
return loadImage(degreeFrameBuffer);
|
||||
}
|
||||
@ -97,8 +87,9 @@ export class Degree {
|
||||
//这个为空底图
|
||||
return new Canvas(1, 1);
|
||||
}
|
||||
const degreeIconBuffer = await downloadFileCache(
|
||||
`${bestdoriUrl}/assets/${Server[server]}/thumb/degree_rip/${iconName}.png`,
|
||||
const degreeIconBuffer = await degreeResourceRepository.getIconBuffer(
|
||||
iconName,
|
||||
server,
|
||||
);
|
||||
return loadImage(degreeIconBuffer);
|
||||
}
|
||||
@ -132,11 +123,12 @@ export async function getFrameFromAnimatedDegreeAsset(
|
||||
server: Server,
|
||||
frame?: number,
|
||||
): Promise<Canvas> {
|
||||
// script
|
||||
// example https://bestdori.com/assets/cn/ani_degree_bilibili_day1_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_bilibili_day1-ani_degree_bilibili_day1.asset
|
||||
const scriptUrl = `${bestdoriUrl}/assets/${Server[server]}/${baseImageName}_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-${baseImageName}-${baseImageName}.asset`;
|
||||
const srciptBuffer = await downloadFileCache(scriptUrl);
|
||||
const script = await readJSONFromBuffer(srciptBuffer);
|
||||
const scriptBuffer =
|
||||
await degreeResourceRepository.getAnimatedScriptBuffer(
|
||||
baseImageName,
|
||||
server,
|
||||
);
|
||||
const script = await readJSONFromBuffer(scriptBuffer);
|
||||
const frames: Array<Frame> = script['Base']['mSprites'] as Array<Frame>;
|
||||
const framecount = frames.length;
|
||||
if (!frame) {
|
||||
@ -144,27 +136,9 @@ export async function getFrameFromAnimatedDegreeAsset(
|
||||
frame = Math.floor(Math.random() * framecount);
|
||||
}
|
||||
|
||||
// texture
|
||||
// example https://bestdori.com/assets/cn/ani_degree_bilibili_day1_rip/ani_degree_bilibili_day1.png
|
||||
// example https://bestdori.com/assets/cn/ani_degree_election_5th_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_election_5th-ani_degree_election_5th.png
|
||||
const textureUrlOld = `${bestdoriUrl}/assets/${Server[server]}/${baseImageName}_rip/${baseImageName}.png`;
|
||||
const textureUrlNew = `${bestdoriUrl}/assets/${Server[server]}/${baseImageName}_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-${baseImageName}-${baseImageName}.png`;
|
||||
// 后期使用了统一的资源路径
|
||||
const useTextureUrlOldAssetWhitelist = [
|
||||
'ani_degree_bilibili_day1',
|
||||
'ani_degree_bilibili_092701',
|
||||
'ani_degree_bilibili_collabo',
|
||||
'ani_degree_bilibili_6years',
|
||||
];
|
||||
let useTextureUrlOld = false;
|
||||
for (const l of useTextureUrlOldAssetWhitelist) {
|
||||
if (baseImageName == l) {
|
||||
useTextureUrlOld = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
const textureBuffer = await downloadFileCache(
|
||||
useTextureUrlOld ? textureUrlOld : textureUrlNew,
|
||||
const textureBuffer = await degreeResourceRepository.getAnimatedTextureBuffer(
|
||||
baseImageName,
|
||||
server,
|
||||
);
|
||||
const texture = await loadImage(textureBuffer);
|
||||
|
||||
|
||||
@ -0,0 +1,118 @@
|
||||
import type { BangDreamDataProvider } from '@/qqbot/plugins/bangDream/tsugu/data-clients/data-provider';
|
||||
import { DegreeResourceRepository } from '@/qqbot/plugins/bangDream/tsugu/models/degree-resource-repository';
|
||||
import { Server } from '@/qqbot/plugins/bangDream/tsugu/models/server';
|
||||
|
||||
function createProviderMock(): jest.Mocked<BangDreamDataProvider> {
|
||||
return {
|
||||
getAsset: jest.fn(),
|
||||
getJson: jest.fn(),
|
||||
getTracker: jest.fn(),
|
||||
name: 'MockBestdori',
|
||||
resolveUrl: jest.fn((pathOrUrl) => `https://bestdori.example${pathOrUrl}`),
|
||||
};
|
||||
}
|
||||
|
||||
describe('BangDream degree resource repository', () => {
|
||||
it('builds degree thumbnail, frame, icon, and animated asset paths', () => {
|
||||
const repository = new DegreeResourceRepository(createProviderMock());
|
||||
|
||||
expect(repository.getThumbnailPath('degree_001', Server.cn)).toBe(
|
||||
'/assets/cn/thumb/degree_rip/degree_001.png',
|
||||
);
|
||||
expect(repository.getFallbackThumbnailPath('degree_001', Server.cn)).toBe(
|
||||
'/assets/cn/thumb/degree_rip/assets-star-forassetbundle-startapp-thumbnail-degree-degree_001.png',
|
||||
);
|
||||
expect(repository.getFramePath('normal_1', Server.cn)).toBe(
|
||||
'/assets/cn/thumb/degree_rip/normal_1.png',
|
||||
);
|
||||
expect(repository.getIconPath('event_1', Server.cn)).toBe(
|
||||
'/assets/cn/thumb/degree_rip/event_1.png',
|
||||
);
|
||||
expect(
|
||||
repository.getAnimatedScriptPath('ani_degree_election_5th', Server.cn),
|
||||
).toBe(
|
||||
'/assets/cn/ani_degree_election_5th_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_election_5th-ani_degree_election_5th.asset',
|
||||
);
|
||||
});
|
||||
|
||||
it('keeps legacy animated degree texture paths for known old assets', () => {
|
||||
const repository = new DegreeResourceRepository(createProviderMock());
|
||||
|
||||
expect(
|
||||
repository.getAnimatedTexturePath('ani_degree_bilibili_day1', Server.cn),
|
||||
).toBe('/assets/cn/ani_degree_bilibili_day1_rip/ani_degree_bilibili_day1.png');
|
||||
expect(
|
||||
repository.getAnimatedTexturePath('ani_degree_election_5th', Server.cn),
|
||||
).toBe(
|
||||
'/assets/cn/ani_degree_election_5th_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_election_5th-ani_degree_election_5th.png',
|
||||
);
|
||||
});
|
||||
|
||||
it('falls back to the unified thumbnail path when the legacy thumbnail is missing', async () => {
|
||||
const provider = createProviderMock();
|
||||
const fallbackBuffer = Buffer.from('fallback-thumbnail');
|
||||
provider.getAsset
|
||||
.mockRejectedValueOnce(new Error('thumbnail missing'))
|
||||
.mockResolvedValueOnce(fallbackBuffer);
|
||||
const repository = new DegreeResourceRepository(provider);
|
||||
|
||||
await expect(
|
||||
repository.getThumbnailBuffer('degree_001', Server.cn),
|
||||
).resolves.toBe(fallbackBuffer);
|
||||
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/assets/cn/thumb/degree_rip/degree_001.png',
|
||||
{ ignoreError: false, memoryCache: false },
|
||||
);
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/assets/cn/thumb/degree_rip/assets-star-forassetbundle-startapp-thumbnail-degree-degree_001.png',
|
||||
{ memoryCache: false },
|
||||
);
|
||||
});
|
||||
|
||||
it('downloads degree frame, icon, script, and texture assets through the provider', async () => {
|
||||
const provider = createProviderMock();
|
||||
const frameBuffer = Buffer.from('frame');
|
||||
const iconBuffer = Buffer.from('icon');
|
||||
const scriptBuffer = Buffer.from('script');
|
||||
const textureBuffer = Buffer.from('texture');
|
||||
provider.getAsset
|
||||
.mockResolvedValueOnce(frameBuffer)
|
||||
.mockResolvedValueOnce(iconBuffer)
|
||||
.mockResolvedValueOnce(scriptBuffer)
|
||||
.mockResolvedValueOnce(textureBuffer);
|
||||
const repository = new DegreeResourceRepository(provider);
|
||||
|
||||
await expect(repository.getFrameBuffer('normal_1', Server.cn)).resolves.toBe(
|
||||
frameBuffer,
|
||||
);
|
||||
await expect(repository.getIconBuffer('event_1', Server.cn)).resolves.toBe(
|
||||
iconBuffer,
|
||||
);
|
||||
await expect(
|
||||
repository.getAnimatedScriptBuffer('ani_degree_election_5th', Server.cn),
|
||||
).resolves.toBe(scriptBuffer);
|
||||
await expect(
|
||||
repository.getAnimatedTextureBuffer('ani_degree_election_5th', Server.cn),
|
||||
).resolves.toBe(textureBuffer);
|
||||
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
1,
|
||||
'/assets/cn/thumb/degree_rip/normal_1.png',
|
||||
);
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
2,
|
||||
'/assets/cn/thumb/degree_rip/event_1.png',
|
||||
);
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
3,
|
||||
'/assets/cn/ani_degree_election_5th_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_election_5th-ani_degree_election_5th.asset',
|
||||
);
|
||||
expect(provider.getAsset).toHaveBeenNthCalledWith(
|
||||
4,
|
||||
'/assets/cn/ani_degree_election_5th_rip/assets-star-forassetbundle-startapp-thumbnail-animedegree-ani_degree_election_5th-ani_degree_election_5th.png',
|
||||
);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in New Issue
Block a user