diff --git a/apps/admin-web/src/api/adminApiClient.test.ts b/apps/admin-web/src/api/adminApiClient.test.ts index aedd8c67a..ecd7501ac 100644 --- a/apps/admin-web/src/api/adminApiClient.test.ts +++ b/apps/admin-web/src/api/adminApiClient.test.ts @@ -3,12 +3,14 @@ import { afterEach, expect, test, vi } from 'vitest'; import { createAdminAccount, executeAdminRechargeRefund, + getAdminAgcTemplates, getAdminFeatureGateConfig, getAdminUserDetail, listAdminRechargeOrders, reconcileAdminUserConsumption, resolveAdminRechargeRefundManualReview, updateAdminAccount, + updateAdminAgcTemplate, uploadAdminEditorShowcaseCampaignImage, upsertAdminFeatureGateConfig, upsertProfileWalletConfig, @@ -18,6 +20,51 @@ afterEach(() => { vi.unstubAllGlobals(); }); +test('模板管理读取和更新复用认证封装,提交 revision 和封面但不提交 ZIP 或版本', async () => { + const library = { revision: 'revision-new', writable: true, templates: [] }; + const fetchMock = vi.fn().mockImplementation( + async () => + new Response(JSON.stringify({ ok: true, data: library }), { + status: 200, + }), + ); + vi.stubGlobal('fetch', fetchMock); + const controller = new AbortController(); + expect(await getAdminAgcTemplates('admin-token', controller.signal)).toEqual( + library, + ); + const update = { + expectedRevision: 'revision-old', + title: '空白模板', + summary: '简介', + tags: ['2D'], + enabled: true, + cover: { contentType: 'image/png', dataBase64: 'aW1hZ2U=' }, + }; + expect( + await updateAdminAgcTemplate('admin-token', 'template/1', update), + ).toEqual(library); + expect(fetchMock.mock.calls[0]).toEqual([ + '/admin/api/agc-templates', + expect.objectContaining({ + method: 'GET', + signal: controller.signal, + headers: expect.objectContaining({ Authorization: 'Bearer admin-token' }), + }), + ]); + expect(fetchMock.mock.calls[1]).toEqual([ + '/admin/api/agc-templates/template%2F1', + expect.objectContaining({ + method: 'PUT', + headers: expect.objectContaining({ + Authorization: 'Bearer admin-token', + 'Content-Type': 'application/json', + }), + body: JSON.stringify(update), + }), + ]); +}); + test('后台账号创建和更新同时携带 Tab 与独立操作权限', async () => { const fetchMock = vi.fn().mockImplementation(() => Promise.resolve( diff --git a/apps/admin-web/src/api/adminApiClient.ts b/apps/admin-web/src/api/adminApiClient.ts index aa35d2d33..47e7ebd67 100644 --- a/apps/admin-web/src/api/adminApiClient.ts +++ b/apps/admin-web/src/api/adminApiClient.ts @@ -1,5 +1,6 @@ import type { AdminAccountListResponse, + AdminAgcTemplateLibraryResponse, AdminConfirmEditorShowcaseCampaignImageUploadRequest, AdminCreateAccountRequest, AdminCreateAccountResponse, @@ -47,6 +48,7 @@ import type { AdminTrackingEventListResponse, AdminUpdateAccountRequest, AdminUpdateAccountResponse, + AdminUpdateAgcTemplateRequest, AdminUploadedEditorShowcaseCampaignImage, AdminUpsertEditorShowcaseCampaignRequest, AdminUpsertFeatureGateConfigRequest, @@ -1176,3 +1178,21 @@ export function saveAgcModelCatalog( { token, method: 'PUT', body }, ); } + +export function getAdminAgcTemplates(token: string, signal?: AbortSignal) { + return request('/admin/api/agc-templates', { + token, + signal, + }); +} + +export function updateAdminAgcTemplate( + token: string, + id: string, + body: AdminUpdateAgcTemplateRequest, +) { + return request( + `/admin/api/agc-templates/${encodeURIComponent(id)}`, + { token, method: 'PUT', body }, + ); +} diff --git a/apps/admin-web/src/api/adminApiTypes.ts b/apps/admin-web/src/api/adminApiTypes.ts index 1a1c6f668..dfcd4b993 100644 --- a/apps/admin-web/src/api/adminApiTypes.ts +++ b/apps/admin-web/src/api/adminApiTypes.ts @@ -1033,3 +1033,35 @@ export interface AdminAgcModelCatalog { defaultModelId: string; models: AdminAgcModel[]; } + +export interface AdminAgcTemplatePayload { + id: string; + title: string; + summary: string; + tags: string[]; + runtime: string; + engine: string; + engineVersion: string; + templateVersion: string; + enabled: boolean; + coverUrl: string; + zipSizeBytes: number; +} + +export interface AdminAgcTemplateLibraryResponse { + revision: string; + writable: boolean; + templates: AdminAgcTemplatePayload[]; +} + +export interface AdminUpdateAgcTemplateRequest { + expectedRevision: string; + title: string; + summary: string; + tags: string[]; + enabled: boolean; + cover?: { + contentType: string; + dataBase64: string; + }; +} diff --git a/apps/admin-web/src/app/AdminApp.tsx b/apps/admin-web/src/app/AdminApp.tsx index 6fac5b208..f09af530e 100644 --- a/apps/admin-web/src/app/AdminApp.tsx +++ b/apps/admin-web/src/app/AdminApp.tsx @@ -19,6 +19,7 @@ import { } from '../auth/adminAuthStore'; import { AdminAccountsPage } from '../pages/AdminAccountsPage'; import { AdminAgcModelsPage } from '../pages/AdminAgcModelsPage'; +import { AdminAgcTemplatesPage } from '../pages/AdminAgcTemplatesPage'; import { AdminDashboardPage } from '../pages/AdminDashboardPage'; import { AdminDatabaseTablesPage } from '../pages/AdminDatabaseTablesPage'; import { AdminDebugHttpPage } from '../pages/AdminDebugHttpPage'; @@ -294,6 +295,12 @@ export function AdminApp() { {activeRouteId === 'agc-models' ? ( ) : null} + {activeRouteId === 'agc-templates' ? ( + + ) : null} {activeRouteId === 'editor-showcase' ? ( ; export function AdminShell({ diff --git a/apps/admin-web/src/app/adminRoutes.test.ts b/apps/admin-web/src/app/adminRoutes.test.ts index 2e1a0e20a..ec87d2bf3 100644 --- a/apps/admin-web/src/app/adminRoutes.test.ts +++ b/apps/admin-web/src/app/adminRoutes.test.ts @@ -147,3 +147,30 @@ test('项目工程入口对 owner 与已授权 member 开放且可分配权限', }), ).not.toContainEqual(route); }); + +test('模板管理只对 owner 或具有 agc-templates 权限的 member 可见', () => { + expect(adminRoutes).toContainEqual({ + id: 'agc-templates', + label: '模板管理', + hash: '#agc-templates', + }); + expect(resolveAdminRoute('#agc-templates')).toBe('agc-templates'); + expect(routeHash('agc-templates')).toBe('#agc-templates'); + expect( + getAccessibleAdminRoutes({ accountRole: 'owner', tabPermissions: [] }).some( + (route) => route.id === 'agc-templates', + ), + ).toBe(true); + expect( + getAccessibleAdminRoutes({ + accountRole: 'member', + tabPermissions: ['agc-templates'], + }).map((route) => route.id), + ).toEqual(['agc-templates']); + expect( + getAccessibleAdminRoutes({ + accountRole: 'member', + tabPermissions: ['editor-assets'], + }).some((route) => route.id === 'agc-templates'), + ).toBe(false); +}); diff --git a/apps/admin-web/src/app/adminRoutes.ts b/apps/admin-web/src/app/adminRoutes.ts index 10503df72..138ac070c 100644 --- a/apps/admin-web/src/app/adminRoutes.ts +++ b/apps/admin-web/src/app/adminRoutes.ts @@ -18,6 +18,7 @@ export type AdminRouteId = | 'editor-assets' | 'project-snapshots' | 'agc-models' + | 'agc-templates' | 'accounts'; export type AdminTabPermission = Exclude< @@ -53,6 +54,7 @@ export const adminRoutes: AdminRouteDefinition[] = [ hash: '#editor-generation-pricing', }, { id: 'agc-models', label: 'AGC 模型', hash: '#agc-models', ownerOnly: true }, + { id: 'agc-templates', label: '模板管理', hash: '#agc-templates' }, { id: 'editor-showcase', label: '精选审核', hash: '#editor-showcase' }, { id: 'editor-assets', label: '素材查询', hash: '#editor-assets' }, { id: 'project-snapshots', label: '项目工程', hash: '#project-snapshots' }, diff --git a/apps/admin-web/src/main.tsx b/apps/admin-web/src/main.tsx index 5d571b851..cb1ffe21e 100644 --- a/apps/admin-web/src/main.tsx +++ b/apps/admin-web/src/main.tsx @@ -1,3 +1,4 @@ +import '@genarrative/shared/styles.css'; import './styles/admin.css'; import { StrictMode } from 'react'; diff --git a/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx b/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx new file mode 100644 index 000000000..b96acd4fd --- /dev/null +++ b/apps/admin-web/src/pages/AdminAgcTemplatesPage.test.tsx @@ -0,0 +1,498 @@ +// @vitest-environment jsdom +import { + act, + cleanup, + fireEvent, + render, + screen, + waitFor, + within, +} from '@testing-library/react'; +import { afterEach, beforeEach, expect, test, vi } from 'vitest'; + +import { + AdminApiError, + getAdminAgcTemplates, + updateAdminAgcTemplate, +} from '../api/adminApiClient'; +import type { + AdminAgcTemplateLibraryResponse, + AdminAgcTemplatePayload, +} from '../api/adminApiTypes'; +import { AdminAgcTemplatesPage } from './AdminAgcTemplatesPage'; + +vi.mock('../api/adminApiClient', async (importOriginal) => ({ + ...(await importOriginal()), + getAdminAgcTemplates: vi.fn(), + updateAdminAgcTemplate: vi.fn(), +})); + +const template: AdminAgcTemplatePayload = { + id: 'cocos-empty-2d', + title: '空白 2D', + summary: '二维项目', + tags: ['2D', '入门'], + runtime: 'cocos', + engine: 'Cocos Creator', + engineVersion: '3.8.8', + templateVersion: '1', + enabled: true, + coverUrl: 'https://example.test/cover.png', + zipSizeBytes: 2048, +}; +const library: AdminAgcTemplateLibraryResponse = { + revision: 'rev-1', + writable: true, + templates: [ + template, + { + ...template, + id: 'godot-starter', + title: 'Godot 起步', + runtime: 'godot', + engine: 'Godot', + engineVersion: '4', + tags: ['3D'], + enabled: false, + }, + ], +}; + +beforeEach(() => { + vi.mocked(getAdminAgcTemplates) + .mockReset() + .mockImplementation(async () => structuredClone(library)); + vi.mocked(updateAdminAgcTemplate) + .mockReset() + .mockImplementation(async (_token, id, update) => ({ + ...structuredClone(library), + revision: 'rev-2', + templates: library.templates.map((entry) => + entry.id === id + ? { + ...entry, + title: update.title, + summary: update.summary, + tags: update.tags, + enabled: update.enabled, + } + : entry, + ), + })); + vi.stubGlobal( + 'URL', + Object.assign(class extends URL {}, { + createObjectURL: vi.fn(() => 'blob:template-cover'), + revokeObjectURL: vi.fn(), + }), + ); +}); + +afterEach(() => { + cleanup(); + vi.unstubAllGlobals(); + vi.restoreAllMocks(); +}); + +async function openEditor() { + fireEvent.click(await screen.findByRole('button', { name: '编辑 空白 2D' })); + return screen.getByRole('dialog', { name: '编辑模板' }); +} + +async function confirmWrite() { + const confirmation = await screen.findByRole('dialog', { name: '确认操作' }); + fireEvent.click(within(confirmation).getByRole('button', { name: '确认' })); +} + +function deferred() { + let resolve!: (value: T) => void; + let reject!: (reason: unknown) => void; + const promise = new Promise((next, fail) => { + resolve = next; + reject = fail; + }); + return { promise, resolve, reject }; +} + +test('按名称、ID、标签、运行时和上下架状态筛选,展示版本与封面', async () => { + render(); + await screen.findByText('空白 2D'); + expect(screen.getByText('Cocos Creator 3.8.8')).not.toBeNull(); + expect(screen.getAllByText('模板 1')).toHaveLength(2); + expect(screen.getAllByText('2.0 KiB')).toHaveLength(2); + expect( + screen.getByRole('img', { name: '空白 2D封面' }).getAttribute('src'), + ).toBe(template.coverUrl); + for (const value of ['空白', 'cocos-empty-2d', '入门']) { + fireEvent.change(screen.getByLabelText('搜索模板'), { target: { value } }); + expect(screen.getByText('空白 2D')).not.toBeNull(); + expect(screen.queryByText('Godot 起步')).toBeNull(); + } + fireEvent.change(screen.getByLabelText('搜索模板'), { + target: { value: '' }, + }); + fireEvent.change(screen.getByLabelText('运行时'), { + target: { value: 'godot' }, + }); + expect(screen.queryByText('空白 2D')).toBeNull(); + fireEvent.change(screen.getByLabelText('上架状态'), { + target: { value: 'enabled' }, + }); + expect(screen.getByText('没有符合筛选条件的模板')).not.toBeNull(); +}); + +test('独立弹窗保存经写确认,只提交展示字段和冻结 revision', async () => { + render(); + const editor = await openEditor(); + expect(editor.closest('.admin-agc-templates')).toBeNull(); + fireEvent.change(within(editor).getByLabelText('名称'), { + target: { value: ' 新名称 ' }, + }); + fireEvent.change(within(editor).getByLabelText('简介'), { + target: { value: '新简介' }, + }); + fireEvent.change(within(editor).getByLabelText('标签'), { + target: { value: '2D, 新手,2D' }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + expect(updateAdminAgcTemplate).not.toHaveBeenCalled(); + await confirmWrite(); + await waitFor(() => + expect(updateAdminAgcTemplate).toHaveBeenCalledWith('token', template.id, { + expectedRevision: 'rev-1', + title: '新名称', + summary: '新简介', + tags: ['2D', '新手'], + enabled: true, + }), + ); + await waitFor(() => + expect(screen.queryByRole('dialog', { name: '编辑模板' })).toBeNull(), + ); + expect(screen.getByText('新名称')).not.toBeNull(); + expect(screen.getByText('已保存')).not.toBeNull(); +}); + +test('上下架复用确认且只用服务端结果更新列表', async () => { + render(); + fireEvent.click(await screen.findByRole('button', { name: '下架 空白 2D' })); + await confirmWrite(); + await waitFor(() => + expect(updateAdminAgcTemplate).toHaveBeenCalledWith('token', template.id, { + expectedRevision: 'rev-1', + title: template.title, + summary: template.summary, + tags: template.tags, + enabled: false, + }), + ); + expect( + await screen.findByRole('button', { name: '上架 空白 2D' }), + ).not.toBeNull(); +}); + +test('只读模式禁用字段、保存与上下架', async () => { + vi.mocked(getAdminAgcTemplates).mockResolvedValue({ + ...library, + writable: false, + }); + render(); + expect( + await screen.findByText('当前为只读模式,无法保存或上下架'), + ).not.toBeNull(); + expect( + (screen.getByRole('button', { name: '下架 空白 2D' }) as HTMLButtonElement) + .disabled, + ).toBe(true); + const editor = await openEditor(); + expect( + (within(editor).getByLabelText('名称') as HTMLInputElement).disabled, + ).toBe(true); + expect( + (within(editor).getByRole('button', { name: '保存' }) as HTMLButtonElement) + .disabled, + ).toBe(true); + expect(updateAdminAgcTemplate).not.toHaveBeenCalled(); +}); + +test('409 保留草稿,显式刷新后重新编辑才采用新 revision,不自动重试', async () => { + vi.mocked(updateAdminAgcTemplate).mockRejectedValueOnce( + new AdminApiError({ message: '模板库已被修改', status: 409 }), + ); + render(); + let editor = await openEditor(); + fireEvent.change(within(editor).getByLabelText('名称'), { + target: { value: '未保存草稿' }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + await confirmWrite(); + await screen.findByText('模板库已被修改'); + expect((screen.getByLabelText('名称') as HTMLInputElement).value).toBe( + '未保存草稿', + ); + expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1); + expect( + (screen.getByRole('button', { name: '保存' }) as HTMLButtonElement) + .disabled, + ).toBe(true); + vi.mocked(getAdminAgcTemplates).mockResolvedValueOnce({ + ...library, + revision: 'rev-3', + templates: [{ ...template, title: '最新名称' }], + }); + fireEvent.click(screen.getByRole('button', { name: '刷新后重新编辑' })); + await screen.findByDisplayValue('最新名称'); + editor = screen.getByRole('dialog', { name: '编辑模板' }); + fireEvent.change(within(editor).getByLabelText('名称'), { + target: { value: '重新编辑' }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + await confirmWrite(); + await waitFor(() => + expect(updateAdminAgcTemplate).toHaveBeenLastCalledWith( + 'token', + template.id, + expect.objectContaining({ expectedRevision: 'rev-3', title: '重新编辑' }), + ), + ); +}); + +test('防止重复确认和保存中重复提交', async () => { + const pending = deferred(); + vi.mocked(updateAdminAgcTemplate).mockReturnValue(pending.promise); + render(); + const editor = await openEditor(); + const form = within(editor).getByLabelText('名称').closest('form')!; + fireEvent.submit(form); + fireEvent.submit(form); + expect(screen.getAllByRole('dialog', { name: '确认操作' })).toHaveLength(1); + await confirmWrite(); + await waitFor(() => expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1)); + fireEvent.submit(form); + expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1); + await act(async () => pending.resolve(library)); +}); + +test.each([409, 503])( + '滚到表单底部后出现 HTTP %s 写入错误,聚焦并仅滚动弹窗到提示区', + async (status) => { + vi.mocked(updateAdminAgcTemplate).mockRejectedValueOnce( + new AdminApiError({ message: '本次保存失败', status }), + ); + render( +
+ +
, + ); + const background = screen.getByTestId('background-page'); + background.scrollTop = 240; + const editor = await openEditor(); + const viewport = editor.querySelector( + '.genarrative-ui-modal__body', + )!; + viewport.scrollTop = 400; + const bounds = (top: number, bottom: number) => + ({ + top, + bottom, + height: bottom - top, + left: 0, + right: 300, + width: 300, + x: 0, + y: top, + toJSON: () => ({}), + }) as DOMRect; + vi.spyOn(viewport, 'getBoundingClientRect').mockReturnValue( + bounds(100, 450), + ); + const originalBounds = HTMLElement.prototype.getBoundingClientRect; + vi.spyOn(HTMLElement.prototype, 'getBoundingClientRect').mockImplementation( + function (this: HTMLElement) { + return this.classList.contains('admin-agc-template-feedback') + ? bounds(-280, -120) + : originalBounds.call(this); + }, + ); + const windowScroll = vi + .spyOn(window, 'scrollTo') + .mockImplementation(() => {}); + const focus = vi.spyOn(HTMLElement.prototype, 'focus'); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + await confirmWrite(); + const message = await screen.findByRole('alert'); + const feedback = message.parentElement!; + expect(document.activeElement).toBe(feedback); + expect(feedback.tabIndex).toBe(-1); + expect(focus).toHaveBeenCalledWith({ preventScroll: true }); + expect(viewport.scrollTop).toBe(20); + expect(background.scrollTop).toBe(240); + expect(windowScroll).not.toHaveBeenCalled(); + if (status === 409) { + expect( + within(feedback).getByRole('button', { name: '刷新后重新编辑' }), + ).not.toBeNull(); + } + }, +); + +test('token 切换后忽略旧列表和未确认写操作', async () => { + const pending = deferred(); + vi.mocked(getAdminAgcTemplates).mockReturnValueOnce(pending.promise); + const props = { onUnauthorized: vi.fn() }; + const view = render(); + view.rerender(); + await screen.findByText('空白 2D'); + await act(async () => + pending.resolve({ + ...library, + templates: [{ ...template, title: '过期列表' }], + }), + ); + expect(screen.queryByText('过期列表')).toBeNull(); + const editor = await openEditor(); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + view.rerender(); + await screen.findByText('空白 2D'); + expect(screen.queryByRole('dialog')).toBeNull(); + expect(updateAdminAgcTemplate).not.toHaveBeenCalled(); +}); + +test.each(['success', 'unauthorized'])( + 'token 切换后忽略旧保存 %s,不污染新会话', + async (result) => { + const pending = deferred(); + vi.mocked(updateAdminAgcTemplate).mockReturnValueOnce(pending.promise); + const onUnauthorized = vi.fn(); + const view = render( + , + ); + const editor = await openEditor(); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + await confirmWrite(); + await waitFor(() => + expect(updateAdminAgcTemplate).toHaveBeenCalledTimes(1), + ); + view.rerender( + , + ); + await screen.findByText('空白 2D'); + await act(async () => { + if (result === 'success') { + pending.resolve({ + ...library, + templates: [{ ...template, title: '旧保存结果' }], + }); + } else { + pending.reject( + new AdminApiError({ status: 401, message: '旧会话失效' }), + ); + } + }); + expect(onUnauthorized).not.toHaveBeenCalled(); + expect(screen.queryByText('旧会话失效')).toBeNull(); + expect(screen.queryByText('旧保存结果')).toBeNull(); + expect(screen.queryByText('已保存')).toBeNull(); + }, +); + +test('封面在草稿中转 base64,保存携带,取消释放预览 URL 且不串到下一条', async () => { + render(); + let editor = await openEditor(); + const file = new File(['cover'], 'cover.png', { type: 'image/png' }); + fireEvent.change(within(editor).getByLabelText('更换封面'), { + target: { files: [file] }, + }); + await waitFor(() => expect(screen.queryByText('正在读取封面')).toBeNull()); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + await confirmWrite(); + await waitFor(() => + expect(updateAdminAgcTemplate).toHaveBeenCalledWith( + 'token', + template.id, + expect.objectContaining({ + cover: { contentType: 'image/png', dataBase64: 'Y292ZXI=' }, + }), + ), + ); + await waitFor(() => + expect(URL.revokeObjectURL).toHaveBeenCalledWith('blob:template-cover'), + ); + editor = await openEditor(); + fireEvent.change(within(editor).getByLabelText('更换封面'), { + target: { files: [file] }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '取消' })); + fireEvent.click(screen.getByRole('button', { name: '编辑 Godot 起步' })); + expect( + screen.getByRole('img', { name: '模板封面预览' }).getAttribute('src'), + ).toBe(template.coverUrl); + expect(URL.revokeObjectURL).toHaveBeenCalledTimes(2); +}); + +test.each([ + ['bad.svg', 'image/svg+xml', 1, '封面仅支持 PNG、JPEG 或 WebP'], + [ + 'large.png', + 'image/png', + 5 * 1024 * 1024 + 1, + '封面文件须大于 0 且不超过 5 MiB', + ], +])('拒绝无效封面 %s,不上传', async (name, type, size, message) => { + render(); + const editor = await openEditor(); + const file = new File(['x'], name, { type }); + Object.defineProperty(file, 'size', { value: size }); + fireEvent.change(within(editor).getByLabelText('更换封面'), { + target: { files: [file] }, + }); + expect(screen.getByText(message)).not.toBeNull(); + expect( + (screen.getByRole('button', { name: '保存' }) as HTMLButtonElement) + .disabled, + ).toBe(true); + expect(URL.createObjectURL).not.toHaveBeenCalled(); + expect(updateAdminAgcTemplate).not.toHaveBeenCalled(); +}); + +test('名称和标签校验失败时不进入写确认', async () => { + render(); + const editor = await openEditor(); + fireEvent.change(within(editor).getByLabelText('名称'), { + target: { value: ' ' }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + expect(screen.getByText('名称须为 1–80 个字符')).not.toBeNull(); + fireEvent.change(within(editor).getByLabelText('名称'), { + target: { value: '有效名称' }, + }); + fireEvent.change(within(editor).getByLabelText('标签'), { + target: { + value: Array.from({ length: 17 }, (_, index) => `标签${index}`).join(','), + }, + }); + fireEvent.click(within(editor).getByRole('button', { name: '保存' })); + expect( + screen.getByText('最多 16 个标签,每个标签不超过 32 个字符'), + ).not.toBeNull(); + expect(updateAdminAgcTemplate).not.toHaveBeenCalled(); +}); + +test('读取失败显示真实错误,401 交给会话处理且不显示空库', async () => { + const onUnauthorized = vi.fn(); + vi.mocked(getAdminAgcTemplates).mockRejectedValueOnce( + new AdminApiError({ status: 503, message: '模板服务不可用' }), + ); + render( + , + ); + await screen.findByText('模板服务不可用'); + expect(screen.queryByText('暂无模板')).toBeNull(); + vi.mocked(getAdminAgcTemplates).mockRejectedValueOnce( + new AdminApiError({ status: 401, message: '会话失效' }), + ); + fireEvent.click(screen.getByRole('button', { name: '刷新' })); + await waitFor(() => + expect(onUnauthorized).toHaveBeenCalledWith('登录状态已失效'), + ); +}); diff --git a/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx b/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx new file mode 100644 index 000000000..989ca2ce0 --- /dev/null +++ b/apps/admin-web/src/pages/AdminAgcTemplatesPage.tsx @@ -0,0 +1,629 @@ +import { + Button, + Modal, + SelectField, + Status, + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, + TextField, +} from '@genarrative/shared/components'; +import { RefreshCcw } from 'lucide-react'; +import { + type FormEvent, + useCallback, + useEffect, + useRef, + useState, +} from 'react'; + +import { + getAdminAgcTemplates, + isAdminApiError, + updateAdminAgcTemplate, +} from '../api/adminApiClient'; +import type { + AdminAgcTemplateLibraryResponse, + AdminAgcTemplatePayload, + AdminUpdateAgcTemplateRequest, +} from '../api/adminApiTypes'; +import { useAdminWriteConfirm } from '../components/useAdminWriteConfirm'; +import { handlePageError, splitLines } from './pageUtils'; + +type PageProps = { token: string; onUnauthorized: (message?: string) => void }; +type EditingTemplate = { + entry: AdminAgcTemplatePayload; + revision: string; + key: number; +}; +const runtimeLabels: Record = { + html: 'HTML', + cocos: 'Cocos', + godot: 'Godot', + unity: 'Unity', +}; +const coverTypes = new Set(['image/png', 'image/jpeg', 'image/webp']); + +export function AdminAgcTemplatesPage(props: PageProps) { + return ; +} + +function AdminAgcTemplatesSession({ token, onUnauthorized }: PageProps) { + const [snapshot, setSnapshot] = + useState(null); + const [query, setQuery] = useState(''); + const [runtime, setRuntime] = useState(''); + const [visibility, setVisibility] = useState(''); + const [loading, setLoading] = useState(true); + const [busy, setBusy] = useState(false); + const [error, setError] = useState(''); + const [notice, setNotice] = useState(''); + const [conflict, setConflict] = useState(false); + const [editing, setEditing] = useState(null); + const mounted = useRef(false); + const readGeneration = useRef(0); + const readController = useRef(null); + const writing = useRef(false); + const editorSequence = useRef(0); + const unauthorized = useRef(onUnauthorized); + unauthorized.current = onUnauthorized; + const { confirmWrite, confirmDialog, isConfirming } = useAdminWriteConfirm(); + + const refresh = useCallback( + async (reopenId?: string) => { + const generation = ++readGeneration.current; + readController.current?.abort(); + const controller = new AbortController(); + readController.current = controller; + setLoading(true); + setError(''); + setNotice(''); + try { + const next = await getAdminAgcTemplates(token, controller.signal); + if (!mounted.current || generation !== readGeneration.current) return; + setSnapshot(next); + if (reopenId) { + const entry = next.templates.find( + (template) => template.id === reopenId, + ); + if (!entry) { + setError('模板已不可用,请关闭编辑窗口后刷新列表'); + return; + } + setEditing({ + entry, + revision: next.revision, + key: ++editorSequence.current, + }); + } + setConflict(false); + } catch (error) { + if ( + !mounted.current || + generation !== readGeneration.current || + controller.signal.aborted + ) + return; + handlePageError(error, unauthorized.current, setError); + } finally { + if (mounted.current && generation === readGeneration.current) + setLoading(false); + } + }, + [token], + ); + + useEffect(() => { + mounted.current = true; + void refresh(); + return () => { + mounted.current = false; + readGeneration.current += 1; + readController.current?.abort(); + }; + }, [refresh]); + + async function writeTemplate( + entry: AdminAgcTemplatePayload, + payload: AdminUpdateAgcTemplateRequest, + action: string, + ) { + if (writing.current || loading || conflict || !snapshot?.writable) return; + writing.current = true; + setBusy(true); + setError(''); + setNotice(''); + try { + const confirmed = await confirmWrite({ + action, + target: `${entry.title}(${entry.id})`, + }); + if (!confirmed || !mounted.current) return; + const next = await updateAdminAgcTemplate(token, entry.id, payload); + if (!mounted.current) return; + readGeneration.current += 1; + readController.current?.abort(); + setSnapshot(next); + setEditing(null); + setConflict(false); + setNotice('已保存'); + } catch (error) { + if (!mounted.current) return; + setConflict(isAdminApiError(error) && error.status === 409); + handlePageError(error, unauthorized.current, setError); + } finally { + writing.current = false; + if (mounted.current) setBusy(false); + } + } + + function closeEditor() { + if (writing.current) return; + setEditing(null); + } + + const terms = query.trim().toLocaleLowerCase().split(/\s+/u).filter(Boolean); + const entries = (snapshot?.templates ?? []).filter((entry) => { + const searchable = [entry.id, entry.title, ...entry.tags] + .join(' ') + .toLocaleLowerCase(); + return ( + terms.every((term) => searchable.includes(term)) && + (!runtime || entry.runtime === runtime) && + (!visibility || entry.enabled === (visibility === 'enabled')) + ); + }); + const runtimes = [ + ...new Set(snapshot?.templates.map((entry) => entry.runtime) ?? []), + ].sort(); + const readOnly = snapshot !== null && !snapshot.writable; + const writesDisabled = busy || loading || conflict || !snapshot?.writable; + + return ( +
+
+

模板管理

+ +
+ {readOnly ? ( + 当前为只读模式,无法保存或上下架 + ) : null} + {!editing && error ? ( + + {error} + + ) : null} + {!editing && conflict ? ( + + ) : null} + {notice ? {notice} : null} +
+ setQuery(event.currentTarget.value)} + /> + setRuntime(event.currentTarget.value)} + > + + {runtimes.map((value) => ( + + ))} + + setVisibility(event.currentTarget.value)} + > + + + + +
+ {loading ? 正在加载模板 : null} + {snapshot ? ( + + + + 封面 + 模板 + 引擎 / 版本 + 包大小 + 状态 + 操作 + + + + {entries.map((entry) => ( + + + {`${entry.title}封面`} + + + {entry.title} + {entry.id} +

{entry.summary}

+
+ {entry.tags.map((tag) => ( + {tag} + ))} +
+
+ + {entry.engine || + runtimeLabels[entry.runtime] || + entry.runtime}{' '} + {entry.engineVersion} + 模板 {entry.templateVersion} + + {formatTemplateSize(entry.zipSizeBytes)} + {entry.enabled ? '已上架' : '已下架'} + +
+ + +
+
+
+ ))} + {!entries.length ? ( + + + {snapshot.templates.length + ? '没有符合筛选条件的模板' + : '暂无模板'} + + + ) : null} +
+
+ ) : null} + {editing ? ( + + void refresh(editing.entry.id)} + onSave={(draft) => + void writeTemplate( + editing.entry, + { + ...draft, + expectedRevision: editing.revision, + enabled: editing.entry.enabled, + }, + '保存模板', + ) + } + /> + {confirmDialog} + + ) : ( + confirmDialog + )} +
+ ); +} + +function TemplateEditor({ + entry, + disabled, + readOnly, + busy, + error, + conflict, + onCancel, + onRefresh, + onSave, +}: { + entry: AdminAgcTemplatePayload; + disabled: boolean; + readOnly: boolean; + busy: boolean; + error: string; + conflict: boolean; + onCancel: () => void; + onRefresh: () => void; + onSave: ( + draft: Omit, + ) => void; +}) { + const [title, setTitle] = useState(entry.title); + const [summary, setSummary] = useState(entry.summary); + const [tags, setTags] = useState(entry.tags.join(', ')); + const [cover, setCover] = useState(); + const [previewUrl, setPreviewUrl] = useState(''); + const [coverError, setCoverError] = useState(''); + const [validationError, setValidationError] = useState(''); + const [reading, setReading] = useState(false); + const [fileInputKey, setFileInputKey] = useState(0); + const reader = useRef(null); + const fileGeneration = useRef(0); + const feedbackRef = useRef(null); + const feedbackMessage = validationError || error; + + useEffect(() => { + if ((!feedbackMessage && !conflict) || busy) return; + const feedback = feedbackRef.current; + if (!feedback) return; + feedback.focus({ preventScroll: true }); + const viewport = feedback.closest( + '.genarrative-ui-modal__body', + ); + if (!viewport) return; + const messageBounds = feedback.getBoundingClientRect(); + const viewportBounds = viewport.getBoundingClientRect(); + const offset = + messageBounds.top < viewportBounds.top + ? messageBounds.top - viewportBounds.top + : Math.max(0, messageBounds.bottom - viewportBounds.bottom); + // 只调整编辑弹窗的滚动容器,避免焦点或 scrollIntoView 滚动背景页面。 + viewport.scrollTop = Math.max(0, viewport.scrollTop + offset); + }, [busy, conflict, feedbackMessage]); + + useEffect( + () => () => { + fileGeneration.current += 1; + reader.current?.abort(); + }, + [], + ); + useEffect( + () => () => { + if (previewUrl) URL.revokeObjectURL(previewUrl); + }, + [previewUrl], + ); + + function resetCover() { + fileGeneration.current += 1; + reader.current?.abort(); + setCover(undefined); + setCoverError(''); + setPreviewUrl(''); + setReading(false); + setFileInputKey((value) => value + 1); + } + + function chooseCover(file?: File) { + if (!file) return; + resetCover(); + if (!coverTypes.has(file.type)) { + setCoverError('封面仅支持 PNG、JPEG 或 WebP'); + return; + } + if (!file.size || file.size > 5 * 1024 * 1024) { + setCoverError('封面文件须大于 0 且不超过 5 MiB'); + return; + } + const generation = fileGeneration.current; + setPreviewUrl(URL.createObjectURL(file)); + setReading(true); + const nextReader = new FileReader(); + reader.current = nextReader; + nextReader.onload = () => { + if (generation !== fileGeneration.current) return; + const result = + typeof nextReader.result === 'string' ? nextReader.result : ''; + const dataBase64 = result.slice(result.indexOf(',') + 1); + if (!result.startsWith('data:') || !dataBase64) + setCoverError('读取封面失败,请重新选择'); + else setCover({ contentType: file.type, dataBase64 }); + setReading(false); + }; + nextReader.onerror = () => { + if (generation !== fileGeneration.current) return; + setCoverError('读取封面失败,请重新选择'); + setReading(false); + }; + nextReader.readAsDataURL(file); + } + + function submit(event: FormEvent) { + event.preventDefault(); + if (disabled || reading || coverError) return; + const nextTitle = title.trim(); + const nextTags = [...new Set(splitLines(tags.replaceAll(',', ',')))]; + if (!nextTitle || Array.from(nextTitle).length > 80) { + setValidationError('名称须为 1–80 个字符'); + return; + } + if (Array.from(summary).length > 1000) { + setValidationError('简介不能超过 1000 个字符'); + return; + } + if ( + nextTags.length > 16 || + nextTags.some((tag) => Array.from(tag).length > 32) + ) { + setValidationError('最多 16 个标签,每个标签不超过 32 个字符'); + return; + } + setValidationError(''); + onSave({ + title: nextTitle, + summary, + tags: nextTags, + ...(cover ? { cover } : {}), + }); + } + + return ( +
+ {readOnly ? 当前为只读模式 : null} + {feedbackMessage || conflict ? ( +
+ {feedbackMessage ? ( + + {feedbackMessage} + + ) : null} + {conflict ? ( + <> + + 当前草稿已保留,刷新后将载入最新内容 + + + + ) : null} +
+ ) : null} + setTitle(event.currentTarget.value)} + /> + setSummary(event.currentTarget.value)} + /> + setTags(event.currentTarget.value)} + /> + 模板封面预览 + chooseCover(event.currentTarget.files?.[0])} + /> + {previewUrl || coverError ? ( + + ) : null} + {reading ? 正在读取封面 : null} +
+ + +
+ + ); +} + +function formatTemplateSize(bytes: number) { + if (bytes < 1024) return `${bytes} B`; + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KiB`; + return `${(bytes / (1024 * 1024)).toFixed(1)} MiB`; +} diff --git a/apps/admin-web/src/styles/admin.css b/apps/admin-web/src/styles/admin.css index 1b62a4258..fdc22f363 100644 --- a/apps/admin-web/src/styles/admin.css +++ b/apps/admin-web/src/styles/admin.css @@ -13,6 +13,87 @@ text-rendering: optimizeLegibility; } +.admin-agc-templates { + min-width: 0; +} + +.admin-agc-template-filters { + display: grid; + grid-template-columns: minmax(180px, 1fr) repeat(2, minmax(130px, 190px)); + gap: 14px; +} + +.admin-agc-template-table { + min-width: 850px; +} + +.admin-agc-template-table td:nth-child(2) { + min-width: 230px; + max-width: 380px; +} + +.admin-agc-template-cover { + display: block; + width: 88px; + height: 62px; + border-radius: 8px; + object-fit: cover; + background: #f8efe7; +} + +.admin-agc-template-summary { + display: -webkit-box; + overflow: hidden; + margin: 8px 0; + color: #866954; + -webkit-line-clamp: 2; + -webkit-box-orient: vertical; +} + +.admin-agc-template-tags, +.admin-agc-template-actions { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: 8px; +} + +.admin-agc-template-tags span { + padding: 3px 7px; + border-radius: 5px; + background: #f8efe7; + font-size: 12px; +} + +.admin-agc-template-dialog { + border-radius: 10px; +} + +.admin-agc-template-editor, +.admin-agc-template-conflict { + display: grid; + gap: 14px; +} + +.admin-agc-template-cover-preview { + display: block; + width: min(100%, 300px); + max-height: 180px; + border-radius: 8px; + object-fit: contain; + background: #f8efe7; +} + +.admin-agc-template-dialog .admin-confirm-backdrop { + z-index: 1100; +} + +@media (max-width: 680px) { + .admin-agc-template-filters { + grid-template-columns: minmax(0, 1fr); + } +} + * { box-sizing: border-box; } diff --git a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs index da12b43c6..b9a4e644f 100644 --- a/apps/ai-game-creator-shell/src-tauri/src/template_library.rs +++ b/apps/ai-game-creator-shell/src-tauri/src/template_library.rs @@ -938,6 +938,24 @@ pub(crate) fn create_project_from_installed_template_at( enforce_project_permission_policy(&project_root, "project.create")?; let _lock = acquire_project_write_lock(&project_root, "project.create")?; copy_template_project(installed_project_dir, &project_root)?; + if discover_local_cocos_project_root(&project_root)?.is_some() { + let package_path = project_root.join("package.json"); + let mut package: serde_json::Value = serde_json::from_slice( + &fs::read(&package_path) + .map_err(|error| format!("读取 Cocos 项目配置失败:{error}"))?, + ) + .map_err(|error| format!("解析 Cocos 项目配置失败:{error}"))?; + package["name"] = serde_json::json!(project_name); + package["uuid"] = serde_json::json!(uuid::Uuid::new_v4().to_string()); + let bytes = serde_json::to_vec_pretty(&package) + .map_err(|error| format!("序列化 Cocos 项目配置失败:{error}"))?; + write_game_creator_private_file(&package_path, &bytes, "Cocos 项目配置")?; + return import_local_cocos_project_at( + &project_root, + &format!("gameagent-{workspace_id}"), + &project_name, + ); + } init_local_game_project_at( &project_root, &format!("gameagent-{workspace_id}"), @@ -1182,6 +1200,39 @@ mod tests { assert!(error.contains("重复模板"), "{error}"); } + #[test] + fn parses_content_addressed_objects_without_changing_the_template_contract() { + let mut index: serde_json::Value = + serde_json::from_str(&sample_index_body()).expect("sample index"); + let keys = [ + ("zipKey", "template.zip", "a"), + ("coverKey", "cover.png", "b"), + ("metadataKey", "template.json", "c"), + ] + .map(|(field, file, hash)| { + ( + field, + format!( + "templates/v1/demo-template/sha256/{}/{file}", + hash.repeat(64) + ), + ) + }); + for (field, key) in &keys { + index["templates"][0][field] = serde_json::json!(key); + } + let (_, templates) = + parse_game_template_library_index(&index.to_string()).expect("content addressed index"); + let parsed = serde_json::to_value(&templates[0]).expect("serialize template"); + for (field, key) in &keys { + assert_eq!(parsed[field], *key); + assert!(template_object_url(key) + .expect("trusted URL") + .ends_with(key)); + } + assert_eq!(templates[0].template_version, "1.0.0"); + } + #[test] fn rejects_object_keys_outside_the_templates_prefix() { assert!(validate_template_object_key("agc/templates/v1/demo/template.zip").is_err()); @@ -1225,6 +1276,10 @@ mod tests { "blank-2d-canvas", "blank-3d-scene", "blank-web", + "cocos-empty-2d", + "cocos-empty-3d", + "cocos-empty-3d-hq", + "cocos-hello-world", "phaser-2d-starter", "threejs-3d-starter", ] @@ -1360,6 +1415,142 @@ mod tests { assert!(error.contains("模板尚未安装"), "{error}"); } + fn template_source_files(root: &Path) -> Vec<(String, Vec)> { + fn collect(root: &Path, directory: &Path, files: &mut Vec<(String, Vec)>) { + for entry in fs::read_dir(directory).expect("read template directory") { + let path = entry.expect("template entry").path(); + if path.is_dir() { + collect(root, &path, files); + } else { + files.push(( + path.strip_prefix(root) + .expect("relative path") + .to_string_lossy() + .replace('\\', "/"), + fs::read(&path).expect("read template file"), + )); + } + } + } + let mut files = Vec::new(); + collect(root, root, &mut files); + files.sort_by(|left, right| left.0.cmp(&right.0)); + files + } + + fn assert_cocos_template_creates_independent_projects( + summary: &GameTemplateSummary, + archive: &[u8], + ) { + let cache_root = tempfile::tempdir().expect("cache directory"); + let projects_root = unique_projects_root(); + let record = install_template_archive(cache_root.path(), summary, archive) + .expect("install Cocos template"); + let installed_root = Path::new(&record.project_dir); + let installed_files = template_source_files(installed_root); + let original_package: serde_json::Value = serde_json::from_slice( + &fs::read(installed_root.join("package.json")).expect("installed package"), + ) + .expect("parse installed package"); + let mut project_uuids = std::collections::HashSet::new(); + for name in ["Cocos 模板项目一", "Cocos 模板项目二"] { + let created = create_project_from_installed_template_at( + &projects_root, + installed_root, + Some(name), + false, + ) + .expect("create Cocos project from template"); + let root = Path::new(&created.project_path); + assert_eq!(created.manifest.name, name); + assert_eq!(created.manifest.cocos_project_root.as_deref(), Some(".")); + assert!(created.manifest.godot_project_root.is_none()); + assert!(root.join(".agent/manifest.json").is_file()); + assert!(root.join(".agent/agent.db").is_file()); + for unexpected in ["game", "memory", "exports", TEMPLATE_INSTALLED_MARKER_FILE] { + assert!(!root.join(unexpected).exists(), "unexpected {unexpected}"); + } + let package: serde_json::Value = serde_json::from_slice( + &fs::read(root.join("package.json")).expect("created package"), + ) + .expect("parse created package"); + assert_eq!(package["name"], name); + assert_eq!(package["creator"]["version"], "3.8.8"); + let uuid = package["uuid"].as_str().expect("project uuid"); + uuid::Uuid::parse_str(uuid).expect("valid project uuid"); + assert_ne!(package["uuid"], original_package["uuid"]); + assert!(project_uuids.insert(uuid.to_string())); + for (relative, bytes) in &installed_files { + if relative != "package.json" && relative != TEMPLATE_INSTALLED_MARKER_FILE { + assert_eq!(&fs::read(root.join(relative)).expect("copied file"), bytes); + } + } + } + assert_eq!(template_source_files(installed_root), installed_files); + fs::remove_dir_all(&projects_root).expect("remove test projects"); + } + + #[test] + fn installs_official_cocos_templates_and_creates_native_projects() { + let library = Path::new(env!("CARGO_MANIFEST_DIR")).join("../template-library/v1"); + for id in [ + "cocos-empty-2d", + "cocos-empty-3d", + "cocos-empty-3d-hq", + "cocos-hello-world", + ] { + let source = library.join(id).join("project"); + let source_files = template_source_files(&source); + let entries = source_files + .iter() + .map(|(path, bytes)| (path.as_str(), bytes.as_slice())) + .collect::>(); + let archive = build_archive(&entries); + let mut summary = sample_summary(); + summary.id = id.to_string(); + summary.zip_size_bytes = archive.len() as u64; + summary.zip_sha256 = sha256_hex(&archive); + assert_cocos_template_creates_independent_projects(&summary, &archive); + assert_eq!(template_source_files(&source), source_files); + } + } + + #[tokio::test] + #[ignore = "需要网络:下载线上 Cocos 模板并验证原生建项"] + async fn downloads_and_creates_live_cocos_templates() { + let base = template_library_base().expect("trusted base"); + let client = build_template_library_client(); + let body = fetch_limited_bytes( + &client, + &format!("{base}/{TEMPLATE_LIBRARY_INDEX_KEY}"), + TEMPLATE_LIBRARY_MAX_INDEX_BYTES, + ) + .await + .expect("fetch live index"); + let (_, templates) = + parse_game_template_library_index(&String::from_utf8(body).expect("utf-8 index")) + .expect("parse live index"); + for id in [ + "cocos-empty-2d", + "cocos-empty-3d", + "cocos-empty-3d-hq", + "cocos-hello-world", + ] { + let summary = templates.iter().find(|entry| entry.id == id).expect(id); + assert_eq!(summary.runtime, "cocos"); + assert_eq!(summary.engine_version, "3.8.8"); + assert_eq!(summary.entry, "package.json"); + let bytes = fetch_limited_bytes( + &client, + &template_object_url(&summary.zip_key).expect("trusted zip url"), + TEMPLATE_ARCHIVE_MAX_BYTES, + ) + .await + .expect("download Cocos template"); + assert_cocos_template_creates_independent_projects(summary, &bytes); + } + } + /// 正式构建(未开 feature)必须恒等透传:注入路径不能出现在默认产物里。 #[cfg(not(feature = "template-library-fixtures"))] #[test] diff --git a/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json index 7cf1b85a7..af66a9886 100644 --- a/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json +++ b/apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json @@ -2,18 +2,13 @@ "schemaVersion": "agc-template-library.v1", "library": "agc-game-templates", "libraryVersion": 1, - "updatedAt": "2026-09-17T03:22:43Z", + "updatedAt": "2026-09-17T13:55:10Z", "templates": [ { "id": "blank-2d-canvas", "title": "空白二维画布工程", "summary": "原生 Canvas 二维空白工程:自适应画布、按设备像素比缩放与 requestAnimationFrame 主循环已就绪。", - "tags": [ - "空白", - "起步工程", - "2d", - "canvas" - ], + "tags": ["空白", "起步工程", "2d", "canvas"], "runtime": "html", "engine": "canvas", "engineVersion": "", @@ -33,12 +28,7 @@ "id": "blank-3d-scene", "title": "空白三维场景工程", "summary": "Three.js 空白场景:空场景、透视相机、网格地面与自适应视口已就绪,适合从零搭三维玩法。", - "tags": [ - "空白", - "起步工程", - "3d", - "three.js" - ], + "tags": ["空白", "起步工程", "3d", "three.js"], "runtime": "html", "engine": "three.js", "engineVersion": "0.180.0", @@ -58,12 +48,7 @@ "id": "blank-web", "title": "空白网页工程", "summary": "最小网页工程(HTML + CSS + 原生 JS + Vite),没有任何引擎依赖,适合从零写玩法。", - "tags": [ - "空白", - "起步工程", - "网页", - "原生" - ], + "tags": ["空白", "起步工程", "网页", "原生"], "runtime": "html", "engine": "none", "engineVersion": "", @@ -79,16 +64,91 @@ "coverSha256": "326a2753386618971b1311043effa46c2e74bc1cfb116862c0ee4097dcd5086a", "metadataKey": "templates/v1/blank-web/template.json" }, + { + "id": "cocos-empty-2d", + "title": "Cocos 空白 2D 工程", + "summary": "官方二维空白模板,保留精灵导入默认值与二维编辑视图。", + "tags": ["空白", "起步工程", "2d", "cocos"], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T13:55:10Z", + "entry": "package.json", + "zipKey": "templates/v1/cocos-empty-2d/template.zip", + "zipSizeBytes": 1976, + "zipSha256": "e30bd6324fd2eb721c9a9fdcc9d8bb9838fba7b3413d09a890d3152c3576c485", + "coverKey": "templates/v1/cocos-empty-2d/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "4ca21e32e890e8a3d20bde2b9f1228993cbd770532c496433de0e5ffecc31342", + "metadataKey": "templates/v1/cocos-empty-2d/template.json" + }, + { + "id": "cocos-empty-3d", + "title": "Cocos 空白 3D 工程", + "summary": "官方三维空白模板,适合从零搭建 Cocos 场景与玩法。", + "tags": ["空白", "起步工程", "3d", "cocos"], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T13:55:10Z", + "entry": "package.json", + "zipKey": "templates/v1/cocos-empty-3d/template.zip", + "zipSizeBytes": 829, + "zipSha256": "e4016ed9bcd3d3b09d5d06e776c2cf4d98415473b3747bc4945e06e93967117a", + "coverKey": "templates/v1/cocos-empty-3d/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "88111f77f6801a77acc05277dc2698378ce9eeb8e9265b7d2dedafde0d893aae", + "metadataKey": "templates/v1/cocos-empty-3d/template.json" + }, + { + "id": "cocos-empty-3d-hq", + "title": "Cocos 高质量 3D 工程", + "summary": "官方高质量三维空白模板,保留天空盒、阴影与线性纹理采样预设。", + "tags": ["空白", "起步工程", "3d", "cocos"], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T13:55:10Z", + "entry": "package.json", + "zipKey": "templates/v1/cocos-empty-3d-hq/template.zip", + "zipSizeBytes": 1226, + "zipSha256": "4b4f12a7dd39be31bc471c7387b272717e9476b1a6eafb21273ce1a3ebc38384", + "coverKey": "templates/v1/cocos-empty-3d-hq/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "276752f234c1209c182ca79b65d97c1d52d9e22b60be9aaee6cb0b134ab666ea", + "metadataKey": "templates/v1/cocos-empty-3d-hq/template.json" + }, + { + "id": "cocos-hello-world", + "title": "Cocos Hello World", + "summary": "官方三维示例场景,包含岛屿、角色、植被、材质与天空盒资源。", + "tags": ["示例", "起步工程", "3d", "cocos"], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "updatedAt": "2026-09-17T13:55:10Z", + "entry": "package.json", + "zipKey": "templates/v1/cocos-hello-world/template.zip", + "zipSizeBytes": 2484948, + "zipSha256": "dac8cc2eae2ec9ff3cd429a1be2c2981b06919be720bfa50226619c0a56e00da", + "coverKey": "templates/v1/cocos-hello-world/cover.svg", + "coverWidth": 960, + "coverHeight": 540, + "coverSha256": "9363b728a792835f57d22d129c598449801d343f739e85159e83e531ba7d949c", + "metadataKey": "templates/v1/cocos-hello-world/template.json" + }, { "id": "phaser-2d-starter", "title": "Phaser 2D 起步工程", "summary": "AGC 新建项目使用的默认二维起步工程(Phaser 4 + Vite),解压后即为可运行项目根。", - "tags": [ - "起步工程", - "2d", - "phaser", - "像素" - ], + "tags": ["起步工程", "2d", "phaser", "像素"], "runtime": "html", "engine": "phaser", "engineVersion": "4.2.1", @@ -108,12 +168,7 @@ "id": "threejs-3d-starter", "title": "Three.js 3D 起步工程", "summary": "网页三维起步工程(Three.js + Vite),自带可旋转立方体场景、方向光与自适应视口。", - "tags": [ - "起步工程", - "3d", - "three.js", - "网页" - ], + "tags": ["起步工程", "3d", "three.js", "网页"], "runtime": "html", "engine": "three.js", "engineVersion": "0.180.0", diff --git a/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserMessageEnvelope.ts b/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserMessageEnvelope.ts new file mode 100644 index 000000000..97e0b0dae --- /dev/null +++ b/apps/ai-game-creator-shell/src/features/project-workspace/generated/DirectCodexUserMessageEnvelope.ts @@ -0,0 +1,4 @@ +// This file was generated by [ts-rs](https://github.com/Aleph-Alpha/ts-rs). Do not edit this file manually. +import type { DirectCodexUserItem } from './DirectCodexUserItem'; + +export type DirectCodexUserMessageEnvelope = { item: DirectCodexUserItem }; diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/cover.svg b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/cover.svg new file mode 100644 index 000000000..c932f8d7d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/cover.svg @@ -0,0 +1,10 @@ + + + + + + + Cocos 空白 2D 工程 + Cocos Creator 3.8.8 + 官方起步模板 · 2D + diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json new file mode 100644 index 000000000..5aa321e36 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/meta.json @@ -0,0 +1,18 @@ +{ + "id": "cocos-empty-2d", + "title": "Cocos 空白 2D 工程", + "summary": "官方二维空白模板,保留精灵导入默认值与二维编辑视图。", + "tags": [ + "空白", + "起步工程", + "2d", + "cocos" + ], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "entry": "package.json", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.creator/default-meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.creator/default-meta.json new file mode 100644 index 000000000..abb1239a2 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.creator/default-meta.json @@ -0,0 +1,5 @@ +{ + "image": { + "type": "sprite-frame" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.gitignore new file mode 100644 index 000000000..a71361e85 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/.gitignore @@ -0,0 +1,31 @@ + +#/////////////////////////// +# Cocos Creator Project +#/////////////////////////// + +/library/ +/temp/ +/local/ +/build/ +/profiles/* +!/profiles/v2/ +/profiles/v2/* +!/profiles/v2/packages/ +/profiles/v2/packages/* +!/profiles/v2/packages/scene.json +/native/engine/android/**/*/assets + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/assets/.gitkeep b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/package.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/package.json new file mode 100644 index 000000000..5d13f7a68 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/package.json @@ -0,0 +1,7 @@ +{ + "name": "cocos-empty-2d", + "uuid": "d58709ec-73d2-4ae8-b5d9-f3e0262bf2ab", + "creator": { + "version": "3.8.8" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/profiles/v2/packages/scene.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/profiles/v2/packages/scene.json new file mode 100644 index 000000000..eb04b1f97 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/profiles/v2/packages/scene.json @@ -0,0 +1,5 @@ +{ + "gizmos-infos": { + "is2D": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/settings/v2/packages/engine.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/settings/v2/packages/engine.json new file mode 100644 index 000000000..1c9518d75 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/settings/v2/packages/engine.json @@ -0,0 +1,171 @@ +{ + "__version__": "1.0.11", + "modules": { + "configs": { + "defaultConfig": { + "name": "Default Config", + "cache": { + "base": { + "_value": true + }, + "gfx-webgl": { + "_value": true + }, + "gfx-webgl2": { + "_value": true + }, + "animation": { + "_value": true + }, + "skeletal-animation": { + "_value": false + }, + "3d": { + "_value": false + }, + "2d": { + "_value": true + }, + "rich-text": { + "_value": true + }, + "mask": { + "_value": true + }, + "graphics": { + "_value": true + }, + "affine-transform": { + "_value": true + }, + "xr": { + "_value": false + }, + "ui": { + "_value": true + }, + "particle": { + "_value": false + }, + "physics": { + "_value": false, + "_option": "physics-ammo" + }, + "physics-ammo": { + "_value": false + }, + "physics-cannon": { + "_value": false + }, + "physics-physx": { + "_value": false + }, + "physics-builtin": { + "_value": false + }, + "physics-2d": { + "_value": true, + "_option": "physics-2d-box2d" + }, + "physics-2d-box2d": { + "_value": false + }, + "physics-2d-builtin": { + "_value": false + }, + "intersection-2d": { + "_value": true + }, + "primitive": { + "_value": false + }, + "profiler": { + "_value": true + }, + "occlusion-query": { + "_value": false + }, + "geometry-renderer": { + "_value": false + }, + "debug-renderer": { + "_value": false + }, + "particle-2d": { + "_value": true + }, + "audio": { + "_value": true + }, + "video": { + "_value": true + }, + "webview": { + "_value": true + }, + "tween": { + "_value": true + }, + "websocket": { + "_value": false + }, + "websocket-server": { + "_value": false + }, + "terrain": { + "_value": false + }, + "light-probe": { + "_value": false + }, + "tiled-map": { + "_value": true + }, + "spine": { + "_value": true, + "_option": "spine-3.8" + }, + "dragon-bones": { + "_value": true + }, + "marionette": { + "_value": false + }, + "render-pipeline": { + "_option": "custom-pipeline" + } + }, + "includeModules": [ + "2d", + "rich-text", + "mask", + "graphics", + "affine-transform", + "animation", + "audio", + "base", + "dragon-bones", + "gfx-webgl", + "gfx-webgl2", + "intersection-2d", + "particle-2d", + "physics-2d-box2d", + "profiler", + "spine-3.8", + "tiled-map", + "tween", + "ui", + "video", + "webview", + "custom-pipeline" + ], + "noDeprecatedFeatures": { + "value": false, + "version": "" + }, + "flags": {} + } + }, + "globalConfigKey": "defaultConfig" + } +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/tsconfig.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/tsconfig.json new file mode 100644 index 000000000..7dc649a95 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-2d/project/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/cover.svg b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/cover.svg new file mode 100644 index 000000000..f52987c73 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/cover.svg @@ -0,0 +1,10 @@ + + + + + + + Cocos 高质量 3D 工程 + Cocos Creator 3.8.8 + 官方起步模板 · 3D + diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json new file mode 100644 index 000000000..a586e3366 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/meta.json @@ -0,0 +1,18 @@ +{ + "id": "cocos-empty-3d-hq", + "title": "Cocos 高质量 3D 工程", + "summary": "官方高质量三维空白模板,保留天空盒、阴影与线性纹理采样预设。", + "tags": [ + "空白", + "起步工程", + "3d", + "cocos" + ], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "entry": "package.json", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.creator/default-meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.creator/default-meta.json new file mode 100644 index 000000000..0760fe6a4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.creator/default-meta.json @@ -0,0 +1,12 @@ +{ + "texture": { + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "linear" + }, + "erp-texture-cube": { + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "linear" + } +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.gitignore new file mode 100644 index 000000000..18bb40c79 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/.gitignore @@ -0,0 +1,26 @@ + +#/////////////////////////// +# Cocos Creator Project +#/////////////////////////// + +/library/ +/temp/ +/local/ +/build/ +/profiles/ +/native/engine/android/**/*/assets + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/assets/.gitkeep b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/package.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/package.json new file mode 100644 index 000000000..0209399b0 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/package.json @@ -0,0 +1,7 @@ +{ + "name": "cocos-empty-3d-hq", + "uuid": "c8b22d4c-c40a-4390-a5c1-39ab26b6602f", + "creator": { + "version": "3.8.8" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/settings/v2/packages/project.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/settings/v2/packages/project.json new file mode 100644 index 000000000..45b386522 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/settings/v2/packages/project.json @@ -0,0 +1,5 @@ +{ + "general": { + "highQuality": true + } +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/tsconfig.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/tsconfig.json new file mode 100644 index 000000000..7dc649a95 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d-hq/project/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/cover.svg b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/cover.svg new file mode 100644 index 000000000..a441eb101 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/cover.svg @@ -0,0 +1,10 @@ + + + + + + + Cocos 空白 3D 工程 + Cocos Creator 3.8.8 + 官方起步模板 · 3D + diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json new file mode 100644 index 000000000..493f35be0 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/meta.json @@ -0,0 +1,18 @@ +{ + "id": "cocos-empty-3d", + "title": "Cocos 空白 3D 工程", + "summary": "官方三维空白模板,适合从零搭建 Cocos 场景与玩法。", + "tags": [ + "空白", + "起步工程", + "3d", + "cocos" + ], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "entry": "package.json", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/.gitignore new file mode 100644 index 000000000..18bb40c79 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/.gitignore @@ -0,0 +1,26 @@ + +#/////////////////////////// +# Cocos Creator Project +#/////////////////////////// + +/library/ +/temp/ +/local/ +/build/ +/profiles/ +/native/engine/android/**/*/assets + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/assets/.gitkeep b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/assets/.gitkeep new file mode 100644 index 000000000..e69de29bb diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/package.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/package.json new file mode 100644 index 000000000..ecf7a32e6 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/package.json @@ -0,0 +1,7 @@ +{ + "name": "cocos-empty-3d", + "uuid": "8152de95-b42e-4f35-84ac-0b16dd658a00", + "creator": { + "version": "3.8.8" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/tsconfig.json b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/tsconfig.json new file mode 100644 index 000000000..7dc649a95 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-empty-3d/project/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/cover.svg b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/cover.svg new file mode 100644 index 000000000..b8b79c0bd --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/cover.svg @@ -0,0 +1,10 @@ + + + + + + + Cocos Hello World + Cocos Creator 3.8.8 + 官方示例场景 · 3D + diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json new file mode 100644 index 000000000..8ff875247 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/meta.json @@ -0,0 +1,18 @@ +{ + "id": "cocos-hello-world", + "title": "Cocos Hello World", + "summary": "官方三维示例场景,包含岛屿、角色、植被、材质与天空盒资源。", + "tags": [ + "示例", + "起步工程", + "3d", + "cocos" + ], + "runtime": "cocos", + "engine": "cocos-creator", + "engineVersion": "3.8.8", + "templateVersion": "0.1.0", + "entry": "package.json", + "coverWidth": 960, + "coverHeight": 540 +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/.gitignore b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/.gitignore new file mode 100644 index 000000000..7d4e485b1 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/.gitignore @@ -0,0 +1,23 @@ +#/////////////////////////// +# Cocos Creator 3D Project +#/////////////////////////// +library/ +temp/ +local/ +build/ +profiles/ + +#////////////////////////// +# NPM +#////////////////////////// +node_modules/ + +#////////////////////////// +# VSCode +#////////////////////////// +.vscode/ + +#////////////////////////// +# WebStorm +#////////////////////////// +.idea/ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material.meta new file mode 100644 index 000000000..e1d76601c --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "bcb14f34-8131-435f-a8f5-29612c45af59", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl new file mode 100644 index 000000000..8ea9e0eae --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl @@ -0,0 +1,34 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a3cd009f-0ab0-420d-9278-b9fdab939bbc" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_TEXTURE": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + { + "mainTexture": { + "__uuid__": "dc4a96c7-321a-48af-81e5-1127ad3ae432@6c48a" + }, + "alphaThreshold": 0 + } + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl.meta new file mode 100644 index 000000000..3667465cf --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/hdcSky.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "482a5162-dad9-446c-b548-8486c7598ee1", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl new file mode 100644 index 000000000..ab9171c38 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl @@ -0,0 +1,34 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a3cd009f-0ab0-420d-9278-b9fdab939bbc" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_TEXTURE": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + { + "mainTexture": { + "__uuid__": "4f4c4a34-2d08-4a4d-9169-834d7ce82cee@6c48a" + }, + "alphaThreshold": 0 + } + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl.meta new file mode 100644 index 000000000..4b1c5314b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/plane.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "23e988d0-7168-4fe2-9d46-f29c114e9e33", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl new file mode 100644 index 000000000..7c7f64b04 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl @@ -0,0 +1,42 @@ +{ + "__type__": "cc.Material", + "_name": "seafloor", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "1baf0fc9-befa-459c-8bdd-af1a450a0319" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_ALBEDO_MAP": true + } + ], + "_states": [ + { + "rasterizerState": {}, + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {} + } + ], + "_props": [ + { + "mainTexture": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@2df3a" + }, + "albedoScale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "metallic": 0.400000005960464, + "roughness": 0.70710676908493, + "alphaThreshold": 0 + } + ] +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl.meta new file mode 100644 index 000000000..eb9a99a72 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/seafloor.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "70d33758-1c1e-424d-b0ab-eac7410559bf", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl new file mode 100644 index 000000000..6e09726d3 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl @@ -0,0 +1,62 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "1baf0fc9-befa-459c-8bdd-af1a450a0319" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_ALBEDO_MAP": true + }, + { + "USE_ALBEDO_MAP": true + }, + { + "USE_ALBEDO_MAP": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + { + "alphaThreshold": 0, + "roughness": 0.70710676908493, + "metallic": 0.400000005960464, + "mainTexture": { + "__uuid__": "95e5b02a-e338-423c-bdbb-17486db1d9eb@6c48a" + } + }, + {}, + {} + ] +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl.meta new file mode 100644 index 000000000..1d1838364 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/shield.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "8e047178-f61c-4322-a2f6-d1adb28b6ae2", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl new file mode 100644 index 000000000..c405305d7 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl @@ -0,0 +1,83 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a7612b54-35e3-4238-a1a9-4a7b54635839" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_OUTLINE_PASS": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + {}, + { + "specular": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 0 + }, + "mainTexture": { + "__uuid__": "6f891a7b-5a08-48e6-9841-ddb364ac86b1@6c48a" + } + }, + {}, + {} + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl.meta new file mode 100644 index 000000000..028926443 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/soldier.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "8a58ddec-f437-40b9-8ec0-1fc87de97fb5", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl new file mode 100644 index 000000000..69da94fa6 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl @@ -0,0 +1,83 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a7612b54-35e3-4238-a1a9-4a7b54635839" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_OUTLINE_PASS": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + {}, + { + "specular": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 0 + }, + "mainTexture": { + "__uuid__": "0718d996-39bf-4ab4-bb63-496666fef467@6c48a" + } + }, + {}, + {} + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl.meta new file mode 100644 index 000000000..eeaf407e8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/stone.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "a155f93b-7769-4ca4-b75f-b13e52193859", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl new file mode 100644 index 000000000..120ce3eaa --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl @@ -0,0 +1,83 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a7612b54-35e3-4238-a1a9-4a7b54635839" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_OUTLINE_PASS": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_1": true, + "BASE_COLOR_MAP_AS_SHADE_MAP_2": true + }, + { + "USE_BASE_COLOR_MAP": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + }, + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + {}, + { + "specular": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 0 + }, + "mainTexture": { + "__uuid__": "c5083e75-ad2e-4ea9-8b33-dee748995b00@6c48a" + } + }, + {}, + {} + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl.meta new file mode 100644 index 000000000..4078086f5 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/material/tree.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "7bf9df40-4bc9-4e25-8cb0-9a500f949102", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model.meta new file mode 100644 index 000000000..2408a58de --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "1ddc11ba-ecbd-4472-841c-f3777cb248da", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld.meta new file mode 100644 index 000000000..9e85fa7a9 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "11a1d348-a622-41b2-89f3-ed24657e5f84", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass.meta new file mode 100644 index 000000000..c5088c9be --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "e00862a8-c500-427c-b76f-bbe5203f19cc", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX new file mode 100644 index 000000000..b4b7ead0c Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX.meta new file mode 100644 index 000000000..4b646a4e8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.FBX.meta @@ -0,0 +1,170 @@ +{ + "ver": "2.0.10", + "importer": "fbx", + "imported": true, + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160", + "files": [], + "subMetas": { + "ef5e1": { + "importer": "gltf-mesh", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1", + "displayName": "", + "id": "ef5e1", + "name": "grass.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "73b7f": { + "importer": "gltf-animation", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f", + "displayName": "", + "id": "73b7f", + "name": "Take 001.animation", + "ver": "1.0.14", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "events": [], + "gltfIndex": 0, + "sample": 30, + "span": { + "from": 0, + "to": 3.3333332538604736 + }, + "wrapMode": 2, + "speed": 1 + } + }, + "438fe": { + "importer": "gltf-skeleton", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe", + "displayName": "", + "id": "438fe", + "name": "UnnamedSkeleton.skeleton", + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0, + "jointsLength": 6 + } + }, + "80e0c": { + "importer": "gltf-embeded-image", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@80e0c", + "displayName": "", + "id": "80e0c", + "name": "grass.png.image", + "ver": "1.0.3", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "9787f": { + "importer": "texture", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@9787f", + "displayName": "", + "id": "9787f", + "name": "grass.texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@80e0c" + } + }, + "3022b": { + "importer": "gltf-scene", + "uuid": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@3022b", + "displayName": "", + "id": "3022b", + "name": "grass.prefab", + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + } + }, + "userData": { + "imageMetas": [ + { + "name": "grass.png", + "uri": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@80e0c" + } + ], + "animationImportSettings": [ + { + "name": "Take 001", + "duration": 3.3333332538604736, + "fps": 30, + "splits": [ + { + "name": "Take 001", + "from": 0, + "to": 3.3333332538604736, + "wrapMode": 2 + } + ] + } + ], + "redirect": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@3022b", + "assetFinder": { + "meshes": [ + "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + ], + "skeletons": [ + "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + ], + "textures": [ + "aade09ee-8f9d-413c-a9e8-8c686ea5e160@9787f" + ], + "materials": [ + "b698e55a-b00b-4987-a8b4-af83cddc59f7" + ], + "scenes": [ + "aade09ee-8f9d-413c-a9e8-8c686ea5e160@3022b" + ] + }, + "useVertexColors": true, + "dumpMaterials": true, + "materialDumpDir": "db://assets/model/helloWorld/grass", + "legacyFbxImporter": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl new file mode 100644 index 000000000..5233edba8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl @@ -0,0 +1,34 @@ +{ + "__type__": "cc.Material", + "_name": "", + "_objFlags": 0, + "_native": "", + "_effectAsset": { + "__uuid__": "a3cd009f-0ab0-420d-9278-b9fdab939bbc" + }, + "_techIdx": 0, + "_defines": [ + { + "USE_TEXTURE": true + } + ], + "_states": [ + { + "blendState": { + "targets": [ + {} + ] + }, + "depthStencilState": {}, + "rasterizerState": {} + } + ], + "_props": [ + { + "mainTexture": { + "__uuid__": "ae18deea-c6e0-4a3d-bf70-ee5533f9ba87@6c48a" + }, + "alphaThreshold": 0 + } + ] +} \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl.meta new file mode 100644 index 000000000..9b34538e9 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.mtl.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.0.9", + "importer": "material", + "imported": true, + "uuid": "b698e55a-b00b-4987-a8b4-af83cddc59f7", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png new file mode 100644 index 000000000..78406d723 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png.meta new file mode 100644 index 000000000..d495b2a68 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.png.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "ae18deea-c6e0-4a3d-bf70-ee5533f9ba87", + "files": [ + ".png", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "ae18deea-c6e0-4a3d-bf70-ee5533f9ba87@6c48a", + "displayName": "grass", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "linear", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "ae18deea-c6e0-4a3d-bf70-ee5533f9ba87" + } + } + }, + "userData": { + "type": "texture", + "redirect": "ae18deea-c6e0-4a3d-bf70-ee5533f9ba87@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab new file mode 100644 index 000000000..867ae6833 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab @@ -0,0 +1,589 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 20 + } + ], + "_prefab": { + "__id__": 21 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 7 + }, + { + "__id__": 13 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": { + "__id__": 6 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 5 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 1 + }, + "_id": "", + "__prefab": { + "__id__": 22 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3aIX8gjK5JFK8ATDBKdax8" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 8 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 12 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [ + { + "__id__": 9 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 11 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 10 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1628kgL41EG4kfuuNtniX1" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d1mgL13wtIwojR/2FqCufO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "f5o8/2y99IcawmyX5Tnjyv" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 14 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 13 + }, + "_children": [ + { + "__id__": 15 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 17 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 14 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 16 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a2tndvTm9M84i3qPpLv8DA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c5xVHa1qBClrp5YbutRSaI" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3fxCLd4O9NUqkwJjBsDQgb" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b7w6U8zppFX54UJURtAWiT" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 1 + }, + "_enabled": true, + "playOnLoad": false, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 23 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "fdSk3ayLBOH7saksB95r+y" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "cdq4wvfd1AkYLKBGkpcnul" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "deki0q/OJHD4jsTyP928BZ" + } +] diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab.meta new file mode 100644 index 000000000..cc5d2798d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grass.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.27", + "importer": "prefab", + "imported": true, + "uuid": "ebe68402-4803-40d3-b0a2-ca696e3f7c60", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "grass" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab new file mode 100644 index 000000000..9e5ed56d8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab @@ -0,0 +1,7035 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Node", + "_name": "grassGoup", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 23 + }, + { + "__id__": 44 + }, + { + "__id__": 65 + }, + { + "__id__": 86 + }, + { + "__id__": 107 + }, + { + "__id__": 128 + }, + { + "__id__": 149 + }, + { + "__id__": 170 + }, + { + "__id__": 191 + }, + { + "__id__": 212 + }, + { + "__id__": 233 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 254 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [ + { + "__id__": 21 + } + ], + "_prefab": { + "__id__": 22 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.852, + "y": 0.892, + "z": -1.49 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.8, + "y": 1.8, + "z": 1.8 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 4 + }, + { + "__id__": 8 + }, + { + "__id__": 14 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 20 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 3 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 5 + } + ], + "_prefab": { + "__id__": 7 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 4 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 6 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 2 + }, + "_id": "", + "__prefab": { + "__id__": 255 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "5fs1r6h0xEiJFeWVPk8SNl" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 3 + }, + "_children": [ + { + "__id__": 9 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 13 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 8 + }, + "_children": [ + { + "__id__": 10 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 12 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 9 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 11 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8e/dPEvbdIMo48q55vpZZ3" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "beKna0bXZNtrHWMkpFZoVU" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "daWdShf/9NHLgm8BB+DJZm" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 3 + }, + "_children": [ + { + "__id__": 15 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 14 + }, + "_children": [ + { + "__id__": 16 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 15 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 17 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "96ATlvvpZKqpZWVYxnyW6U" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8fu8biB4BPFKtDBKsYv4K/" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "94aP4+w7dE/qUrh7Cfcv3Y" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "54SHCY049LyqH1HRGY3ZVR" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 256 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "87M1Av0v5LhZ3LsJOTzwr3" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 24 + } + ], + "_active": true, + "_components": [ + { + "__id__": 42 + } + ], + "_prefab": { + "__id__": 43 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 1.271, + "y": 0.836, + "z": -2.68 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": -0.09886819763876702, + "z": 0, + "w": 0.9951005373808527 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.4, + "y": 1.4, + "z": 1.4 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -11.348, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 23 + }, + "_children": [ + { + "__id__": 25 + }, + { + "__id__": 29 + }, + { + "__id__": 35 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 41 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 24 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 26 + } + ], + "_prefab": { + "__id__": 28 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 25 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 27 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 23 + }, + "_id": "", + "__prefab": { + "__id__": 257 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "74Q+wg/aVBj5Bkvd+Cqz4L" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 24 + }, + "_children": [ + { + "__id__": 30 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 29 + }, + "_children": [ + { + "__id__": 31 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 33 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 30 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 32 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "af9wSIs/ZHx5FXOjfa1Jum" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "25LOOT8GBDpr0h1KIC5gCx" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4fGc2baulLjbJeePpPYcqt" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 24 + }, + "_children": [ + { + "__id__": 36 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 40 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 35 + }, + "_children": [ + { + "__id__": 37 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 39 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 36 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 38 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "65QAhjUVBAtLwEci9t05Ey" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e75gT5ogJMOoh/2sggS7OQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b8DSzxi5tJyKiMnjvG0NLg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "bdiB2XMldLgpbPP0u9vyiH" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 23 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 258 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8avX4W7ZtLOLCZ8n5QtiPm" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 45 + } + ], + "_active": true, + "_components": [ + { + "__id__": 63 + } + ], + "_prefab": { + "__id__": 64 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.816, + "y": 0.874, + "z": -2.029 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.009594606467145706, + "y": -0.09840154486124139, + "z": -0.09656895017241425, + "w": 0.9904037143297977 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.8, + "y": 1.8, + "z": 1.8 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -11.348, + "z": -11.138 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 44 + }, + "_children": [ + { + "__id__": 46 + }, + { + "__id__": 50 + }, + { + "__id__": 56 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 62 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 45 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 47 + } + ], + "_prefab": { + "__id__": 49 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 46 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 48 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 44 + }, + "_id": "", + "__prefab": { + "__id__": 259 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "60njdFxDhJWowG9HZr9kRy" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 45 + }, + "_children": [ + { + "__id__": 51 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 55 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 50 + }, + "_children": [ + { + "__id__": 52 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 54 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 51 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 53 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "91jR835RZHKbRhzSn1x2pJ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3bw6IJ5hBJ0aqjcninjsA3" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "dfSzBMugdCkL5lyChYPcpt" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 45 + }, + "_children": [ + { + "__id__": 57 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 61 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 56 + }, + "_children": [ + { + "__id__": 58 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 60 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 57 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 59 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "977HJ5Wy5HiapZRnYB3I8X" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3da6ppQblGu7DGQDIjczFt" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "36km6+kJtJZ4iiy8pwU3RO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0cYonIu91LHamoapMUydbc" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 44 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 260 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "5fAmHUz0xO9YFym/gsZawP" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 66 + } + ], + "_active": true, + "_components": [ + { + "__id__": 84 + } + ], + "_prefab": { + "__id__": 85 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 1.505, + "y": 0.835, + "z": -2.352 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.009051343059323664, + "y": 0.011005731118140823, + "z": -0.042668681053864996, + "w": 0.9989876529409666 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.8, + "y": 1.8, + "z": 1.8 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.094, + "y": 1.309, + "z": -4.879 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 65 + }, + "_children": [ + { + "__id__": 67 + }, + { + "__id__": 71 + }, + { + "__id__": 77 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 83 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 66 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 68 + } + ], + "_prefab": { + "__id__": 70 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 67 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 69 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 65 + }, + "_id": "", + "__prefab": { + "__id__": 261 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "f2zxp5nX1CJq18JC2EndYO" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 66 + }, + "_children": [ + { + "__id__": 72 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 76 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 71 + }, + "_children": [ + { + "__id__": 73 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 75 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 72 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 74 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "baL59AVvRKlKmpQqSjNAYI" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "89NJAVd1tBZ6C0iX/YbeY2" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3c+BtM6atJC4XerbYlNlM0" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 66 + }, + "_children": [ + { + "__id__": 78 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 82 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 77 + }, + "_children": [ + { + "__id__": 79 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 81 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 78 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 80 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6fAgsRfRRHeoZrCvNeGhDK" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8exO45eF5F2KzRxFbNE1M/" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9byDe6eNtGPIztX4VSZKlQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2at9CGY1VK9qtNpM8kC/KU" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 65 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 262 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9bIVhdYYpFl7JZmL4oubNS" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 87 + } + ], + "_active": true, + "_components": [ + { + "__id__": 105 + } + ], + "_prefab": { + "__id__": 106 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 2.591, + "y": 0.697, + "z": -3.192 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.009569056200142995, + "y": -0.27238145223532967, + "z": -0.02174826425431747, + "w": 0.961895935454317 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.4, + "y": 1.4, + "z": 1.4 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -1.735, + "y": -31.653, + "z": -2.099 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 86 + }, + "_children": [ + { + "__id__": 88 + }, + { + "__id__": 92 + }, + { + "__id__": 98 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 104 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 87 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 89 + } + ], + "_prefab": { + "__id__": 91 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 88 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 90 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 86 + }, + "_id": "", + "__prefab": { + "__id__": 263 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a2WxB7prNBKZLuuRtE7RC2" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 87 + }, + "_children": [ + { + "__id__": 93 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 97 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 92 + }, + "_children": [ + { + "__id__": 94 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 96 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 93 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 95 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "65JgfqDUlBhq62ZblXl99a" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "613BOOO+hIlqnTjr1LZ7yD" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d9cSWZZzxJ0JJz3NZ3KYKr" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 87 + }, + "_children": [ + { + "__id__": 99 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 103 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 98 + }, + "_children": [ + { + "__id__": 100 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 102 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 99 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 101 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4bzVNkn1hF04sTb5BrBwWZ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "aea82LsKdAiqVuBFPi2Vfz" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "daMGy+QoZIMbYWUh1/PBDR" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "75DE0pyqBGsIOI2RSOFxap" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 86 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 264 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2d66RAsipF1brG2RGpHJY1" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 108 + } + ], + "_active": true, + "_components": [ + { + "__id__": 126 + } + ], + "_prefab": { + "__id__": 127 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.85, + "y": 0.766, + "z": -2.616 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.007707220961668685, + "y": -0.0985673335815995, + "z": 0.07757256533280044, + "w": 0.9920723646001587 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.8, + "y": 1.8, + "z": 1.8 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -11.348, + "z": 8.942 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 107 + }, + "_children": [ + { + "__id__": 109 + }, + { + "__id__": 113 + }, + { + "__id__": 119 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 125 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 108 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 110 + } + ], + "_prefab": { + "__id__": 112 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 109 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 111 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 107 + }, + "_id": "", + "__prefab": { + "__id__": 265 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "32O+zfC79Ly5/JDvR+zPy3" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 108 + }, + "_children": [ + { + "__id__": 114 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 118 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 113 + }, + "_children": [ + { + "__id__": 115 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 117 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 114 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 116 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "f31n3srf1OQ6ztTn+Z0+bi" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "39pnXuaeZGe5VqFTUXoXAo" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2csxgVNAlDEIWU7lnXQfKS" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 108 + }, + "_children": [ + { + "__id__": 120 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 124 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 119 + }, + "_children": [ + { + "__id__": 121 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 123 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 120 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 122 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c8HIUzEBxJx72ffmHr7Yzh" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "82tqRzYDNKi6iXruWWHldm" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e1Z+TO4odC37Bc8FKLNniF" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "12tzV7tYdKJJxbVmqiEpry" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 107 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 266 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ffwse8PqZBf4sIEkNdsA+i" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 129 + } + ], + "_active": true, + "_components": [ + { + "__id__": 147 + } + ], + "_prefab": { + "__id__": 148 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.252, + "y": 0.663, + "z": -3.355 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.005303598000440666, + "y": -0.09872584440048117, + "z": 0.053380291603709065, + "w": 0.9936677633716235 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.6, + "y": 1.6, + "z": 1.6 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 9.996766443584073e-17, + "y": -11.348, + "z": 6.150000000000002 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 128 + }, + "_children": [ + { + "__id__": 130 + }, + { + "__id__": 134 + }, + { + "__id__": 140 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 146 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 129 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 131 + } + ], + "_prefab": { + "__id__": 133 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 130 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 132 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 128 + }, + "_id": "", + "__prefab": { + "__id__": 267 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3c/GljN9JNKpGUQDJiCaAz" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 129 + }, + "_children": [ + { + "__id__": 135 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 139 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 134 + }, + "_children": [ + { + "__id__": 136 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 138 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 135 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 137 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9fnTS8hnVIWqrzTdhgEpre" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "77DlSvZMVCFLNlyGWiwIha" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "17tlC2EGZMWoKAixVNAp0A" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 129 + }, + "_children": [ + { + "__id__": 141 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 145 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 140 + }, + "_children": [ + { + "__id__": 142 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 144 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 141 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 143 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8cUixi2TpMHINjSklAPeYi" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b7XmnH2bBCVpvgTAKnVXA2" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d1EnMgh6RE7o+kI5cUNlG0" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0eVA3+ezRBf4EQOOoMIo4S" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 128 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 268 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "adNP+ELEhC4awfcKkY8jYJ" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 150 + } + ], + "_active": true, + "_components": [ + { + "__id__": 168 + } + ], + "_prefab": { + "__id__": 169 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -4.716, + "y": 0.534, + "z": -2.539 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.011417842217574529, + "y": 0.10201162854238845, + "z": 0.09441482697624534, + "w": 0.9902267926936081 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.2999999999999998, + "y": 1.3, + "z": 1.3 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -2.442, + "y": 11.991, + "z": 10.641 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 149 + }, + "_children": [ + { + "__id__": 151 + }, + { + "__id__": 155 + }, + { + "__id__": 161 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 167 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 150 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 152 + } + ], + "_prefab": { + "__id__": 154 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 151 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 153 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 149 + }, + "_id": "", + "__prefab": { + "__id__": 269 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "43yCcO1I5D6qP+LJIIGKjX" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 150 + }, + "_children": [ + { + "__id__": 156 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 160 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 155 + }, + "_children": [ + { + "__id__": 157 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 159 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 156 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 158 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d5T2sohQVAEa4ljoSzlMKU" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "93hV+qqgFP44pIvuByy6RE" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0aJTcuk/5M56nEkb+D7tN9" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 150 + }, + "_children": [ + { + "__id__": 162 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 166 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 161 + }, + "_children": [ + { + "__id__": 163 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 165 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 162 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 164 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6c2sTe+khLGYrsZVSADL4H" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ecsSU+lvpNf7DhOOeFG7w2" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6etpava3ZGlovr3HG7SpRo" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "53OGGCuwFNs4KQ0nQRdow5" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 149 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 270 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "33gYFxG8dO2r8iKjzjEx4z" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 171 + } + ], + "_active": true, + "_components": [ + { + "__id__": 189 + } + ], + "_prefab": { + "__id__": 190 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.151, + "y": 0.706, + "z": -2.905 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.01606605470413201, + "y": 0.10576273729137807, + "z": 0.0511818299419083, + "w": 0.9929433748057723 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.6, + "y": 1.6, + "z": 1.6 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -2.461, + "y": 12.281, + "z": 5.638 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 170 + }, + "_children": [ + { + "__id__": 172 + }, + { + "__id__": 176 + }, + { + "__id__": 182 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 188 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 171 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 173 + } + ], + "_prefab": { + "__id__": 175 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 172 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 174 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 170 + }, + "_id": "", + "__prefab": { + "__id__": 271 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "87cW2HzEtNk4+RMW95NbLB" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 171 + }, + "_children": [ + { + "__id__": 177 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 181 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 176 + }, + "_children": [ + { + "__id__": 178 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 180 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 177 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 179 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "5bcVkwfGRPza49BU4T6rqn" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "fcqb+cLDVJ3KNvBTyJYBwm" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "16ssZzbLdO76GQg4Dvdp/y" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 171 + }, + "_children": [ + { + "__id__": 183 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 187 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 182 + }, + "_children": [ + { + "__id__": 184 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 186 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 183 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 185 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0b/yPLcR1FiZPqyfaG8HBZ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0fPapM97RGmYmlB69FuuSF" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "0eRKfLOh9OdLsOOjTosv1c" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "560/1xR41BPKG+lcc5935I" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 170 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 272 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "27mCip0lFNkL+6Dj/Bpwj4" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 192 + } + ], + "_active": true, + "_components": [ + { + "__id__": 210 + } + ], + "_prefab": { + "__id__": 211 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 2.179, + "y": 0.714, + "z": -3.159 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.0002046174412504865, + "y": -0.2725489284000769, + "z": -0.05622568582446521, + "w": 0.9604976376926917 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.4, + "y": 1.4, + "z": 1.4 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -1.744, + "y": -31.778, + "z": -6.207 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 191 + }, + "_children": [ + { + "__id__": 193 + }, + { + "__id__": 197 + }, + { + "__id__": 203 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 209 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 192 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 194 + } + ], + "_prefab": { + "__id__": 196 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 193 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 195 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 191 + }, + "_id": "", + "__prefab": { + "__id__": 273 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "08LJZ1ezRCiqnszI5cXQTx" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 192 + }, + "_children": [ + { + "__id__": 198 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 202 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 197 + }, + "_children": [ + { + "__id__": 199 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 201 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 198 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 200 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d8OdalQsZKwLFf2mFuR1VJ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ecFy8CTgxCib8Fky/YcoUL" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c9g0zchT9L8IQmY+99TABR" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 192 + }, + "_children": [ + { + "__id__": 204 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 208 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 203 + }, + "_children": [ + { + "__id__": 205 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 207 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 204 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 206 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "66R8TA3S1NT4HLywjDiP1Q" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "963dgjTmdES7q6KBvCTllJ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "91+xPZxDtGRp/x7cNrBUhM" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "25mf8t9bhJIo3E0lcGCo1H" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 191 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 274 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "50aim7sdtB2rmsHB4KRkd5" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 213 + } + ], + "_active": true, + "_components": [ + { + "__id__": 231 + } + ], + "_prefab": { + "__id__": 232 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 5.041, + "y": 0.406, + "z": -3.58 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.026333680128465056, + "y": -0.27127042474669716, + "z": -0.14805365417229147, + "w": 0.9506834433368816 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.4, + "y": 1.4, + "z": 1.4 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -1.815, + "y": -32.126, + "z": -17.205 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 212 + }, + "_children": [ + { + "__id__": 214 + }, + { + "__id__": 218 + }, + { + "__id__": 224 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 230 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 213 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 215 + } + ], + "_prefab": { + "__id__": 217 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 214 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 216 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 212 + }, + "_id": "", + "__prefab": { + "__id__": 275 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "203+J3Vb1LP7KUgkjLvNtX" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 213 + }, + "_children": [ + { + "__id__": 219 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 223 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 218 + }, + "_children": [ + { + "__id__": 220 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 222 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 219 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 221 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "3aJII/zG9KaYapX0uizphW" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "48r+NZR1BD7Y5qEv/ruQsO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9eOEdVVahA+rbDiF4AJNNX" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 213 + }, + "_children": [ + { + "__id__": 225 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 229 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 224 + }, + "_children": [ + { + "__id__": 226 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 228 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 225 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 227 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1fFpy0ihVOYbzhBLRLkL1Y" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d9iCqzyppAILXebiHSqtDk" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "469eCD0ttKHZacZ3xtfeuD" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9dXaYb8ytBBb2yYH4yN6Vh" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 212 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 276 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "70DWXrCRRL4pWATtzgi7DN" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 234 + } + ], + "_active": true, + "_components": [ + { + "__id__": 252 + } + ], + "_prefab": { + "__id__": 253 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 4.153, + "y": 0.548, + "z": -3.566 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.01263456693718553, + "y": -0.27225485806840527, + "z": -0.09997674733537244, + "w": 0.9569338064718487 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1.4, + "y": 1.4, + "z": 1.4 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -1.769, + "y": -31.94, + "z": -11.433 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 233 + }, + "_children": [ + { + "__id__": 235 + }, + { + "__id__": 239 + }, + { + "__id__": 245 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 251 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "grass", + "_objFlags": 0, + "_parent": { + "__id__": 234 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 236 + } + ], + "_prefab": { + "__id__": 238 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.37992816729770207, + "y": 0.5963678291908521, + "z": 0.5963678291908521, + "w": -0.37992816729770207 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.614784121513367, + "y": 0.614784121513367, + "z": 0.614784121513367 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -115.0000056286655, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 235 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "b698e55a-b00b-4987-a8b4-af83cddc59f7" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 237 + }, + "_mesh": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@ef5e1" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@438fe" + }, + "_skinningRoot": { + "__id__": 233 + }, + "_id": "", + "__prefab": { + "__id__": 277 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "80TsYKkUVBAIbPGc2Ao+X4" + }, + { + "__type__": "cc.Node", + "_name": "Bone001", + "_objFlags": 0, + "_parent": { + "__id__": 234 + }, + "_children": [ + { + "__id__": 240 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 244 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0461842827498913, + "y": 0.0000118009265861474, + "z": -0.0284814611077309 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone002", + "_objFlags": 0, + "_parent": { + "__id__": 239 + }, + "_children": [ + { + "__id__": 241 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 243 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -3.57627860658738e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone003", + "_objFlags": 0, + "_parent": { + "__id__": 240 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 242 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0397140197455883, + "y": -1.19209286886246e-9, + "z": 7.15255721317476e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6aDs2ciZhDh6tahiNmO4QO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1er+9JvftCj5j548DE5PNk" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "54j/6cneBCF4PpXQIO+ZrT" + }, + { + "__type__": "cc.Node", + "_name": "Bone004", + "_objFlags": 0, + "_parent": { + "__id__": 234 + }, + "_children": [ + { + "__id__": 246 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 250 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0710692703723907, + "y": 0.0000118009265861474, + "z": 0.0136896027252078 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0000017283479103639154, + "y": -0.000001696768662714476, + "z": 0.7049074170330618, + "w": 0.7092993256770451 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.0005507418826256091, + "y": 0.00027320859410338513, + "z": 89.64412979694991 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone005", + "_objFlags": 0, + "_parent": { + "__id__": 245 + }, + "_children": [ + { + "__id__": 247 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 249 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0404664427042007, + "y": -4.76837147544984e-9, + "z": 5.9604643443123e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.000002421418716486127, + "y": 1.5182564735607207e-8, + "z": 0.006269989150778519, + "w": 0.9999803434219023 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000277479600078167, + "y": -3.18993241527229e-13, + "z": 0.7184925395739944 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bone006", + "_objFlags": 0, + "_parent": { + "__id__": 246 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 248 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.039714016020298, + "y": 0, + "z": 7.74860353658369e-9 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 1.4018830392883508e-10, + "y": -1.4662893682703937e-13, + "z": -0.0010459420626472054, + "w": 0.9999994530024512 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 1.606440509162768e-8, + "y": -1.4726229276075984e-18, + "z": -0.11985615346346049 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1eZ45FwjhMoY5AmDd/ApaW" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "7fkJUsKFZMHbutZKiHcKAf" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c8ON8aNTZH1YV9KZsolNGO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "fa4L3FVuFMwYhNq67HoU7c" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "grass", + "_objFlags": 0, + "node": { + "__id__": 233 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "aade09ee-8f9d-413c-a9e8-8c686ea5e160@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 278 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "7aOtJ3fThN27Zj4fmkg0ut" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e5Peksu5tL9peMeABb8/JC" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a6J1jU/r1BPKNIcVMEREit" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "11JgtTUn5OJKE3LJ8pTj/2" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "28WH2cvhNDFbN3it+8Q+XK" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "abXR2STONCQrmlGd7QjDWb" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "87exLhmM1P35jx6GLN7j5f" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3eRlu7+XFMf4IKjhzVuyYo" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "dduLonFhNK+q908BUlLM7f" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "22rvun9aNNBbAK//dWXzkX" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "af0f1GeGlKf5MvKMEuH8MJ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "0f8P8jwTVOzaIGGhmhMYJ6" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e9BPt5G81CloUmA5IJNHIQ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b6LjWqcFFEYa267NpDVpBB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "7dt8ZZ5EBNDIxOgpt1XbGu" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a5eATDuwJGuL6l09vX0n3S" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "c4Nekl9YtKUKupS0ASX7It" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "8fKHzMP19M76liKGOWy5fp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "b9cuhcVRxPvZTPHcAyE7eS" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "63dAWSpBBOqo5RjIXi5sEW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "f3T3OFBBJBYrGbPSQ+k7GJ" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "5c8UINxBNN1bAOwYYO6ZIW" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d0A5LfhGhAfrDnBGJ3JzOe" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2dbXeRDsVAWK67o3tGgUEl" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "161SEdWiFO/abXxUI8RkYk" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "42733HPMdIEqobiaBVXbRJ" + } +] diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab.meta new file mode 100644 index 000000000..cd7732184 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/grass/grassGoup.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.27", + "importer": "prefab", + "imported": true, + "uuid": "5e4d48c4-0e34-45af-a268-89485197e8bc", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "grassGoup" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX new file mode 100644 index 000000000..e8c5d984b Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX.meta new file mode 100644 index 000000000..e2abe3df4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.FBX.meta @@ -0,0 +1,110 @@ +{ + "ver": "2.0.10", + "importer": "fbx", + "imported": true, + "uuid": "929e58ce-66a4-4e04-9036-4244456a1220", + "files": [], + "subMetas": { + "f6832": { + "importer": "gltf-mesh", + "uuid": "929e58ce-66a4-4e04-9036-4244456a1220@f6832", + "displayName": "", + "id": "f6832", + "name": "hdcSky.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "7f40d": { + "importer": "gltf-embeded-image", + "uuid": "929e58ce-66a4-4e04-9036-4244456a1220@7f40d", + "displayName": "", + "id": "7f40d", + "name": "hdcSky.image", + "ver": "1.0.3", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "d6067": { + "importer": "texture", + "uuid": "929e58ce-66a4-4e04-9036-4244456a1220@d6067", + "displayName": "", + "id": "d6067", + "name": "hdcSky.texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "929e58ce-66a4-4e04-9036-4244456a1220@7f40d" + } + }, + "bfc57": { + "importer": "gltf-scene", + "uuid": "929e58ce-66a4-4e04-9036-4244456a1220@bfc57", + "displayName": "", + "id": "bfc57", + "name": "hdcSky.prefab", + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + } + }, + "userData": { + "imageMetas": [ + { + "name": "hdcSky", + "uri": "929e58ce-66a4-4e04-9036-4244456a1220@7f40d" + } + ], + "redirect": "929e58ce-66a4-4e04-9036-4244456a1220@bfc57", + "assetFinder": { + "meshes": [ + "929e58ce-66a4-4e04-9036-4244456a1220@f6832" + ], + "skeletons": [], + "textures": [ + "929e58ce-66a4-4e04-9036-4244456a1220@d6067" + ], + "materials": [ + "482a5162-dad9-446c-b548-8486c7598ee1" + ], + "scenes": [ + "929e58ce-66a4-4e04-9036-4244456a1220@bfc57" + ] + }, + "dumpMaterials": true, + "materialDumpDir": "db://assets/material", + "legacyFbxImporter": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab new file mode 100644 index 000000000..81af9a12d --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab @@ -0,0 +1,223 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Node", + "_name": "hdcSky", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 8 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 7 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "hdcSky", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": { + "__id__": 6 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0.00000556361783310422 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.57702493667603, + "y": 2.57702493667603, + "z": 2.57702493667603 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "482a5162-dad9-446c-b548-8486c7598ee1" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 5 + }, + "_mesh": { + "__uuid__": "929e58ce-66a4-4e04-9036-4244456a1220@f6832" + }, + "_shadowCastingMode": 0, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 9 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "537wfATPdERIxNoHyImaal" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "68pBvn4L5LzqzYlxlqIh1Z" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a0C8RfybZDJbzD2rhAA/j8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "faUTUMvuxGGLzyygmsEygr" + } +] diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab.meta new file mode 100644 index 000000000..18a55d389 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/hdcSky.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.27", + "importer": "prefab", + "imported": true, + "uuid": "f0512d7a-e4f6-4209-8dc0-ed1de7149c85", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "hdcSky" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX new file mode 100644 index 000000000..fb0021b52 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX.meta new file mode 100644 index 000000000..2f8d2ec4b --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.FBX.meta @@ -0,0 +1,366 @@ +{ + "ver": "2.0.10", + "importer": "fbx", + "imported": true, + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623", + "files": [], + "subMetas": { + "71919": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@71919", + "displayName": "", + "id": "71919", + "name": "tree1-6.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 6 + } + }, + "2b0a8": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@2b0a8", + "displayName": "", + "id": "2b0a8", + "name": "plane01-0.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "4a7d8": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@4a7d8", + "displayName": "", + "id": "4a7d8", + "name": "stone1-1.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 1 + } + }, + "0e750": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@0e750", + "displayName": "", + "id": "0e750", + "name": "tree1-2.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 2 + } + }, + "ef86b": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@ef86b", + "displayName": "", + "id": "ef86b", + "name": "tree1-3.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 3 + } + }, + "426f2": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@426f2", + "displayName": "", + "id": "426f2", + "name": "tree1-4.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 4 + } + }, + "754a2": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@754a2", + "displayName": "", + "id": "754a2", + "name": "tree1-5.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 5 + } + }, + "1332c": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@1332c", + "displayName": "", + "id": "1332c", + "name": "tree1-7.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 7 + } + }, + "4d16f": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@4d16f", + "displayName": "", + "id": "4d16f", + "name": "tree1-8.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 8 + } + }, + "efe84": { + "importer": "gltf-mesh", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@efe84", + "displayName": "", + "id": "efe84", + "name": "tree1-9.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 9 + } + }, + "baeab": { + "importer": "gltf-embeded-image", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@baeab", + "displayName": "", + "id": "baeab", + "name": "seafloor.jpg.image", + "ver": "1.0.3", + "imported": true, + "files": [ + ".jpg", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "eeccb": { + "importer": "gltf-embeded-image", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@eeccb", + "displayName": "", + "id": "eeccb", + "name": "stone.jpg.image", + "userData": { + "gltfIndex": 1 + }, + "ver": "1.0.3", + "imported": true, + "files": [ + ".jpg", + ".json" + ], + "subMetas": {} + }, + "3b6f8": { + "importer": "gltf-embeded-image", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@3b6f8", + "displayName": "", + "id": "3b6f8", + "name": "tree.png.image", + "userData": { + "gltfIndex": 2 + }, + "ver": "1.0.3", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "2df3a": { + "importer": "texture", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@2df3a", + "displayName": "", + "id": "2df3a", + "name": "seafloor.texture", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "0ab3142a-6968-4073-95af-026bc3b23623@baeab" + }, + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "91a84": { + "importer": "texture", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@91a84", + "displayName": "", + "id": "91a84", + "name": "stone.texture", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "0ab3142a-6968-4073-95af-026bc3b23623@eeccb" + }, + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "0595c": { + "importer": "texture", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@0595c", + "displayName": "", + "id": "0595c", + "name": "tree.texture", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "0ab3142a-6968-4073-95af-026bc3b23623@3b6f8" + }, + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "cc8e5": { + "importer": "gltf-scene", + "uuid": "0ab3142a-6968-4073-95af-026bc3b23623@cc8e5", + "displayName": "", + "id": "cc8e5", + "name": "islands.prefab", + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + } + }, + "userData": { + "imageMetas": [ + { + "name": "seafloor.jpg", + "uri": "0ab3142a-6968-4073-95af-026bc3b23623@baeab" + }, + { + "name": "stone.jpg", + "uri": "0ab3142a-6968-4073-95af-026bc3b23623@eeccb" + }, + { + "name": "tree.png", + "uri": "0ab3142a-6968-4073-95af-026bc3b23623@3b6f8" + } + ], + "redirect": "0ab3142a-6968-4073-95af-026bc3b23623@cc8e5", + "assetFinder": { + "meshes": [ + "0ab3142a-6968-4073-95af-026bc3b23623@2b0a8", + "0ab3142a-6968-4073-95af-026bc3b23623@4a7d8", + "0ab3142a-6968-4073-95af-026bc3b23623@0e750", + "0ab3142a-6968-4073-95af-026bc3b23623@ef86b", + "0ab3142a-6968-4073-95af-026bc3b23623@426f2", + "0ab3142a-6968-4073-95af-026bc3b23623@754a2", + "0ab3142a-6968-4073-95af-026bc3b23623@71919", + "0ab3142a-6968-4073-95af-026bc3b23623@1332c", + "0ab3142a-6968-4073-95af-026bc3b23623@4d16f", + "0ab3142a-6968-4073-95af-026bc3b23623@efe84" + ], + "skeletons": [], + "textures": [ + "0ab3142a-6968-4073-95af-026bc3b23623@2df3a", + "0ab3142a-6968-4073-95af-026bc3b23623@91a84", + "0ab3142a-6968-4073-95af-026bc3b23623@0595c" + ], + "materials": [ + "70d33758-1c1e-424d-b0ab-eac7410559bf", + "a155f93b-7769-4ca4-b75f-b13e52193859", + "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + ], + "scenes": [ + "0ab3142a-6968-4073-95af-026bc3b23623@cc8e5" + ] + }, + "dumpMaterials": true, + "materialDumpDir": "db://assets/material", + "legacyFbxImporter": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab new file mode 100644 index 000000000..8ff0cf567 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab @@ -0,0 +1,1178 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Node", + "_name": "islands", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 44 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 7 + }, + { + "__id__": 11 + }, + { + "__id__": 15 + }, + { + "__id__": 19 + }, + { + "__id__": 23 + }, + { + "__id__": 27 + }, + { + "__id__": 31 + }, + { + "__id__": 35 + }, + { + "__id__": 39 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 43 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "plane01", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": { + "__id__": 6 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": -2.35098645956703e-40, + "z": 2.1031643981928e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 4.25968408584595, + "y": 4.25968408584595, + "z": 2.35247683525085 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "70d33758-1c1e-424d-b0ab-eac7410559bf" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 5 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@2b0a8" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 45 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "70dgNwYARPfbvgVcNs+gIQ" + }, + { + "__type__": "cc.Node", + "_name": "stone1", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 8 + } + ], + "_prefab": { + "__id__": 10 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -1.68451106548309, + "y": 0.804959058761597, + "z": -2.55509376525879 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 8.146034247147303e-8, + "y": 0, + "z": 0, + "w": 0.9999999999999967 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 0.111417099833488, + "y": 0.111417099833488, + "z": 0.111417099833488 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.000009334667642611398, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 7 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "a155f93b-7769-4ca4-b75f-b13e52193859" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 9 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@4a7d8" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 46 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "fd517lz3tOuqVWGd5300X6" + }, + { + "__type__": "cc.Node", + "_name": "tree1", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 12 + } + ], + "_prefab": { + "__id__": 14 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 2.69967889785767, + "y": 0.392187118530273, + "z": -3.67192149162292 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 11 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 13 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@0e750" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 47 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1evO3wfhhGVomhJPkvbiM/" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 3)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 16 + } + ], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 4.34285020828247, + "y": 0.273025780916214, + "z": -4.5796275138855 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 15 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 17 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@ef86b" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 48 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "91DpAWXZ9CFJ+Wk1gnOU27" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 4)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 20 + } + ], + "_prefab": { + "__id__": 22 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.78155946731567, + "y": 0.366120487451553, + "z": -5.44366216659546 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 19 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 21 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@426f2" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 49 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "03YnQPZo5Nc7TYZfZ1EVIK" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 5)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 24 + } + ], + "_prefab": { + "__id__": 26 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -3.78196001052856, + "y": 0.328564822673798, + "z": -3.62895131111145 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865447, + "y": -6.18172403853676e-8, + "z": -6.18172403853676e-8, + "w": 0.7071067811865447 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -89.99999999999999, + "y": -0.000010017912624975451, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 23 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 25 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@754a2" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 50 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "aehmnp6BdEt5duOREy07Ic" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 6)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 28 + } + ], + "_prefab": { + "__id__": 30 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -6.02857780456543, + "y": 0.0573978498578072, + "z": -3.32550001144409 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 27 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 29 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@71919" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 51 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "1dXVprqA1AkpKbrdcroE4U" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 7)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 32 + } + ], + "_prefab": { + "__id__": 34 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.547172009944916, + "y": 0.595235526561737, + "z": -3.40697646141052 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 31 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 33 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@1332c" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 52 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c2n9MRvPxJRbv1PP2mhkZO" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 8)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 36 + } + ], + "_prefab": { + "__id__": 38 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -6.58904409408569, + "y": 0.117208734154701, + "z": -1.02060234546661 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865476, + "y": 0, + "z": 0, + "w": 0.7071067811865476 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90.00000000000003, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 35 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 37 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@4d16f" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 53 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "e7VpTFkQ1Ev40vpNxYe7EG" + }, + { + "__type__": "cc.Node", + "_name": "tree1(__autogen 9)", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 40 + } + ], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 5.92053079605103, + "y": 0.1805190294981, + "z": -2.71322011947632 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7071067811865447, + "y": -6.18172403853676e-8, + "z": -6.18172403853676e-8, + "w": 0.7071067811865447 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 2.53999996185303, + "y": 2.53999996185303, + "z": 2.53999996185303 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -89.99999999999999, + "y": -0.000010017912624975451, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.MeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 39 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "7bf9df40-4bc9-4e25-8cb0-9a500f949102" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 41 + }, + "_mesh": { + "__uuid__": "0ab3142a-6968-4073-95af-026bc3b23623@efe84" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_id": "", + "__prefab": { + "__id__": 54 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a1wQefYUNInYWhsOmPzInv" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "068WXOAs1HrIDx+RBQ6XoV" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "26LvC5hbxAuJZ9Jl2SB/IV" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ddvMFij+ZIL5lu+/NQX8Nf" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e3XMjUJKFLH41z39JaWzyB" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "a5Viy6l3VLDpf6gY5yZF+v" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "e4If/bQrJCsb9D9JopZa2h" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "d7Vkz0NV5Mn4RUce28JVTp" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "3eVATgLQJKDbqOvE33W2uc" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "2akU/MgO5Ovo6QcS0c/I7e" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "54xaKIQbtPLK5r21VY0qmM" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "23UFBqszxJ/6Otof04QwYT" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "c7PE6gNTdDw45ytwlOEWR3" + } +] diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab.meta new file mode 100644 index 000000000..f64a322d8 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/islands.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.27", + "importer": "prefab", + "imported": true, + "uuid": "ccc3a755-7d3d-4304-aa3b-ca4792d79d9f", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "islands" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg new file mode 100644 index 000000000..137b9b4f5 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg.meta new file mode 100644 index 000000000..65200fca7 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/seafloor.jpg.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "4f4c4a34-2d08-4a4d-9169-834d7ce82cee", + "files": [ + ".jpg", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "4f4c4a34-2d08-4a4d-9169-834d7ce82cee@6c48a", + "displayName": "seafloor", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "4f4c4a34-2d08-4a4d-9169-834d7ce82cee" + } + } + }, + "userData": { + "type": "texture", + "redirect": "4f4c4a34-2d08-4a4d-9169-834d7ce82cee@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg new file mode 100644 index 000000000..3fa717c5e Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg.meta new file mode 100644 index 000000000..24954d71e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/shield.jpg.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "95e5b02a-e338-423c-bdbb-17486db1d9eb", + "files": [ + ".jpg", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "95e5b02a-e338-423c-bdbb-17486db1d9eb@6c48a", + "displayName": "shield", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "95e5b02a-e338-423c-bdbb-17486db1d9eb" + } + } + }, + "userData": { + "type": "texture", + "redirect": "95e5b02a-e338-423c-bdbb-17486db1d9eb@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png new file mode 100644 index 000000000..4699ff7c6 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png.meta new file mode 100644 index 000000000..a5a6075d4 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/sky.png.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "dc4a96c7-321a-48af-81e5-1127ad3ae432", + "files": [ + ".png", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "dc4a96c7-321a-48af-81e5-1127ad3ae432@6c48a", + "displayName": "sky", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "dc4a96c7-321a-48af-81e5-1127ad3ae432" + } + } + }, + "userData": { + "type": "texture", + "redirect": "dc4a96c7-321a-48af-81e5-1127ad3ae432@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX new file mode 100644 index 000000000..d6f52fd85 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX.meta new file mode 100644 index 000000000..a18dd7c66 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.FBX.meta @@ -0,0 +1,307 @@ +{ + "ver": "2.0.10", + "importer": "fbx", + "imported": true, + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289", + "files": [], + "subMetas": { + "18751": { + "importer": "gltf-mesh", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@18751", + "displayName": "", + "id": "18751", + "name": "soldier.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + }, + "30732": { + "importer": "gltf-skeleton", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@30732", + "displayName": "", + "id": "30732", + "name": "UnnamedSkeleton-0.skeleton", + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0, + "jointsLength": 22 + } + }, + "da6f3": { + "importer": "gltf-mesh", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@da6f3", + "displayName": "", + "id": "da6f3", + "name": "shield.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 1 + } + }, + "4b929": { + "importer": "gltf-mesh", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@4b929", + "displayName": "", + "id": "4b929", + "name": "sword.mesh", + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 2 + } + }, + "39f7b": { + "importer": "gltf-mesh", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@39f7b", + "displayName": "", + "id": "39f7b", + "name": "shield01.mesh", + "userData": { + "gltfIndex": 3 + }, + "ver": "1.1.0", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {} + }, + "73b7f": { + "importer": "gltf-animation", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@73b7f", + "displayName": "", + "id": "73b7f", + "name": "Take 001.animation", + "ver": "1.0.14", + "imported": true, + "files": [ + ".bin", + ".json" + ], + "subMetas": {}, + "userData": { + "events": [], + "gltfIndex": 0, + "sample": 30, + "span": { + "from": 0.03333333333333333, + "to": 1.3333333730697632 + }, + "wrapMode": 2, + "speed": 1 + } + }, + "f1394": { + "importer": "gltf-skeleton", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@f1394", + "displayName": "", + "id": "f1394", + "name": "UnnamedSkeleton-1.skeleton", + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 1, + "jointsLength": 1 + } + }, + "75ee4": { + "importer": "gltf-skeleton", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@75ee4", + "displayName": "", + "id": "75ee4", + "name": "UnnamedSkeleton-2.skeleton", + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 2, + "jointsLength": 1 + } + }, + "a72ab": { + "importer": "gltf-skeleton", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@a72ab", + "displayName": "", + "id": "a72ab", + "name": "UnnamedSkeleton-3.skeleton", + "ver": "1.0.1", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 3, + "jointsLength": 1 + } + }, + "e94f1": { + "importer": "gltf-embeded-image", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@e94f1", + "displayName": "", + "id": "e94f1", + "name": "shield.jpg.image", + "userData": { + "gltfIndex": 1 + }, + "ver": "1.0.3", + "imported": true, + "files": [ + ".jpg", + ".json" + ], + "subMetas": {} + }, + "f3caa": { + "importer": "texture", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@f3caa", + "displayName": "", + "id": "f3caa", + "name": "soldier.texture", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": false, + "imageUuidOrDatabaseUri": "db://assets/model/helloWorld/soldier.png" + }, + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "a2be1": { + "importer": "texture", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@a2be1", + "displayName": "", + "id": "a2be1", + "name": "shield.texture", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "e3553cad-2f15-4293-859a-8f43c780f289@e94f1" + }, + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {} + }, + "d252c": { + "importer": "gltf-scene", + "uuid": "e3553cad-2f15-4293-859a-8f43c780f289@d252c", + "displayName": "", + "id": "d252c", + "name": "soldier.prefab", + "ver": "1.0.12", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "gltfIndex": 0 + } + } + }, + "userData": { + "imageMetas": [ + { + "name": "soldier", + "uri": "db://assets/model/helloWorld/soldier.png" + }, + { + "name": "shield.jpg", + "uri": "e3553cad-2f15-4293-859a-8f43c780f289@e94f1" + } + ], + "redirect": "e3553cad-2f15-4293-859a-8f43c780f289@d252c", + "assetFinder": { + "meshes": [ + "e3553cad-2f15-4293-859a-8f43c780f289@18751", + "e3553cad-2f15-4293-859a-8f43c780f289@da6f3", + "e3553cad-2f15-4293-859a-8f43c780f289@4b929", + "e3553cad-2f15-4293-859a-8f43c780f289@39f7b" + ], + "skeletons": [ + "e3553cad-2f15-4293-859a-8f43c780f289@30732", + "e3553cad-2f15-4293-859a-8f43c780f289@f1394", + "e3553cad-2f15-4293-859a-8f43c780f289@75ee4", + "e3553cad-2f15-4293-859a-8f43c780f289@a72ab" + ], + "textures": [ + "e3553cad-2f15-4293-859a-8f43c780f289@f3caa", + "e3553cad-2f15-4293-859a-8f43c780f289@a2be1" + ], + "materials": [ + "8a58ddec-f437-40b9-8ec0-1fc87de97fb5", + "8e047178-f61c-4322-a2f6-d1adb28b6ae2" + ], + "scenes": [ + "e3553cad-2f15-4293-859a-8f43c780f289@d252c" + ] + }, + "dumpMaterials": true, + "materialDumpDir": "db://assets/material", + "animationImportSettings": [ + { + "name": "Take 001", + "duration": 1.3333333730697632, + "fps": 30, + "splits": [ + { + "name": "Take 001", + "from": 0.03333333333333333, + "to": 1.3333333730697632, + "wrapMode": 2 + } + ] + } + ], + "legacyFbxImporter": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png new file mode 100644 index 000000000..00ce19176 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png.meta new file mode 100644 index 000000000..586379404 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.png.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "6f891a7b-5a08-48e6-9841-ddb364ac86b1", + "files": [ + ".png", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "6f891a7b-5a08-48e6-9841-ddb364ac86b1@6c48a", + "displayName": "soldier", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "6f891a7b-5a08-48e6-9841-ddb364ac86b1" + } + } + }, + "userData": { + "type": "texture", + "redirect": "6f891a7b-5a08-48e6-9841-ddb364ac86b1@6c48a", + "hasAlpha": true + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab new file mode 100644 index 000000000..381707a36 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab @@ -0,0 +1,2132 @@ +[ + { + "__type__": "cc.Prefab", + "_name": "", + "_objFlags": 0, + "_native": "", + "data": { + "__id__": 1 + }, + "optimizationPolicy": 0, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Node", + "_name": "soldier", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + } + ], + "_active": true, + "_components": [ + { + "__id__": 76 + } + ], + "_prefab": { + "__id__": 77 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0.956, + "z": 1.402 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "RootNode", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [ + { + "__id__": 3 + }, + { + "__id__": 7 + }, + { + "__id__": 63 + }, + { + "__id__": 67 + }, + { + "__id__": 71 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 75 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0, + "y": 0, + "z": 0, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "soldier", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 4 + } + ], + "_prefab": { + "__id__": 6 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.9999999999999953, + "y": 0, + "z": 0, + "w": -9.735359185469814e-8 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 87.7489852905273, + "y": 87.7489852905273, + "z": 87.7489852905273 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -179.99998884410013, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 3 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "8a58ddec-f437-40b9-8ec0-1fc87de97fb5" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 5 + }, + "_mesh": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@18751" + }, + "_shadowCastingMode": 1, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@30732" + }, + "_skinningRoot": { + "__id__": 1 + }, + "_id": "", + "__prefab": { + "__id__": 78 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "824q3ldSpHoYl9xi6zWeg4" + }, + { + "__type__": "cc.Node", + "_name": "Bip001", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [ + { + "__id__": 8 + }, + { + "__id__": 10 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 62 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0.738336980342865, + "z": -3.74271143591121e-12 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.5000003576277411, + "y": -0.4999996423720031, + "z": -0.4999996423720031, + "w": 0.5000003576277411 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -90, + "y": -89.99991803772988, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Footsteps", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 9 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -5.16987871290063e-28, + "y": 0, + "z": -0.734457075595856 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -6.921034890422563e-34, + "y": 6.921044533149828e-34, + "z": 0.7071063043492202, + "w": 0.7071072580235535 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -1.1216009582263186e-31, + "z": 90 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "24sP2WouNOyq9Pp+iqRgiX" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Pelvis", + "_objFlags": 0, + "_parent": { + "__id__": 7 + }, + "_children": [ + { + "__id__": 11 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 61 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.4999999925491744, + "y": -0.4999999925491744, + "z": -0.4999993070957696, + "w": 0.5000007078049007 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -89.9999197452773, + "y": -89.9999197452773, + "z": -0.0000017074910412130916 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Spine", + "_objFlags": 0, + "_parent": { + "__id__": 10 + }, + "_children": [ + { + "__id__": 12 + }, + { + "__id__": 22 + }, + { + "__id__": 32 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 60 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0765029862523079, + "y": -0.000119566058856435, + "z": 1.06260117149759e-7 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.000002080475910607327, + "y": -6.936759723456761e-7, + "z": 0.0003981589901667568, + "w": 0.9999999207323014 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.00023837338537102568, + "y": -0.00007939450691674313, + "z": 0.045625660794268635 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Thigh", + "_objFlags": 0, + "_parent": { + "__id__": 11 + }, + "_children": [ + { + "__id__": 13 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 21 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0765029117465019, + "y": 0.000180334449396469, + "z": 0.0665242150425911 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.015412850192755845, + "y": 0.9969698713847834, + "z": 0.002043751751963479, + "w": -0.07621904406852757 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.098918034361683, + "y": -171.25793904676914, + "z": -1.7789668366715101 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Calf", + "_objFlags": 0, + "_parent": { + "__id__": 12 + }, + "_children": [ + { + "__id__": 14 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 20 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.368020862340927, + "y": -2.38418573772492e-9, + "z": 1.90734859017994e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -3.77253945323161e-21, + "y": -4.658679694134547e-18, + "z": -0.0008097870399876981, + "w": 0.999999672122421 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -8.646032309262241e-19, + "y": -5.338462443351186e-16, + "z": -0.09279476953314847 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Foot", + "_objFlags": 0, + "_parent": { + "__id__": 13 + }, + "_children": [ + { + "__id__": 15 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 19 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.29802131652832, + "y": -2.38418573772492e-9, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.0020737143220202685, + "y": -0.07621822672483194, + "z": 0.016616994877612996, + "w": 0.9969505289155877 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.3822464844159605, + "y": -8.749946017824339, + "z": 1.8805865416881686 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Toe0", + "_objFlags": 0, + "_parent": { + "__id__": 14 + }, + "_children": [ + { + "__id__": 16 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 18 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0765028968453407, + "y": 0.0984558463096619, + "z": 1.90734859017994e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -1.5454308319985125e-8, + "y": -1.5454308319985125e-8, + "z": 0.7071067811865472, + "w": 0.7071067811865472 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -0.0000025044778683729224, + "z": 90 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Toe0Nub", + "_objFlags": 0, + "_parent": { + "__id__": 15 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 17 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.00997863244265318, + "y": -2.98023217215615e-10, + "z": 3.55271359939116e-17 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 9.55341250274695e-16, + "y": -2.4399608215727e-23, + "z": 1, + "w": 1.83758927467374e-15 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": -1, + "y": -1, + "z": -1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 180, + "y": -179.9999999999999, + "z": 2.1057221983462293e-13 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b7ARhVJWFJP4yiboWbM1Xs" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "b7IX4wc+BII5SOxaDKm4bu" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "71/rP0sWxM1bfHuNwls2lv" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "71+6ovA8dDUowhlyNGBIbM" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4aENadJ59Io4SS6BMn9Aw4" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Thigh", + "_objFlags": 0, + "_parent": { + "__id__": 11 + }, + "_children": [ + { + "__id__": 23 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 31 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.0765029117465019, + "y": 0.000180703471414745, + "z": -0.0665242150425911 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.0154126518850906, + "y": 0.9969698755273314, + "z": -0.0020409899233692465, + "w": 0.07621910398987286 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.09860398393799301, + "y": 171.25792731952373, + "z": -1.7789200579558455 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Calf", + "_objFlags": 0, + "_parent": { + "__id__": 22 + }, + "_children": [ + { + "__id__": 24 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 30 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.36802089214325, + "y": 0, + "z": -3.55271359939116e-17 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 2.226103102352033e-19, + "y": 7.125063520427799e-18, + "z": -0.0008097898921612967, + "w": 0.9999996721201113 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 2.6170459347986135e-17, + "y": 8.164935972515574e-16, + "z": -0.09279509636827488 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Foot", + "_objFlags": 0, + "_parent": { + "__id__": 23 + }, + "_children": [ + { + "__id__": 25 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 29 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.29802131652832, + "y": 0, + "z": -3.81469718035987e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.002073778117879438, + "y": 0.07621821928385913, + "z": 0.016617013506159614, + "w": 0.9969505290412618 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.38225392566993516, + "y": 8.749945293799216, + "z": 1.8805881155171484 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Toe0", + "_objFlags": 0, + "_parent": { + "__id__": 24 + }, + "_children": [ + { + "__id__": 26 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 28 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0765028968453407, + "y": 0.0984558537602425, + "z": 1.90734859017994e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -1.5454308319985125e-8, + "y": -1.5454308319985125e-8, + "z": 0.7071067811865472, + "w": 0.7071067811865472 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0, + "y": -0.0000025044778683729224, + "z": 90 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Toe0Nub", + "_objFlags": 0, + "_parent": { + "__id__": 25 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 27 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.00997863244265318, + "y": -2.98023217215615e-10, + "z": 7.10542719878232e-17 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -2.35364776340013e-23, + "y": 9.55341356153813e-16, + "z": -1.77635726291672e-15, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -2.6970814715996306e-21, + "y": 1.0947405540383584e-13, + "z": -2.0355554814507762e-13 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "77BI3YwgNJqKMpf0rJhZ9s" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "26R5ll6SNGP5ArlFhrFOzH" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "cdWn1utjJOUaNdTLXxb/CR" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "8a4oYAyD9MkZi9aL5G15gW" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "baRkxlpMJMup2UzoK3llZt" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Spine1", + "_objFlags": 0, + "_parent": { + "__id__": 11 + }, + "_children": [ + { + "__id__": 33 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 59 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.150201484560966, + "y": -0.000119601711048745, + "z": -3.31727090241429e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -3.01176726787145e-14, + "y": -1.39586728368323e-23, + "z": -1.0842021724855e-19, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -3.451231066493619e-12, + "y": -1.599546082682973e-21, + "z": -1.2424041724466812e-17 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Neck", + "_objFlags": 0, + "_parent": { + "__id__": 32 + }, + "_children": [ + { + "__id__": 34 + }, + { + "__id__": 44 + }, + { + "__id__": 54 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 58 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.15020164847374, + "y": -0.0000435724105045665, + "z": -1.20852522433879e-10 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -3.01176726787145e-14, + "y": -9.0989867380833e-23, + "z": -2.74040313253867e-36, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -3.451231066493619e-12, + "y": -1.0426670758753618e-20, + "z": -1.0276926252606202e-41 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Clavicle", + "_objFlags": 0, + "_parent": { + "__id__": 33 + }, + "_children": [ + { + "__id__": 35 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 43 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0.0000435078145528678, + "z": 0.0232834853231907 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.6087613827170933, + "y": -0.0002434881366867595, + "z": 0.7933532758994495, + "w": 0.0003150325566206266 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 179.9558877717535, + "y": -104.99999523006848, + "z": 0.01165464185401893 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L UpperArm", + "_objFlags": 0, + "_parent": { + "__id__": 34 + }, + "_children": [ + { + "__id__": 36 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 42 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.272405058145523, + "y": 4.54747340722069e-15, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.004462069723208538, + "y": 0.4381802273458216, + "z": -0.0019434620153348766, + "w": 0.8988739629404916 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 0.5572016837940187, + "y": 51.97620557313351, + "z": 0.02386521946895269 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Forearm", + "_objFlags": 0, + "_parent": { + "__id__": 35 + }, + "_children": [ + { + "__id__": 37 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 41 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.263291478157043, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -2.7197715504795845e-20, + "y": -3.0893820368327117e-17, + "z": -0.0008803606294924629, + "w": 0.9999996124825059 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -6.233264281423144e-18, + "y": -3.5401778996822195e-15, + "z": -0.10088191006996569 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 L Hand", + "_objFlags": 0, + "_parent": { + "__id__": 36 + }, + "_children": [ + { + "__id__": 38 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 40 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.199572831392288, + "y": -5.9604643443123e-10, + "z": 1.42108543975646e-16 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.7068252124052271, + "y": 1.0458620867947887e-8, + "z": 1.0614867442646885e-8, + "w": 0.7073882378922519 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -89.95437890588059, + "y": 0.0000017075473071153707, + "z": 1.3340228971442967e-8 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "sword01", + "_objFlags": 0, + "_parent": { + "__id__": 37 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 39 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0919111594557762, + "y": 0.0340489186346531, + "z": -0.14926840364933 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.6409670678902519, + "y": -0.3070256396863465, + "z": -0.5278458569202463, + "w": -0.4650540030872693 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -112.37885078724646, + "y": 75.2002383569922, + "z": 5.587571211625747 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "98Mxm4C+NAIp1/ms1CXDWg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "7cWlWOkFtKX6ILXuZfhYqO" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "503RTkgyFOwZRj7QxbXFGT" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "66qUHYCHZNMoAvIMM8jTZn" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "c67UVUfoFCNLAcx8dX7SW1" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Clavicle", + "_objFlags": 0, + "_parent": { + "__id__": 33 + }, + "_children": [ + { + "__id__": 45 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 53 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0.0000436370064562652, + "z": -0.0232834853231907 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.6087613827171046, + "y": 0.00024128768333211222, + "z": 0.793353275899464, + "w": 0.0003167210153793466 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -179.95597003338875, + "y": 104.99999512047448, + "z": 0.011961643569376679 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R UpperArm", + "_objFlags": 0, + "_parent": { + "__id__": 44 + }, + "_children": [ + { + "__id__": 46 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 52 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.272405058145523, + "y": 6.82120985672115e-14, + "z": -1.42108543975646e-16 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.030114175418424236, + "y": -0.43755956999841955, + "z": 0.05059855375302801, + "w": 0.897259463867486 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -0.5631334715243025, + "y": -51.96038786477057, + "z": 6.727854372934277 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Forearm", + "_objFlags": 0, + "_parent": { + "__id__": 45 + }, + "_children": [ + { + "__id__": 47 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 51 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.263291418552399, + "y": 0, + "z": -7.62939436071974e-8 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -8.302567498559346e-18, + "y": -5.588016764292516e-17, + "z": -0.13176086831298384, + "w": 0.9912815309393233 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -1.8510999406615195e-15, + "y": -6.705762043264369e-15, + "z": -15.14271605062808 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 R Hand", + "_objFlags": 0, + "_parent": { + "__id__": 46 + }, + "_children": [ + { + "__id__": 48 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 50 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.199572905898094, + "y": 8.88178399847791e-18, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.7068252124052272, + "y": -1.9327187937127104e-17, + "z": 1.9311804031753923e-17, + "w": 0.707388237892252 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 89.95437890588059, + "y": -3.1308585900006548e-15, + "z": -7.956237644289779e-23 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "shield01", + "_objFlags": 0, + "_parent": { + "__id__": 47 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 49 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.124533005058765, + "y": -0.00692871073260903, + "z": 0.0576667860150337 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.5417342069004817, + "y": -0.1379658103002438, + "z": 0.11668003843623054, + "w": 0.8208990515846776 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 67.2841187503732, + "y": -20.685866498064005, + "z": 2.411917394086867 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4f4kzbhYBIMIVjz0nZTcvw" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "19JEI2tAtAo5mcAZ7XSRWH" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "5eAc4VAlRExZ6nABKN5Ihq" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a72KzY4dJDrb/Ifgjl7CSF" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9fRB6KFOpAz6pb5W8NAqZg" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 Head", + "_objFlags": 0, + "_parent": { + "__id__": 33 + }, + "_children": [ + { + "__id__": 55 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 57 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.0547203049063683, + "y": -1.38777874976217e-19, + "z": 2.71050537062924e-22 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -7.265599787883251e-14, + "y": 1.1042733174299354e-9, + "z": -0.0003988305609355657, + "w": 0.9999999204670887 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": 4.214242912539149e-11, + "y": 1.2654042790710837e-7, + "z": -0.04570261697650697 + }, + "_id": "" + }, + { + "__type__": "cc.Node", + "_name": "Bip001 HeadNub", + "_objFlags": 0, + "_parent": { + "__id__": 54 + }, + "_children": [], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 56 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.475234657526016, + "y": -1.45519149031062e-13, + "z": 2.99510855318999e-20 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -4.09752188234268e-20, + "y": 3.5879314498657e-21, + "z": 1.47015827086174e-40, + "w": 1 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -4.6954142064147255e-18, + "y": 4.1114665851911787e-19, + "z": -5.14740158672216e-44 + }, + "_id": "" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "ebvw49UjBMKoIvQOBhsyUF" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "4ckJuubwNG3KpsMgkuxo12" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "de+U9IqPVD+qUUY0y7H/86" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "78+5xpqyNL2aHMIBr4YHvg" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "deedfocu1E0amfQjzHnnD8" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "9brOPlFAxPnLhkrpP6KbLQ" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "15XJ/3651AFKLyr6XRj3d7" + }, + { + "__type__": "cc.Node", + "_name": "shield", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 64 + } + ], + "_prefab": { + "__id__": 66 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.9999999999999878, + "y": 0, + "z": 0, + "w": -1.569582366300871e-7 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 87.7489852905273, + "y": 87.7489852905273, + "z": 87.7489852905273 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -179.99998201391097, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 63 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "8a58ddec-f437-40b9-8ec0-1fc87de97fb5" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 65 + }, + "_mesh": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@da6f3" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@f1394" + }, + "_skinningRoot": { + "__id__": 1 + }, + "_id": "", + "__prefab": { + "__id__": 79 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "2fgzyRcBxNmZKzBdJpnT0Q" + }, + { + "__type__": "cc.Node", + "_name": "sword", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 68 + } + ], + "_prefab": { + "__id__": 70 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0, + "y": 0, + "z": 0 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.9999999999999878, + "y": 0, + "z": 0, + "w": -1.569582366300871e-7 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 87.7489852905273, + "y": 87.7489852905273, + "z": 87.7489852905273 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -179.99998201391097, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 67 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "8a58ddec-f437-40b9-8ec0-1fc87de97fb5" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 69 + }, + "_mesh": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@4b929" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@75ee4" + }, + "_skinningRoot": { + "__id__": 1 + }, + "_id": "", + "__prefab": { + "__id__": 80 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "d0xEUY/HZBVZJ0EMEz3doQ" + }, + { + "__type__": "cc.Node", + "_name": "shield01", + "_objFlags": 0, + "_parent": { + "__id__": 2 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 72 + } + ], + "_prefab": { + "__id__": 74 + }, + "_lpos": { + "__type__": "cc.Vec3", + "x": -0.00499999988824129, + "y": 0, + "z": 0.00499999988824129 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": 0.9999999999999878, + "y": 0, + "z": 0, + "w": -1.569582366300871e-7 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 87.7489852905273, + "y": 87.7489852905273, + "z": 87.7489852905273 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -179.99998201391097, + "y": 0, + "z": 0 + }, + "_id": "" + }, + { + "__type__": "cc.SkinnedMeshRenderer", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 71 + }, + "_enabled": true, + "_materials": [ + { + "__uuid__": "8e047178-f61c-4322-a2f6-d1adb28b6ae2" + } + ], + "_visFlags": 0, + "lightmapSettings": { + "__id__": 73 + }, + "_mesh": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@39f7b" + }, + "_shadowCastingMode": 0, + "_shadowReceivingMode": 1, + "_enableMorph": true, + "_skeleton": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@a72ab" + }, + "_skinningRoot": { + "__id__": 1 + }, + "_id": "", + "__prefab": { + "__id__": 81 + } + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "464KPea2NOibwROP5moUvA" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "a5uiqiY3JH2IVaNOBBe7fQ" + }, + { + "__type__": "cc.SkeletalAnimation", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 1 + }, + "_enabled": true, + "playOnLoad": true, + "_clips": [ + { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@73b7f" + } + ], + "_defaultClip": { + "__uuid__": "e3553cad-2f15-4293-859a-8f43c780f289@73b7f" + }, + "_useBakedAnimation": true, + "_sockets": [], + "_id": "", + "__prefab": { + "__id__": 82 + } + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 1 + }, + "asset": { + "__id__": 0 + }, + "fileId": "6dMvPN2t1B66O9Zc3HG8dr" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "78XZsd31xPjIsSP2888FcN" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ffoVYmt2NOmIBz5DHpacF8" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "9aCVBuMeZPv6so1VxT6c40" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "ca1srfPsJJgKKJww9GO/JE" + }, + { + "__type__": "cc.CompPrefabInfo", + "fileId": "04W3Kzvb9BZbZUGFZzfzi5" + } +] diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab.meta new file mode 100644 index 000000000..4242fd697 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/soldier.prefab.meta @@ -0,0 +1,13 @@ +{ + "ver": "1.1.27", + "importer": "prefab", + "imported": true, + "uuid": "cfc53c4e-7956-482b-aebc-3fb1dcd36eef", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "syncNodeName": "soldier" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg new file mode 100644 index 000000000..61dd4296c Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg.meta new file mode 100644 index 000000000..7061e1687 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/stone.jpg.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "0718d996-39bf-4ab4-bb63-496666fef467", + "files": [ + ".jpg", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "0718d996-39bf-4ab4-bb63-496666fef467@6c48a", + "displayName": "stone", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "0718d996-39bf-4ab4-bb63-496666fef467" + } + } + }, + "userData": { + "type": "texture", + "redirect": "0718d996-39bf-4ab4-bb63-496666fef467@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png new file mode 100644 index 000000000..3692d92c3 Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png.meta new file mode 100644 index 000000000..83cb155ef --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/model/helloWorld/tree.png.meta @@ -0,0 +1,41 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "c5083e75-ad2e-4ea9-8b33-dee748995b00", + "files": [ + ".png", + ".json" + ], + "subMetas": { + "6c48a": { + "importer": "texture", + "uuid": "c5083e75-ad2e-4ea9-8b33-dee748995b00@6c48a", + "displayName": "tree", + "id": "6c48a", + "name": "texture", + "ver": "1.0.20", + "imported": true, + "files": [ + ".json" + ], + "subMetas": {}, + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "none", + "premultiplyAlpha": false, + "anisotropy": 1, + "isUuid": true, + "imageUuidOrDatabaseUri": "c5083e75-ad2e-4ea9-8b33-dee748995b00" + } + } + }, + "userData": { + "type": "texture", + "redirect": "c5083e75-ad2e-4ea9-8b33-dee748995b00@6c48a", + "hasAlpha": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene.meta new file mode 100644 index 000000000..822e98fcd --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "b0a4abb1-db32-49c3-9e09-a45b922a2094", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene new file mode 100644 index 000000000..96117e872 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene @@ -0,0 +1,2158 @@ +[ + { + "__type__": "cc.SceneAsset", + "_name": "", + "_objFlags": 0, + "_native": "", + "scene": { + "__id__": 1 + }, + "asyncLoadAssets": false + }, + { + "__type__": "cc.Scene", + "_name": "", + "_objFlags": 0, + "_parent": null, + "_children": [ + { + "__id__": 2 + }, + { + "__id__": 5 + }, + { + "__id__": 7 + }, + { + "__id__": 44 + }, + { + "__id__": 59 + } + ], + "_active": true, + "_components": [], + "_prefab": { + "__id__": 124 + }, + "autoReleaseAssets": false, + "_globals": { + "__id__": 173 + }, + "_id": "7c3e7fab-7b1e-4865-ba84-3cf81b48b9fb" + }, + { + "__type__": "cc.Node", + "_name": "Main Light", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 3 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": -2.955, + "y": 3.412, + "z": 5.118 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.24999999999999997, + "y": -0.24999999999999997, + "z": -0.06698729810778066, + "w": 0.9330127018922194 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -30, + "y": -30, + "z": 0 + }, + "_id": "c0y6F5f+pAvI805TdmxIjx" + }, + { + "__type__": "cc.DirectionalLight", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 2 + }, + "_enabled": true, + "__prefab": null, + "_color": { + "__type__": "cc.Color", + "r": 255, + "g": 255, + "b": 255, + "a": 255 + }, + "_useColorTemperature": false, + "_colorTemperature": 6550, + "_staticSettings": { + "__id__": 4 + }, + "_illuminance": 125000, + "_id": "597uMYCbhEtJQc0ffJlcgA" + }, + { + "__type__": "cc.StaticLightSettings", + "_baked": false, + "_editorOnly": false, + "_bakeable": false, + "_castShadow": false + }, + { + "__type__": "cc.Node", + "_name": "Main Camera", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_children": [], + "_active": true, + "_components": [ + { + "__id__": 6 + } + ], + "_prefab": null, + "_lpos": { + "__type__": "cc.Vec3", + "x": 0.4563737338172984, + "y": 4.020698998822525, + "z": 7.83104356477376 + }, + "_lrot": { + "__type__": "cc.Quat", + "x": -0.07236081996736556, + "y": 0.03501809641207027, + "z": 0.002542173940871125, + "w": 0.9967603433167774 + }, + "_lscale": { + "__type__": "cc.Vec3", + "x": 1, + "y": 1, + "z": 1 + }, + "_layer": 1073741824, + "_euler": { + "__type__": "cc.Vec3", + "x": -8.304321541008003, + "y": 4.024165472580301, + "z": 9.93923337957349e-17 + }, + "_id": "c9DMICJLFO5IeO07EPon7U" + }, + { + "__type__": "cc.Camera", + "_name": "", + "_objFlags": 0, + "node": { + "__id__": 5 + }, + "_enabled": true, + "__prefab": null, + "_projection": 1, + "_priority": 0, + "_fov": 45, + "_fovAxis": 0, + "_orthoHeight": 10, + "_near": 1, + "_far": 1000, + "_color": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + }, + "_depth": 1, + "_stencil": 0, + "_clearFlags": 14, + "_rect": { + "__type__": "cc.Rect", + "x": 0, + "y": 0, + "width": 1, + "height": 1 + }, + "_aperture": 19, + "_shutter": 7, + "_iso": 0, + "_screenScale": 1, + "_visibility": 1820327937, + "_targetTexture": null, + "_id": "7dWQTpwS5LrIHnc1zAPUtf" + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 8 + }, + "_id": "6dkcA2pVdK6o9h8rGVX3bm" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 7 + }, + "asset": { + "__uuid__": "ccc3a755-7d3d-4304-aa3b-ca4792d79d9f" + }, + "fileId": "26LvC5hbxAuJZ9Jl2SB/IV", + "instance": { + "__id__": 9 + } + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "cdNOYl3LRPhapW8a8hi/Iy", + "prefabRootNode": null, + "mountedChildren": [], + "propertyOverrides": [ + { + "__id__": 10 + }, + { + "__id__": 13 + }, + { + "__id__": 16 + }, + { + "__id__": 18 + }, + { + "__id__": 21 + }, + { + "__id__": 23 + }, + { + "__id__": 26 + }, + { + "__id__": 29 + }, + { + "__id__": 32 + }, + { + "__id__": 35 + }, + { + "__id__": 38 + }, + { + "__id__": 41 + } + ], + "removedComponents": [] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 11 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 12 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "ddvMFij+ZIL5lu+/NQX8Nf" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 14 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 15 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "e3XMjUJKFLH41z39JaWzyB" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 17 + }, + "propertyPath": [ + "position" + ], + "value": { + "__type__": "cc.Vec3", + "x": -1.341, + "y": 0.805, + "z": -2.555 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "fd517lz3tOuqVWGd5300X6" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 19 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 20 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "a5Viy6l3VLDpf6gY5yZF+v" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 22 + }, + "propertyPath": [ + "_shadowCastingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "a5Viy6l3VLDpf6gY5yZF+v" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 24 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 25 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "e4If/bQrJCsb9D9JopZa2h" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 27 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 28 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "d7Vkz0NV5Mn4RUce28JVTp" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 30 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 31 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "3eVATgLQJKDbqOvE33W2uc" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 33 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 34 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "2akU/MgO5Ovo6QcS0c/I7e" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 36 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 37 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "54xaKIQbtPLK5r21VY0qmM" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 39 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 40 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "23UFBqszxJ/6Otof04QwYT" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 42 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 43 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "c7PE6gNTdDw45ytwlOEWR3" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 45 + }, + "_id": "fcY5TMXBxOuo8tyzFfp6B9" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 44 + }, + "asset": { + "__uuid__": "cfc53c4e-7956-482b-aebc-3fb1dcd36eef" + }, + "fileId": "6dMvPN2t1B66O9Zc3HG8dr", + "instance": { + "__id__": 46 + } + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "93xtJEZ71OF5Gk8u497J9k", + "prefabRootNode": null, + "mountedChildren": [], + "propertyOverrides": [ + { + "__id__": 47 + }, + { + "__id__": 50 + }, + { + "__id__": 53 + }, + { + "__id__": 56 + } + ], + "removedComponents": [] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 48 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 49 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "78XZsd31xPjIsSP2888FcN" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 51 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 52 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "ffoVYmt2NOmIBz5DHpacF8" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 54 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 55 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "9aCVBuMeZPv6so1VxT6c40" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 57 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 58 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "ca1srfPsJJgKKJww9GO/JE" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "cc.Node", + "_objFlags": 0, + "_parent": { + "__id__": 1 + }, + "_prefab": { + "__id__": 60 + }, + "_id": "96ghi0rklFc4YnsYx0Rtjm" + }, + { + "__type__": "cc.PrefabInfo", + "root": { + "__id__": 59 + }, + "asset": { + "__uuid__": "5e4d48c4-0e34-45af-a268-89485197e8bc" + }, + "fileId": "e5Peksu5tL9peMeABb8/JC", + "instance": { + "__id__": 61 + } + }, + { + "__type__": "cc.PrefabInstance", + "fileId": "7e4SH9jydKyaQjXZtS5AiQ", + "prefabRootNode": null, + "mountedChildren": [], + "propertyOverrides": [ + { + "__id__": 62 + }, + { + "__id__": 65 + }, + { + "__id__": 67 + }, + { + "__id__": 70 + }, + { + "__id__": 72 + }, + { + "__id__": 75 + }, + { + "__id__": 77 + }, + { + "__id__": 80 + }, + { + "__id__": 82 + }, + { + "__id__": 84 + }, + { + "__id__": 87 + }, + { + "__id__": 89 + }, + { + "__id__": 92 + }, + { + "__id__": 94 + }, + { + "__id__": 97 + }, + { + "__id__": 99 + }, + { + "__id__": 102 + }, + { + "__id__": 104 + }, + { + "__id__": 107 + }, + { + "__id__": 109 + }, + { + "__id__": 112 + }, + { + "__id__": 114 + }, + { + "__id__": 117 + }, + { + "__id__": 119 + }, + { + "__id__": 122 + } + ], + "removedComponents": [] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 63 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 64 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "a6J1jU/r1BPKNIcVMEREit" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 66 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "a6J1jU/r1BPKNIcVMEREit" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 68 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 69 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "28WH2cvhNDFbN3it+8Q+XK" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 71 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "28WH2cvhNDFbN3it+8Q+XK" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 73 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 74 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "87exLhmM1P35jx6GLN7j5f" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 76 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "87exLhmM1P35jx6GLN7j5f" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 78 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 79 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "dduLonFhNK+q908BUlLM7f" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 81 + }, + "propertyPath": [ + "_shadowCastingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "dduLonFhNK+q908BUlLM7f" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 83 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "dduLonFhNK+q908BUlLM7f" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 85 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 86 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "af0f1GeGlKf5MvKMEuH8MJ" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 88 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "af0f1GeGlKf5MvKMEuH8MJ" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 90 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 91 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "e9BPt5G81CloUmA5IJNHIQ" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 93 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "e9BPt5G81CloUmA5IJNHIQ" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 95 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 96 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "7dt8ZZ5EBNDIxOgpt1XbGu" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 98 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "7dt8ZZ5EBNDIxOgpt1XbGu" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 100 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 101 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "c4Nekl9YtKUKupS0ASX7It" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 103 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "c4Nekl9YtKUKupS0ASX7It" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 105 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 106 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b9cuhcVRxPvZTPHcAyE7eS" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 108 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "b9cuhcVRxPvZTPHcAyE7eS" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 110 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 111 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "f3T3OFBBJBYrGbPSQ+k7GJ" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 113 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "f3T3OFBBJBYrGbPSQ+k7GJ" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 115 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 116 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "d0A5LfhGhAfrDnBGJ3JzOe" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 118 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "d0A5LfhGhAfrDnBGJ3JzOe" + ] + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 120 + }, + "propertyPath": [ + "lightmapSettings" + ], + "value": { + "__id__": 121 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "161SEdWiFO/abXxUI8RkYk" + ] + }, + { + "__type__": "cc.ModelLightmapSettings", + "texture": null, + "uvParam": { + "__type__": "cc.Vec4", + "x": 0, + "y": 0, + "z": 0, + "w": 0 + }, + "_bakeable": false, + "_castShadow": false, + "_receiveShadow": false, + "_recieveShadow": false, + "_lightmapSize": 64 + }, + { + "__type__": "CCPropertyOverrideInfo", + "targetInfo": { + "__id__": 123 + }, + "propertyPath": [ + "_shadowReceivingMode" + ], + "value": 1 + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "161SEdWiFO/abXxUI8RkYk" + ] + }, + { + "__type__": "cc.PrefabInfo", + "fileId": "", + "targetOverrides": [ + { + "__id__": 125 + }, + { + "__id__": 128 + }, + { + "__id__": 131 + }, + { + "__id__": 134 + }, + { + "__id__": 137 + }, + { + "__id__": 140 + }, + { + "__id__": 143 + }, + { + "__id__": 146 + }, + { + "__id__": 149 + }, + { + "__id__": 152 + }, + { + "__id__": 155 + }, + { + "__id__": 158 + }, + { + "__id__": 161 + }, + { + "__id__": 164 + }, + { + "__id__": 167 + }, + { + "__id__": 170 + } + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 44 + }, + "sourceInfo": { + "__id__": 126 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 44 + }, + "targetInfo": { + "__id__": 127 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "04W3Kzvb9BZbZUGFZzfzi5" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6dMvPN2t1B66O9Zc3HG8dr" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 44 + }, + "sourceInfo": { + "__id__": 129 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 44 + }, + "targetInfo": { + "__id__": 130 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "04W3Kzvb9BZbZUGFZzfzi5" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6dMvPN2t1B66O9Zc3HG8dr" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 44 + }, + "sourceInfo": { + "__id__": 132 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 44 + }, + "targetInfo": { + "__id__": 133 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "04W3Kzvb9BZbZUGFZzfzi5" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6dMvPN2t1B66O9Zc3HG8dr" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 44 + }, + "sourceInfo": { + "__id__": 135 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 44 + }, + "targetInfo": { + "__id__": 136 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "04W3Kzvb9BZbZUGFZzfzi5" + ] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "6dMvPN2t1B66O9Zc3HG8dr" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 138 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 139 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "87M1Av0v5LhZ3LsJOTzwr3" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 141 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 142 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "8avX4W7ZtLOLCZ8n5QtiPm" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 144 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 145 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "5fAmHUz0xO9YFym/gsZawP" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 147 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 148 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "9bIVhdYYpFl7JZmL4oubNS" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 150 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 151 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "2d66RAsipF1brG2RGpHJY1" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 153 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 154 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "ffwse8PqZBf4sIEkNdsA+i" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 156 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 157 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "adNP+ELEhC4awfcKkY8jYJ" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 159 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 160 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "33gYFxG8dO2r8iKjzjEx4z" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 162 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 163 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "27mCip0lFNkL+6Dj/Bpwj4" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 165 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 166 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "50aim7sdtB2rmsHB4KRkd5" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 168 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 169 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "70DWXrCRRL4pWATtzgi7DN" + ] + }, + { + "__type__": "cc.TargetOverrideInfo", + "source": { + "__id__": 59 + }, + "sourceInfo": { + "__id__": 171 + }, + "propertyPath": [ + "_skinningRoot" + ], + "target": { + "__id__": 59 + }, + "targetInfo": { + "__id__": 172 + } + }, + { + "__type__": "cc.TargetInfo", + "localID": [] + }, + { + "__type__": "cc.TargetInfo", + "localID": [ + "7aOtJ3fThN27Zj4fmkg0ut" + ] + }, + { + "__type__": "cc.SceneGlobals", + "ambient": { + "__id__": 174 + }, + "shadows": { + "__id__": 175 + }, + "_skybox": { + "__id__": 176 + }, + "fog": { + "__id__": 177 + } + }, + { + "__type__": "cc.AmbientInfo", + "_skyColor": { + "__type__": "cc.Color", + "r": 51, + "g": 128, + "b": 204, + "a": 1 + }, + "_skyIllum": 20000, + "_groundAlbedo": { + "__type__": "cc.Color", + "r": 51, + "g": 51, + "b": 51, + "a": 255 + } + }, + { + "__type__": "cc.ShadowsInfo", + "_type": 1, + "_enabled": true, + "_normal": { + "__type__": "cc.Vec3", + "x": 0, + "y": 1, + "z": 0 + }, + "_distance": 1, + "_shadowColor": { + "__type__": "cc.Color", + "r": 0, + "g": 0, + "b": 0, + "a": 115 + }, + "_autoAdapt": true, + "_pcf": 2, + "_bias": 0.000001, + "_near": 0.1, + "_far": 50, + "_aspect": 1, + "_shadowDistance": 10, + "_invisibleOcclusionRange": 200, + "_orthoSize": 10, + "_maxReceived": 4, + "_size": { + "__type__": "cc.Vec2", + "x": 512, + "y": 512 + } + }, + { + "__type__": "cc.SkyboxInfo", + "_envmap": { + "__uuid__": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0" + }, + "_isRGBE": false, + "_enabled": true, + "_useIBL": false + }, + { + "__type__": "cc.FogInfo", + "_type": 0, + "_fogColor": { + "__type__": "cc.Color", + "r": 200, + "g": 200, + "b": 200, + "a": 255 + }, + "_enabled": false, + "_fogDensity": 0.3, + "_fogStart": 0.5, + "_fogEnd": 300, + "_fogAtten": 5, + "_fogTop": 1.5, + "_fogRange": 1.2 + } +] \ No newline at end of file diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene.meta new file mode 100644 index 000000000..edfd26999 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/scene/main.scene.meta @@ -0,0 +1,11 @@ +{ + "ver": "1.1.27", + "importer": "scene", + "imported": true, + "uuid": "7c3e7fab-7b1e-4865-ba84-3cf81b48b9fb", + "files": [ + ".json" + ], + "subMetas": {}, + "userData": {} +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox.meta new file mode 100644 index 000000000..46fb35c84 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox.meta @@ -0,0 +1,12 @@ +{ + "ver": "1.1.0", + "importer": "directory", + "imported": true, + "uuid": "9e344b1f-8681-4ddf-bcc6-bb014c332bb8", + "files": [], + "subMetas": {}, + "userData": { + "compressionType": {}, + "isRemoteBundle": {} + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg new file mode 100644 index 000000000..633a4b71c Binary files /dev/null and b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg differ diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg.meta b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg.meta new file mode 100644 index 000000000..22bccfd8a --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/assets/skybox/sunnySkyBox.jpg.meta @@ -0,0 +1,131 @@ +{ + "ver": "1.0.21", + "importer": "image", + "imported": true, + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb", + "files": [ + ".jpg", + ".json" + ], + "subMetas": { + "b47c0": { + "importer": "erp-texture-cube", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0", + "displayName": "sunnySkyBox", + "id": "b47c0", + "name": "textureCube", + "userData": { + "wrapModeS": "repeat", + "wrapModeT": "repeat", + "minfilter": "linear", + "magfilter": "linear", + "mipfilter": "linear", + "anisotropy": 1, + "isRGBE": false, + "imageDatabaseUri": "db://assets/skybox/sunnySkyBox.jpg" + }, + "ver": "1.0.9", + "imported": true, + "files": [ + ".json" + ], + "subMetas": { + "7d38f": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@7d38f", + "displayName": "", + "id": "7d38f", + "name": "bottom", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "40c10": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@40c10", + "displayName": "", + "id": "40c10", + "name": "back", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "e9a6d": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@e9a6d", + "displayName": "", + "id": "e9a6d", + "name": "front", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "bb97f": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@bb97f", + "displayName": "", + "id": "bb97f", + "name": "top", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "8fd34": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@8fd34", + "displayName": "", + "id": "8fd34", + "name": "left", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + }, + "74afd": { + "importer": "texture-cube-face", + "uuid": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0@74afd", + "displayName": "", + "id": "74afd", + "name": "right", + "userData": {}, + "ver": "1.0.0", + "imported": true, + "files": [ + ".png", + ".json" + ], + "subMetas": {} + } + } + } + }, + "userData": { + "hasAlpha": false, + "type": "texture cube", + "redirect": "5af201b5-5951-4e2c-a81f-ac4aad9132cb@b47c0" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/package.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/package.json new file mode 100644 index 000000000..9f0043444 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/package.json @@ -0,0 +1,8 @@ +{ + "name": "cocos-hello-world", + "uuid": "429dad17-4262-4828-bcbc-51cad5e1e20f", + "version": "3.0.0", + "creator": { + "version": "3.8.8" + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/builder.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/builder.json new file mode 100644 index 000000000..ad47cd6ea --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/builder.json @@ -0,0 +1,3 @@ +{ + "__version__": "1.2.4" +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/engine.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/engine.json new file mode 100644 index 000000000..88d6913aa --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/engine.json @@ -0,0 +1,18 @@ +{ + "modules": { + "cache": {}, + "includeModules": [ + "base", + "gfx-webgl", + "gfx-webgl2", + "ui", + "particle", + "physics-cannon", + "physics-framework", + "audio", + "tween", + "terrain" + ] + }, + "__version__": "1.0.1" +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/project.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/project.json new file mode 100644 index 000000000..4e787967e --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/1.2.0/packages/project.json @@ -0,0 +1,7 @@ +{ + "__version__": "1.0.1", + "script": { + "useDefineForClassFields": false, + "allowDeclareFields": false + } +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/v2/packages/scene.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/v2/packages/scene.json new file mode 100644 index 000000000..176b0c9b0 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/settings/v2/packages/scene.json @@ -0,0 +1,4 @@ +{ + "__version__": "1.0.0", + "current-scene": "7c3e7fab-7b1e-4865-ba84-3cf81b48b9fb" +} diff --git a/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/tsconfig.json b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/tsconfig.json new file mode 100644 index 000000000..7dc649a95 --- /dev/null +++ b/apps/ai-game-creator-shell/template-library/v1/cocos-hello-world/project/tsconfig.json @@ -0,0 +1,9 @@ +{ + /* Base configuration. Do not edit this field. */ + "extends": "./temp/tsconfig.cocos.json", + + /* Add your custom configuration here. */ + "compilerOptions": { + "strict": false + } +} diff --git a/docs/project-memory/plans/【实施计划】后台模板管理-2026-09-19.md b/docs/project-memory/plans/【实施计划】后台模板管理-2026-09-19.md new file mode 100644 index 000000000..786b02e97 --- /dev/null +++ b/docs/project-memory/plans/【实施计划】后台模板管理-2026-09-19.md @@ -0,0 +1,18 @@ +# 后台模板管理实施计划 + +| 字段 | 值 | +| --- | --- | +| Milestone | `docs/project-memory/plans/【里程碑】后台模板管理-2026-09-19.md` | +| Status | in-progress | +| Owner | Codex | + +## 修改边界与顺序 + +1. 评审主规范和 DTO,确认权限、单清单 inactive 投影、锁与凭据配置。 +2. 前端独立负责 admin-web 页面/路由/类型/API/tests;存储分支负责 platform-oss 模板 adapter;领域分支负责 module-assets 纯规则、shared-contracts/admin DTO;主线程负责 API/配置/状态装配、CLI状态兼容、文档和集成验证。 +3. 后端共用已定义锁语义,CLI同步inactive合并;并行完成定向测试,Cargo依赖编译串行协调。 +4. 复审、API与浏览器smoke,回填证据,删除本里程碑临时计划。 + +## 时间盒与风险 + +按规格评审、最小读写闭环、权限/冲突边界、界面验收四个检查点推进。旧客户端仅识别templates,不能把下架条目留在该数组;正文与index不能跨bucket;异步请求取消不能误释放结果不明的锁。只新增必要执行层,保留现有Cocos改动和个人工具数据,不扩大为新模板上传系统。 diff --git a/docs/project-memory/plans/【里程碑】后台模板管理-2026-09-19.md b/docs/project-memory/plans/【里程碑】后台模板管理-2026-09-19.md new file mode 100644 index 000000000..66de95200 --- /dev/null +++ b/docs/project-memory/plans/【里程碑】后台模板管理-2026-09-19.md @@ -0,0 +1,23 @@ +# 后台模板管理 + +| 字段 | 值 | +| --- | --- | +| Version | 1.0 | +| Status | approved | +| Date | 2026-09-19 | +| Parent Spec | `docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md` | + +## 目标与范围 + +后台管理员按权限查看并编辑 AGC 模板名称/简介/标签/封面,支持上架/下架。用户已明确选择此范围,不新增模板、ZIP 上传、版本发布、删除、部署或提交。 + +## 合同与依赖 + +OSS index 单一真相,active/inactive 两组;CLI/Rust 共用不可变对象及互斥发布锁;客户端沿用 active 读取。后台现有鉴权/权限/组件复用;不改 SpacetimeDB schema。 + +## 验收 + +- 页面、导航、权限和请求/响应合同一致,未授权无读取/写入。 +- 编辑、封面、上下架真实通过存储适配器写回;ZIP与版本不变,其他模板及未知字段保留。 +- 并发、旧revision、失败/未知写入和页面迟到响应失败关闭;CLI不重新上架下架项。 +- 定向测试/类型/构建/编码/格式与文档检查通过;本地API健康检查与桌面/移动受控浏览器smoke,真实OSS不写。 diff --git a/docs/project-memory/shared-memory/project-overview.md b/docs/project-memory/shared-memory/project-overview.md index 70f9f7de4..3cd48c7d9 100644 --- a/docs/project-memory/shared-memory/project-overview.md +++ b/docs/project-memory/shared-memory/project-overview.md @@ -51,6 +51,8 @@ SpacetimeDB crate、SDK、CLI / standalone 与生成 bindings 按 `2.8.3` 对齐 ## AGC DirectProject 与 UI workflow +- AGC 模板库包含 Creator 3.8.8 的四个官方 Cocos 模板;Cocos 建项复用原生导入,重建项目 UUID 并保留场景与资源。发布使用内容地址保留历史对象,并在确认 Bucket 从未开启版本控制后获取排他锁;`--only` 在锁内合并最新清单,清单写入结果不明时留锁,同版本 ZIP 变化拒绝发布。详细合同见 [AGC 模板库与模板建项](../../technical/【技术方案】AGC模板库与模板建项-2026-09-17.md)。 + - AGC 的本地 `llm.customEnabled` 默认关闭,只能手动修改配置文件;开启后设置支持自定义 Responses 端点、读取 `/models`、勾选和预览 `visibleModels`。对话下拉只显示勾选项,LLM 请求经客户端凭据代理直连自定义上游;不会回退官方中转,平台资源服务仍使用账号权限。详见 AGC 后台模型别名与对话选择规范。 - DirectProject 对话先在完整历史中按回合/原始 item 身份关联,再分页渲染;每个回合只有一个呈现入口。有流按 item `seq` 交替文本和工具,无流采用历史正文;禁止位置猜配或同时展示累计回复与 item 正文。流写入单调归并,收尾等待落盘任务,不按磁盘“最后一段”猜最终回复位置。详见 AGC 实施计划的“DirectProject 回合展示唯一归属”。 diff --git a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md index e2f5df3a6..684cd4df2 100644 --- a/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md +++ b/docs/technical/【技术方案】AGC模板库与模板建项-2026-09-17.md @@ -19,51 +19,73 @@ AGC 客户端接入公共 OSS 上的**游戏模板库**(真·游戏模板, - 验证入口:后台灰度页面测试、`useTemplateLibrary.test.tsx`、AppSurface 实际挂载的 template 用例、原生 `template_library` 测试和后端 `frontend_runtime_config` 测试。退出开始使用既有平台会话代次立即撤销,建项返回、revision 读取和预览核验后的旧回调均不得导航或登记最近项目;同主体 token 轮换不误撤销原生身份。 - 本地隔离数据库已验证 Gate 经后台 API 保存后可重新读回,匿名运行时配置为 false;后台受控浏览器 smoke 验证桌面和 320px 布局及保存确认交互。线上 OSS 下载和正式安装包登录后的端到端操作不由这些测试替代。 +## 后台模板管理 + +- 后台新增 `#agc-templates`「模板管理」页签,复用现有后台布局、列表、公共表单/独立弹窗及写入确认。提供名称/ID/标签搜索、运行时和上下架筛选,展示封面、名称、简介、标签、引擎/版本、包大小与上架状态。 +- 本轮只允许编辑名称、简介、标签、封面和上架状态;不新增模板、不上传 ZIP、不修改 ID、运行时、引擎或模板版本,不删除包、历史对象或已建项目。 +- 唯一数据真相仍是 OSS `templates/index.json`:`templates` 保存上架条目,新增可选 `inactiveTemplates` 保存下架条目,两个数组之间 ID 唯一。后台合并展示两组;AGC 客户端仍只读取 `templates`,刷新后不展示下架项。下架不是资源访问撤销,旧清单缓存和已下载项目不受影响。下架条目元数据位于公开清单,不承载私密草稿。 +- 后台读取/写入分别为 `GET /admin/api/agc-templates`、`PUT /admin/api/agc-templates/{id}`,均经过现有后台认证及 `agc-templates` 页签权限。owner 默认可用,member 需显式分配该权限;不新增数据库表或 schema。部署时后台、API 与引用权限白名单的 SpacetimeDB 模块需同步更新,成员账号才可保存新页签权限。 +- GET 返回 `{ revision, writable, templates }`:revision 是完整原始清单字节的 SHA-256;每条包含 `id/title/summary/tags/runtime/engine/engineVersion/templateVersion/enabled/coverUrl/zipSizeBytes`。不可用或格式错误返回可诊断错误,不能当作空模板库。 +- PUT 请求 `{ expectedRevision, title, summary, tags, enabled, cover? }`,禁止未知字段。名称 trim 后 1–80 字符、简介最多 1000 字符、最多 16 个去重标签(每项 1–32 字符)。封面可选 `{ contentType, dataBase64 }`,仅接受真实 PNG/JPEG/WebP,解码前后校验,原始字节最多 5 MiB、单边最多 4096 像素且不超过 1600 万像素;原有 SVG 引用可继续展示。HTTP body 上限 8 MiB。返回与 GET 同形的最新快照。 +- 写入复用同一 OSS 发布锁、版本控制前置检查及不确定清单写入留锁协议;在锁内读取最新清单并比较 expectedRevision,过期或锁忙返回 409,不自动重试写入。封面及更新后的 template.json 以内容摘要键写入并回读校验,再一次提交清单;ZIP、版本和未知元数据字段保留。清单响应不明返回 503 并留锁,后续需核对;任何 API 错误不回显凭据或上游正文。 +- 页面保存/上下架前使用既有确认交互。编辑弹窗保持独立;保存中防重复提交,失败保留输入并展示错误,409 引导刷新后重新编辑;刷新、切页、换账号和晚到响应不能覆盖新页面状态。封面仅作为待保存草稿预览,成功后使用后端返回的正式地址。 +- 模板 OSS 目标固定为当前 AGC 公开 bucket/endpoint,不能误用通用素材 Bucket。凭据优先成套读取 `GENARRATIVE_AGC_TEMPLATE_LIBRARY_OSS_ACCESS_KEY_ID/SECRET`,两项均未配置时可成套复用已有 `ALIYUN_OSS_ACCESS_KEY_ID/SECRET`;半配置不拼接回退且禁止写入。无可用写凭据时后台可只读,写接口返回 503,页面明确不可保存。 +- CLI 与后台共享锁和清单合同。CLI 合并保留未选条目与 `inactiveTemplates`,更新下架模板仍保持下架;新增模板默认上架,CLI 不承担删除或上下架。显式发布选中 ID 时,展示字段按该模板源更新,后台编辑结果持续有效直到下一次显式发布该 ID。两端都必须保留另一组条目,不能因本地模板源较旧而抹掉后台记录。 +- 验收包含权限、入口挂载、过滤、编辑与图片校验、上下架往返、未知字段保留、过期 revision/锁争用、上传失败与未知提交、CLI 对下架项的更新/保留,以及桌面/窄屏真实浏览器验证。真实 OSS 写入和生产部署不在本轮验证范围,使用隔离存储替身。 + ## OSS 契约 ```text templates/ index.json # 模板库清单,客户端唯一读取入口 + .publish-lock.json # 发布互斥锁,不供客户端读取 v1// - template.json # 单模板元数据(含文件级摘要) - template.zip # 模板正文,zip 根 == AGC 项目根(如 game/index.html) - cover.(png|jpg|webp|svg) # 封面图(卡片展示,客户端 直接取) + sha256//template.zip # 模板正文,zip 根 == AGC 项目根 + sha256//cover. # 封面图 + sha256//template.json # 单模板元数据(含文件级摘要) ``` `index.json`(schema `agc-template-library.v1`): -| 字段 | 说明 | -| --- | --- | -| `schemaVersion` | 固定 `agc-template-library.v1`;破坏性变更换 schema,不原地改语义 | -| `library` / `libraryVersion` / `updatedAt` | 库标识、库格式版本、本次更新时间 | -| `templates[].id` | 稳定标识,`[a-z0-9][a-z0-9._-]{0,63}`,同时是目录名 | -| `templates[].title/summary/tags[]` | 展示与搜索/筛选用文案;`tags` 参与标签筛选与关键词命中 | -| `templates[].runtime` | `html` / `unity` / `godot` / `cocos` | -| `templates[].engine` / `engineVersion` | 引擎标识与版本(如 `phaser` 4.2.1、`three.js` 0.180.0) | -| `templates[].templateVersion` / `updatedAt` | 模板内容版本;客户端按它判断是否需要重新下载 | -| `templates[].entry` | 解压后的项目入口相对路径,如 `game/index.html` | -| `templates[].zipKey` / `zipSizeBytes` / `zipSha256` | 模板包对象键、字节数、SHA-256(下载后强校验) | -| `templates[].coverKey` / `coverWidth` / `coverHeight` / `coverSha256` | 封面对象键与尺寸/摘要 | -| `templates[].metadataKey` | 单模板元数据对象键(`template.json`) | +| 字段 | 说明 | +| --------------------------------------------------------------------- | ----------------------------------------------------------------- | +| `schemaVersion` | 固定 `agc-template-library.v1`;破坏性变更换 schema,不原地改语义 | +| `library` / `libraryVersion` / `updatedAt` | 库标识、库格式版本、本次更新时间 | +| `templates[].id` | 稳定标识,`[a-z0-9][a-z0-9._-]{0,63}`,同时是目录名 | +| `templates[].title/summary/tags[]` | 展示与搜索/筛选用文案;`tags` 参与标签筛选与关键词命中 | +| `templates[].runtime` | `html` / `unity` / `godot` / `cocos` | +| `templates[].engine` / `engineVersion` | 引擎标识与版本(如 `phaser` 4.2.1、`three.js` 0.180.0) | +| `templates[].templateVersion` / `updatedAt` | 模板内容版本;客户端按它判断是否需要重新下载 | +| `templates[].entry` | 解压后的项目入口相对路径,如 `game/index.html` | +| `templates[].zipKey` / `zipSizeBytes` / `zipSha256` | 模板包对象键、字节数、SHA-256(下载后强校验) | +| `templates[].coverKey` / `coverWidth` / `coverHeight` / `coverSha256` | 封面对象键与尺寸/摘要 | +| `templates[].metadataKey` | 单模板元数据对象键(`template.json`) | 约束: - 所有对象键必须落在 `templates/` 前缀内;客户端只用「受信任 OSS 主机 + 对象键」自行拼 URL,**不直接信任清单里的地址**。 - 任何一项校验失败(schema、标识符、sha256、尺寸、键前缀)都让整次清单读取失败,前端拿到的是全有或全无的清单。 - 模板源在仓库 `apps/ai-game-creator-shell/template-library/`:`v1//{meta.json, project/**, cover.(png|jpg|webp|svg)}`,`template.zip` **不落仓库**,由脚本按 `project/` 现场打包(条目排序、固定时间戳,同内容重复打包摘要一致)。 -- 上传与校验由 [`scripts/agc-template-library-publish.mjs`](../../scripts/agc-template-library-publish.mjs) 完成:`--source apps/ai-game-creator-shell/template-library [--dry-run] [--prune]`,脚本生成 `template.json` 与 `index.json`、上传后回读 zip 摘要;`--prune` 清理该模板前缀下本次没有产出的旧对象(例如换封面扩展名后的残留)。 -- 当前模板:`blank-web`(空白网页)、`blank-2d-canvas`(空白二维画布)、`blank-3d-scene`(空白三维场景)、`phaser-2d-starter`(Phaser 2D 起步工程)、`threejs-3d-starter`(Three.js 3D 起步工程)。 +- 上传与校验由 [`scripts/agc-template-library-publish.mjs`](../../scripts/agc-template-library-publish.mjs) 完成:`--source apps/ai-game-creator-shell/template-library [--dry-run] [--only ]`。ZIP、封面和元数据分别以自身字节的 SHA-256 定位,只创建新对象或复用逐字节校验一致的已有对象;全部对象回读一致后才更新 `index.json`。失败不回收已上传对象,旧清单及其引用始终可读。 +- 只更新指定模板时使用 `--only `,在发布锁内读取最新清单,只替换指定 ID,其余条目和未知扩展字段保留。首次清单 404 可由本次选择初始化;读取异常或清单非法时停止。全量发布也遵守相同锁与版本门禁。 +- 正式发布先通过 `GetBucketVersioning` 确认 Bucket 从未开启版本控制,再用 `x-oss-forbid-overwrite: true` 原子创建 `.publish-lock.json`;版本控制 Enabled、Suspended、检查无权限或无法判定时均在写入前停止。所有写同一清单的发布进程必须使用此锁,发布期间不得改变 Bucket 版本控制配置。锁没有自动过期或抢占机制,已被占用时直接失败,重新执行须重新获取锁并读取最新清单。 +- 只释放本任务已明确获取且 owner 标识仍一致的锁。获取结果不明时不猜测删除。正文对象不可变,其写入失败可安全释放本任务的锁;清单 PUT 已发起后若遇到断连、超时或服务端 5xx 等不确定结果,必须保留锁并报错,防止旧在途请求晚于下一发布者写入。清单收到确定成功或确定拒绝响应后才进入正常解锁路径;不自动重发清单写入,也不凭一次 GET 猜测在途 PUT 已结束。遗留锁须在确认原请求及进程已终止或完成后由运维处理;释放失败必须报告,不伪装为发布成功。 +- `--dry-run` 仅构造和读取合并计划,不读取凭据、不获取锁、不 PUT/DELETE。发布不删除历史对象,也不提供随发布清理的选项;旧客户端缓存和未完成下载可能仍引用旧键。 +- 同一 ID、同一 `templateVersion` 的 ZIP 大小或摘要改变时,在上传正文前拒绝,要求更新模板版本;只把相同 ZIP 迁移到新键可保留版本。客户端现有 `/` 缓存行为不变。 +- 当前模板:`blank-web`(空白网页)、`blank-2d-canvas`(空白二维画布)、`blank-3d-scene`(空白三维场景)、`phaser-2d-starter`(Phaser 2D 起步工程)、`threejs-3d-starter`(Three.js 3D 起步工程),以及 Cocos Creator 3.8.8 的 `cocos-empty-2d`、`cocos-empty-3d`、`cocos-empty-3d-hq`、`cocos-hello-world`。 +- Cocos 内容来自 Creator 3.8.8 随附的 `resources/templates/{empty-2d,empty,empty-quality,hello-3d-world}`,保留官方资源、`.meta`、设置和模板预设;补齐 `package.json.creator.version`,空模板以 `assets/.gitkeep` 保证资源目录进入 Git 和 ZIP。`entry` 为 `package.json`,不打包编辑器生成的缓存或用户项目数据。 +- Cocos 建项在复制后按实际 `package.json.creator.version + assets/` 识别,复用既有 Cocos 导入流程,写入 `cocosProjectRoot: "."`;每个新项目重建 `package.json.uuid` 并写入所选项目名。只创建 `.agent` 管理目录,不生成 Web 占位入口;模板源与本机安装缓存不被改写。 - 客户端可用 `AGC_TEMPLATE_LIBRARY_BASE_URL` 覆盖库地址;只接受 `https://agc-dev.oss-rg-china-mainland.aliyuncs.com`(拒绝其他主机、路径、http)。 ## 客户端实现 ### Rust:`apps/ai-game-creator-shell/src-tauri/src/template_library.rs` -| 命令 | 行为 | -| --- | --- | -| `fetch_game_template_library` | 读 `templates/index.json`(≤4 MiB),校验后缓存到 `/templates/index.json`;网络失败时回退本机缓存并在 `source` 标 `cache` | -| `download_game_template` | 取清单里对应条目,流式下载 zip(≤512 MiB),校验字节数与 SHA-256,解压到 `/templates/installed///`,最后写 `installed.json` 作为安装完成的唯一标记 | -| `create_automatic_local_game_project_from_template` | 需要时先安装模板,然后在 `/projects/` 下按既有自动工作区规则建目录:先复制模板文件,再走 `init_local_game_project_at` 补 `.agent` 清单与标准目录。根目录可用 `projectsRoot` 覆盖(必须来自本机目录选择器并通过私有路径门禁),未指定时仍是 `/projects/`;见 [`【实施计划】AGC项目创建目录可选-2026-09-17.md`](../project-memory/plans/【实施计划】AGC项目创建目录可选-2026-09-17.md) | +| 命令 | 行为 | +| --------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `fetch_game_template_library` | 读 `templates/index.json`(≤4 MiB),校验后缓存到 `/templates/index.json`;网络失败时回退本机缓存并在 `source` 标 `cache` | +| `download_game_template` | 取清单里对应条目,流式下载 zip(≤512 MiB),校验字节数与 SHA-256,解压到 `/templates/installed///`,最后写 `installed.json` 作为安装完成的唯一标记 | +| `create_automatic_local_game_project_from_template` | 需要时先安装模板,然后在自动工作区根目录下按既有自动工作区规则建目录:先复制模板文件;Cocos 项目更新自身身份后走既有 Cocos 导入,其余沿现有 `init_local_game_project_at` 初始化。根目录默认是 `/projects/`,用户可选 `projectsRoot` 覆盖(必须来自本机目录选择器并通过私有路径门禁),见 [`【实施计划】AGC项目创建目录可选-2026-09-17.md`](../project-memory/plans/【实施计划】AGC项目创建目录可选-2026-09-17.md) | 安全与健壮性: @@ -86,6 +108,12 @@ templates/ ## 验收与验证 +并发发布验收必须覆盖真实发布编排的离线故障注入:正文写入或清单提交失败后旧清单引用保持一致;两个发布者互斥、后续重试从新清单合并;条件请求头正确签名;版本控制状态不安全时零写入;外部锁不被删除;同版本改 ZIP 被拒绝;已存在内容对象冲突及释放失败可诊断。普通打包和纯合并函数测试不能替代此门禁。OSS 协议依据:[PutObject](https://help.aliyun.com/zh/oss/developer-reference/putobject)、[GetBucketVersioning](https://help.aliyun.com/zh/oss/developer-reference/getbucketversioning) 与 [V1 签名](https://help.aliyun.com/zh/oss/developer-reference/include-signatures-in-the-authorization-header)。 + +Cocos 回归分别覆盖仓库模板和线上真实 ZIP 的安装、连续建项、独立 UUID、`cocosProjectRoot`、原文件保留与安装缓存不变;Web 模板继续走原有回归。此处验证的是模板下载与建项,不替代 Creator 内场景运行验收。Cocos 原生建项分流需要包含此实现的客户端,旧二进制须重新构建或更新。 + +`2026-09-19` 发布一致性验收:Node 发布回归 22 项通过,覆盖两个发布者竞争、正文/清单写入失败、迟到清单 PUT、锁归属、版本控制拒绝、V1 签名及真实 CLI 的无写入 dry-run。Rust 定向回归 15 项通过,新增内容地址的清单解析与 URL 保留校验;3 项线上用例本轮未重复执行,此前同日线上下载及原生建项已通过。只读 dry-run 保留线上九个模板并仅计划更新四个 Cocos 条目。格式、编码、文档索引、定向 ESLint 与 diff 检查通过。全部并发/故障写入证据来自离线替身,未执行真实 OSS 锁写入或发布,也未验证 Creator 内场景运行。 + ## 本地压测假数据注入(feature 控制) 模板库的数据源在 Rust 侧(清单校验、安装状态、下载与建项目都在这里),TS 只消费快照做渲染,所以假数据注入也放在 Rust 侧,走与真实完全一致的链路。 @@ -122,6 +150,10 @@ AGC_TEMPLATE_LIBRARY_SYNTHETIC_COUNT=300 AGC_DEV_CARGO_FEATURES=template-library - 前端回归:1000 条渲染 + 已安装过滤(334)/标签过滤(50)/关键词过滤数量自洽,见 `apps/ai-game-creator-shell/tests/templateLibraryView.test.tsx`。 ```bash +# 模板内容、确定性打包与定向发布合并回归 +node --test scripts/agc-template-library-publish.test.mjs +# 仅发布 Cocos 模板(先加 --dry-run 核对合并清单) +node scripts/agc-template-library-publish.mjs --source apps/ai-game-creator-shell/template-library --only cocos-empty-2d,cocos-empty-3d,cocos-empty-3d-hq,cocos-hello-world # 模板库单测(清单校验、键安全、解压路径逃逸、安装与建项目) cargo test --manifest-path apps/ai-game-creator-shell/src-tauri/Cargo.toml --bin genarrative-ai-game-creator-shell template_library # 模板库真连检查(可选,需要网络):读线上清单、下载安装线上模板包并据此建项目 @@ -144,4 +176,4 @@ curl -s https://agc-dev.oss-rg-china-mainland.aliyuncs.com/templates/index.json - 清单读不到且没有本机缓存:模板库页显示错误与重试,首页推荐位显示「模板库暂时没有可用的模板」。 - 版本落后:`installedVersion != templateVersion` 视为需要重新下载,点「使用模板」会先重下再建项目。 -- 需要回退整条链路时,删除 `templates/` 前缀即可让客户端回到"空模板库";客户端代码路径不受影响。 +- 回退清单同样必须使用发布互斥协议并保留其引用的历史对象,不能删除整个 `templates/` 前缀来回退。 diff --git a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md index b29fbed4f..4e8f879dd 100644 --- a/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md +++ b/docs/【开发运维】本地开发验证与生产运维-2026-05-15.md @@ -143,7 +143,7 @@ BgFilter 对已经落入私有 OSS 的生成原图、动作抽取帧和手动去 `Genarrative-Scheduled-Revision-Trigger` 是唯一的定时入口,每小时检查一次(`H * * * *`,分钟由 Jenkins 按 Job 名散列,不等同于整点)。它只用 `git ls-remote` 解析 `SOURCE_BRANCH`(默认 `master`)的远端 HEAD,不 checkout 工作区;解析出的完整 commit 与上一次触发过的 revision 相同则标记 `NOT_BUILT` 并结束,不触发任何下游。 -revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。两条下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 +revision 变化时,调度管线把同一个完整 commit 通过 `COMMIT_HASH` 同时传给 `Genarrative-Full-Build-And-Deploy`、`Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`,各条管线都按这个 commit 检出(Full Job 继续把 `env.SOURCE_COMMIT` 透传给 Web / API / Stdb 的 Build、Publish、Deploy),因此两个产物必然来自同一个版本,不会各自解析分支 HEAD 造成漂移。这些下游管线自身不带任何定时触发器,也不在管线内部做版本比较。Windows 客户端发布额外按路径过滤:调度管线比较「上一轮已触发的 revision」与本次 revision 之间的变更路径,只有出现 `apps/ai-game-creator-shell/`、`packages/`、`server-rs/crates/`、`plugins/agc-cocos-editor/`、`apps/desktop-shell/src-tauri/icons/`、`package.json` 或 `package-lock.json` 时才触发 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build`(两个平台分区发布同一个发号 Job 下发的总版本号),纯文档或流水线自身的提交只触发 Full Build、不推高客户端版本号;判定取消或失败一律按「需要发布」处理,勾选 `FORCE_TRIGGER` 可强制两条都触发。两条下游各自判定:AGC Windows Build 采用「客户端相关路径白名单」,Full Build 采用「与线上站点 / 后端无关的路径黑名单」(`docs/`、`.codex/`、`jenkins/`、`apps/ai-game-creator-shell/`、`apps/mobile-shell/`、`apps/desktop-shell/`、`apps/preview-deployer-web/`、`tools/`、根级 `*.md`),改动只要落在黑名单之外就会照常部署,避免漏发线上站点或后端;两条同时被判为跳过时调度管线只推进 revision 状态、不触发任何发布。AGC 客户端版本号不再由渠道各自递增:唯一发号源是 OSS 对象 `agc/global-version.json`,发号收口到 `Genarrative-Agc-Global-Version-Issue`(`disableConcurrentBuilds()` + 写后回读校验;集群未装 `lockable-resources` 插件)。调度管线与手动发布管线都先调用该 Job 发号,再用归档产物 `agc-global-version.txt` 读取总号并作为 `AGC_RELEASE_VERSION` 透传给客户端构建;统一构建只发一次号供各渠道共用,单渠道热修只把号传给该渠道。显式传入的号低于本渠道当前清单版本时构建失败关闭;`AGC_RELEASE_DRY_RUN` 只预览下一位,不写回、不烧号。一次性播种用 `SEED_ONLY`:基线取「仓库版本 / 各渠道清单 / 旧迁移指针」的最大值,播种本身不烧号。仓库里的 5 个版本文件仍由构建改写,只作构建输入参考,不是事实源。客户端渠道清单的更新摘要同样自动生成:发布脚本读取上一份渠道清单的 `commit` 字段,把该提交到本次提交之间触及客户端相关路径的提交标题逐条写进 `notes`(旧协议清单写入 `releaseNotes`,并落盘归档文件 `release-notes.txt`);`AGC_UPDATE_RELEASE_NOTES` 非空时以手动文案为准,缺少上一份 `commit` 时不写摘要。Full Job 默认以 `DEPLOY_TARGET=development`、`STDB_API_ROLLOUT_MODE=normal` 对仅供开发使用的 dev 服务器执行 Stdb → API → Web 完整发布,不进入人工 rollout gate;三个下游 Build 都由 Full Job 显式传 `PUBLISH_AFTER_BUILD=false`,统一 Build 完成后仍由 Full Job 按固定顺序发布。人工维护窗口才选择 `pause-after-stdb`,且必须配置 `STDB_API_ROLLOUT_APPROVERS`。 `Genarrative-Agc-Windows-Build` 与 `Genarrative-Agc-MacOS-Build` 结束后都会触发 `Genarrative-Notify-Email`;只有正式 OSS 发布成功时,通知正文才附加本次生成的 `latest.json` 首装包 URL,演练、失败或跳过构建时留空,避免把未写入或不完整的对象地址发给收件人。 调度状态是调度 Job 工作区里的 `.jenkins-last-triggered-revision`,构建描述同时回显本次 revision 与结果。工作区被清理(例如 `Wipe Out Workspace`)或状态文件缺失时,下一次运行按“版本变化”处理并触发一次,之后恢复稳定;需要重建同一版本时勾选 `FORCE_TRIGGER`。Job 按仓库内 `jenkins/scheduled-revision-trigger-job-config.xml` 创建:`scriptPath=jenkins/Jenkinsfile.scheduled-revision-trigger`、Git 入口 `ssh://git@127.0.0.1:2222/GenarrativeAI/Genarrative.git`、凭据 `genarrative-local-gitea-ssh`、`` 留空(定时器写在 Jenkinsfile 里)。推送后必须让三个 live Job 各自加载一次新 Jenkinsfile,并只读核对 `config.xml`:Full 与 AGC 不再有 cron,定时只来自新调度 Job;只改 Jenkinsfile 而不确认 live 配置时,旧 cron 仍会继续触发。 @@ -730,7 +730,7 @@ Pingora current release 自审脚本 `scripts/ops/pingora-current-release-audit. Mac 单架构(arm64)构建脚本位于 `jenkins/Jenkinsfile.ai-game-creator-shell-macos-build`,只对专用 `genarrative-agc-macos` 标签运行。节点按 EXCLUSIVE、单 executor 配置,Job 禁止并发、自身不设 cron(定时只来自调度 Job);它已接入每小时版本调度:`Genarrative-Scheduled-Revision-Trigger` 先调发号 Job 拿同一个总号,再把 `SOURCE_BRANCH`、固定 `COMMIT_HASH`、`AGC_UPDATE_CHANNEL=dev`、`AGC_RELEASE_VERSION=<总号>` 与 `SKIP_IF_SUPERSEDED=true` 透传给本 Job,**因此正常发布不需要(也不应该)手工点构建**;手工触发只是排障手段。它不改 Windows 发布职责。它使它只在专用 Agent 目录下构建:默认按约定匹配 `$HOME/Library/Jenkins/agents//workspace/`(不写死节点名,节点改名后仍成立),也可用 `AGC_AGENT_ROOT` 显式覆盖;禁止指向开发 checkout 或共享其可写 target/node_modules。 -Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenkinsfile,参数为 `SOURCE_BRANCH`、`COMMIT_HASH`、`AGC_UPDATE_CHANNEL`、`AGC_RELEASE_VERSION`、`AGC_RELEASE_DRY_RUN`、`AGC_UPDATE_RELEASE_NOTES`、`OSSUTIL_BIN`、`CARGO_BUILD_JOBS`。渠道参数是基础名(不含系统,默认 `dev`),脚本不接受 `dev-mac` 这类系统后缀,写入分区固定推导为 `-mac`:这与 Windows Job 的 `-win` 对称,也延续已发布客户端的端点。 +Job 名为 `Genarrative-Agc-MacOS-Build`,SCM 直接读取仓库内上述 Jenkinsfile,参数为 `SOURCE_BRANCH`、`COMMIT_HASH`、`AGC_UPDATE_CHANNEL`、`AGC_RELEASE_VERSION`、`AGC_RELEASE_DRY_RUN`、`AGC_UPDATE_RELEASE_NOTES`、`OSSUTIL_BIN`、`CARGO_BUILD_JOBS`、`NOTIFICATION_EMAILS`。渠道参数是基础名(不含系统,默认 `dev`),脚本不接受 `dev-mac` 这类系统后缀,写入分区固定推导为 `-mac`:这与 Windows Job 的 `-win` 对称,也延续已发布客户端的端点。 该 Job 的职责是构建并发布 `-mac` 分区更新:执行 `npm ci` 后使用锁文件校验并补齐两种 macOS Codex 原生依赖,再调用 `scripts/build-macos-ci.mjs`(AGC 应用目录下)生成 arm64 单架构 app、arm64 隔离 smoke、arm64 DMG(`<产品名>_<版本>_aarch64.dmg`)与分区清单 `latest.json`,用产物内烘焙的公钥复核更新包签名(`verify-updater-signature.mjs`),最后按 `AGC_RELEASE_DRY_RUN` 决定是否上传 OSS。签到会同时取 `master`,让渠道清单里上一次发布的 commit 可解析——缺了它更新摘要会退化成「最近客户端改动」(该步失败只降级摘要,不阻断发布)。产物名(`*.app`、updater 归档、DMG、卷名)一律从 Tauri `productName` 推导,校验脚本从包内 `Info.plist` 读取可执行名,改产品名不会让入口静默找错对象;隔离 smoke 用 `ditto --clone` 复制副本(实测整轮 6.8 秒,此前整包复制约 1 分钟),并在构建前删除本次将写出的 DMG/更新包/签名,保证归档产物一定来自本次构建。归档限 `artifacts/` 下的 DMG、SHA-256、`latest.json`、更新包签名、更新摘要、非敏感构建清单和源码 commit;不归档用户 HOME、Jenkins secret、原始工作目录或全量日志。 diff --git a/jenkins/Jenkinsfile.ai-game-creator-shell-build b/jenkins/Jenkinsfile.ai-game-creator-shell-build index b48e5ec1f..d48f8d670 100644 --- a/jenkins/Jenkinsfile.ai-game-creator-shell-build +++ b/jenkins/Jenkinsfile.ai-game-creator-shell-build @@ -26,6 +26,7 @@ pipeline { booleanParam(name: 'AGC_RELEASE_DRY_RUN', defaultValue: false, description: '勾选后只构建并打印将要执行的上传命令,不写入 OSS') text(name: 'AGC_UPDATE_RELEASE_NOTES', defaultValue: '', description: '可选,支持多行文本;留空则由本次发布的客户端相关提交自动生成更新摘要') string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 或 ossutil.exe 的绝对路径/命令名') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与持久收件人凭据合并发送') } stages { @@ -209,6 +210,57 @@ pipeline { } post { + always { + script { + def ossDownloadUrl = '' + def releaseVersion = params.AGC_RELEASE_VERSION ?: '' + def sourceCommit = params.COMMIT_HASH ?: '' + def manifestPath = 'apps/ai-game-creator-shell/src-tauri/target/x86_64-pc-windows-msvc/release/bundle/latest.json' + if (currentBuild.currentResult == 'SUCCESS' && fileExists(manifestPath)) { + try { + def manifest = new groovy.json.JsonSlurperClassic().parseText(readFile(file: manifestPath)) + releaseVersion = manifest?.version?.toString()?.trim() ?: releaseVersion + if (!params.AGC_RELEASE_DRY_RUN) { + def firstDownload = manifest?.downloads instanceof Map + ? manifest.downloads.values().find { it instanceof Map && it.url?.toString()?.trim() } + : null + ossDownloadUrl = firstDownload?.url?.toString()?.trim() ?: '' + } + } catch (error) { + echo "读取 AGC OSS 下载链接失败,邮件不附加链接:${error.message}" + } + } + if (!sourceCommit && fileExists('.jenkins-source-commit')) { + sourceCommit = readFile(file: '.jenkins-source-commit').trim() + } + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: sourceCommit), + string(name: 'BUILD_VERSION', value: releaseVersion), + string(name: 'DEPLOY_TARGET', value: "${params.AGC_UPDATE_CHANNEL ?: 'dev'}-win"), + string(name: 'OSS_DOWNLOAD_URL', value: ossDownloadUrl), + string(name: 'SUMMARY', value: params.AGC_RELEASE_DRY_RUN + ? 'AGC Windows 渠道演练结束,未写入 OSS' + : 'AGC Windows 渠道构建与 OSS 发布结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } success { echo params.AGC_RELEASE_DRY_RUN ? "AGC ${params.AGC_UPDATE_CHANNEL} 渠道演练完成:已构建并生成清单,未写入 OSS。" diff --git a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build index 71146a824..7048f0241 100644 --- a/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build +++ b/jenkins/Jenkinsfile.ai-game-creator-shell-macos-build @@ -19,6 +19,7 @@ pipeline { string(name: 'AGC_UPDATE_RELEASE_NOTES', defaultValue: '', description: '可选单行更新摘要;留空则由发布脚本按提交自动汇总') string(name: 'OSSUTIL_BIN', defaultValue: 'ossutil', description: 'ossutil 命令名或绝对路径(Mac 节点默认装在 ~/.local/bin/ossutil)') string(name: 'CARGO_BUILD_JOBS', defaultValue: '8', description: '并行 rustc 任务数,默认吃满节点 8 核(4P+4E)。该值同时作为 rustc codegen 的 jobserver 令牌上限;节点只有 24 GB 内存且是日常办公机,若构建期间出现明显换页可临时调低。只影响本次构建') + string(name: 'NOTIFICATION_EMAILS', defaultValue: '', description: '本次运行追加邮件通知收件人;会与持久收件人凭据合并发送') } environment { GIT_REMOTE_URL = 'ssh://git@192.168.35.82:2222/GenarrativeAI/Genarrative.git' @@ -193,6 +194,57 @@ pipeline { } } post { + always { + script { + def ossDownloadUrl = '' + def releaseVersion = params.AGC_RELEASE_VERSION ?: '' + def sourceCommit = params.COMMIT_HASH ?: '' + def manifestPath = 'artifacts/latest.json' + if (currentBuild.currentResult == 'SUCCESS' && fileExists(manifestPath)) { + try { + def manifest = new groovy.json.JsonSlurperClassic().parseText(readFile(file: manifestPath)) + releaseVersion = manifest?.version?.toString()?.trim() ?: releaseVersion + if (!params.AGC_RELEASE_DRY_RUN) { + def firstDownload = manifest?.downloads instanceof Map + ? manifest.downloads.values().find { it instanceof Map && it.url?.toString()?.trim() } + : null + ossDownloadUrl = firstDownload?.url?.toString()?.trim() ?: '' + } + } catch (error) { + echo "读取 AGC OSS 下载链接失败,邮件不附加链接:${error.message}" + } + } + if (!sourceCommit && fileExists('.jenkins-source-commit')) { + sourceCommit = readFile(file: '.jenkins-source-commit').trim() + } + def notificationParameters = [ + string(name: 'SOURCE_JOB_NAME', value: env.JOB_NAME), + string(name: 'SOURCE_BUILD_NUMBER', value: env.BUILD_NUMBER), + string(name: 'SOURCE_BUILD_URL', value: env.BUILD_URL ?: ''), + string(name: 'SOURCE_RESULT', value: currentBuild.currentResult ?: 'UNKNOWN'), + string(name: 'SOURCE_BRANCH', value: params.SOURCE_BRANCH ?: ''), + string(name: 'SOURCE_COMMIT', value: sourceCommit), + string(name: 'BUILD_VERSION', value: releaseVersion), + string(name: 'DEPLOY_TARGET', value: "${params.AGC_UPDATE_CHANNEL ?: 'dev'}-mac"), + string(name: 'OSS_DOWNLOAD_URL', value: ossDownloadUrl), + string(name: 'SUMMARY', value: params.AGC_RELEASE_DRY_RUN + ? 'AGC macOS 渠道演练结束,未写入 OSS' + : 'AGC macOS 渠道构建与 OSS 发布结束'), + ] + def notificationRecipients = params.NOTIFICATION_EMAILS?.trim() + if (notificationRecipients) { + notificationParameters.add(string(name: 'EMAIL_RECIPIENTS', value: notificationRecipients)) + } + try { + build job: 'Genarrative-Notify-Email', + wait: false, + propagate: false, + parameters: notificationParameters + } catch (error) { + echo "邮件通知触发失败: ${error.message}" + } + } + } failure { echo 'macOS 发布失败:先看本构建控制台末尾。常见原因——更新包缺 .sig(误传 --no-sign 或签名私钥未注入)、DMG 幂等失败(workspace 残留同名产物)、可用空间低于 8 GiB、以及被 SKIP_IF_SUPERSEDED 之外的提交校验拒绝。' } diff --git a/jenkins/Jenkinsfile.production-notify-email b/jenkins/Jenkinsfile.production-notify-email index 38936efec..65e9bf319 100644 --- a/jenkins/Jenkinsfile.production-notify-email +++ b/jenkins/Jenkinsfile.production-notify-email @@ -21,6 +21,7 @@ pipeline { string(name: 'BUILD_VERSION', defaultValue: '', description: '发布版本号') string(name: 'DEPLOY_TARGET', defaultValue: '', description: '部署目标') string(name: 'DATABASE', defaultValue: '', description: 'SpacetimeDB database') + string(name: 'OSS_DOWNLOAD_URL', defaultValue: '', description: '客户端或其它产物的 OSS 下载链接') string(name: 'SUMMARY', defaultValue: '', description: '补充摘要') } @@ -59,6 +60,7 @@ pipeline { 发布版本: ${params.BUILD_VERSION ?: ''} 部署目标: ${params.DEPLOY_TARGET ?: ''} 数据库: ${params.DATABASE ?: ''} +OSS 下载链接: ${params.OSS_DOWNLOAD_URL?.trim() ?: '(无)'} 摘要: ${params.SUMMARY ?: ''} """ diff --git a/scripts/agc-template-library-publish.mjs b/scripts/agc-template-library-publish.mjs index aa0281caa..e74b37c3f 100644 --- a/scripts/agc-template-library-publish.mjs +++ b/scripts/agc-template-library-publish.mjs @@ -3,7 +3,7 @@ * 打包并发布 AGC 模板库到 OSS。 * * 用法: - * node scripts/agc-template-library-publish.mjs --source [--dry-run] [--prune] + * node scripts/agc-template-library-publish.mjs --source [--dry-run] * [--bucket agc-dev] [--endpoint oss-rg-china-mainland.aliyuncs.com] [--prefix templates] * [--index-out ] * @@ -13,14 +13,14 @@ * /v1//project/** 模板正文(就是解压后的项目根内容) * * 脚本按 `project/` 现场打包 `template.zip`(zip 根 == AGC 项目根),再上传 - * `v1//{template.zip,cover.*,template.json}` 与库清单 `index.json`; - * `--prune` 会删除该模板前缀下本次没有产出的旧对象(例如换了封面扩展名)。 - * 上传前完成全部校验,任何一项不合法都不发请求。 + * `v1//sha256/<内容摘要>/<文件名>` 与库清单 `index.json`。 + * 内容对象只创建、不覆盖;持有 OSS 发布锁时读取并更新库清单。 */ -import { createHash, createHmac } from 'node:crypto'; +import { createHash, createHmac, randomUUID } from 'node:crypto'; import { existsSync, readdirSync, readFileSync, writeFileSync } from 'node:fs'; import { join, resolve } from 'node:path'; +import { pathToFileURL } from 'node:url'; import { deflateRawSync } from 'node:zlib'; const SCHEMA_VERSION = 'agc-template-library.v1'; @@ -39,9 +39,10 @@ const COVER_CONTENT_TYPES = new Map([ function usage() { console.log( [ - '用法: node scripts/agc-template-library-publish.mjs --source [--dry-run] [--prune]', + '用法: node scripts/agc-template-library-publish.mjs --source [--dry-run]', ' [--bucket agc-dev] [--endpoint oss-rg-china-mainland.aliyuncs.com] [--prefix templates]', ' [--index-out ]', + ' [--only ] 只发布指定模板,保留线上其他模板', ].join('\n'), ); } @@ -56,7 +57,7 @@ function parseArgs(argv) { prefix: 'templates', indexOut: '', dryRun: false, - prune: false, + only: [], }; for (let index = 0; index < argv.length; index += 1) { const value = argv[index]; @@ -65,8 +66,12 @@ function parseArgs(argv) { else if (value === '--endpoint') args.endpoint = argv[(index += 1)] ?? ''; else if (value === '--prefix') args.prefix = argv[(index += 1)] ?? ''; else if (value === '--index-out') args.indexOut = argv[(index += 1)] ?? ''; - else if (value === '--dry-run') args.dryRun = true; - else if (value === '--prune') args.prune = true; + else if (value === '--only') { + args.only = (argv[(index += 1)] ?? '').split(','); + if (args.only.some((id) => !TEMPLATE_ID_PATTERN.test(id))) { + throw new Error('--only 必须是逗号分隔的模板 ID'); + } + } else if (value === '--dry-run') args.dryRun = true; else if (value === '--help' || value === '-h') { usage(); process.exit(0); @@ -250,7 +255,7 @@ function readMeta(templateRoot, templateId) { return meta; } -function buildLibrary(source, prefix) { +export function buildLibrary(source, prefix, only = []) { const versionRoot = join(source, 'v1'); if (!existsSync(versionRoot)) throw new Error(`源目录缺少 v1/:${versionRoot}`); @@ -258,14 +263,17 @@ function buildLibrary(source, prefix) { .filter((entry) => entry.isDirectory()) .map((entry) => entry.name) .sort((left, right) => left.localeCompare(right)); + for (const id of only) { + if (!templateIds.includes(id)) throw new Error(`源目录中不存在模板:${id}`); + } if (templateIds.length === 0) throw new Error('源目录 v1/ 下没有模板'); const updatedAt = new Date().toISOString().replace(/\.\d{3}Z$/u, 'Z'); const templates = []; const objects = []; - const managedPrefixes = []; for (const templateId of templateIds) { + if (only.length && !only.includes(templateId)) continue; const templateRoot = join(versionRoot, templateId); const meta = readMeta(templateRoot, templateId); const projectFiles = readProjectFiles(join(templateRoot, 'project')); @@ -281,9 +289,11 @@ function buildLibrary(source, prefix) { const coverName = coverNames[0]; const coverBytes = readFileSync(join(templateRoot, coverName)); - const zipKey = `${prefix}/v1/${templateId}/template.zip`; - const coverKey = `${prefix}/v1/${templateId}/${coverName}`; - const metadataKey = `${prefix}/v1/${templateId}/template.json`; + const zipHash = sha256(zipBytes); + const coverHash = sha256(coverBytes); + const objectPrefix = `${prefix}/v1/${templateId}/sha256`; + const zipKey = `${objectPrefix}/${zipHash}/template.zip`; + const coverKey = `${objectPrefix}/${coverHash}/${coverName}`; const templateMetadata = { schemaVersion: TEMPLATE_SCHEMA_VERSION, @@ -300,13 +310,13 @@ function buildLibrary(source, prefix) { zip: { key: zipKey, sizeBytes: zipBytes.length, - sha256: sha256(zipBytes), + sha256: zipHash, }, cover: { key: coverKey, width: Number.isInteger(meta.coverWidth) ? meta.coverWidth : 0, height: Number.isInteger(meta.coverHeight) ? meta.coverHeight : 0, - sha256: sha256(coverBytes), + sha256: coverHash, }, files: projectFiles.map((file) => ({ path: file.path, @@ -314,6 +324,11 @@ function buildLibrary(source, prefix) { sha256: sha256(file.bytes), })), }; + const metadataBytes = Buffer.from( + `${JSON.stringify(templateMetadata, null, 2)}\n`, + 'utf8', + ); + const metadataKey = `${objectPrefix}/${sha256(metadataBytes)}/template.json`; templates.push({ id: templateMetadata.id, @@ -336,7 +351,6 @@ function buildLibrary(source, prefix) { metadataKey, }); - managedPrefixes.push(`${prefix}/v1/${templateId}/`); objects.push( { key: zipKey, body: zipBytes, contentType: 'application/zip' }, { @@ -348,10 +362,7 @@ function buildLibrary(source, prefix) { }, { key: metadataKey, - body: Buffer.from( - `${JSON.stringify(templateMetadata, null, 2)}\n`, - 'utf8', - ), + body: metadataBytes, contentType: 'application/json', }, ); @@ -369,85 +380,464 @@ function buildLibrary(source, prefix) { body: Buffer.from(`${JSON.stringify(indexJson, null, 2)}\n`, 'utf8'), contentType: 'application/json', }); - return { objects, indexJson, managedPrefixes }; + return { objects, indexJson }; } -function createClient({ bucket, endpoint, accessKeyId, accessKeySecret }) { - function authorize(method, resourcePath, contentType) { +export function mergeLibraryIndex(existing, selected) { + if ( + existing.schemaVersion !== SCHEMA_VERSION || + existing.library !== selected.library || + existing.libraryVersion !== selected.libraryVersion || + !Array.isArray(existing.templates) || + (existing.inactiveTemplates !== undefined && + !Array.isArray(existing.inactiveTemplates)) + ) { + throw new Error('线上模板库清单契约不匹配,不能合并'); + } + const entries = new Map(); + const inactive = new Map(); + for (const entry of existing.templates) { + if (!TEMPLATE_ID_PATTERN.test(entry.id) || entries.has(entry.id)) { + throw new Error('线上模板库含非法或重复 ID,不能合并'); + } + entries.set(entry.id, entry); + } + for (const entry of existing.inactiveTemplates ?? []) { + if ( + !TEMPLATE_ID_PATTERN.test(entry.id) || + entries.has(entry.id) || + inactive.has(entry.id) + ) { + throw new Error('线上模板库含非法或重复 ID,不能合并'); + } + inactive.set(entry.id, entry); + } + for (const entry of selected.templates) { + if (inactive.has(entry.id)) inactive.set(entry.id, entry); + else entries.set(entry.id, entry); + } + return { + ...existing, + updatedAt: selected.updatedAt, + templates: [...entries.values()].sort((a, b) => a.id.localeCompare(b.id)), + inactiveTemplates: [...inactive.values()].sort((a, b) => + a.id.localeCompare(b.id), + ), + }; +} + +export function createClient({ + bucket, + endpoint, + accessKeyId, + accessKeySecret, + fetchImpl = globalThis.fetch, +}) { + async function request( + method, + key, + body, + contentType = '', + extraHeaders = {}, + subresource = '', + ) { + const headers = new Headers(extraHeaders); const date = new Date().toUTCString(); - const stringToSign = `${method}\n\n${contentType}\n${date}\n${resourcePath}`; + headers.set('Date', date); + if (contentType) headers.set('Content-Type', contentType); + const ossHeaders = [...headers.entries()] + .filter(([name]) => name.startsWith('x-oss-')) + .sort(([left], [right]) => (left < right ? -1 : left > right ? 1 : 0)) + .map(([name, value]) => `${name}:${value.trim()}\n`) + .join(''); + const suffix = subresource ? `?${subresource}` : ''; + const resource = `/${bucket}/${key}${suffix}`; + const stringToSign = `${method}\n${headers.get('Content-MD5') ?? ''}\n${headers.get('Content-Type') ?? ''}\n${date}\n${ossHeaders}${resource}`; const signature = createHmac('sha1', accessKeySecret) .update(stringToSign, 'utf8') .digest('base64'); - return { date, authorization: `OSS ${accessKeyId}:${signature}` }; - } - - async function put(key, body, contentType) { - const { date, authorization } = authorize( - 'PUT', - `/${bucket}/${key}`, - contentType, - ); - return fetch(`https://${bucket}.${endpoint}/${key}`, { - method: 'PUT', - headers: { - Date: date, - Authorization: authorization, - ...(contentType ? { 'Content-Type': contentType } : {}), - }, - body, + headers.set('Authorization', `OSS ${accessKeyId}:${signature}`); + const encodedKey = key.split('/').map(encodeURIComponent).join('/'); + return fetchImpl(`https://${bucket}.${endpoint}/${encodedKey}${suffix}`, { + method, + headers, + ...(body === undefined ? {} : { body }), + redirect: 'error', + signal: AbortSignal.timeout(30_000), }); } - async function get(key) { - const { date, authorization } = authorize('GET', `/${bucket}/${key}`, ''); - return fetch(`https://${bucket}.${endpoint}/${key}`, { - headers: { Date: date, Authorization: authorization }, - }); - } - - async function remove(key) { - const { date, authorization } = authorize( - 'DELETE', - `/${bucket}/${key}`, - '', - ); - return fetch(`https://${bucket}.${endpoint}/${key}`, { - method: 'DELETE', - headers: { Date: date, Authorization: authorization }, - }); - } - - async function listKeys(prefix) { - const { date, authorization } = authorize('GET', `/${bucket}/`, ''); - const response = await fetch( - `https://${bucket}.${endpoint}/?prefix=${encodeURIComponent(prefix)}&max-keys=1000`, - { headers: { Date: date, Authorization: authorization } }, - ); - const body = await response.text(); - if (!response.ok) - throw new Error( - `列举对象失败:HTTP ${response.status} ${body.slice(0, 200)}`, - ); - return [...body.matchAll(/([\s\S]*?)<\/Key>/gu)].map( - (match) => match[1], - ); - } - - return { put, get, remove, listKeys }; + return { + put: (key, body, contentType, headers = {}) => + request('PUT', key, body, contentType, headers), + get: (key) => request('GET', key), + remove: (key) => request('DELETE', key), + getBucketVersioning: () => + request('GET', '', undefined, '', {}, 'versioning'), + }; } +function validatePrefix(prefix) { + if ( + typeof prefix !== 'string' || + !/^[a-zA-Z0-9][a-zA-Z0-9._/-]*$/u.test(prefix) || + prefix.split('/').some((part) => !part || part === '.' || part === '..') + ) { + throw new Error('模板库对象前缀无效'); + } +} + +function validateLibraryIndex(index, prefix, label) { + if ( + !index || + index.schemaVersion !== SCHEMA_VERSION || + index.library !== 'agc-game-templates' || + index.libraryVersion !== 1 || + !Array.isArray(index.templates) + ) { + throw new Error(`${label}清单契约不匹配`); + } + const ids = new Set(); + const validHash = (hash) => + typeof hash === 'string' && /^[a-f0-9]{64}$/iu.test(hash); + if ( + index.inactiveTemplates !== undefined && + !Array.isArray(index.inactiveTemplates) + ) { + throw new Error(`${label}下架模板清单格式非法`); + } + for (const entry of [ + ...index.templates, + ...(index.inactiveTemplates ?? []), + ]) { + if ( + !entry || + typeof entry.id !== 'string' || + !TEMPLATE_ID_PATTERN.test(entry.id ?? '') || + ids.has(entry.id) || + typeof entry.templateVersion !== 'string' || + !TEMPLATE_VERSION_PATTERN.test(entry.templateVersion ?? '') || + typeof entry.title !== 'string' || + !entry.title.trim() || + !RUNTIMES.has(entry.runtime) || + !Array.isArray(entry.tags) || + entry.tags.some((tag) => typeof tag !== 'string' || !tag.trim()) || + !Number.isSafeInteger(entry.zipSizeBytes) || + entry.zipSizeBytes <= 0 || + !validHash(entry.zipSha256) || + (entry.coverSha256 !== undefined && + (typeof entry.coverSha256 !== 'string' || + (entry.coverSha256 !== '' && !validHash(entry.coverSha256)))) || + [ + 'summary', + 'engine', + 'engineVersion', + 'updatedAt', + 'entry', + 'metadataKey', + ].some( + (field) => + entry[field] !== undefined && typeof entry[field] !== 'string', + ) || + ['coverWidth', 'coverHeight'].some( + (field) => + entry[field] !== undefined && + (!Number.isInteger(entry[field]) || + entry[field] < 0 || + entry[field] > 0xffffffff), + ) + ) { + throw new Error(`${label}清单含非法或重复模板条目`); + } + const objectPrefix = `${prefix}/v1/${entry.id}/`; + for (const key of [entry.zipKey, entry.coverKey, entry.metadataKey].filter( + (key, index) => index < 2 || key, + )) { + if ( + typeof key !== 'string' || + !key.startsWith(objectPrefix) || + /[\\?#\r\n\0]/u.test(key) || + key.split('/').some((part) => !part || part === '.' || part === '..') + ) { + throw new Error(`${label}清单对象键不在模板前缀内`); + } + } + ids.add(entry.id); + } +} + +function planLibraryIndex(existing, selected, { prefix }) { + validateLibraryIndex(selected, prefix, '本地'); + if (!existing) return selected; + validateLibraryIndex(existing, prefix, '线上'); + const previous = new Map( + [...existing.templates, ...(existing.inactiveTemplates ?? [])].map( + (entry) => [entry.id, entry], + ), + ); + for (const entry of selected.templates) { + const old = previous.get(entry.id); + if ( + old?.templateVersion === entry.templateVersion && + (old.zipSha256.toLowerCase() !== entry.zipSha256.toLowerCase() || + old.zipSizeBytes !== entry.zipSizeBytes) + ) { + throw new Error( + `${entry.id}@${entry.templateVersion} 同版本 ZIP 内容或尺寸变化,请递增 templateVersion`, + ); + } + } + return mergeLibraryIndex(existing, selected); +} + +async function checkedRequest(run, label) { + try { + return await run(); + } catch { + throw new Error(`${label}失败或响应不明`); + } +} + +async function readResponseBytes(response, label) { + try { + return Buffer.from(await response.arrayBuffer()); + } catch { + throw new Error(`${label}读取失败或响应不明`); + } +} + +function parseResponseJson(bytes, label) { + try { + return JSON.parse(bytes.toString('utf8')); + } catch { + throw new Error(`${label}不是有效 JSON`); + } +} + +async function readExistingIndex(client, key) { + const response = await checkedRequest(() => client.get(key), '读取线上清单'); + if (response.status === 404) return null; + if (!response.ok) + throw new Error(`读取线上清单失败:HTTP ${response.status}`); + const index = parseResponseJson( + await readResponseBytes(response, '线上清单'), + '线上清单', + ); + if (!index || typeof index !== 'object' || Array.isArray(index)) { + throw new Error('线上清单契约不匹配'); + } + return index; +} + +async function assertBucketVersioningDisabled(client) { + const response = await checkedRequest( + () => client.getBucketVersioning(), + '检查 Bucket 版本控制', + ); + if (response.status !== 200) { + throw new Error(`无法确认 Bucket 版本控制状态:HTTP ${response.status}`); + } + const xml = (await readResponseBytes(response, 'Bucket 版本控制')) + .toString('utf8') + .trim(); + // 只接受 OSS 明确返回的空配置;Status、未知节点、声明或属性均不能当成未启用。 + const emptyConfiguration = + /^(?:<\?xml\s+version=(["'])1\.[01]\1(?:\s+encoding=(["'])(?:UTF-8|utf-8)\2)?(?:\s+standalone=(["'])(?:yes|no)\3)?\s*\?>\s*)?|>\s*<\/VersioningConfiguration>)$/u; + if (!emptyConfiguration.test(xml)) { + throw new Error('Bucket 版本控制已启用、已暂停或状态无法判定,停止发布'); + } +} + +function contentObjects(library, prefix) { + const { objects, indexJson } = library; + const indexKey = `${prefix}/index.json`; + if (!Array.isArray(objects) || objects.at(-1)?.key !== indexKey) { + throw new Error('本地发布对象必须以库清单结尾'); + } + const expected = new Set( + indexJson.templates.flatMap((entry) => [ + entry.zipKey, + entry.coverKey, + entry.metadataKey, + ]), + ); + const seen = new Set(); + const result = objects.slice(0, -1); + for (const object of result) { + if ( + !expected.has(object.key) || + seen.has(object.key) || + !Buffer.isBuffer(object.body) + ) { + throw new Error('本地发布对象缺失、重复或不在清单内'); + } + const parts = object.key.split('/'); + if (parts.at(-3) !== 'sha256' || parts.at(-2) !== sha256(object.body)) { + throw new Error('本地对象键与内容 SHA-256 不一致'); + } + seen.add(object.key); + } + if (seen.size !== expected.size) + throw new Error('本地清单引用的内容对象不完整'); + for (const entry of indexJson.templates) { + const zip = result.find((object) => object.key === entry.zipKey); + if ( + zip.body.length !== entry.zipSizeBytes || + sha256(zip.body) !== entry.zipSha256.toLowerCase() + ) { + throw new Error('本地 ZIP 与清单摘要或尺寸不一致'); + } + } + return result; +} + +async function verifyObject(client, key, expected) { + const response = await checkedRequest(() => client.get(key), '回读对象'); + if (!response.ok) throw new Error(`回读对象失败:HTTP ${response.status}`); + if (!(await readResponseBytes(response, '对象')).equals(expected)) { + throw new Error(`回读对象内容不一致:${key}`); + } +} + +async function releasePublishLock(client, lockKey, owner) { + const response = await checkedRequest( + () => client.get(lockKey), + '读取发布锁 owner', + ); + if (!response.ok) + throw new Error(`无法确认发布锁 owner:HTTP ${response.status}`); + const lock = parseResponseJson( + await readResponseBytes(response, '发布锁'), + '发布锁', + ); + if (lock?.owner !== owner) + throw new Error('发布锁 owner 已变化,不能释放其他发布者的锁'); + const removed = await checkedRequest( + () => client.remove(lockKey), + '释放发布锁', + ); + if (!removed.ok) throw new Error(`释放发布锁失败:HTTP ${removed.status}`); +} + +export async function publishLibrary( + library, + { client, prefix = 'templates', only = [], log = console.log }, +) { + validatePrefix(prefix); + validateLibraryIndex(library.indexJson, prefix, '本地'); + const objects = contentObjects(library, prefix); + const indexKey = `${prefix}/index.json`; + const lockKey = `${prefix}/.publish-lock.json`; + await assertBucketVersioningDisabled(client); + const owner = randomUUID(); + const lockBody = Buffer.from( + JSON.stringify({ owner, createdAt: new Date().toISOString() }), + ); + const acquired = await checkedRequest( + () => + client.put(lockKey, lockBody, 'application/json', { + 'x-oss-forbid-overwrite': 'true', + }), + '获取发布锁', + ); + if (acquired.status === 409) + throw new Error('模板发布锁已被占用,请等待当前发布结束后重新执行'); + if (acquired.status !== 200) + throw new Error(`获取发布锁未明确成功:HTTP ${acquired.status}`); + + let releaseAllowed = true; + let failure; + let indexJson; + try { + const existing = await readExistingIndex(client, indexKey); + indexJson = planLibraryIndex(existing, library.indexJson, { prefix, only }); + for (const object of objects) { + const response = await checkedRequest( + () => + client.put(object.key, object.body, object.contentType, { + 'x-oss-forbid-overwrite': 'true', + }), + '上传内容对象', + ); + if (!response.ok && response.status !== 409) { + throw new Error(`上传内容对象失败:HTTP ${response.status}`); + } + await verifyObject(client, object.key, object.body); + log(`verify ${object.key} (${object.body.length} B) ok`); + } + const indexBytes = Buffer.from( + `${JSON.stringify(indexJson, null, 2)}\n`, + 'utf8', + ); + // 指针写入结果不明时旧请求可能晚到,必须留锁,不能让后续发布者越过它。 + releaseAllowed = false; + const published = await checkedRequest( + () => client.put(indexKey, indexBytes, 'application/json'), + '发布清单', + ); + if (published.ok) { + releaseAllowed = true; + } else if ( + published.status >= 400 && + published.status < 500 && + published.status !== 408 + ) { + releaseAllowed = true; + throw new Error(`发布清单被拒绝:HTTP ${published.status}`); + } else { + throw new Error(`发布清单结果不明:HTTP ${published.status}`); + } + await verifyObject(client, indexKey, indexBytes); + } catch (error) { + failure = error; + } finally { + if (releaseAllowed) { + try { + await releasePublishLock(client, lockKey, owner); + } catch (error) { + failure = new Error( + failure ? `${failure.message};${error.message}` : error.message, + ); + } + } else { + failure = new Error( + `${failure?.message ?? '发布清单结果不明'};已保留本次发布锁,须确认在途请求已结束后再处理`, + ); + } + } + if (failure) throw failure; + return indexJson; +} async function main() { const args = parseArgs(process.argv.slice(2)); if (!args.source) { usage(); throw new Error('必须提供 --source'); } - const source = resolve(args.source); - const { objects, indexJson, managedPrefixes } = buildLibrary( - source, - args.prefix, - ); + validatePrefix(args.prefix); + const library = buildLibrary(resolve(args.source), args.prefix, args.only); + let indexJson; + if (args.dryRun) { + const publicClient = { + get: (key) => + fetch( + `https://${args.bucket}.${args.endpoint}/${key.split('/').map(encodeURIComponent).join('/')}`, + { redirect: 'error', signal: AbortSignal.timeout(30_000) }, + ), + }; + const existing = await readExistingIndex( + publicClient, + `${args.prefix}/index.json`, + ); + indexJson = planLibraryIndex(existing, library.indexJson, args); + } else { + const client = createClient({ ...args, ...loadAccessKeys() }); + indexJson = await publishLibrary(library, { + client, + prefix: args.prefix, + only: args.only, + }); + } if (args.indexOut) { writeFileSync( resolve(args.indexOut), @@ -465,58 +855,22 @@ async function main() { } if (args.dryRun) { console.log('dry-run:未上传。计划上传对象:'); - for (const object of objects) + for (const object of library.objects.slice(0, -1)) { console.log(` PUT ${object.key} (${object.body.length} B)`); - return; - } - - const credentials = loadAccessKeys(); - const client = createClient({ ...args, ...credentials }); - for (const object of objects) { - const response = await client.put( - object.key, - object.body, - object.contentType, - ); - if (!response.ok) { - throw new Error( - `上传失败 ${object.key}:HTTP ${response.status} ${await response.text()}`, - ); } console.log( - `PUT ${object.key} (${object.body.length} B) -> ${response.status}`, + ` PUT ${args.prefix}/index.json (${Buffer.byteLength(`${JSON.stringify(indexJson, null, 2)}\n`)} B)`, ); + } else { + console.log('模板库发布完成。'); } - - if (args.prune) { - const uploaded = new Set(objects.map((object) => object.key)); - for (const prefix of managedPrefixes) { - for (const key of await client.listKeys(prefix)) { - if (uploaded.has(key) || key === prefix) continue; - const response = await client.remove(key); - console.log(`DELETE ${key} -> ${response.status}`); - } - } - } - - const verify = await client.get(`${args.prefix}/index.json`); - if (!verify.ok) throw new Error(`回读清单失败:HTTP ${verify.status}`); - const liveIndex = JSON.parse(await verify.text()); - for (const template of liveIndex.templates) { - const zipResponse = await client.get(template.zipKey); - const zipBytes = Buffer.from(await zipResponse.arrayBuffer()); - const digest = sha256(zipBytes); - if (digest !== template.zipSha256) { - throw new Error(`回读校验失败:${template.zipKey}`); - } - console.log( - `verify ${template.zipKey} size=${zipBytes.length} sha256=${digest.slice(0, 12)}… ok`, - ); - } - console.log('模板库发布完成。'); } - -main().catch((error) => { - console.error(`[agc-template-library-publish] ${error.message}`); - process.exit(1); -}); +if ( + process.argv[1] && + import.meta.url === pathToFileURL(resolve(process.argv[1])).href +) { + main().catch((error) => { + console.error(`[agc-template-library-publish] ${error.message}`); + process.exit(1); + }); +} diff --git a/scripts/agc-template-library-publish.test.mjs b/scripts/agc-template-library-publish.test.mjs new file mode 100644 index 000000000..0bc802a13 --- /dev/null +++ b/scripts/agc-template-library-publish.test.mjs @@ -0,0 +1,718 @@ +import assert from 'node:assert/strict'; +import { spawnSync } from 'node:child_process'; +import { createHash, createHmac } from 'node:crypto'; +import { mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs'; +import { tmpdir } from 'node:os'; +import { basename, join, resolve } from 'node:path'; +import test from 'node:test'; +import { fileURLToPath, pathToFileURL } from 'node:url'; + +import JSZip from 'jszip'; + +import { + buildLibrary, + createClient, + mergeLibraryIndex, + publishLibrary, +} from './agc-template-library-publish.mjs'; + +const source = fileURLToPath( + new URL('../apps/ai-game-creator-shell/template-library/', import.meta.url), +); +const ids = [ + 'cocos-empty-2d', + 'cocos-empty-3d', + 'cocos-empty-3d-hq', + 'cocos-hello-world', +]; + +test('官方 Cocos 模板生成完整且可重复的原生项目包', async () => { + const first = buildLibrary(source, 'templates', ids); + const second = buildLibrary(source, 'templates', ids); + assert.deepEqual( + first.indexJson.templates.map((entry) => entry.id), + ids, + ); + assert.equal(first.objects.length, 13); + for (const entry of first.indexJson.templates) { + assert.equal(entry.runtime, 'cocos'); + assert.equal(entry.engineVersion, '3.8.8'); + assert.equal(entry.entry, 'package.json'); + const archive = first.objects.find( + (object) => object.key === entry.zipKey, + ).body; + assert.deepEqual( + archive, + second.objects.find((object) => object.key === entry.zipKey).body, + ); + assert.equal( + createHash('sha256').update(archive).digest('hex'), + entry.zipSha256, + ); + const zip = await JSZip.loadAsync(archive, { checkCRC32: true }); + const names = Object.keys(zip.files); + assert.ok(names.some((name) => name.startsWith('assets/'))); + assert.ok( + !names.some((name) => /^(game|library|temp|\.agent)\//u.test(name)), + ); + const pkg = JSON.parse(await zip.file('package.json').async('string')); + assert.equal(pkg.creator.version, '3.8.8'); + const metadata = JSON.parse( + first.objects.find((object) => object.key === entry.metadataKey).body, + ); + assert.equal(names.length, metadata.files.length); + for (const file of metadata.files) { + const bytes = await zip.file(file.path).async('nodebuffer'); + assert.equal(bytes.length, file.sizeBytes); + assert.equal( + createHash('sha256').update(bytes).digest('hex'), + file.sha256, + ); + } + if (entry.id === 'cocos-empty-2d') { + assert.equal( + JSON.parse( + await zip.file('profiles/v2/packages/scene.json').async('string'), + )['gizmos-infos'].is2D, + true, + ); + } + if (entry.id === 'cocos-hello-world') { + const scene = JSON.parse( + await zip.file('assets/scene/main.scene').async('string'), + ); + assert.ok(scene.some((item) => item.__type__ === 'cc.Scene')); + assert.ok(names.some((name) => name.endsWith('.FBX'))); + } + } +}); + +const digest = (bytes) => createHash('sha256').update(bytes).digest('hex'); +const indexKey = 'templates/index.json'; +const lockKey = 'templates/.publish-lock.json'; +const encode = (value) => Buffer.from(JSON.stringify(value)); + +function library(id = 'cocos-empty-2d') { + return buildLibrary(source, 'templates', [id]); +} + +function oldPublishedLibrary() { + const built = library(); + const entry = structuredClone(built.indexJson.templates[0]); + const cover = built.objects.find( + (object) => object.key === entry.coverKey, + ).body; + const oldZip = Buffer.from('previous published template'); + Object.assign(entry, { + templateVersion: '0.0.9', + zipKey: 'templates/v1/cocos-empty-2d/template.zip', + zipSizeBytes: oldZip.length, + zipSha256: digest(oldZip), + coverKey: 'templates/v1/cocos-empty-2d/cover.svg', + metadataKey: 'templates/v1/cocos-empty-2d/template.json', + }); + const indexJson = { ...built.indexJson, templates: [entry] }; + return { + indexJson, + objects: [ + { key: entry.zipKey, body: oldZip }, + { key: entry.coverKey, body: cover }, + { key: entry.metadataKey, body: encode({ id: entry.id }) }, + { key: indexKey, body: encode(indexJson) }, + ], + }; +} + +function memoryOss(initial = null, options = {}) { + const objects = new Map( + (initial?.objects ?? []).map(({ key, body }) => [key, Buffer.from(body)]), + ); + const calls = []; + const fetchImpl = async (input, init = {}) => { + const url = new URL(input); + assert.equal(url.origin, 'https://fixture-bucket.oss.example.test'); + const request = { + method: init.method ?? 'GET', + key: decodeURIComponent(url.pathname.slice(1)), + url, + headers: new Headers(init.headers), + body: init.body == null ? null : Buffer.from(init.body), + }; + calls.push(request); + const intercepted = await options.intercept?.(request, objects); + if (intercepted) return intercepted; + if (request.method === 'GET' && url.search === '?versioning') { + return new Response( + options.versioningXml ?? + '', + { status: options.versioningStatus ?? 200 }, + ); + } + if (request.method === 'GET') { + return objects.has(request.key) + ? new Response(Buffer.from(objects.get(request.key))) + : new Response('', { status: 404 }); + } + if (request.method === 'PUT') { + if ( + request.headers.get('x-oss-forbid-overwrite') === 'true' && + objects.has(request.key) + ) { + return new Response('FileAlreadyExists', { + status: 409, + }); + } + objects.set(request.key, Buffer.from(request.body)); + return new Response('', { status: 200 }); + } + if (request.method === 'DELETE') { + assert.equal(request.key, lockKey, '发布不能删除历史正文'); + objects.delete(request.key); + return new Response(null, { status: 204 }); + } + throw new Error(`unexpected request ${request.method} ${request.key}`); + }; + const client = createClient({ + bucket: 'fixture-bucket', + endpoint: 'oss.example.test', + accessKeyId: 'fixture-id', + accessKeySecret: 'fixture-secret', + fetchImpl, + }); + return { client, calls, objects }; +} + +const publish = (built, store) => + publishLibrary(built, { + client: store.client, + prefix: 'templates', + only: built.indexJson.templates.map((item) => item.id), + log: () => {}, + }); + +test('所有发布正文均使用自身字节摘要定位,重复打包复用ZIP与封面地址', () => { + const built = library(); + for (const object of built.objects.filter( + (object) => object.key !== indexKey, + )) { + assert.ok( + object.key.includes(`/sha256/${digest(object.body)}/`), + object.key, + ); + } + const again = library(); + assert.equal( + again.indexJson.templates[0].zipKey, + built.indexJson.templates[0].zipKey, + ); + assert.equal( + again.indexJson.templates[0].coverKey, + built.indexJson.templates[0].coverKey, + ); +}); + +test('正文全部校验后才发布清单,旧固定路径和未选模板保持不变', async () => { + const initial = oldPublishedLibrary(); + const store = memoryOss(initial); + const built = library('cocos-empty-3d'); + const result = await publish(built, store); + assert.deepEqual( + result.templates.map((item) => item.id), + ['cocos-empty-2d', 'cocos-empty-3d'], + ); + assert.deepEqual(result.templates[0], initial.indexJson.templates[0]); + for (const object of initial.objects.filter( + (object) => object.key !== indexKey, + )) { + assert.deepEqual(store.objects.get(object.key), object.body); + } + const commitAt = store.calls.findIndex( + (call) => call.method === 'PUT' && call.key === indexKey, + ); + for (const object of built.objects.filter( + (object) => object.key !== indexKey, + )) { + const readAt = store.calls.findIndex( + (call) => call.method === 'GET' && call.key === object.key, + ); + assert.ok(readAt >= 0 && readAt < commitAt); + } + assert.equal(store.objects.has(lockKey), false); +}); + +test('清单被明确拒绝后,旧清单仍能下载其原始ZIP且释放自己的锁', async () => { + const initial = oldPublishedLibrary(); + const store = memoryOss(initial, { + intercept: async (request) => + request.method === 'PUT' && request.key === indexKey + ? new Response('', { status: 403 }) + : undefined, + }); + await assert.rejects(publish(library(), store)); + assert.deepEqual( + store.objects.get(indexKey), + initial.objects.find((object) => object.key === indexKey).body, + ); + const entry = initial.indexJson.templates[0]; + assert.equal(digest(store.objects.get(entry.zipKey)), entry.zipSha256); + assert.equal(store.objects.has(lockKey), false); +}); + +test('正文写入中断不会改坏旧清单,允许后续发布重新取得锁', async () => { + const initial = oldPublishedLibrary(); + const built = library(); + const store = memoryOss(initial, { + intercept: async (request) => { + if ( + request.method === 'PUT' && + request.key === built.indexJson.templates[0].coverKey + ) + throw new Error('body upload interrupted'); + }, + }); + await assert.rejects(publish(built, store), /上传内容对象/u); + assert.equal( + store.calls.some((call) => call.method === 'PUT' && call.key === indexKey), + false, + ); + assert.equal( + digest(store.objects.get(initial.indexJson.templates[0].zipKey)), + initial.indexJson.templates[0].zipSha256, + ); + assert.equal(store.objects.has(lockKey), false); +}); + +test('两个真实发布编排互斥,竞争者重新执行时保留前一个发布者的新条目', async () => { + const initial = library('cocos-empty-3d-hq'); + const a = library('cocos-empty-2d'); + const b = library('cocos-empty-3d'); + let resume; + let started; + const paused = new Promise((resolve) => { + started = resolve; + }); + const continuation = new Promise((resolve) => { + resume = resolve; + }); + let pauseOnce = true; + const store = memoryOss(initial, { + intercept: async (request) => { + if ( + pauseOnce && + request.method === 'PUT' && + request.key === a.indexJson.templates[0].zipKey + ) { + pauseOnce = false; + started(); + await continuation; + } + }, + }); + const first = publish(a, store); + await paused; + try { + await assert.rejects(publish(b, store)); + assert.equal( + store.calls.some( + (call) => + call.method === 'PUT' && call.key === b.indexJson.templates[0].zipKey, + ), + false, + ); + assert.equal( + store.calls.some((call) => call.method === 'DELETE'), + false, + ); + } finally { + resume(); + } + await first; + const result = await publish(b, store); + assert.deepEqual( + result.templates.map((item) => item.id), + ['cocos-empty-2d', 'cocos-empty-3d', 'cocos-empty-3d-hq'], + ); +}); + +test('清单PUT回执丢失时保留锁,阻止迟到写入覆盖下一个发布者', async () => { + const store = memoryOss(null, { + intercept: async (request) => { + if (request.method === 'PUT' && request.key === indexKey) { + delayedCommit = () => + store.objects.set(indexKey, Buffer.from(request.body)); + throw new Error('index acknowledgement lost'); + } + }, + }); + let delayedCommit; + await assert.rejects(publish(library(), store)); + assert.equal(store.objects.has(lockKey), true); + await assert.rejects(publish(library('cocos-empty-3d'), store)); + assert.equal( + store.calls.filter((call) => call.method === 'PUT' && call.key === indexKey) + .length, + 1, + ); + assert.equal( + store.calls.some((call) => call.method === 'DELETE'), + false, + ); + delayedCommit(); + assert.deepEqual( + JSON.parse(store.objects.get(indexKey)).templates.map((item) => item.id), + ['cocos-empty-2d'], + ); +}); + +test('清单PUT返回5xx也不能立即解锁或自动重发', async () => { + const store = memoryOss(null, { + intercept: async (request) => + request.method === 'PUT' && request.key === indexKey + ? new Response('', { status: 500 }) + : undefined, + }); + await assert.rejects(publish(library(), store)); + assert.equal(store.objects.has(lockKey), true); + assert.equal( + store.calls.filter((call) => call.method === 'PUT' && call.key === indexKey) + .length, + 1, + ); +}); + +test('锁获取响应不明时不能删除可能已存在的锁', async () => { + const store = memoryOss(null, { + intercept: async (request, objects) => { + if (request.method === 'PUT' && request.key === lockKey) { + objects.set(lockKey, Buffer.from(request.body)); + throw new Error('lock acknowledgement lost'); + } + }, + }); + await assert.rejects(publish(library(), store)); + assert.equal(store.objects.has(lockKey), true); + assert.equal( + store.calls.some((call) => call.method === 'DELETE'), + false, + ); +}); + +for (const [label, options] of [ + [ + 'Enabled', + { + versioningXml: + 'Enabled', + }, + ], + [ + 'Suspended', + { + versioningXml: + 'Suspended', + }, + ], + [ + '命名空间Status', + { + versioningXml: + 'Enabled', + }, + ], + ['格式错误', { versioningXml: '' }], + ['无权限', { versioningStatus: 403 }], +]) { + test(`Bucket版本控制${label}时在任何写入前停止`, async () => { + const store = memoryOss(null, options); + await assert.rejects(publish(library(), store)); + assert.equal( + store.calls.some((call) => call.method !== 'GET'), + false, + ); + }); +} + +test('同版本改ZIP必须拒绝发布,不能让客户端复用旧缓存', async () => { + const built = library(); + const old = oldPublishedLibrary(); + old.indexJson.templates[0].templateVersion = + built.indexJson.templates[0].templateVersion; + old.objects.find((object) => object.key === indexKey).body = encode( + old.indexJson, + ); + const store = memoryOss(old); + await assert.rejects(publish(built, store), /版本/u); + assert.equal( + store.calls.some((call) => call.method === 'PUT' && call.key !== lockKey), + false, + ); + assert.equal(store.objects.has(lockKey), false); +}); + +test('更新下架模板保持下架,并保留未选中的后台条目', async () => { + const initial = library('cocos-empty-2d'); + const hidden = initial.indexJson.templates[0]; + initial.indexJson.inactiveTemplates = [hidden]; + initial.indexJson.templates = []; + initial.objects.find((object) => object.key === indexKey).body = encode( + initial.indexJson, + ); + const store = memoryOss(initial); + const result = await publish(library('cocos-empty-2d'), store); + assert.equal(result.templates.length, 0); + assert.deepEqual( + result.inactiveTemplates.map((entry) => entry.id), + ['cocos-empty-2d'], + ); + const next = await publishLibrary(library('cocos-empty-3d'), { + client: store.client, + log: () => {}, + }); + assert.deepEqual( + next.templates.map((entry) => entry.id), + ['cocos-empty-3d'], + ); + assert.deepEqual( + next.inactiveTemplates.map((entry) => entry.id), + ['cocos-empty-2d'], + ); +}); + +test('下架模板也受同版本ZIP一致性门禁保护', async () => { + const selected = library(); + const initial = oldPublishedLibrary(); + initial.indexJson.templates[0].templateVersion = + selected.indexJson.templates[0].templateVersion; + initial.indexJson.inactiveTemplates = initial.indexJson.templates; + initial.indexJson.templates = []; + initial.objects.find((object) => object.key === indexKey).body = encode( + initial.indexJson, + ); + const store = memoryOss(initial); + await assert.rejects(publish(selected, store), /版本/u); + assert.equal( + store.calls.some((call) => call.method === 'PUT' && call.key !== lockKey), + false, + ); +}); + +test('active与inactive不能出现同一个模板ID', async () => { + const initial = library(); + initial.indexJson.inactiveTemplates = structuredClone( + initial.indexJson.templates, + ); + initial.objects.find((object) => object.key === indexKey).body = encode( + initial.indexJson, + ); + const store = memoryOss(initial); + await assert.rejects(publish(library(), store), /重复/u); + assert.equal( + store.calls.some((call) => call.method === 'PUT' && call.key !== lockKey), + false, + ); +}); + +test('已有内容地址必须逐字节一致才能复用,不覆盖异常对象', async () => { + const built = library(); + const store = memoryOss(built); + await publish(built, store); + const zipKey = built.indexJson.templates[0].zipKey; + store.objects.set(zipKey, Buffer.from('corrupt existing content object')); + const committedBefore = store.calls.filter( + (call) => call.method === 'PUT' && call.key === indexKey, + ).length; + await assert.rejects(publish(built, store)); + assert.equal( + store.calls.filter((call) => call.method === 'PUT' && call.key === indexKey) + .length, + committedBefore, + ); + assert.equal( + store.objects.get(zipKey).toString(), + 'corrupt existing content object', + ); +}); + +test('锁owner变化时不删除别人的锁', async () => { + const otherOwner = encode({ + owner: 'another-publisher', + startedAt: '2026-09-19T00:00:00Z', + }); + const store = memoryOss(null, { + intercept: async (request, objects) => { + if (request.method === 'PUT' && request.key === indexKey) + objects.set(lockKey, otherOwner); + }, + }); + await assert.rejects(publish(library(), store)); + assert.deepEqual(store.objects.get(lockKey), otherOwner); + assert.equal( + store.calls.some((call) => call.method === 'DELETE'), + false, + ); +}); + +test('锁删除失败不能报告发布成功', async () => { + const store = memoryOss(null, { + intercept: async (request) => + request.method === 'DELETE' + ? new Response('', { status: 503 }) + : undefined, + }); + await assert.rejects(publish(library(), store)); + assert.equal(store.objects.has(lockKey), true); +}); + +test('OSS V1签名覆盖禁止覆盖头,版本控制子资源保留尾斜线', async () => { + const store = memoryOss(); + await store.client.getBucketVersioning(); + const versionRequest = store.calls[0]; + const sign = (text) => + `OSS fixture-id:${createHmac('sha1', 'fixture-secret').update(text).digest('base64')}`; + assert.equal( + versionRequest.headers.get('authorization'), + sign( + `GET\n\n\n${versionRequest.headers.get('date')}\n/fixture-bucket/?versioning`, + ), + ); + await store.client.put( + 'templates/signature-test', + Buffer.from('test'), + 'application/json', + { + 'X-OSS-Meta-Z': ' z ', + 'x-oss-meta-a': 'a', + 'x-oss-forbid-overwrite': 'true', + }, + ); + const request = store.calls[1]; + assert.equal( + request.headers.get('authorization'), + sign( + `PUT\n\napplication/json\n${request.headers.get('date')}\nx-oss-forbid-overwrite:true\nx-oss-meta-a:a\nx-oss-meta-z:z\n/fixture-bucket/templates/signature-test`, + ), + ); +}); + +function runOfflineCli(args) { + const directory = mkdtempSync( + join(tmpdir(), 'agc-template-publish-cli-test-'), + ); + const loader = join(directory, 'mock-fetch.mjs'); + const callsPath = join(directory, 'calls.json'); + writeFileSync(join(directory, 'index.json'), encode(library().indexJson)); + writeFileSync( + loader, + ` +import { readFileSync, writeFileSync } from 'node:fs'; +import net from 'node:net'; +import tls from 'node:tls'; +const calls = []; +const denyNetwork = () => { throw new Error('real network forbidden'); }; +net.connect = net.createConnection = tls.connect = denyNetwork; +globalThis.fetch = async (input, init = {}) => { + const method = init.method ?? 'GET'; + const headers = new Headers(init.headers); + calls.push({ method, url: String(input), authorization: headers.has('authorization') }); + if (method !== 'GET') throw new Error('dry-run mutation forbidden'); + if (!String(input).endsWith('/templates/index.json')) throw new Error('unexpected request'); + return new Response(readFileSync(new URL('./index.json', import.meta.url))); +}; +process.on('exit', () => writeFileSync(new URL('./calls.json', import.meta.url), JSON.stringify(calls))); +`, + ); + try { + const result = spawnSync( + process.execPath, + [ + '--import', + pathToFileURL(loader).href, + fileURLToPath( + new URL('./agc-template-library-publish.mjs', import.meta.url), + ), + '--source', + source, + '--only', + 'cocos-empty-2d', + ...args, + ], + { + cwd: directory, + env: { + ...process.env, + ALIYUN_OSS_ACCESS_KEY_ID: '', + ALIYUN_OSS_ACCESS_KEY_SECRET: '', + }, + encoding: 'utf8', + timeout: 20_000, + }, + ); + if (result.error) throw result.error; + return { ...result, calls: JSON.parse(readFileSync(callsPath, 'utf8')) }; + } finally { + assert.equal(resolve(directory, '..'), resolve(tmpdir())); + assert.ok(basename(directory).startsWith('agc-template-publish-cli-test-')); + rmSync(directory, { recursive: true, force: true }); + } +} + +test('真实CLI dry-run无需凭据且只读公共清单,不加锁或写入', () => { + const result = runOfflineCli(['--dry-run']); + assert.equal(result.status, 0, result.stderr); + assert.match(result.stdout, /dry-run.*未上传/u); + assert.ok(result.calls.length > 0); + assert.ok( + result.calls.every((call) => call.method === 'GET' && !call.authorization), + ); +}); + +test('真实CLI拒绝发布时清理历史对象的选项,且不发网络请求', () => { + const result = runOfflineCli(['--prune']); + assert.notEqual(result.status, 0); + assert.match(result.stderr, /未知参数/u); + assert.deepEqual(result.calls, []); +}); + +test('定向发布保留线上已有模板和库字段,只替换选中的 ID', () => { + const selected = buildLibrary(source, 'templates', ids).indexJson; + const oldEntry = { + id: 'remote-only', + zipSha256: 'remote-original', + extra: { preserve: true }, + }; + const existing = { + ...selected, + note: 'retain', + templates: [oldEntry, { id: ids[0], templateVersion: 'older' }], + }; + const snapshot = structuredClone(existing); + const merged = mergeLibraryIndex(existing, selected); + assert.deepEqual(existing, snapshot); + assert.deepEqual( + merged.templates.find((entry) => entry.id === oldEntry.id), + oldEntry, + ); + assert.deepEqual( + merged.templates.find((entry) => entry.id === ids[0]), + selected.templates[0], + ); + assert.equal(merged.templates.length, 5); + assert.equal(merged.note, 'retain'); + assert.throws( + () => + mergeLibraryIndex({ ...existing, schemaVersion: 'unknown' }, selected), + /契约/u, + ); + assert.throws( + () => + mergeLibraryIndex( + { ...existing, templates: [oldEntry, oldEntry] }, + selected, + ), + /重复/u, + ); + assert.throws( + () => buildLibrary(source, 'templates', ['missing-template']), + /不存在/u, + ); +}); diff --git a/scripts/check-production-ops-guardrails.mjs b/scripts/check-production-ops-guardrails.mjs index f6f2cf056..83270f8f7 100644 --- a/scripts/check-production-ops-guardrails.mjs +++ b/scripts/check-production-ops-guardrails.mjs @@ -1041,8 +1041,7 @@ const checks = [ { file: 'scripts/deploy/production-stdb-publish.sh', includes: '--freeze-dir', - reason: - '发布前备份默认使用 minimal 热备,避免 40G 级冷备空间门槛与停服。', + reason: '发布前备份默认使用 minimal 热备,避免 40G 级冷备空间门槛与停服。', }, { file: 'scripts/database-backup-to-oss.mjs', @@ -7718,6 +7717,14 @@ const agcPipelineContent = readFileSync( 'jenkins/Jenkinsfile.ai-game-creator-shell-build', 'utf8', ); +const agcMacosPipelineContent = readFileSync( + 'jenkins/Jenkinsfile.ai-game-creator-shell-macos-build', + 'utf8', +); +const notifyEmailPipelineContent = readFileSync( + 'jenkins/Jenkinsfile.production-notify-email', + 'utf8', +); const scheduledRevisionTriggerContent = readFileSync( 'jenkins/Jenkinsfile.scheduled-revision-trigger', 'utf8', @@ -7740,6 +7747,49 @@ for (const [file, content] of [ } } +for (const [file, content] of [ + ['jenkins/Jenkinsfile.ai-game-creator-shell-build', agcPipelineContent], + [ + 'jenkins/Jenkinsfile.ai-game-creator-shell-macos-build', + agcMacosPipelineContent, + ], +]) { + for (const [snippet, reason] of [ + [ + "build job: 'Genarrative-Notify-Email'", + '客户端打包管线必须触发统一邮件通知 Job。', + ], + [ + "string(name: 'OSS_DOWNLOAD_URL', value: ossDownloadUrl)", + '客户端打包管线必须把本次 OSS 首装包链接传给邮件通知 Job。', + ], + [ + "string(name: 'NOTIFICATION_EMAILS'", + '客户端打包管线必须支持追加邮件收件人。', + ], + ['latest.json', '客户端打包管线必须从本次生成的渠道清单读取下载链接。'], + ]) { + if (!content.includes(snippet)) { + failed = true; + console.error(`[check:production-ops] ${file} ${reason}`); + } + } +} +for (const [snippet, reason] of [ + [ + "string(name: 'OSS_DOWNLOAD_URL'", + '统一邮件通知 Job 必须接收 OSS 下载链接参数。', + ], + ['OSS 下载链接:', '统一邮件通知正文必须展示 OSS 下载链接。'], +]) { + if (!notifyEmailPipelineContent.includes(snippet)) { + failed = true; + console.error( + `[check:production-ops] Jenkinsfile.production-notify-email ${reason}`, + ); + } +} + for (const [snippet, reason] of [ ["cron('H * * * *')", '必须每小时检查一次远端版本'], ['disableConcurrentBuilds()', '必须禁止并发触发,避免同一版本重复触发下游'], diff --git a/server-rs/Cargo.lock b/server-rs/Cargo.lock index 69643a84f..08962c699 100644 --- a/server-rs/Cargo.lock +++ b/server-rs/Cargo.lock @@ -2869,6 +2869,7 @@ dependencies = [ "platform-oss", "reqwest", "serde", + "serde_json", "shared-kernel", "spacetimedb", ] diff --git a/server-rs/crates/api-server/src/admin.rs b/server-rs/crates/api-server/src/admin.rs index 8e70570f8..ee33a4373 100644 --- a/server-rs/crates/api-server/src/admin.rs +++ b/server-rs/crates/api-server/src/admin.rs @@ -2188,6 +2188,8 @@ fn admin_permission_requirement(_method: &Method, path: &str) -> AdminPermission match path { "/admin/api/me" => Authenticated, "/admin/api/agc-models" => OwnerOnly, + "/admin/api/agc-templates" => AnyTab(&["agc-templates"]), + path if path.starts_with("/admin/api/agc-templates/") => AnyTab(&["agc-templates"]), "/admin/api/dashboard" => AnyTab(&["dashboard"]), "/admin/api/overview" => AnyTab(&["overview"]), "/admin/api/external-api-keys" => AnyTab(&["tables"]), @@ -6845,6 +6847,38 @@ mod tests { )); } + #[test] + fn agc_template_routes_require_the_template_tab_permission() { + for (method, path) in [ + (Method::GET, "/admin/api/agc-templates"), + (Method::PUT, "/admin/api/agc-templates/cocos-empty-2d"), + ] { + assert!(enforce_admin_request_permission("owner", &[], &[], &method, path).is_ok()); + assert!( + enforce_admin_request_permission( + "member", + &["agc-templates".to_string()], + &[], + &method, + path, + ) + .is_ok() + ); + assert_eq!( + enforce_admin_request_permission( + "member", + &["editor-assets".to_string()], + &[], + &method, + path, + ) + .expect_err("unassigned template permission") + .status_code(), + StatusCode::FORBIDDEN + ); + } + } + #[test] fn admin_tab_permissions_cover_shared_and_sensitive_routes() { assert!( diff --git a/server-rs/crates/api-server/src/admin_templates.rs b/server-rs/crates/api-server/src/admin_templates.rs new file mode 100644 index 000000000..18116eca0 --- /dev/null +++ b/server-rs/crates/api-server/src/admin_templates.rs @@ -0,0 +1,429 @@ +use std::io::Cursor; + +use axum::{ + Json, + extract::{Extension, Path, State}, + http::{HeaderValue, StatusCode, header::CACHE_CONTROL}, + response::{IntoResponse, Response}, +}; +use base64::{Engine as _, engine::general_purpose::STANDARD}; +use module_assets::template_library::{ + TemplateCover, TemplateDomainError, TemplateEdit, list_templates, prepare_template_edit, +}; +use platform_oss::template_library::{TemplateLibraryStore, TemplateStoreError}; +use serde_json::Value; +use sha2::{Digest, Sha256}; +use shared_contracts::admin::{ + AdminAgcTemplateCoverInput, AdminAgcTemplateListResponse, AdminAgcTemplatePayload, + AdminUpdateAgcTemplateRequest, +}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + +use crate::{ + admin::AuthenticatedAdmin, api_response::json_success_body, http_error::AppError, + request_context::RequestContext, state::AppState, +}; + +const MAX_COVER_BYTES: usize = 5 * 1024 * 1024; +const MAX_DOCUMENT_BYTES: usize = 4 * 1024 * 1024; +const PUBLIC_BASE: &str = "https://agc-dev.oss-rg-china-mainland.aliyuncs.com/"; + +fn fingerprint(bytes: &[u8]) -> String { + format!("{:x}", Sha256::digest(bytes)) +} + +fn decode_index(bytes: &[u8]) -> Result { + serde_json::from_slice(bytes).map_err(|_| { + AppError::from_status(StatusCode::BAD_GATEWAY).with_message("模板库清单格式无效") + }) +} + +fn public_url(key: &str) -> String { + let mut url = reqwest::Url::parse(PUBLIC_BASE).expect("constant OSS URL"); + url.path_segments_mut() + .expect("HTTPS path") + .extend(key.split('/')); + url.to_string() +} + +fn snapshot(bytes: &[u8], writable: bool) -> Result { + let entries = list_templates(&decode_index(bytes)?).map_err(map_domain_error)?; + Ok(AdminAgcTemplateListResponse { + revision: fingerprint(bytes), + writable, + templates: entries + .into_iter() + .map(|entry| AdminAgcTemplatePayload { + id: entry.id, + title: entry.title, + summary: entry.summary, + tags: entry.tags, + runtime: entry.runtime, + engine: entry.engine, + engine_version: entry.engine_version, + template_version: entry.template_version, + enabled: entry.enabled, + cover_url: public_url(&entry.cover_key), + zip_size_bytes: entry.zip_size_bytes, + }) + .collect(), + }) +} + +fn json_snapshot(context: &RequestContext, value: AdminAgcTemplateListResponse) -> Response { + let mut response = json_success_body(Some(context), value).into_response(); + response + .headers_mut() + .insert(CACHE_CONTROL, HeaderValue::from_static("no-store")); + response +} + +pub async fn admin_list_agc_templates( + State(state): State, + Extension(context): Extension, + Extension(_admin): Extension, +) -> Result { + let store = state.template_library_store(); + let bytes = match store { + Some(store) => store.read_index().await, + None => TemplateLibraryStore::read_public_index().await, + } + .map_err(map_store_error)?; + Ok(json_snapshot(&context, snapshot(&bytes, store.is_some())?)) +} + +struct ValidatedCover { + bytes: Vec, + content_type: &'static str, + extension: &'static str, + width: u32, + height: u32, +} + +fn validate_cover(input: AdminAgcTemplateCoverInput) -> Result { + let invalid = |message| AppError::from_status(StatusCode::BAD_REQUEST).with_message(message); + if input.data_base64.len() > MAX_COVER_BYTES.div_ceil(3) * 4 { + return Err(invalid("封面不能超过 5 MiB")); + } + let bytes = STANDARD + .decode(&input.data_base64) + .map_err(|_| invalid("封面编码无效"))?; + if bytes.is_empty() || bytes.len() > MAX_COVER_BYTES { + return Err(invalid("封面内容为空或超过 5 MiB")); + } + let format = image::guess_format(&bytes).map_err(|_| invalid("封面不是有效图片"))?; + let (content_type, extension) = match format { + image::ImageFormat::Png => ("image/png", "png"), + image::ImageFormat::Jpeg => ("image/jpeg", "jpg"), + image::ImageFormat::WebP => ("image/webp", "webp"), + _ => return Err(invalid("封面仅支持 PNG、JPEG 或 WebP")), + }; + if input.content_type != content_type { + return Err(invalid("封面格式与文件内容不一致")); + } + let mut reader = image::ImageReader::with_format(Cursor::new(&bytes), format); + let mut limits = image::Limits::default(); + limits.max_image_width = Some(4096); + limits.max_image_height = Some(4096); + limits.max_alloc = Some(64 * 1024 * 1024); + reader.limits(limits); + let decoded = reader + .decode() + .map_err(|_| invalid("封面损坏或图片尺寸过大"))?; + let (width, height) = (decoded.width(), decoded.height()); + if width == 0 || height == 0 || u64::from(width) * u64::from(height) > 16_000_000 { + return Err(invalid("封面最多允许 1600 万像素")); + } + Ok(ValidatedCover { + bytes, + content_type, + extension, + width, + height, + }) +} + +fn document_bytes(value: &Value) -> Result, AppError> { + let mut bytes = serde_json::to_vec_pretty(value) + .map_err(|_| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR))?; + bytes.push(b'\n'); + if bytes.len() > MAX_DOCUMENT_BYTES { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_message("模板库元数据超过大小限制") + ); + } + Ok(bytes) +} + +fn check_revision(bytes: &[u8], expected: &str) -> Result<(), AppError> { + if expected.len() != 64 || !expected.bytes().all(|byte| byte.is_ascii_hexdigit()) { + return Err( + AppError::from_status(StatusCode::BAD_REQUEST).with_message("模板库版本标识无效") + ); + } + if fingerprint(bytes) != expected.to_ascii_lowercase() { + return Err(AppError::from_status(StatusCode::CONFLICT) + .with_code("TEMPLATE_LIBRARY_CONFLICT") + .with_message("模板库已更新,请刷新后重新编辑")); + } + Ok(()) +} + +pub async fn admin_update_agc_template( + State(state): State, + Extension(context): Extension, + Extension(_admin): Extension, + Path(id): Path, + Json(mut input): Json, +) -> Result { + let store = state.template_library_store().cloned().ok_or_else(|| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) + .with_message("模板管理未配置可用的存储凭据,当前仅支持查看") + })?; + let cover = match input.cover.take() { + Some(cover) => Some( + tokio::task::spawn_blocking(move || validate_cover(cover)) + .await + .map_err(|_| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR))??, + ), + None => None, + }; + // 接受后的有限写入由独立任务持有,HTTP 断连不能在清单 PUT 在途时提前解锁。 + let result = tokio::spawn(async move { update_template(store, id, input, cover).await }) + .await + .map_err(|_| { + AppError::from_status(StatusCode::SERVICE_UNAVAILABLE) + .with_message("模板保存结果需要核对,请刷新列表;若发布锁仍被占用请联系运维") + })??; + Ok(json_snapshot(&context, result)) +} + +async fn update_template( + store: TemplateLibraryStore, + id: String, + input: AdminUpdateAgcTemplateRequest, + cover: Option, +) -> Result { + let mut session = store + .begin_publish(uuid::Uuid::new_v4().to_string()) + .await + .map_err(map_store_error)?; + let outcome = async { + let current = session.read_index().await.map_err(map_store_error)?; + check_revision(¤t, &input.expected_revision)?; + let index = decode_index(¤t)?; + let entry = list_templates(&index) + .map_err(map_domain_error)? + .into_iter() + .find(|entry| entry.id == id) + .ok_or_else(|| { + AppError::from_status(StatusCode::NOT_FOUND).with_message("模板不存在") + })?; + let metadata_bytes = session + .read_object(&entry.metadata_key, MAX_DOCUMENT_BYTES) + .await + .map_err(map_store_error)?; + let metadata = decode_index(&metadata_bytes)?; + let cover_reference = cover.as_ref().map(|cover| { + let sha256 = fingerprint(&cover.bytes); + TemplateCover { + key: format!( + "templates/v1/{id}/sha256/{sha256}/cover.{}", + cover.extension + ), + sha256, + width: cover.width, + height: cover.height, + } + }); + let updated_at = OffsetDateTime::now_utc() + .format(&Rfc3339) + .map_err(|_| AppError::from_status(StatusCode::INTERNAL_SERVER_ERROR))?; + let mut prepared = prepare_template_edit( + index, + metadata, + &id, + TemplateEdit { + title: input.title, + summary: input.summary, + tags: input.tags, + enabled: input.enabled, + }, + cover_reference.clone(), + &updated_at, + ) + .map_err(map_domain_error)?; + let metadata_bytes = document_bytes(&prepared.metadata)?; + let metadata_key = format!( + "templates/v1/{id}/sha256/{}/template.json", + fingerprint(&metadata_bytes) + ); + prepared + .set_metadata_key(&id, &metadata_key) + .map_err(map_domain_error)?; + let next_index_bytes = document_bytes(&prepared.index)?; + if let (Some(cover), Some(reference)) = (cover, cover_reference) { + session + .put_immutable(&reference.key, cover.bytes, cover.content_type) + .await + .map_err(map_store_error)?; + } + session + .put_immutable(&metadata_key, metadata_bytes, "application/json") + .await + .map_err(map_store_error)?; + session + .commit_index(next_index_bytes.clone()) + .await + .map_err(map_store_error)?; + snapshot(&next_index_bytes, true) + } + .await; + let released = session.finish().await.map_err(map_store_error); + match (outcome, released) { + (Ok(result), Ok(())) => Ok(result), + (Err(error), Ok(())) => Err(error), + (_, Err(error)) => Err(error), + } +} + +fn map_domain_error(error: TemplateDomainError) -> AppError { + match error { + TemplateDomainError::InvalidEdit(message) => { + AppError::from_status(StatusCode::BAD_REQUEST).with_message(message) + } + TemplateDomainError::NotFound => { + AppError::from_status(StatusCode::NOT_FOUND).with_message("模板不存在") + } + _ => AppError::from_status(StatusCode::BAD_GATEWAY) + .with_message("模板库元数据无效,未进行修改"), + } +} + +fn map_store_error(error: TemplateStoreError) -> AppError { + let (status, code) = match error { + TemplateStoreError::NotFound => (StatusCode::NOT_FOUND, "TEMPLATE_LIBRARY_NOT_FOUND"), + TemplateStoreError::Busy => (StatusCode::CONFLICT, "TEMPLATE_LIBRARY_BUSY"), + TemplateStoreError::Uncertain => ( + StatusCode::SERVICE_UNAVAILABLE, + "TEMPLATE_LIBRARY_UNCERTAIN", + ), + _ => ( + StatusCode::SERVICE_UNAVAILABLE, + "TEMPLATE_LIBRARY_UNAVAILABLE", + ), + }; + AppError::from_status(status) + .with_code(code) + .with_message(error.to_string()) +} + +#[cfg(test)] +mod tests { + use super::*; + + fn png_input() -> AdminAgcTemplateCoverInput { + let image = image::DynamicImage::new_rgb8(4, 3); + let mut bytes = Cursor::new(Vec::new()); + image + .write_to(&mut bytes, image::ImageFormat::Png) + .expect("PNG"); + AdminAgcTemplateCoverInput { + content_type: "image/png".to_string(), + data_base64: STANDARD.encode(bytes.into_inner()), + } + } + + #[test] + fn template_cover_validates_actual_bytes_and_dimensions() { + let cover = validate_cover(png_input()).expect("valid PNG"); + assert_eq!((cover.width, cover.height, cover.extension), (4, 3, "png")); + let mut mismatched = png_input(); + mismatched.content_type = "image/jpeg".to_string(); + assert_eq!( + validate_cover(mismatched) + .err() + .expect("mismatch") + .status_code(), + StatusCode::BAD_REQUEST + ); + let svg = AdminAgcTemplateCoverInput { + content_type: "image/svg+xml".to_string(), + data_base64: STANDARD.encode(b""), + }; + assert!(validate_cover(svg).is_err()); + let mut truncated = png_input(); + truncated.data_base64 = STANDARD.encode([137, 80, 78, 71, 13, 10, 26, 10]); + assert!(validate_cover(truncated).is_err()); + } + + #[test] + fn template_snapshot_lists_both_groups_and_uses_trusted_cover_urls() { + let mut index: Value = serde_json::from_slice(include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../../../apps/ai-game-creator-shell/src-tauri/tests/fixtures/agc-template-library-index.json" + ))).expect("published fixture"); + let entries = index["templates"].as_array_mut().expect("templates"); + let count = entries.len(); + let hidden = entries.remove(0); + let hidden_id = hidden["id"].as_str().expect("id").to_string(); + index["inactiveTemplates"] = serde_json::json!([hidden]); + let bytes = serde_json::to_vec(&index).expect("index bytes"); + let result = snapshot(&bytes, false).expect("snapshot"); + assert_eq!(result.revision, fingerprint(&bytes)); + assert!(!result.writable); + assert_eq!(result.templates.len(), count); + assert!( + !result + .templates + .iter() + .find(|entry| entry.id == hidden_id) + .expect("hidden") + .enabled + ); + assert!( + result + .templates + .iter() + .all(|entry| entry.cover_url.starts_with(PUBLIC_BASE)) + ); + } + + #[test] + fn template_cover_size_and_dimension_limits_reject_before_publication() { + let oversized = AdminAgcTemplateCoverInput { + content_type: "image/png".to_string(), + data_base64: "A".repeat(MAX_COVER_BYTES.div_ceil(3) * 4 + 4), + }; + assert!(validate_cover(oversized).is_err()); + let mut bytes = Cursor::new(Vec::new()); + image::DynamicImage::new_rgb8(4097, 1) + .write_to(&mut bytes, image::ImageFormat::Png) + .expect("wide PNG"); + assert!( + validate_cover(AdminAgcTemplateCoverInput { + content_type: "image/png".to_string(), + data_base64: STANDARD.encode(bytes.into_inner()), + }) + .is_err() + ); + } + + #[test] + fn template_edit_requires_the_exact_snapshot_revision() { + let bytes = br#"{"templates":[]}"#; + assert!(check_revision(bytes, &fingerprint(bytes)).is_ok()); + assert_eq!( + check_revision(bytes, &"0".repeat(64)) + .expect_err("stale") + .status_code(), + StatusCode::CONFLICT + ); + assert_eq!( + check_revision(bytes, "invalid") + .expect_err("invalid") + .status_code(), + StatusCode::BAD_REQUEST + ); + } +} diff --git a/server-rs/crates/api-server/src/config.rs b/server-rs/crates/api-server/src/config.rs index 77f803958..b3ff4c366 100644 --- a/server-rs/crates/api-server/src/config.rs +++ b/server-rs/crates/api-server/src/config.rs @@ -179,6 +179,9 @@ pub struct AppConfig { pub project_snapshot_oss_endpoint: String, pub project_snapshot_oss_access_key_id: Option, pub project_snapshot_oss_access_key_secret: Option, + /// AGC 模板库独立凭据;只在两项均缺省时成套复用通用 OSS 凭据。 + pub template_library_oss_access_key_id: Option, + pub template_library_oss_access_key_secret: Option, pub spacetime_server_url: String, pub spacetime_database: String, pub spacetime_token: Option, @@ -491,6 +494,8 @@ impl Default for AppConfig { project_snapshot_oss_endpoint: DEFAULT_AGC_PROJECT_SNAPSHOT_OSS_ENDPOINT.to_string(), project_snapshot_oss_access_key_id: None, project_snapshot_oss_access_key_secret: None, + template_library_oss_access_key_id: None, + template_library_oss_access_key_secret: None, spacetime_server_url: "http://127.0.0.1:3000".to_string(), spacetime_database: "genarrative-dev".to_string(), spacetime_token: None, @@ -1156,6 +1161,11 @@ impl AppConfig { "ALIYUN_OSS_ACCESS_KEY_SECRET", ]); + config.template_library_oss_access_key_id = + read_first_non_empty_env(&["GENARRATIVE_AGC_TEMPLATE_LIBRARY_OSS_ACCESS_KEY_ID"]); + config.template_library_oss_access_key_secret = + read_first_non_empty_env(&["GENARRATIVE_AGC_TEMPLATE_LIBRARY_OSS_ACCESS_KEY_SECRET"]); + if let Some(spacetime_server_url) = read_first_non_empty_env(&["GENARRATIVE_SPACETIME_SERVER_URL"]) { diff --git a/server-rs/crates/api-server/src/main.rs b/server-rs/crates/api-server/src/main.rs index 566a5fe9a..ed69e82d2 100644 --- a/server-rs/crates/api-server/src/main.rs +++ b/server-rs/crates/api-server/src/main.rs @@ -4,6 +4,7 @@ mod admin; mod admin_accounts; mod admin_project_snapshots; mod admin_recharge; +mod admin_templates; mod agc_models; mod ai_tasks; mod aliyun_matting; diff --git a/server-rs/crates/api-server/src/modules/admin.rs b/server-rs/crates/api-server/src/modules/admin.rs index 230c0c66d..fd418e6e7 100644 --- a/server-rs/crates/api-server/src/modules/admin.rs +++ b/server-rs/crates/api-server/src/modules/admin.rs @@ -47,6 +47,15 @@ pub fn router(state: AppState) -> Router { "/admin/api/project-snapshots/{user_id}/{project_id}/download", get(crate::admin_project_snapshots::admin_download_project_snapshot), ), + ( + "/admin/api/agc-templates", + get(crate::admin_templates::admin_list_agc_templates), + ), + ( + "/admin/api/agc-templates/{id}", + axum::routing::put(crate::admin_templates::admin_update_agc_template) + .layer(axum::extract::DefaultBodyLimit::max(8 * 1024 * 1024)), + ), ( "/admin/api/agc-models", get(crate::agc_models::admin_get_agc_models) @@ -217,6 +226,8 @@ mod route_contract_tests { "/admin/api/project-snapshots/{user_id}/{project_id}/download", &["GET"], ), + ("/admin/api/agc-templates", &["GET"]), + ("/admin/api/agc-templates/{id}", &["PUT"]), ("/admin/api/agc-models", &["GET", "PUT"]), ("/admin/api/accounts", &["GET", "POST"]), ("/admin/api/accounts/{account_id}", &["PUT"]), diff --git a/server-rs/crates/api-server/src/state.rs b/server-rs/crates/api-server/src/state.rs index 52513b078..bf96f65c3 100644 --- a/server-rs/crates/api-server/src/state.rs +++ b/server-rs/crates/api-server/src/state.rs @@ -25,6 +25,7 @@ use platform_auth::{ }; use platform_llm::{LlmClient, LlmConfig, LlmError, LlmProvider, OpenAiChatTokenBudgetField}; use platform_matting::{MattingClient, MattingConfig}; +use platform_oss::template_library::TemplateLibraryStore; use platform_oss::{OssClient, OssConfig, OssError}; use platform_wechat::{WechatClient, WechatConfig, pay::WechatPayClient}; #[cfg(test)] @@ -281,6 +282,7 @@ pub struct AppStateInner { oss_client: Option, /// AGC 项目快照专用 OSS 客户端:bucket 与凭据可以独立于资源 bucket。 project_snapshot_oss_client: Option, + template_library_store: Option, #[cfg_attr(test, allow(dead_code))] auth_store: InMemoryAuthStore, /// 当前进程工作集所基于的正式认证投影版本;跨节点写入使用它做 CAS。 @@ -561,6 +563,7 @@ impl AppState { )?; let oss_client = build_oss_client(&config)?; let project_snapshot_oss_client = build_project_snapshot_oss_client(&config)?; + let template_library_store = build_template_library_store(&config); let sms_provider = SmsAuthProvider::new(SmsAuthConfig::new( SmsAuthProviderKind::parse(&config.sms_auth_provider).ok_or_else(|| { SmsProviderError::InvalidConfig("短信 provider 配置非法".to_string()) @@ -687,6 +690,7 @@ impl AppState { test_external_background_removal_enqueue: Arc::new(Mutex::new(None)), oss_client, project_snapshot_oss_client, + template_library_store, auth_store, auth_projection_version: AtomicI64::new(auth_projection_version), auth_projection_synced_revision: AtomicU64::new(initial_auth_store_revision), @@ -1374,6 +1378,10 @@ impl AppState { self.project_snapshot_oss_client.as_ref() } + pub fn template_library_store(&self) -> Option<&TemplateLibraryStore> { + self.template_library_store.as_ref() + } + pub fn password_entry_service(&self) -> &PasswordEntryService { &self.password_entry_service } @@ -2375,6 +2383,47 @@ impl AdminRuntime { /// 目标 bucket 独立于资源 bucket:专用凭据未配置时回退 `ALIYUN_OSS_*`,而 bucket 与 /// endpoint 默认指向 AGC 发行 bucket。凭据缺失或只配置一半时返回 `None`;路由层把 /// "未配置" 当作失败关闭,不写空对象也不推进客户端索引。 +fn build_template_library_store(config: &AppConfig) -> Option { + let dedicated = config.template_library_oss_access_key_id.is_some() + || config.template_library_oss_access_key_secret.is_some(); + let (id, secret) = if dedicated { + ( + config.template_library_oss_access_key_id.as_deref(), + config.template_library_oss_access_key_secret.as_deref(), + ) + } else { + ( + config.oss_access_key_id.as_deref(), + config.oss_access_key_secret.as_deref(), + ) + }; + let (Some(id), Some(secret)) = (id, secret) else { + if dedicated { + warn!("模板库独立凭据不完整,后台模板管理仅提供只读能力"); + } + return None; + }; + if id.trim().is_empty() || secret.trim().is_empty() { + return None; + } + let result = OssConfig::new( + "agc-dev".to_string(), + "oss-rg-china-mainland.aliyuncs.com".to_string(), + id.trim().to_string(), + secret.to_string(), + config.oss_read_expire_seconds, + config.oss_post_expire_seconds, + config.oss_post_max_size_bytes, + config.oss_success_action_status, + ) + .ok() + .and_then(|config| TemplateLibraryStore::new(OssClient::new(config)).ok()); + if result.is_none() { + warn!("模板库存储配置不可用,后台模板管理仅提供只读能力"); + } + result +} + fn build_project_snapshot_oss_client( config: &AppConfig, ) -> Result, AppStateInitError> { @@ -2722,6 +2771,22 @@ mod tests { use super::*; + #[test] + fn template_library_credentials_never_mix_dedicated_and_general_pairs() { + let mut config = AppConfig::default(); + assert!(build_template_library_store(&config).is_none()); + config.oss_access_key_id = Some("general-id".to_string()); + config.oss_access_key_secret = Some("general-secret".to_string()); + config.oss_bucket = Some("unrelated-assets-bucket".to_string()); + assert!(build_template_library_store(&config).is_some()); + config.template_library_oss_access_key_id = Some("dedicated-id".to_string()); + assert!(build_template_library_store(&config).is_none()); + config.template_library_oss_access_key_secret = Some("dedicated-secret".to_string()); + assert!(build_template_library_store(&config).is_some()); + config.template_library_oss_access_key_id = None; + assert!(build_template_library_store(&config).is_none()); + } + #[test] fn debug_summaries_redact_all_runtime_credentials() { const SENSITIVE_KEY_LURE: &str = "ISSUE_148_DEBUG_SECRET_LURE"; @@ -2732,6 +2797,8 @@ mod tests { editor_bgfilter_token: secret(), aliyun_matting_access_key_id: secret(), aliyun_matting_access_key_secret: secret(), + template_library_oss_access_key_id: secret(), + template_library_oss_access_key_secret: secret(), admin_username: Some("debug-admin".to_string()), admin_password: secret(), internal_api_secret: secret(), diff --git a/server-rs/crates/module-assets/Cargo.toml b/server-rs/crates/module-assets/Cargo.toml index 8522ac372..be18231f3 100644 --- a/server-rs/crates/module-assets/Cargo.toml +++ b/server-rs/crates/module-assets/Cargo.toml @@ -11,6 +11,7 @@ spacetime-types = ["dep:spacetimedb"] [dependencies] serde = { workspace = true } +serde_json = { workspace = true } reqwest = { workspace = true, features = ["rustls-tls"], optional = true } spacetimedb = { workspace = true, optional = true } platform-oss = { workspace = true, optional = true } diff --git a/server-rs/crates/module-assets/src/lib.rs b/server-rs/crates/module-assets/src/lib.rs index 410967b8f..b252a4a85 100644 --- a/server-rs/crates/module-assets/src/lib.rs +++ b/server-rs/crates/module-assets/src/lib.rs @@ -3,6 +3,7 @@ mod commands; mod domain; mod errors; mod events; +pub mod template_library; mod asset_object_core; #[cfg(feature = "server-service")] diff --git a/server-rs/crates/module-assets/src/template_library.rs b/server-rs/crates/module-assets/src/template_library.rs new file mode 100644 index 000000000..37a6d841b --- /dev/null +++ b/server-rs/crates/module-assets/src/template_library.rs @@ -0,0 +1,613 @@ +//! 模板库的纯领域投影与编辑规则;OSS 读写、图片解码和发布锁由适配器承担。 + +use std::{collections::BTreeSet, error::Error, fmt}; + +use serde_json::{Map, Value, json}; + +const GROUPS: [(&str, bool); 2] = [("templates", true), ("inactiveTemplates", false)]; + +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum TemplateDomainError { + InvalidIndex, + InvalidEdit(String), + NotFound, + InvalidMetadata, +} + +impl fmt::Display for TemplateDomainError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::InvalidIndex => formatter.write_str("模板库清单格式无效"), + Self::InvalidEdit(message) => formatter.write_str(message), + Self::NotFound => formatter.write_str("模板不存在"), + Self::InvalidMetadata => formatter.write_str("模板元数据无效或与清单不一致"), + } + } +} + +impl Error for TemplateDomainError {} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct ManagedTemplate { + pub id: String, + pub title: String, + pub summary: String, + pub runtime: String, + pub engine: String, + pub engine_version: String, + pub template_version: String, + pub zip_key: String, + pub zip_sha256: String, + pub cover_key: String, + pub cover_sha256: String, + pub metadata_key: String, + pub tags: Vec, + pub enabled: bool, + pub zip_size_bytes: u64, + pub cover_width: u32, + pub cover_height: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TemplateEdit { + pub title: String, + pub summary: String, + pub tags: Vec, + pub enabled: bool, +} + +/// 已由图片适配器验证原始格式的封面引用。 +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct TemplateCover { + pub key: String, + pub sha256: String, + pub width: u32, + pub height: u32, +} + +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct PreparedTemplateEdit { + pub index: Value, + pub metadata: Value, +} + +fn string_field<'a>(entry: &'a Value, field: &str) -> Result<&'a str, TemplateDomainError> { + entry + .get(field) + .and_then(Value::as_str) + .ok_or(TemplateDomainError::InvalidIndex) +} + +fn optional_string(entry: &Value, field: &str) -> Result { + match entry.get(field) { + None => Ok(String::new()), + Some(value) => value + .as_str() + .map(str::to_owned) + .ok_or(TemplateDomainError::InvalidIndex), + } +} + +fn optional_dimension(entry: &Value, field: &str) -> Result { + match entry.get(field) { + None => Ok(0), + Some(value) => value + .as_u64() + .and_then(|value| u32::try_from(value).ok()) + .ok_or(TemplateDomainError::InvalidIndex), + } +} + +fn valid_identifier(value: &str, maximum: usize) -> bool { + !value.is_empty() + && value.len() <= maximum + && value.as_bytes()[0].is_ascii_alphanumeric() + && value.bytes().all(|byte| { + byte.is_ascii_lowercase() || byte.is_ascii_digit() || matches!(byte, b'.' | b'_' | b'-') + }) + && !value.contains("..") +} + +fn valid_hash(value: &str) -> bool { + value.len() == 64 && value.bytes().all(|byte| byte.is_ascii_hexdigit()) +} + +fn valid_object_key(key: &str) -> bool { + key.starts_with("templates/") + && !key.contains("..") + && !key.chars().any(|value| { + value.is_control() || value.is_whitespace() || matches!(value, '\\' | '?' | '#' | '%') + }) + && key.split('/').all(|part| !part.is_empty() && part != ".") +} + +fn parse_entry(entry: &Value, enabled: bool) -> Result { + let id = string_field(entry, "id")?; + let title = string_field(entry, "title")?; + let template_version = string_field(entry, "templateVersion")?; + let runtime = string_field(entry, "runtime")?; + let zip_key = string_field(entry, "zipKey")?; + let zip_sha256 = string_field(entry, "zipSha256")?; + let cover_key = string_field(entry, "coverKey")?; + let cover_sha256 = optional_string(entry, "coverSha256")?; + let metadata_key = optional_string(entry, "metadataKey")?; + let zip_size_bytes = entry + .get("zipSizeBytes") + .and_then(Value::as_u64) + .ok_or(TemplateDomainError::InvalidIndex)?; + let tags = match entry.get("tags") { + None => Vec::new(), + Some(value) => value + .as_array() + .ok_or(TemplateDomainError::InvalidIndex)? + .iter() + .map(|tag| { + tag.as_str() + .map(str::to_owned) + .ok_or(TemplateDomainError::InvalidIndex) + }) + .collect::, _>>()?, + }; + if !valid_identifier(id, 64) + || !valid_identifier(template_version, 32) + || title.trim().is_empty() + || title.chars().count() > 120 + || !matches!(runtime, "html" | "unity" | "godot" | "cocos") + || !valid_object_key(zip_key) + || !valid_object_key(cover_key) + || (!metadata_key.is_empty() && !valid_object_key(&metadata_key)) + || !valid_hash(zip_sha256) + || (!cover_sha256.is_empty() && !valid_hash(&cover_sha256)) + || zip_size_bytes == 0 + || zip_size_bytes > 512 * 1024 * 1024 + || tags.len() > 32 + || tags.iter().any(|tag| tag.trim().is_empty()) + { + return Err(TemplateDomainError::InvalidIndex); + } + Ok(ManagedTemplate { + id: id.to_owned(), + title: title.to_owned(), + template_version: template_version.to_owned(), + runtime: runtime.to_owned(), + zip_key: zip_key.to_owned(), + zip_sha256: zip_sha256.to_owned(), + cover_key: cover_key.to_owned(), + cover_sha256, + metadata_key, + tags, + enabled, + zip_size_bytes, + summary: optional_string(entry, "summary")?, + engine: optional_string(entry, "engine")?, + engine_version: optional_string(entry, "engineVersion")?, + cover_width: optional_dimension(entry, "coverWidth")?, + cover_height: optional_dimension(entry, "coverHeight")?, + }) +} + +/// 两组共享同一 ID 命名空间;下架条目仍保留在公开清单中。 +pub fn list_templates(index: &Value) -> Result, TemplateDomainError> { + if index.get("schemaVersion").and_then(Value::as_str) != Some("agc-template-library.v1") + || index.get("library").and_then(Value::as_str) != Some("agc-game-templates") + || index.get("libraryVersion").and_then(Value::as_u64) != Some(1) + { + return Err(TemplateDomainError::InvalidIndex); + } + let mut ids = BTreeSet::new(); + let mut templates = Vec::new(); + for (group, enabled) in GROUPS { + let entries = match index.get(group) { + None if !enabled => continue, + Some(value) => value.as_array().ok_or(TemplateDomainError::InvalidIndex)?, + None => return Err(TemplateDomainError::InvalidIndex), + }; + for entry in entries { + let template = parse_entry(entry, enabled)?; + if !ids.insert(template.id.clone()) { + return Err(TemplateDomainError::InvalidIndex); + } + templates.push(template); + } + } + templates.sort_by(|left, right| left.id.cmp(&right.id)); + Ok(templates) +} + +fn normalize_edit(edit: TemplateEdit) -> Result { + let title = edit.title.trim().to_owned(); + let summary = edit.summary.trim().to_owned(); + if title.is_empty() || title.chars().count() > 80 { + return Err(TemplateDomainError::InvalidEdit( + "模板名称须为 1–80 个字符".to_owned(), + )); + } + if summary.chars().count() > 1000 { + return Err(TemplateDomainError::InvalidEdit( + "模板简介不能超过 1000 个字符".to_owned(), + )); + } + let mut seen = BTreeSet::new(); + let mut tags = Vec::new(); + for tag in edit.tags { + let tag = tag.trim().to_owned(); + if tag.is_empty() || tag.chars().count() > 32 { + return Err(TemplateDomainError::InvalidEdit( + "每个标签须为 1–32 个字符".to_owned(), + )); + } + if seen.insert(tag.clone()) { + tags.push(tag); + } + } + if tags.len() > 16 { + return Err(TemplateDomainError::InvalidEdit( + "模板标签不能超过 16 个".to_owned(), + )); + } + Ok(TemplateEdit { + title, + summary, + tags, + enabled: edit.enabled, + }) +} + +fn validate_metadata( + metadata: &Value, + template: &ManagedTemplate, +) -> Result<(), TemplateDomainError> { + if metadata.get("schemaVersion").and_then(Value::as_str) != Some("agc-template.v1") + || metadata.get("id").and_then(Value::as_str) != Some(template.id.as_str()) + || metadata.get("templateVersion").and_then(Value::as_str) + != Some(template.template_version.as_str()) + || metadata.pointer("/zip/key").and_then(Value::as_str) != Some(template.zip_key.as_str()) + || metadata.pointer("/zip/sizeBytes").and_then(Value::as_u64) + != Some(template.zip_size_bytes) + || !metadata + .pointer("/zip/sha256") + .and_then(Value::as_str) + .is_some_and(|hash| hash.eq_ignore_ascii_case(&template.zip_sha256)) + { + return Err(TemplateDomainError::InvalidMetadata); + } + Ok(()) +} + +fn set_display_fields(object: &mut Map, edit: &TemplateEdit, updated_at: &str) { + object.insert("title".to_owned(), json!(edit.title)); + object.insert("summary".to_owned(), json!(edit.summary)); + object.insert("tags".to_owned(), json!(edit.tags)); + object.insert("updatedAt".to_owned(), json!(updated_at)); +} + +/// 只产生待提交快照,保留未编辑条目、ZIP 合同与未知扩展字段。 +pub fn prepare_template_edit( + mut index: Value, + mut metadata: Value, + id: &str, + edit: TemplateEdit, + cover: Option, + updated_at: &str, +) -> Result { + let template = list_templates(&index)? + .into_iter() + .find(|entry| entry.id == id) + .ok_or(TemplateDomainError::NotFound)?; + let edit = normalize_edit(edit)?; + validate_metadata(&metadata, &template)?; + if updated_at.trim().is_empty() { + return Err(TemplateDomainError::InvalidEdit( + "模板更新时间无效".to_owned(), + )); + } + if let Some(cover) = &cover { + if !valid_object_key(&cover.key) + || !valid_hash(&cover.sha256) + || cover.width == 0 + || cover.height == 0 + || cover.width > 4096 + || cover.height > 4096 + || u64::from(cover.width) * u64::from(cover.height) > 16_000_000 + { + return Err(TemplateDomainError::InvalidEdit( + "模板封面引用或尺寸无效".to_owned(), + )); + } + } + let source = if template.enabled { + "templates" + } else { + "inactiveTemplates" + }; + let position = index[source] + .as_array() + .ok_or(TemplateDomainError::InvalidIndex)? + .iter() + .position(|entry| entry.get("id").and_then(Value::as_str) == Some(id)) + .ok_or(TemplateDomainError::NotFound)?; + let mut entry = index[source][position].clone(); + let entry_object = entry + .as_object_mut() + .ok_or(TemplateDomainError::InvalidIndex)?; + let metadata_object = metadata + .as_object_mut() + .ok_or(TemplateDomainError::InvalidMetadata)?; + set_display_fields(entry_object, &edit, updated_at); + set_display_fields(metadata_object, &edit, updated_at); + if let Some(cover) = cover { + entry_object.insert("coverKey".to_owned(), json!(cover.key)); + entry_object.insert("coverSha256".to_owned(), json!(cover.sha256)); + entry_object.insert("coverWidth".to_owned(), json!(cover.width)); + entry_object.insert("coverHeight".to_owned(), json!(cover.height)); + let metadata_cover = metadata_object + .entry("cover") + .or_insert_with(|| json!({})) + .as_object_mut() + .ok_or(TemplateDomainError::InvalidMetadata)?; + metadata_cover.insert("key".to_owned(), json!(cover.key)); + metadata_cover.insert("sha256".to_owned(), json!(cover.sha256)); + metadata_cover.insert("width".to_owned(), json!(cover.width)); + metadata_cover.insert("height".to_owned(), json!(cover.height)); + } + if template.enabled == edit.enabled { + index[source][position] = entry; + } else { + index[source] + .as_array_mut() + .ok_or(TemplateDomainError::InvalidIndex)? + .remove(position); + let destination = if edit.enabled { + "templates" + } else { + "inactiveTemplates" + }; + index + .as_object_mut() + .ok_or(TemplateDomainError::InvalidIndex)? + .entry(destination) + .or_insert_with(|| json!([])) + .as_array_mut() + .ok_or(TemplateDomainError::InvalidIndex)? + .push(entry); + } + index["updatedAt"] = json!(updated_at); + Ok(PreparedTemplateEdit { index, metadata }) +} + +impl PreparedTemplateEdit { + /// 元数据字节摘要由存储适配器计算后,回填唯一被编辑条目的对象键。 + pub fn set_metadata_key(&mut self, id: &str, key: &str) -> Result<(), TemplateDomainError> { + let template = list_templates(&self.index)? + .into_iter() + .find(|entry| entry.id == id) + .ok_or(TemplateDomainError::NotFound)?; + validate_metadata(&self.metadata, &template)?; + if !valid_object_key(key) { + return Err(TemplateDomainError::InvalidMetadata); + } + for (group, _) in GROUPS { + let Some(entries) = self.index.get_mut(group).and_then(Value::as_array_mut) else { + continue; + }; + if let Some(entry) = entries + .iter_mut() + .find(|entry| entry.get("id").and_then(Value::as_str) == Some(id)) + { + entry["metadataKey"] = json!(key); + return Ok(()); + } + } + Err(TemplateDomainError::NotFound) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn entry(id: &str) -> Value { + json!({ + "id": id, "title": "模板", "summary": "简介", "tags": ["cocos"], + "runtime": "cocos", "engine": "cocos-creator", "engineVersion": "3.8.8", + "templateVersion": "0.1.0", "entry": "package.json", "updatedAt": "old", + "zipKey": format!("templates/v1/{id}/template.zip"), "zipSizeBytes": 10, + "zipSha256": "a".repeat(64), "coverKey": format!("templates/v1/{id}/cover.svg"), + "coverSha256": "b".repeat(64), "coverWidth": 960, "coverHeight": 540, + "metadataKey": format!("templates/v1/{id}/template.json"), "extension": {"keep": true} + }) + } + + fn index() -> Value { + json!({"schemaVersion": "agc-template-library.v1", "library": "agc-game-templates", + "libraryVersion": 1, "updatedAt": "old", "extension": [1, 2], + "templates": [entry("b"), entry("a")], "inactiveTemplates": [entry("c")]}) + } + + fn metadata() -> Value { + json!({"schemaVersion": "agc-template.v1", "id": "a", "templateVersion": "0.1.0", + "title": "模板", "summary": "简介", "tags": ["cocos"], "runtime": "cocos", + "engine": "cocos-creator", "engineVersion": "3.8.8", "entry": "package.json", + "zip": {"key": "templates/v1/a/template.zip", "sizeBytes": 10, "sha256": "a".repeat(64), "extra": 1}, + "cover": {"key": "templates/v1/a/cover.svg", "sha256": "b".repeat(64), "width": 960, "height": 540, "extra": 2}, + "files": [{"path": "package.json", "sizeBytes": 10, "sha256": "c".repeat(64)}], "unknown": {"keep": true}}) + } + + fn edit(enabled: bool) -> TemplateEdit { + TemplateEdit { + title: " 新名称 ".to_owned(), + summary: " 新简介 ".to_owned(), + tags: vec![" cocos ".to_owned(), "cocos".to_owned(), "二维".to_owned()], + enabled, + } + } + + #[test] + fn template_library_edits_round_trip_active_and_inactive_without_losing_fields() { + let original = index(); + let old_metadata = metadata(); + let mut off = prepare_template_edit( + original.clone(), + old_metadata.clone(), + "a", + edit(false), + None, + "new", + ) + .unwrap(); + assert_eq!(off.index["templates"], json!([entry("b")])); + let listed = list_templates(&off.index).unwrap(); + assert_eq!( + listed + .iter() + .map(|item| item.id.as_str()) + .collect::>(), + ["a", "b", "c"] + ); + assert!(!listed[0].enabled); + assert_eq!(listed[0].tags, ["cocos", "二维"]); + assert_eq!(off.index["extension"], original["extension"]); + for field in [ + "zip", + "files", + "runtime", + "engine", + "engineVersion", + "templateVersion", + "entry", + "unknown", + ] { + assert_eq!(off.metadata[field], old_metadata[field], "{field}"); + } + let key = format!("templates/v1/a/sha256/{}/template.json", "d".repeat(64)); + assert_eq!( + off.set_metadata_key("b", &key).unwrap_err(), + TemplateDomainError::InvalidMetadata + ); + off.set_metadata_key("a", &key).unwrap(); + assert_eq!(list_templates(&off.index).unwrap()[0].metadata_key, key); + let on = + prepare_template_edit(off.index, off.metadata, "a", edit(true), None, "newer").unwrap(); + assert_eq!(on.index["inactiveTemplates"], json!([entry("c")])); + let template = list_templates(&on.index).unwrap().remove(0); + assert!(template.enabled); + assert_eq!(template.title, "新名称"); + assert_eq!(template.zip_key, "templates/v1/a/template.zip"); + assert_eq!(template.zip_sha256, "a".repeat(64)); + assert_eq!(template.template_version, "0.1.0"); + } + + #[test] + fn template_library_cover_edit_preserves_nested_metadata_extensions() { + let key = format!("templates/v1/a/sha256/{}/cover.png", "e".repeat(64)); + let prepared = prepare_template_edit( + index(), + metadata(), + "a", + edit(true), + Some(TemplateCover { + key: key.clone(), + sha256: "e".repeat(64), + width: 1024, + height: 576, + }), + "new", + ) + .unwrap(); + let template = list_templates(&prepared.index).unwrap().remove(0); + assert_eq!(template.cover_key, key); + assert_eq!((template.cover_width, template.cover_height), (1024, 576)); + assert_eq!(prepared.metadata["cover"]["extra"], 2); + assert_eq!(prepared.metadata["cover"]["key"], key); + assert_eq!(prepared.metadata["zip"], metadata()["zip"]); + } + + #[test] + fn template_library_rejects_metadata_from_another_template_or_package() { + for (pointer, value) in [ + ("/id", json!("b")), + ("/templateVersion", json!("0.2.0")), + ("/zip/key", json!("templates/v1/b/template.zip")), + ("/zip/sizeBytes", json!(11)), + ("/zip/sha256", json!("f".repeat(64))), + ("/schemaVersion", json!("unknown")), + ] { + let mut wrong = metadata(); + *wrong.pointer_mut(pointer).unwrap() = value; + assert_eq!( + prepare_template_edit(index(), wrong, "a", edit(true), None, "new").unwrap_err(), + TemplateDomainError::InvalidMetadata + ); + } + } + + #[test] + fn template_library_rejects_duplicate_ids_and_unsafe_references() { + let mut duplicate = index(); + duplicate["inactiveTemplates"] = json!([entry("a")]); + assert_eq!( + list_templates(&duplicate).unwrap_err(), + TemplateDomainError::InvalidIndex + ); + for key in [ + "other/v1/a/template.zip", + "templates/../private", + "templates/%2e%2e/private", + "templates/v1/a/file?token=secret", + ] { + let mut invalid = index(); + invalid["templates"][0]["zipKey"] = json!(key); + assert_eq!( + list_templates(&invalid).unwrap_err(), + TemplateDomainError::InvalidIndex + ); + } + let mut legacy = index(); + legacy.as_object_mut().unwrap().remove("inactiveTemplates"); + assert_eq!(list_templates(&legacy).unwrap().len(), 2); + legacy["inactiveTemplates"] = Value::Null; + assert_eq!( + list_templates(&legacy).unwrap_err(), + TemplateDomainError::InvalidIndex + ); + } + + #[test] + fn template_library_edit_limits_count_unicode_characters_and_unique_tags() { + let mut maximum = edit(true); + maximum.title = "名".repeat(80); + maximum.summary = "介".repeat(1000); + maximum.tags = (0..16).map(|number| format!("标签{number}")).collect(); + prepare_template_edit(index(), metadata(), "a", maximum.clone(), None, "new").unwrap(); + let mut invalid_edits = Vec::new(); + let mut invalid = maximum.clone(); + invalid.title.push('名'); + invalid_edits.push(invalid); + let mut invalid = maximum.clone(); + invalid.title = " ".to_owned(); + invalid_edits.push(invalid); + let mut invalid = maximum.clone(); + invalid.summary.push('介'); + invalid_edits.push(invalid); + let mut invalid = maximum.clone(); + invalid.tags.push("第十七个".to_owned()); + invalid_edits.push(invalid); + let mut invalid = maximum.clone(); + invalid.tags = vec!["标".repeat(33)]; + invalid_edits.push(invalid); + let mut invalid = maximum; + invalid.tags = vec![" ".to_owned()]; + invalid_edits.push(invalid); + for invalid in invalid_edits { + assert!(matches!( + prepare_template_edit(index(), metadata(), "a", invalid, None, "new"), + Err(TemplateDomainError::InvalidEdit(_)) + )); + } + assert_eq!( + prepare_template_edit(index(), metadata(), "missing", edit(true), None, "new") + .unwrap_err(), + TemplateDomainError::NotFound + ); + } +} diff --git a/server-rs/crates/platform-oss/src/lib.rs b/server-rs/crates/platform-oss/src/lib.rs index 7a8aabaa1..8c911a531 100644 --- a/server-rs/crates/platform-oss/src/lib.rs +++ b/server-rs/crates/platform-oss/src/lib.rs @@ -13,6 +13,7 @@ use tracing::{info, warn}; pub mod client_downloads; pub mod project_snapshots; +pub mod template_library; type HmacSha256 = Hmac; @@ -2503,14 +2504,20 @@ fn build_v4_additional_headers(headers: &BTreeMap) -> String { } fn build_canonical_query_string(params: &BTreeMap) -> String { - params + let mut encoded = params .iter() + .map(|(key, value)| (encode_url_query_value(key), encode_url_query_value(value))) + .collect::>(); + encoded.sort_by(|left, right| left.0.cmp(&right.0)); + encoded + .into_iter() + // OSS V4 的空值子资源只保留名称,例如 versioning;不套用 S3 的尾随等号。 .map(|(key, value)| { - format!( - "{}={}", - encode_url_query_value(key), - encode_url_query_value(value) - ) + if value.is_empty() { + key + } else { + format!("{key}={value}") + } }) .collect::>() .join("&") diff --git a/server-rs/crates/platform-oss/src/template_library.rs b/server-rs/crates/platform-oss/src/template_library.rs new file mode 100644 index 000000000..0d538d7d0 --- /dev/null +++ b/server-rs/crates/platform-oss/src/template_library.rs @@ -0,0 +1,1207 @@ +//! 模板清单与 CLI 共享发布锁;清单写入结果不明时保留锁,不猜测或重试提交。 + +use std::{collections::BTreeMap, fmt, time::Duration}; + +use reqwest::{Client, Method, Response, StatusCode, Url, redirect::Policy}; +use serde_json::{Value, json}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; + +use crate::{OssClient, sha256_hex, signed_request_builder}; + +const BUCKET: &str = "agc-dev"; +const ENDPOINT: &str = "oss-rg-china-mainland.aliyuncs.com"; +const PUBLIC_BASE: &str = "https://agc-dev.oss-rg-china-mainland.aliyuncs.com/"; +const INDEX_KEY: &str = "templates/index.json"; +const LOCK_KEY: &str = "templates/.publish-lock.json"; +const MAX_INDEX_BYTES: usize = 4 * 1024 * 1024; +const MAX_OBJECT_BYTES: usize = 5 * 1024 * 1024; +const MAX_CONTROL_BYTES: usize = 64 * 1024; + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum TemplateStoreError { + NotFound, + Busy, + Unavailable, + Invalid, + Transport, + Uncertain, +} + +impl fmt::Display for TemplateStoreError { + fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str(match self { + Self::NotFound => "模板库对象不存在", + Self::Busy => "模板库正在修改或发布锁归属已变化,请稍后刷新", + Self::Unavailable => "模板库存储暂不可用或不满足安全发布条件", + Self::Invalid => "模板库存储请求或对象内容无效", + Self::Transport => "模板库存储请求失败,请稍后重试", + Self::Uncertain => "模板保存或发布锁状态需要核对,已停止自动操作,请联系运维", + }) + } +} + +impl std::error::Error for TemplateStoreError {} + +#[derive(Clone)] +pub struct TemplateLibraryStore { + oss: OssClient, + http: Client, + #[cfg(test)] + test_base: Option, +} + +// 不实现 Clone/Drop:只能显式释放一次,任务取消或进程退出时留锁。 +pub struct TemplatePublishSession { + store: TemplateLibraryStore, + owner: String, + release_allowed: bool, + objects_verified: bool, +} + +impl TemplateLibraryStore { + pub fn new(oss: OssClient) -> Result { + if oss.config.bucket() != BUCKET || oss.config.endpoint() != ENDPOINT { + return Err(TemplateStoreError::Invalid); + } + Ok(Self { + oss, + http: build_http_client()?, + #[cfg(test)] + test_base: None, + }) + } + + pub async fn read_index(&self) -> Result, TemplateStoreError> { + self.read_key(INDEX_KEY, MAX_INDEX_BYTES).await + } + + pub async fn read_public_index() -> Result, TemplateStoreError> { + let http = build_http_client()?; + let url = Url::parse(&format!("{PUBLIC_BASE}{INDEX_KEY}")) + .map_err(|_| TemplateStoreError::Invalid)?; + read_public_index_at(&http, url).await + } + + pub async fn begin_publish( + &self, + owner: String, + ) -> Result { + if owner.is_empty() + || owner.len() > 128 + || !owner + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || byte == b'-') + { + return Err(TemplateStoreError::Invalid); + } + let response = self + .request( + Method::GET, + None, + Some("versioning"), + None, + None, + "no-cache", + false, + ) + .await?; + if response.status() != StatusCode::OK { + return Err(TemplateStoreError::Unavailable); + } + let versioning = read_bytes(response, MAX_CONTROL_BYTES).await?; + if !is_unversioned_configuration(&versioning) { + return Err(TemplateStoreError::Unavailable); + } + let created_at = OffsetDateTime::now_utc() + .format(&Rfc3339) + .map_err(|_| TemplateStoreError::Invalid)?; + let body = serde_json::to_vec(&json!({ "owner": owner, "createdAt": created_at })) + .map_err(|_| TemplateStoreError::Invalid)?; + let acquired = self + .request( + Method::PUT, + Some(LOCK_KEY), + None, + Some(body), + Some("application/json"), + "no-store", + true, + ) + .await + .map_err(|_| TemplateStoreError::Uncertain)?; + match acquired.status() { + StatusCode::OK => Ok(TemplatePublishSession { + store: self.clone(), + owner, + release_allowed: true, + objects_verified: true, + }), + StatusCode::CONFLICT => Err(TemplateStoreError::Busy), + status if is_certain_rejection(status) => Err(TemplateStoreError::Unavailable), + _ => Err(TemplateStoreError::Uncertain), + } + } + + async fn read_key(&self, key: &str, max_bytes: usize) -> Result, TemplateStoreError> { + let response = self + .request(Method::GET, Some(key), None, None, None, "no-cache", false) + .await?; + read_success_bytes(response, max_bytes).await + } + + #[allow(clippy::too_many_arguments)] + async fn request( + &self, + method: Method, + key: Option<&str>, + query: Option<&str>, + body: Option>, + content_type: Option<&str>, + cache_control: &str, + create_only: bool, + ) -> Result { + let mut url = self.target_url(key)?; + url.set_query(query); + let mut headers = + BTreeMap::from([("cache-control".to_string(), cache_control.to_string())]); + if create_only { + headers.insert("x-oss-forbid-overwrite".to_string(), "true".to_string()); + } + let mut request = signed_request_builder( + &self.http, + &self.oss.config, + method, + key, + url, + content_type, + &headers, + ) + .map_err(|_| TemplateStoreError::Invalid)?; + if let Some(body) = body { + request = request + .header(reqwest::header::CONTENT_LENGTH, body.len()) + .body(body); + } + request + .send() + .await + .map_err(|_| TemplateStoreError::Transport) + } + + fn target_url(&self, key: Option<&str>) -> Result { + #[cfg(test)] + let base = self + .test_base + .as_ref() + .map(Url::as_str) + .unwrap_or(PUBLIC_BASE); + #[cfg(not(test))] + let base = PUBLIC_BASE; + let mut url = Url::parse(base).map_err(|_| TemplateStoreError::Invalid)?; + if let Some(key) = key { + url.path_segments_mut() + .map_err(|_| TemplateStoreError::Invalid)? + .clear() + .extend(key.split('/')); + } + Ok(url) + } +} + +impl TemplatePublishSession { + pub async fn read_index(&self) -> Result, TemplateStoreError> { + self.store.read_index().await + } + + pub async fn read_object( + &self, + key: &str, + max_bytes: usize, + ) -> Result, TemplateStoreError> { + validate_content_key(key)?; + if max_bytes == 0 || max_bytes > MAX_OBJECT_BYTES { + return Err(TemplateStoreError::Invalid); + } + self.store.read_key(key, max_bytes).await + } + + pub async fn put_immutable( + &mut self, + key: &str, + bytes: Vec, + content_type: &str, + ) -> Result<(), TemplateStoreError> { + if !self.release_allowed { + return Err(TemplateStoreError::Uncertain); + } + if !self.objects_verified { + return Err(TemplateStoreError::Invalid); + } + validate_content_key(key)?; + let parts: Vec<_> = key.split('/').collect(); + let max_bytes = match content_type { + "application/json" => MAX_INDEX_BYTES, + "image/png" | "image/jpeg" | "image/webp" => MAX_OBJECT_BYTES, + _ => return Err(TemplateStoreError::Invalid), + }; + if bytes.is_empty() + || bytes.len() > max_bytes + || parts.len() != 6 + || parts[3] != "sha256" + || parts[4] != sha256_hex(&bytes) + { + return Err(TemplateStoreError::Invalid); + } + self.objects_verified = false; + let response = self + .store + .request( + Method::PUT, + Some(key), + None, + Some(bytes.clone()), + Some(content_type), + "public, max-age=31536000, immutable", + true, + ) + .await?; + if !response.status().is_success() && response.status() != StatusCode::CONFLICT { + return Err(TemplateStoreError::Unavailable); + } + let existing = self.store.read_key(key, max_bytes).await?; + if existing != bytes { + return Err(TemplateStoreError::Invalid); + } + self.objects_verified = true; + Ok(()) + } + + pub async fn commit_index(&mut self, bytes: Vec) -> Result<(), TemplateStoreError> { + if !self.release_allowed { + return Err(TemplateStoreError::Uncertain); + } + if !self.objects_verified || bytes.is_empty() || bytes.len() > MAX_INDEX_BYTES { + return Err(TemplateStoreError::Invalid); + } + self.release_allowed = false; + let response = self + .store + .request( + Method::PUT, + Some(INDEX_KEY), + None, + Some(bytes.clone()), + Some("application/json"), + "no-store", + false, + ) + .await + .map_err(|_| TemplateStoreError::Uncertain)?; + if response.status().is_success() { + self.release_allowed = true; + } else if is_certain_rejection(response.status()) { + self.release_allowed = true; + return Err(TemplateStoreError::Unavailable); + } else { + return Err(TemplateStoreError::Uncertain); + } + if self.store.read_index().await? != bytes { + return Err(TemplateStoreError::Invalid); + } + Ok(()) + } + + pub async fn finish(self) -> Result<(), TemplateStoreError> { + if !self.release_allowed { + return Err(TemplateStoreError::Uncertain); + } + let bytes = self.store.read_key(LOCK_KEY, MAX_CONTROL_BYTES).await?; + let lock: Value = + serde_json::from_slice(&bytes).map_err(|_| TemplateStoreError::Invalid)?; + if lock.get("owner").and_then(Value::as_str) != Some(self.owner.as_str()) { + return Err(TemplateStoreError::Busy); + } + let response = self + .store + .request( + Method::DELETE, + Some(LOCK_KEY), + None, + None, + None, + "no-store", + false, + ) + .await + .map_err(|_| TemplateStoreError::Uncertain)?; + if response.status().is_success() { + Ok(()) + } else if is_certain_rejection(response.status()) { + Err(TemplateStoreError::Unavailable) + } else { + Err(TemplateStoreError::Uncertain) + } + } +} + +fn build_http_client() -> Result { + Client::builder() + .redirect(Policy::none()) + .retry(reqwest::retry::never()) + .timeout(Duration::from_secs(30)) + .build() + .map_err(|_| TemplateStoreError::Unavailable) +} + +async fn read_public_index_at(http: &Client, url: Url) -> Result, TemplateStoreError> { + let response = http + .get(url) + .header(reqwest::header::CACHE_CONTROL, "no-cache") + .send() + .await + .map_err(|_| TemplateStoreError::Transport)?; + read_success_bytes(response, MAX_INDEX_BYTES).await +} + +async fn read_success_bytes( + response: Response, + max_bytes: usize, +) -> Result, TemplateStoreError> { + match response.status() { + StatusCode::OK => read_bytes(response, max_bytes).await, + StatusCode::NOT_FOUND => Err(TemplateStoreError::NotFound), + _ => Err(TemplateStoreError::Unavailable), + } +} + +async fn read_bytes( + mut response: Response, + max_bytes: usize, +) -> Result, TemplateStoreError> { + if response + .content_length() + .is_some_and(|length| length > max_bytes as u64) + { + return Err(TemplateStoreError::Invalid); + } + let mut bytes = Vec::new(); + while let Some(chunk) = response + .chunk() + .await + .map_err(|_| TemplateStoreError::Transport)? + { + if chunk.len() > max_bytes - bytes.len() { + return Err(TemplateStoreError::Invalid); + } + bytes.extend_from_slice(&chunk); + } + Ok(bytes) +} + +fn is_certain_rejection(status: StatusCode) -> bool { + status.is_client_error() && status != StatusCode::REQUEST_TIMEOUT +} + +fn validate_content_key(key: &str) -> Result<(), TemplateStoreError> { + if !key.starts_with("templates/v1/") + || key.len() > 1024 + || key.chars().any(|ch| { + ch.is_control() || ch.is_whitespace() || matches!(ch, '\\' | ':' | '%' | '?' | '#') + }) + || key + .split('/') + .any(|part| part.is_empty() || part == "." || part == "..") + || key.split('/').count() < 4 + { + return Err(TemplateStoreError::Invalid); + } + Ok(()) +} + +// 只接受官方空配置的 XML 子集;未知元素、状态、命名空间或畸形 XML 均失败关闭。 +fn is_unversioned_configuration(bytes: &[u8]) -> bool { + let Ok(text) = std::str::from_utf8(bytes) else { + return false; + }; + let mut text = text.trim_start_matches('\u{feff}').trim(); + if let Some(declaration) = text.strip_prefix("") else { + return false; + }; + let Some(attributes) = xml_attributes(attributes) else { + return false; + }; + if attributes.get("version").map(String::as_str) != Some("1.0") + || attributes.iter().any(|(key, value)| match key.as_str() { + "version" => false, + "encoding" => !value.eq_ignore_ascii_case("UTF-8"), + "standalone" => !matches!(value.as_str(), "yes" | "no"), + _ => true, + }) + { + return false; + } + text = remaining.trim(); + } + let Some(root) = text.strip_prefix("') else { + return false; + }; + let (attributes, empty) = attributes + .strip_suffix('/') + .map_or((attributes, false), |value| (value, true)); + let Some(attributes) = xml_attributes(attributes) else { + return false; + }; + if attributes + .iter() + .any(|(key, value)| key != "xmlns" || value != "http://doc.oss-cn-hangzhou.aliyuncs.com") + { + return false; + } + if empty { + remaining.trim().is_empty() + } else { + remaining.trim() == "" + } +} + +fn xml_attributes(mut text: &str) -> Option> { + let mut result = BTreeMap::new(); + while !text.is_empty() { + if !text.starts_with(char::is_whitespace) { + return None; + } + text = text.trim_start(); + if text.is_empty() { + break; + } + let name_end = text.find(|ch: char| !ch.is_ascii_alphanumeric() && ch != '-')?; + let (name, remaining) = text.split_at(name_end); + if name.is_empty() { + return None; + } + let remaining = remaining.trim_start().strip_prefix('=')?.trim_start(); + let quote = remaining.chars().next()?; + if !matches!(quote, '\'' | '"') { + return None; + } + let (value, rest) = remaining[1..].split_once(quote)?; + if value.contains(['<', '>', '&']) + || result.insert(name.to_string(), value.to_string()).is_some() + { + return None; + } + text = rest; + } + Some(result) +} + +#[cfg(test)] +mod tests { + use super::*; + use hmac::{Hmac, Mac}; + use sha2::{Digest, Sha256}; + use std::sync::{Arc, Mutex}; + use tokio::{ + io::{AsyncReadExt, AsyncWriteExt}, + net::{TcpListener, TcpStream}, + task::JoinHandle, + }; + + #[derive(Clone)] + struct RecordedRequest { + method: String, + target: String, + headers: BTreeMap, + body: Vec, + } + + struct MockState { + objects: BTreeMap>, + requests: Vec, + versioning_status: u16, + versioning_body: Vec, + index_status: Option, + disconnect_index: bool, + disconnect_acquire: bool, + delete_status: Option, + chunked_reads: bool, + } + + impl Default for MockState { + fn default() -> Self { + Self { + objects: BTreeMap::from([(INDEX_KEY.to_string(), b"{\"templates\":[]}".to_vec())]), + requests: Vec::new(), + versioning_status: 200, + versioning_body: + br#""# + .to_vec(), + index_status: None, + disconnect_index: false, + disconnect_acquire: false, + delete_status: None, + chunked_reads: false, + } + } + } + + struct MockServer { + store: TemplateLibraryStore, + state: Arc>, + task: JoinHandle<()>, + } + + impl Drop for MockServer { + fn drop(&mut self) { + self.task.abort(); + } + } + + fn test_oss(bucket: &str) -> OssClient { + OssClient::new( + crate::OssConfig::new( + bucket.to_string(), + ENDPOINT.to_string(), + "test-id".to_string(), + "test-secret".to_string(), + 600, + 600, + 20 * 1024 * 1024, + 200, + ) + .unwrap(), + ) + } + + impl MockServer { + async fn start() -> Self { + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let base = Url::parse(&format!("http://{}/", listener.local_addr().unwrap())).unwrap(); + let mut store = TemplateLibraryStore::new(test_oss(BUCKET)).unwrap(); + store.test_base = Some(base); + store.http = Client::builder() + .no_proxy() + .redirect(Policy::none()) + .retry(reqwest::retry::never()) + .timeout(Duration::from_secs(2)) + .build() + .unwrap(); + let state = Arc::new(Mutex::new(MockState::default())); + let server_state = state.clone(); + let task = tokio::spawn(async move { + loop { + let Ok((socket, _)) = listener.accept().await else { + break; + }; + let state = server_state.clone(); + tokio::spawn(serve_connection(socket, state)); + } + }); + Self { store, state, task } + } + + fn count(&self, method: &str, key: &str) -> usize { + self.state + .lock() + .unwrap() + .requests + .iter() + .filter(|request| request.method == method && request.target == format!("/{key}")) + .count() + } + } + + async fn read_request(socket: &mut TcpStream) -> Option { + let mut bytes = Vec::new(); + let header_end = loop { + if let Some(position) = bytes.windows(4).position(|window| window == b"\r\n\r\n") { + break position; + } + let mut buffer = [0; 8192]; + let read = socket.read(&mut buffer).await.ok()?; + if read == 0 { + return None; + } + bytes.extend_from_slice(&buffer[..read]); + }; + let head = std::str::from_utf8(&bytes[..header_end]).ok()?; + let mut lines = head.split("\r\n"); + let mut request_line = lines.next()?.split_whitespace(); + let method = request_line.next()?.to_string(); + let target = request_line.next()?.to_string(); + let headers: BTreeMap<_, _> = lines + .filter_map(|line| { + let (name, value) = line.split_once(':')?; + Some((name.to_ascii_lowercase(), value.trim().to_string())) + }) + .collect(); + let length = headers + .get("content-length") + .and_then(|value| value.parse::().ok()) + .unwrap_or(0); + if length > MAX_OBJECT_BYTES + 1024 { + return None; + } + let start = header_end + 4; + while bytes.len() - start < length { + let mut buffer = [0; 8192]; + let read = socket.read(&mut buffer).await.ok()?; + if read == 0 { + return None; + } + bytes.extend_from_slice(&buffer[..read]); + } + Some(RecordedRequest { + method, + target, + headers, + body: bytes[start..start + length].to_vec(), + }) + } + + fn mock_response( + state: &mut MockState, + request: RecordedRequest, + ) -> Option<(u16, Vec, bool)> { + state.requests.push(request.clone()); + if request.target == "/?versioning" { + return Some(( + state.versioning_status, + state.versioning_body.clone(), + false, + )); + } + let key = request.target.trim_start_matches('/'); + match request.method.as_str() { + "GET" => Some(match state.objects.get(key) { + Some(bytes) => (200, bytes.clone(), state.chunked_reads), + None => (404, b"private upstream details".to_vec(), false), + }), + "PUT" => { + if request + .headers + .get("x-oss-forbid-overwrite") + .map(String::as_str) + == Some("true") + && state.objects.contains_key(key) + { + return Some((409, b"FileAlreadyExists".to_vec(), false)); + } + if key == INDEX_KEY + && let Some(status) = state.index_status + { + return Some((status, b"private upstream details".to_vec(), false)); + } + state.objects.insert(key.to_string(), request.body); + if (key == INDEX_KEY && state.disconnect_index) + || (key == LOCK_KEY && state.disconnect_acquire) + { + return None; + } + Some((200, Vec::new(), false)) + } + "DELETE" => { + if let Some(status) = state.delete_status { + return Some((status, b"private upstream details".to_vec(), false)); + } + state.objects.remove(key); + Some((204, Vec::new(), false)) + } + _ => Some((405, Vec::new(), false)), + } + } + + async fn serve_connection(mut socket: TcpStream, state: Arc>) { + let Some(request) = read_request(&mut socket).await else { + return; + }; + let response = { mock_response(&mut state.lock().unwrap(), request) }; + let Some((status, body, chunked)) = response else { + return; + }; + let mut bytes = if chunked { + format!("HTTP/1.1 {status} Test\r\nTransfer-Encoding: chunked\r\nConnection: close\r\n\r\n{:x}\r\n", body.len()).into_bytes() + } else { + format!("HTTP/1.1 {status} Test\r\nContent-Length: {}\r\nLocation: /redirected\r\nConnection: close\r\n\r\n", body.len()).into_bytes() + }; + bytes.extend_from_slice(&body); + if chunked { + bytes.extend_from_slice(b"\r\n0\r\n\r\n"); + } + let _ = socket.write_all(&bytes).await; + } + + fn content_key(bytes: &[u8]) -> String { + format!( + "templates/v1/demo/sha256/{}/template.json", + sha256_hex(bytes) + ) + } + + #[test] + fn fixed_target_and_strict_unversioned_xml_fail_closed() { + assert!(TemplateLibraryStore::new(test_oss("resource-bucket")).is_err()); + for valid in [ + "", + "", + "\n", + ] { + assert!(is_unversioned_configuration(valid.as_bytes()), "{valid}"); + } + for invalid in [ + "", + "", + "Enabled", + "Suspended", + "", + "", + "", + "", + "", + "", + "", + "", + ] { + assert!( + !is_unversioned_configuration(invalid.as_bytes()), + "{invalid}" + ); + } + } + + #[test] + fn v4_signs_versioning_query_and_create_only_header() { + let client = build_http_client().unwrap(); + let oss = test_oss(BUCKET); + for (method, key, query, content_type, create_only) in [ + (Method::GET, None, Some("versioning"), None, false), + ( + Method::PUT, + Some(LOCK_KEY), + None, + Some("application/json"), + true, + ), + ] { + let mut url = Url::parse(PUBLIC_BASE).unwrap(); + url.set_path(key.unwrap_or("")); + url.set_query(query); + let mut headers = + BTreeMap::from([("cache-control".to_string(), "no-store".to_string())]); + if create_only { + headers.insert("x-oss-forbid-overwrite".to_string(), "true".to_string()); + } + let request = signed_request_builder( + &client, + &oss.config, + method.clone(), + key, + url, + content_type, + &headers, + ) + .unwrap() + .build() + .unwrap(); + let signed_at = request.headers()["x-oss-date"].to_str().unwrap(); + let authorization = request.headers()["authorization"].to_str().unwrap(); + let scope = authorization + .split("Credential=test-id/") + .nth(1) + .unwrap() + .split(',') + .next() + .unwrap(); + let uri = key.map_or("/agc-dev/".to_string(), |key| format!("/agc-dev/{key}")); + let query = if query.is_some() { "versioning" } else { "" }; + let content_header = + content_type.map_or(String::new(), |value| format!("content-type:{value}\n")); + let condition_header = if create_only { + "x-oss-forbid-overwrite:true\n" + } else { + "" + }; + let canonical = format!( + "{method}\n{uri}\n{query}\ncache-control:no-store\n{content_header}host:agc-dev.oss-rg-china-mainland.aliyuncs.com\nx-oss-content-sha256:UNSIGNED-PAYLOAD\nx-oss-date:{signed_at}\n{condition_header}\ncache-control;host\nUNSIGNED-PAYLOAD" + ); + // 独立按官方 Python SDK oss2/auth.py 的 V4 算法计算,不调用生产签名 helper。 + let string_to_sign = format!( + "OSS4-HMAC-SHA256\n{signed_at}\n{scope}\n{:x}", + Sha256::digest(canonical.as_bytes()) + ); + let hmac = |key: &[u8], message: &[u8]| { + let mut mac = Hmac::::new_from_slice(key).unwrap(); + mac.update(message); + mac.finalize().into_bytes().to_vec() + }; + let mut signing_key = b"aliyun_v4test-secret".to_vec(); + for part in scope.split('/') { + signing_key = hmac(&signing_key, part.as_bytes()); + } + let signature = hmac(&signing_key, string_to_sign.as_bytes()) + .iter() + .map(|byte| format!("{byte:02x}")) + .collect::(); + assert_eq!( + authorization, + format!( + "OSS4-HMAC-SHA256 Credential=test-id/{scope},AdditionalHeaders=cache-control;host,Signature={signature}" + ) + ); + if create_only { + assert_eq!(request.headers()["x-oss-forbid-overwrite"], "true"); + } + } + } + + #[tokio::test] + async fn publish_roundtrip_uses_shared_owner_and_verifies_before_committing() { + let server = MockServer::start().await; + let legacy = "templates/v1/demo/template.json"; + server + .state + .lock() + .unwrap() + .objects + .insert(legacy.to_string(), b"legacy metadata".to_vec()); + let mut session = server + .store + .begin_publish("rust-owner".to_string()) + .await + .unwrap(); + assert_eq!( + session.read_object(legacy, MAX_INDEX_BYTES).await.unwrap(), + b"legacy metadata" + ); + let metadata = b"{\"title\":\"new title\"}".to_vec(); + let key = content_key(&metadata); + session + .put_immutable(&key, metadata.clone(), "application/json") + .await + .unwrap(); + let index = b"{\"templates\":[],\"inactiveTemplates\":[]}".to_vec(); + session.commit_index(index.clone()).await.unwrap(); + session.finish().await.unwrap(); + let state = server.state.lock().unwrap(); + assert_eq!(state.objects[INDEX_KEY], index); + assert_eq!(state.objects[&key], metadata); + assert!(!state.objects.contains_key(LOCK_KEY)); + let acquired = state + .requests + .iter() + .find(|r| r.method == "PUT" && r.target == format!("/{LOCK_KEY}")) + .unwrap(); + let owner: Value = serde_json::from_slice(&acquired.body).unwrap(); + assert_eq!(owner["owner"], "rust-owner"); + assert!(owner["createdAt"].as_str().unwrap().ends_with('Z')); + assert_eq!(acquired.headers["x-oss-forbid-overwrite"], "true"); + assert_eq!(acquired.headers["cache-control"], "no-store"); + let upload = state + .requests + .iter() + .find(|r| r.method == "PUT" && r.target == format!("/{key}")) + .unwrap(); + assert_eq!( + upload.headers["cache-control"], + "public, max-age=31536000, immutable" + ); + let commit = state + .requests + .iter() + .position(|r| r.method == "PUT" && r.target == format!("/{INDEX_KEY}")) + .unwrap(); + let verified = state + .requests + .iter() + .position(|r| r.method == "GET" && r.target == format!("/{key}")) + .unwrap(); + assert!(verified < commit); + assert_eq!(state.requests[commit].headers["cache-control"], "no-store"); + assert_eq!( + state + .requests + .iter() + .filter(|r| r.method == "DELETE") + .count(), + 1 + ); + } + + #[tokio::test] + async fn unsafe_or_unknown_versioning_never_sends_a_put() { + for (status, xml) in [ + ( + 200, + "Enabled", + ), + ( + 200, + "Suspended", + ), + (200, "unknown"), + (200, ""), + (403, "private access details"), + (302, ""), + (500, ""), + ] { + let server = MockServer::start().await; + { + let mut state = server.state.lock().unwrap(); + state.versioning_status = status; + state.versioning_body = xml.as_bytes().to_vec(); + } + assert!(matches!( + server.store.begin_publish("owner".to_string()).await, + Err(TemplateStoreError::Unavailable) + )); + assert!( + server + .state + .lock() + .unwrap() + .requests + .iter() + .all(|r| r.method == "GET") + ); + assert_eq!(server.state.lock().unwrap().requests.len(), 1); + } + } + + #[tokio::test] + async fn javascript_owner_lock_is_preserved_and_publishers_are_mutually_exclusive() { + let server = MockServer::start().await; + let js_lock = + br#"{"owner":"js-cli-owner","createdAt":"2026-09-19T00:00:00.000Z"}"#.to_vec(); + server + .state + .lock() + .unwrap() + .objects + .insert(LOCK_KEY.to_string(), js_lock.clone()); + assert!(matches!( + server.store.begin_publish("rust-owner".to_string()).await, + Err(TemplateStoreError::Busy) + )); + assert_eq!(server.state.lock().unwrap().objects[LOCK_KEY], js_lock); + assert_eq!(server.count("DELETE", LOCK_KEY), 0); + server.state.lock().unwrap().objects.remove(LOCK_KEY); + let (first, second) = tokio::join!( + server.store.begin_publish("publisher-one".to_string()), + server.store.begin_publish("publisher-two".to_string()), + ); + let mut winner = match (first, second) { + (Ok(session), Err(TemplateStoreError::Busy)) + | (Err(TemplateStoreError::Busy), Ok(session)) => session, + _ => panic!("exactly one publisher must acquire the shared lock"), + }; + winner + .commit_index(b"{\"newIndex\":true}".to_vec()) + .await + .unwrap(); + winner.finish().await.unwrap(); + let later = server + .store + .begin_publish("later-owner".to_string()) + .await + .unwrap(); + assert_eq!(later.read_index().await.unwrap(), b"{\"newIndex\":true}"); + later.finish().await.unwrap(); + } + + #[tokio::test] + async fn acquisition_disconnect_leaves_an_unclaimed_lock_without_delete_or_retry() { + let server = MockServer::start().await; + server.state.lock().unwrap().disconnect_acquire = true; + assert!(matches!( + server + .store + .begin_publish("uncertain-owner".to_string()) + .await, + Err(TemplateStoreError::Uncertain) + )); + assert!(server.state.lock().unwrap().objects.contains_key(LOCK_KEY)); + assert_eq!(server.count("PUT", LOCK_KEY), 1); + assert_eq!(server.count("DELETE", LOCK_KEY), 0); + } + + #[tokio::test] + async fn immutable_conflict_requires_identical_readback_and_blocks_commit_on_mismatch() { + for same in [true, false] { + let server = MockServer::start().await; + let body = b"metadata".to_vec(); + let key = content_key(&body); + server.state.lock().unwrap().objects.insert( + key.clone(), + if same { + body.clone() + } else { + b"other content".to_vec() + }, + ); + let mut session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + let result = session.put_immutable(&key, body, "application/json").await; + if same { + assert_eq!(result, Ok(())); + } else { + assert_eq!(result, Err(TemplateStoreError::Invalid)); + assert_eq!( + session.commit_index(b"{}".to_vec()).await, + Err(TemplateStoreError::Invalid) + ); + assert_eq!(server.count("PUT", INDEX_KEY), 0); + } + session.finish().await.unwrap(); + assert_eq!(server.count("GET", &key), 1); + } + } + + #[tokio::test] + async fn uncertain_commit_never_retries_or_releases_even_if_the_write_reached_storage() { + for status in [Some(500), Some(408), Some(302), None] { + let server = MockServer::start().await; + { + let mut state = server.state.lock().unwrap(); + state.index_status = status; + state.disconnect_index = status.is_none(); + } + let mut session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + let next = b"{\"committed\":true}".to_vec(); + assert_eq!( + session.commit_index(next.clone()).await, + Err(TemplateStoreError::Uncertain) + ); + assert_eq!( + session.commit_index(next.clone()).await, + Err(TemplateStoreError::Uncertain) + ); + assert_eq!(session.finish().await, Err(TemplateStoreError::Uncertain)); + assert_eq!(server.count("PUT", INDEX_KEY), 1); + assert_eq!(server.count("GET", INDEX_KEY), 0); + assert_eq!(server.count("DELETE", LOCK_KEY), 0); + let state = server.state.lock().unwrap(); + assert!(state.objects.contains_key(LOCK_KEY)); + if status.is_none() { + assert_eq!(state.objects[INDEX_KEY], next); + } + } + } + + #[tokio::test] + async fn certain_rejection_releases_but_foreign_owner_and_release_failure_do_not() { + let server = MockServer::start().await; + server.state.lock().unwrap().index_status = Some(403); + let mut session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + assert_eq!( + session.commit_index(b"{}".to_vec()).await, + Err(TemplateStoreError::Unavailable) + ); + session.finish().await.unwrap(); + assert!(!server.state.lock().unwrap().objects.contains_key(LOCK_KEY)); + + let session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + server + .state + .lock() + .unwrap() + .objects + .insert(LOCK_KEY.to_string(), br#"{"owner":"other-owner"}"#.to_vec()); + assert_eq!(session.finish().await, Err(TemplateStoreError::Busy)); + assert_eq!(server.count("DELETE", LOCK_KEY), 1); + server.state.lock().unwrap().objects.remove(LOCK_KEY); + let session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + server.state.lock().unwrap().delete_status = Some(503); + assert_eq!(session.finish().await, Err(TemplateStoreError::Uncertain)); + assert_eq!(server.count("DELETE", LOCK_KEY), 2); + assert!(server.state.lock().unwrap().objects.contains_key(LOCK_KEY)); + } + + #[tokio::test] + async fn index_reads_are_bounded_and_public_reads_never_send_credentials() { + let server = MockServer::start().await; + let url = server.store.target_url(Some(INDEX_KEY)).unwrap(); + assert_eq!( + read_public_index_at(&server.store.http, url).await.unwrap(), + b"{\"templates\":[]}" + ); + assert!( + !server.state.lock().unwrap().requests[0] + .headers + .contains_key("authorization") + ); + for chunked in [false, true] { + { + let mut state = server.state.lock().unwrap(); + state.chunked_reads = chunked; + state + .objects + .insert(INDEX_KEY.to_string(), vec![b' '; MAX_INDEX_BYTES + 1]); + } + assert_eq!( + server.store.read_index().await, + Err(TemplateStoreError::Invalid) + ); + } + server.state.lock().unwrap().objects.remove(INDEX_KEY); + assert_eq!( + server.store.read_index().await, + Err(TemplateStoreError::NotFound) + ); + } + + #[tokio::test] + async fn object_access_cannot_reach_the_lock_index_or_other_prefixes() { + let server = MockServer::start().await; + let mut session = server + .store + .begin_publish("owner".to_string()) + .await + .unwrap(); + let before = server.state.lock().unwrap().requests.len(); + for key in [ + LOCK_KEY, + INDEX_KEY, + "agc/project-snapshots/v1/a/b", + "templates/v1/a/../index.json", + "templates/v1/a/%2e%2e/index.json", + "templates/v1/a/file?x", + "/templates/v1/a/file", + ] { + assert_eq!( + session.read_object(key, MAX_INDEX_BYTES).await, + Err(TemplateStoreError::Invalid) + ); + assert_eq!( + session + .put_immutable(key, b"{}".to_vec(), "application/json") + .await, + Err(TemplateStoreError::Invalid) + ); + } + assert_eq!(server.state.lock().unwrap().requests.len(), before); + session.finish().await.unwrap(); + } +} diff --git a/server-rs/crates/shared-contracts/src/admin.rs b/server-rs/crates/shared-contracts/src/admin.rs index 96d916ee5..889b94223 100644 --- a/server-rs/crates/shared-contracts/src/admin.rs +++ b/server-rs/crates/shared-contracts/src/admin.rs @@ -10,7 +10,7 @@ use crate::creation_entry_config::{ }; /// 后台 member 可被授予的一级 Tab 权限;账号管理仅 owner 可见,不进入该集合。 -pub const ADMIN_TAB_PERMISSIONS: [&str; 17] = [ +pub const ADMIN_TAB_PERMISSIONS: [&str; 18] = [ "dashboard", "overview", "tables", @@ -26,6 +26,7 @@ pub const ADMIN_TAB_PERMISSIONS: [&str; 17] = [ "editor-generation-pricing", "editor-showcase", "editor-assets", + "agc-templates", "error-reports", "project-snapshots", ]; @@ -87,6 +88,53 @@ pub struct AdminLoginRequest { pub password: String, } +/// AGC 模板管理快照;revision 对应读取到的完整 OSS 清单字节。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminAgcTemplateListResponse { + pub revision: String, + pub writable: bool, + pub templates: Vec, +} + +/// 合并上架与下架条目的后台展示投影。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase")] +pub struct AdminAgcTemplatePayload { + pub id: String, + pub title: String, + pub summary: String, + pub tags: Vec, + pub runtime: String, + pub engine: String, + pub engine_version: String, + pub template_version: String, + pub enabled: bool, + pub cover_url: String, + pub zip_size_bytes: u64, +} + +/// 仅修改模板展示字段及上下架状态,不接受包、引擎或版本字段。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AdminUpdateAgcTemplateRequest { + pub expected_revision: String, + pub title: String, + pub summary: String, + pub tags: Vec, + pub enabled: bool, + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cover: Option, +} + +/// 待保存的封面原始图片;格式、字节和尺寸由后端验证。 +#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)] +#[serde(rename_all = "camelCase", deny_unknown_fields)] +pub struct AdminAgcTemplateCoverInput { + pub content_type: String, + pub data_base64: String, +} + // 登录成功后返回管理员访问令牌与基础会话信息。 /// 后台创作入口开关列表响应。 @@ -1153,10 +1201,67 @@ mod tests { use serde_json::json; use super::{ + ADMIN_TAB_PERMISSIONS, AdminAgcTemplateListResponse, AdminAgcTemplatePayload, AdminConfirmEditorShowcaseCampaignImageUploadRequest, AdminEditorImageSequenceFramePayload, AdminEditorShowcaseAssetPayload, AdminRechargeRefundManualReviewResolveRequest, + AdminUpdateAgcTemplateRequest, }; + #[test] + fn agc_template_snapshot_uses_camel_case_and_assignable_tab_permission() { + let value = serde_json::to_value(AdminAgcTemplateListResponse { + revision: "a".repeat(64), + writable: true, + templates: vec![AdminAgcTemplatePayload { + id: "cocos-empty-2d".to_owned(), + title: "二维模板".to_owned(), + summary: "简介".to_owned(), + tags: vec!["cocos".to_owned()], + runtime: "cocos".to_owned(), + engine: "cocos-creator".to_owned(), + engine_version: "3.8.8".to_owned(), + template_version: "0.1.0".to_owned(), + enabled: false, + cover_url: "https://example.test/cover.svg".to_owned(), + zip_size_bytes: 1024, + }], + }) + .unwrap(); + assert_eq!( + value, + json!({ + "revision": "a".repeat(64), "writable": true, + "templates": [{"id": "cocos-empty-2d", "title": "二维模板", "summary": "简介", + "tags": ["cocos"], "runtime": "cocos", "engine": "cocos-creator", + "engineVersion": "3.8.8", "templateVersion": "0.1.0", "enabled": false, + "coverUrl": "https://example.test/cover.svg", "zipSizeBytes": 1024}] + }) + ); + assert!(ADMIN_TAB_PERMISSIONS.contains(&"agc-templates")); + } + + #[test] + fn agc_template_edit_rejects_uneditable_and_unknown_cover_fields() { + let request = json!({"expectedRevision": "a".repeat(64), "title": "模板", "summary": "", + "tags": [], "enabled": true}); + let parsed: AdminUpdateAgcTemplateRequest = + serde_json::from_value(request.clone()).unwrap(); + assert!(parsed.cover.is_none()); + assert_eq!(serde_json::to_value(parsed).unwrap(), request); + for field in ["id", "templateVersion", "runtime", "zipKey"] { + let mut invalid = request.clone(); + invalid[field] = json!("不可修改"); + assert!(serde_json::from_value::(invalid).is_err()); + } + let mut with_cover = request; + with_cover["cover"] = json!({"contentType": "image/png", "dataBase64": "AQID"}); + let parsed: AdminUpdateAgcTemplateRequest = + serde_json::from_value(with_cover.clone()).unwrap(); + assert_eq!(parsed.cover.unwrap().data_base64, "AQID"); + with_cover["cover"]["url"] = json!("https://example.test/image.png"); + assert!(serde_json::from_value::(with_cover).is_err()); + } + #[test] fn refund_manual_review_resolution_request_uses_camel_case_contract() { let value = serde_json::to_value(AdminRechargeRefundManualReviewResolveRequest { diff --git a/server-rs/crates/spacetime-module/src/admin_account_storage.rs b/server-rs/crates/spacetime-module/src/admin_account_storage.rs index 8c9d43dcf..69d53aa4e 100644 --- a/server-rs/crates/spacetime-module/src/admin_account_storage.rs +++ b/server-rs/crates/spacetime-module/src/admin_account_storage.rs @@ -534,6 +534,13 @@ mod tests { assert_eq!(normalized, r#"["dashboard","tracking","editor-assets"]"#); } + #[test] + fn agc_template_tab_can_be_assigned_without_changing_account_schema() { + let normalized = normalize_tab_permissions_json(r#"["agc-templates","agc-templates"]"#) + .expect("template management permission"); + assert_eq!(normalized, r#"["agc-templates"]"#); + } + #[test] fn unknown_tab_permission_is_rejected() { let error = normalize_tab_permissions_json(r#"["dashboard","accounts"]"#)