From 54cb0ce2f626ab3d962712f46a2e0b16a110319b Mon Sep 17 00:00:00 2001 From: Knox Date: Tue, 26 Nov 2024 13:57:53 +0800 Subject: [PATCH 01/13] Improve spine i18n (#17906) * Improve spine i18n * refine i18n --- cocos/spine/skeleton.ts | 16 --------- editor/i18n/en/localization.js | 14 ++------ editor/i18n/en/modules/ui.js | 61 ++++++++++++++++++++++++++++++++++ editor/i18n/zh/localization.js | 10 ------ editor/i18n/zh/modules/ui.js | 59 ++++++++++++++++++++++++++++++++ 5 files changed, 122 insertions(+), 38 deletions(-) diff --git a/cocos/spine/skeleton.ts b/cocos/spine/skeleton.ts index 62e7446cacd..c27c2ef7b05 100644 --- a/cocos/spine/skeleton.ts +++ b/cocos/spine/skeleton.ts @@ -353,7 +353,6 @@ export class Skeleton extends UIRenderer { */ @editable @type(SkeletonData) - @displayName('SkeletonData') get skeletonData (): SkeletonData | null { return this._skeletonData; } @@ -374,9 +373,7 @@ export class Skeleton extends UIRenderer { /** * @engineInternal */ - @displayName('Default Skin') @type(DefaultSkinsEnum) - @tooltip('i18n:COMPONENT.skeleton.default_skin') get _defaultSkinIndex (): number { if (this.skeletonData) { const skinsEnum = this.skeletonData.getSkinsEnum(); @@ -425,9 +422,7 @@ export class Skeleton extends UIRenderer { /** * @engineInternal */ - @displayName('Animation') @type(SpineDefaultAnimsEnum) - @tooltip('i18n:COMPONENT.skeleton.animation') get _animationIndex (): number { const animationName = EDITOR_NOT_IN_PREVIEW ? this.defaultAnimation : this.animation; if (this.skeletonData) { @@ -475,8 +470,6 @@ export class Skeleton extends UIRenderer { * @en Animation mode, with options for real-time mode, private cached, or public cached mode. * @zh 动画模式,可选实时模式,私有 cached 或公共 cached 模式。 */ - @displayName('Animation Cache Mode') - @tooltip('i18n:COMPONENT.skeleton.animation_cache_mode') @editable @type(SpineAnimationCacheMode) get defaultCacheMode (): SpineAnimationCacheMode { @@ -492,7 +485,6 @@ export class Skeleton extends UIRenderer { * @zh 是否启用 alpha 预乘。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.premultipliedAlpha') get premultipliedAlpha (): boolean { return this._premultipliedAlpha; } set premultipliedAlpha (v: boolean) { if (v !== this._premultipliedAlpha) { @@ -507,14 +499,12 @@ export class Skeleton extends UIRenderer { * @zh 是否循环播放当前骨骼动画。 */ @serializable - @tooltip('i18n:COMPONENT.skeleton.loop') public loop = true; /** * @en The time scale of this skeleton. * @zh 当前骨骼中所有动画的时间缩放率。 */ - @tooltip('i18n:COMPONENT.skeleton.time_scale') @editable get timeScale (): number { return this._timeScale; } set timeScale (value) { @@ -530,7 +520,6 @@ export class Skeleton extends UIRenderer { * @zh 是否启用染色效果。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.use_tint') get useTint (): boolean { return this._useTint; } set useTint (value) { if (value !== this._useTint) { @@ -545,7 +534,6 @@ export class Skeleton extends UIRenderer { * @zh 如果渲染大量相同纹理,且结构简单的骨骼动画,开启合批可以降低 draw call 数量提升渲染性能。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.enabled_batch') get enableBatch (): boolean { return this._enableBatch; } set enableBatch (value) { if (value !== this._enableBatch) { @@ -561,7 +549,6 @@ export class Skeleton extends UIRenderer { * 当前动画组件维护的挂点数组。一个挂点组件包括动画节点路径和目标节点。 */ @type([SpineSocket]) - @tooltip('i18n:animation.sockets') get sockets (): SpineSocket[] { return this._sockets; } @@ -579,7 +566,6 @@ export class Skeleton extends UIRenderer { * @zh 是否显示 slot 的 debug 信息。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.debug_slots') get debugSlots (): boolean { return this._debugSlots; } set debugSlots (v: boolean) { if (v !== this._debugSlots) { @@ -594,7 +580,6 @@ export class Skeleton extends UIRenderer { * @zh 是否显示 bone 的 debug 信息。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.debug_bones') get debugBones (): boolean { return this._debugBones; } set debugBones (v: boolean) { if (v !== this._debugBones) { @@ -609,7 +594,6 @@ export class Skeleton extends UIRenderer { * @zh 是否显示 mesh 的 debug 信息。 */ @editable - @tooltip('i18n:COMPONENT.skeleton.debug_mesh') get debugMesh (): boolean { return this._debugMesh; } set debugMesh (value) { if (value !== this._debugMesh) { diff --git a/editor/i18n/en/localization.js b/editor/i18n/en/localization.js index 1b9f07c7e4c..3800451c7d9 100755 --- a/editor/i18n/en/localization.js +++ b/editor/i18n/en/localization.js @@ -10,8 +10,8 @@ const url = 'https://docs.cocos.com/creator'; module.exports = link(mixin({ common: { 'attribute': { - 'title': 'Attribute: ', - 'description': 'Description: ', + 'title': 'Attribute: ', + 'description': 'Description: ', }, }, classes: { @@ -668,16 +668,6 @@ module.exports = link(mixin({ design_size: 'Design resolution of the SubContextView, dynamic updates at runtime is not possible', fps: 'Update frame rate for the SubContextView', }, - skeleton: { - skeleton_data: 'The skeleton data contains the skeleton information,
drag the json file exported from Spine to get started.', - default_skin: 'Choose the default skin.', - animation: 'The name of current playing animation.', - loop: 'Whether loop current animation', - time_scale: 'The time scale of animations of this skeleton', - debug_slots: 'Indicates whether show debug slots.', - debug_bones: 'Indicates whether show debug bones.', - premultipliedAlpha: 'Indicates whether to enable premultiplied alpha.', - }, dragon_bones: { dragon_bones_asset: 'The json data contains the DragonBones information,
drag the json file exported from DragonBones to get started.', diff --git a/editor/i18n/en/modules/ui.js b/editor/i18n/en/modules/ui.js index c4724350a81..7c5da212f97 100644 --- a/editor/i18n/en/modules/ui.js +++ b/editor/i18n/en/modules/ui.js @@ -236,5 +236,66 @@ module.exports = { }, }, }, + 'sp': { + 'Skeleton': { + properties: { + __extends__: 'classes.cc.UIRenderer.properties', + 'skeletonData': { + displayName: 'SkeletonData', + tooltip: 'The skeleton data contains the skeleton information,
drag the json file exported from Spine to get started.', + }, + '_defaultSkinIndex': { + displayName: 'Default skin', + tooltip: 'Choose the default skin.', + }, + '_animationIndex': { + displayName: 'Animation', + tooltip: 'The name of current playing animation.', + }, + 'defaultCacheMode': { + displayName: 'Animation Cache Mode', + tooltip: 'Animation mode, with options for real-time mode,
private cached, or public cached mode.', + }, + 'loop': { + displayName: 'Loop', + tooltip: 'Whether loop current animation', + }, + 'timeScale': { + displayName: 'Time Scale', + tooltip: 'The time scale of animations of this skeleton', + }, + 'debugSlots': { + displayName: 'Debug Slots', + tooltip: 'Indicates whether show debug slots.', + }, + 'debugBones': { + displayName: 'Debug Bones', + tooltip: 'Indicates whether show debug bones.', + }, + 'debugMesh': { + displayName: 'Debug Mesh', + tooltip: 'Indicates whether open debug mesh.', + }, + 'useTint': { + displayName: 'Use Tint', + tooltip: 'Enabled two color tint.', + }, + 'premultipliedAlpha': { + displayName: 'Premultiplied Alpha', + tooltip: 'Indicates whether to enable premultiplied alpha.', + }, + 'enableBatch': { + displayName: 'Enable Batch', + tooltip: 'If rendering a large number of identical textures and simple skeletal animations,
' + + 'enabling batching can reduce the number of draw calls and improve rendering performance.', + }, + 'sockets': { + displayName: 'Sockets', + tooltip: 'The bone sockets this animation component maintains.
' + + 'A SpineSocket object contains a path reference to bone, and a target node.', + }, + }, + }, + }, }, }; diff --git a/editor/i18n/zh/localization.js b/editor/i18n/zh/localization.js index 9b68c2c0c19..7fd7f56d43f 100755 --- a/editor/i18n/zh/localization.js +++ b/editor/i18n/zh/localization.js @@ -653,16 +653,6 @@ module.exports = link(mixin({ design_size: '开放数据域的设计分辨率,禁止在运行时动态更新', fps: '主域更新开放数据域贴图的频率', }, - skeleton: { - skeleton_data: '骨骼信息数据,拖拽 Spine 导出的骨骼动画信息 json 资源到这里来开始使用', - default_skin: '选择默认的皮肤', - animation: '正在播放的动画名称', - loop: '是否循环播放当前动画', - time_scale: '当前骨骼中所有动画的时间缩放率', - debug_slots: '是否显示 slot 的 debug 信息', - debug_bones: '是否显示 bone 的 debug 信息', - premultipliedAlpha: '是否启用贴图预乘', - }, dragon_bones: { dragon_bones_asset: '骨骼信息数据,拖拽 Dragon Bones 导出的骨骼动画信息 json 资源到这里来开始使用', dragon_bones_atlas_asset: 'Texture 信息数据,拖拽 Dragon Bones 导出的 Texture 信息 json 资源到这里来开始使用', diff --git a/editor/i18n/zh/modules/ui.js b/editor/i18n/zh/modules/ui.js index 4e7afd6879f..a32bd7fe44f 100644 --- a/editor/i18n/zh/modules/ui.js +++ b/editor/i18n/zh/modules/ui.js @@ -224,5 +224,64 @@ module.exports = { }, }, }, + 'sp': { + 'Skeleton': { + properties: { + __extends__: 'classes.cc.UIRenderer.properties', + 'skeletonData': { + displayName: 'SkeletonData', + tooltip: '骨骼信息数据,拖拽 Spine 导出的骨骼动画信息 json 资源到这里来开始使用', + }, + '_defaultSkinIndex': { + displayName: 'Default skin', + tooltip: '选择默认的皮肤', + }, + '_animationIndex': { + displayName: 'Animation', + tooltip: '正在播放的动画名称', + }, + 'defaultCacheMode': { + displayName: 'Animation Cache Mode', + tooltip: '动画模式,可选实时模式,私有 cached 或公共 cached 模式', + }, + 'loop': { + displayName: 'Loop', + tooltip: '是否循环播放当前动画', + }, + 'timeScale': { + displayName: 'Time Scale', + tooltip: '当前骨骼中所有动画的时间缩放率', + }, + 'debugSlots': { + displayName: 'Debug Slots', + tooltip: '是否显示 slot 的 debug 信息', + }, + 'debugBones': { + displayName: 'Debug Bones', + tooltip: '是否显示 bone 的 debug 信息', + }, + 'debugMesh': { + displayName: 'Debug Mesh', + tooltip: '是否显示 mesh 的 debug 信息', + }, + 'useTint': { + displayName: 'Use Tint', + tooltip: '是否启用染色效果', + }, + 'premultipliedAlpha': { + displayName: 'Premultiplied Alpha', + tooltip: '是否启用贴图预乘', + }, + 'enableBatch': { + displayName: 'Enable Batch', + tooltip: '如果渲染大量相同纹理,且结构简单的骨骼动画,开启合批可以降低 draw call 数量提升渲染性能', + }, + 'sockets': { + displayName: 'Sockets', + tooltip: '当前动画组件维护的挂点数组。一个挂点组件包括动画节点路径和目标节点', + }, + }, + }, + }, }, }; From 5e671ae5dccb6db95c75d9ffaaf7429be41ab941 Mon Sep 17 00:00:00 2001 From: bofeng-song Date: Tue, 26 Nov 2024 18:09:15 +0800 Subject: [PATCH 02/13] Fix audio load error on TaoBao-MiniGame platform (#17921) --- templates/taobao-mini-game/game.ejs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/templates/taobao-mini-game/game.ejs b/templates/taobao-mini-game/game.ejs index 0c5f811417f..3ae735585bd 100644 --- a/templates/taobao-mini-game/game.ejs +++ b/templates/taobao-mini-game/game.ejs @@ -1,4 +1,7 @@ -globalThis.window = globalThis; + +if (!globalThis.window) { + globalThis.window = globalThis; +} globalThis.self = $global; globalThis.__taobaoRequire = (urlNoSchema) => { @@ -71,7 +74,6 @@ function loadCC() { function onApplicationCreated(application) { return System.import('cc').then((cc) => { - globalThis.cc = cc; require('./engine-adapter'); return application.init(cc); }).then(() => { return application.start(); }); From 77b3ab8683cdfc929cd24342ad642119501713d0 Mon Sep 17 00:00:00 2001 From: James Chen Date: Thu, 28 Nov 2024 10:39:42 +0800 Subject: [PATCH 03/13] [v3.8.5] Package size check needs to ignore exports/vendor.ts (#17979) --- .github/workflows/package-size-check.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/workflows/package-size-check.js b/.github/workflows/package-size-check.js index ea58509b211..8f30f2e8099 100644 --- a/.github/workflows/package-size-check.js +++ b/.github/workflows/package-size-check.js @@ -11,7 +11,9 @@ const features = []; files.forEach(file => { const filePath = ps.join(exportsDir, file); const feature = ps.parse(ps.basename(filePath)).name; - features.push(feature); + if (feature !== 'vendor') { + features.push(feature); + } }); console.log(`features: [ ${features.join(', ')} ]`); From 92123fb9c4be6e6966c52a2f3edc14a2759d9a64 Mon Sep 17 00:00:00 2001 From: GengineJS <476393671@qq.com> Date: Thu, 28 Nov 2024 12:01:56 +0800 Subject: [PATCH 04/13] Fix the error caused by the lag in the WebGPU API (#17980) --- @types/webGPU.d.ts | 607 ++++++++++++++++-------------- cocos/gfx/webgpu/webgpu-device.ts | 2 +- 2 files changed, 317 insertions(+), 292 deletions(-) diff --git a/@types/webGPU.d.ts b/@types/webGPU.d.ts index 8b2bebe872b..4d8b09d919d 100644 --- a/@types/webGPU.d.ts +++ b/@types/webGPU.d.ts @@ -68,15 +68,8 @@ type GPUColor = | GPUColorDict; type GPUColorWriteFlags = number; -type GPUDepthBias = - number; -type GPUExtent3D = - - | Iterable - | GPUExtent3DDict; -type GPUFlagsConstant = - number; -type GPUImageCopyExternalImageSource = +declare interface VideoFrame {} +type GPUCopyExternalImageSource = | ImageBitmap | ImageData @@ -85,6 +78,14 @@ type GPUImageCopyExternalImageSource = | VideoFrame | HTMLCanvasElement | OffscreenCanvas; +type GPUDepthBias = + number; +type GPUExtent3D = + + | Iterable + | GPUExtent3DDict; +type GPUFlagsConstant = + number; type GPUIndex32 = number; type GPUIntegerCoordinate = @@ -142,7 +143,11 @@ type GPUBlendFactor = | 'one-minus-dst-alpha' | 'src-alpha-saturated' | 'constant' - | 'one-minus-constant'; + | 'one-minus-constant' + | 'src1' + | 'one-minus-src1' + | 'src1-alpha' + | 'one-minus-src1-alpha'; type GPUBlendOperation = | 'add' @@ -164,6 +169,10 @@ type GPUCanvasAlphaMode = | 'opaque' | 'premultiplied'; +type GPUCanvasToneMappingMode = + + | 'standard' + | 'extended'; type GPUCompareFunction = | 'never' @@ -198,14 +207,19 @@ type GPUFeatureName = | 'depth-clip-control' | 'depth32float-stencil8' | 'texture-compression-bc' + | 'texture-compression-bc-sliced-3d' | 'texture-compression-etc2' | 'texture-compression-astc' + | 'texture-compression-astc-sliced-3d' | 'timestamp-query' | 'indirect-first-instance' | 'shader-f16' | 'rg11b10ufloat-renderable' | 'bgra8unorm-storage' - | 'float32-filterable'; + | 'float32-filterable' + | 'float32-blendable' + | 'clip-distances' + | 'dual-source-blending'; type GPUFilterMode = | 'nearest' @@ -393,22 +407,31 @@ type GPUTextureViewDimension = | '3d'; type GPUVertexFormat = + | 'uint8' | 'uint8x2' | 'uint8x4' + | 'sint8' | 'sint8x2' | 'sint8x4' + | 'unorm8' | 'unorm8x2' | 'unorm8x4' + | 'snorm8' | 'snorm8x2' | 'snorm8x4' + | 'uint16' | 'uint16x2' | 'uint16x4' + | 'sint16' | 'sint16x2' | 'sint16x4' + | 'unorm16' | 'unorm16x2' | 'unorm16x4' + | 'snorm16' | 'snorm16x2' | 'snorm16x4' + | 'float16' | 'float16x2' | 'float16x4' | 'float32' @@ -423,7 +446,8 @@ type GPUVertexFormat = | 'sint32x2' | 'sint32x3' | 'sint32x4' - | 'unorm10-10-10-2'; + | 'unorm10-10-10-2' + | 'unorm8x4-bgra'; type GPUVertexStepMode = | 'vertex' @@ -458,6 +482,9 @@ interface GPUBindGroupEntry { interface GPUBindGroupLayoutDescriptor extends GPUObjectDescriptorBase { + /** + * A list of entries describing the shader resource bindings for a bind group. + */ entries: Iterable; } @@ -574,7 +601,7 @@ interface GPUBufferBindingLayout { */ minBindingSize?: GPUSize64; } -type PredefinedGPUColorSpace = 'display-p3' | 'srgb'; + interface GPUBufferDescriptor extends GPUObjectDescriptorBase { /** @@ -625,7 +652,12 @@ interface GPUCanvasConfiguration { * The color space that values written into textures returned by * {@link GPUCanvasContext#getCurrentTexture} should be displayed with. */ - colorSpace?: PredefinedGPUColorSpace; + colorSpace?: PredefinedColorSpace; + /** + * The tone mapping determines how the content of textures returned by + * {@link GPUCanvasContext#getCurrentTexture} are to be displayed. + */ + toneMapping?: GPUCanvasToneMapping; /** * Determines the effect that alpha values will have on the content of textures returned by * {@link GPUCanvasContext#getCurrentTexture} when read, displayed, or used as an image source. @@ -633,6 +665,25 @@ interface GPUCanvasConfiguration { alphaMode?: GPUCanvasAlphaMode; } +interface GPUCanvasConfigurationOut + extends Required< + Omit< + GPUCanvasConfiguration, + 'toneMapping' + > + > { + /** {@inheritDoc GPUCanvasConfiguration.viewFormats} */ + viewFormats: GPUTextureFormat[]; + /** + * {@inheritDoc GPUCanvasConfiguration.toneMapping} + */ + toneMapping?: GPUCanvasToneMapping; +} + +interface GPUCanvasToneMapping { + mode?: GPUCanvasToneMappingMode; +} + interface GPUColorDict { /** * The red channel value. @@ -709,6 +760,53 @@ interface GPUComputePipelineDescriptor compute: GPUProgrammableStage; } +interface GPUCopyExternalImageDestInfo + extends GPUTexelCopyTextureInfo { + /** + * Describes the color space and encoding used to encode data into the destination texture. + * This [[#color-space-conversions|may result]] in values outside of the range [0, 1] + * being written to the target texture, if its format can represent them. + * Otherwise, the results are clamped to the target texture format's range. + * Note: + * If {@link GPUCopyExternalImageDestInfo#colorSpace} matches the source image, + * conversion may not be necessary. See [[#color-space-conversion-elision]]. + */ + colorSpace?: PredefinedColorSpace; + /** + * Describes whether the data written into the texture should have its RGB channels + * premultiplied by the alpha channel, or not. + * If this option is set to `true` and the {@link GPUCopyExternalImageSourceInfo#source} is also + * premultiplied, the source RGB values must be preserved even if they exceed their + * corresponding alpha values. + * Note: + * If {@link GPUCopyExternalImageDestInfo#premultipliedAlpha} matches the source image, + * conversion may not be necessary. See [[#color-space-conversion-elision]]. + */ + premultipliedAlpha?: boolean; +} + +interface GPUCopyExternalImageSourceInfo { + /** + * The source of the texel copy. The copy source data is captured at the moment that + * {@link GPUQueue#copyExternalImageToTexture} is issued. Source size is determined as described + * by the external source dimensions table. + */ + source: GPUCopyExternalImageSource; + /** + * Defines the origin of the copy - the minimum (top-left) corner of the source sub-region to copy from. + * Together with `copySize`, defines the full copy sub-region. + */ + origin?: GPUOrigin2D; + /** + * Describes whether the source image is vertically flipped, or not. + * If this option is set to `true`, the copy is flipped vertically: the bottom row of the source + * region is copied into the first row of the destination region, and so on. + * The {@link GPUCopyExternalImageSourceInfo#origin} option is still relative to the top-left corner + * of the source image, increasing downward. + */ + flipY?: boolean; +} + interface GPUDepthStencilState { /** * The {@link GPUTextureViewDescriptor#format} of {@link GPURenderPassDescriptor#depthStencilAttachment} @@ -744,15 +842,15 @@ interface GPUDepthStencilState { */ stencilWriteMask?: GPUStencilValue; /** - * Constant depth bias added to each fragment. See [$biased fragment depth$] for details. + * Constant depth bias added to each triangle fragment. See [$biased fragment depth$] for details. */ depthBias?: GPUDepthBias; /** - * Depth bias that scales with the fragment’s slope. See [$biased fragment depth$] for details. + * Depth bias that scales with the triangle fragment’s slope. See [$biased fragment depth$] for details. */ depthBiasSlopeScale?: number; /** - * The maximum depth bias of a fragment. See [$biased fragment depth$] for details. + * The maximum depth bias of a triangle fragment. See [$biased fragment depth$] for details. */ depthBiasClamp?: number; } @@ -769,9 +867,9 @@ interface GPUDeviceDescriptor /** * Specifies the limits that are required by the device request. * The request will fail if the adapter cannot provide these limits. - * Each key must be the name of a member of supported limits. - * Exactly the specified limits, and no limit/better or worse, - * will be allowed in validation of API calls on the resulting device. + * Each key with a non-`undefined` value must be the name of a member of supported limits. + * API calls on the resulting device perform validation according to the exact limits of the + * device (not the adapter; see [[#limits]]). * + + + + + + + + + + + + + + diff --git a/templates/google-play/template/app/build.gradle b/templates/google-play/template/app/build.gradle new file mode 100644 index 00000000000..184a68947f0 --- /dev/null +++ b/templates/google-play/template/app/build.gradle @@ -0,0 +1,113 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'com.android.application' + +RES_PATH = RES_PATH.replace("\\", "/") +COCOS_ENGINE_PATH = COCOS_ENGINE_PATH.replace("\\", "/") + +buildDir = "${RES_PATH}/proj/build/${project.name ==~ /^[_a-zA-Z0-9-]+$/ ? project.name : 'CocosGame'}" +android { + compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger() + buildToolsVersion PROP_BUILD_TOOLS_VERSION + ndkPath PROP_NDK_PATH + namespace APPLICATION_ID + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + applicationId APPLICATION_ID + minSdkVersion PROP_MIN_SDK_VERSION + targetSdkVersion PROP_TARGET_SDK_VERSION + versionCode 1 + versionName "1.0" + + externalNativeBuild { + cmake { + targets "cocos" + arguments "-DRES_DIR=${RES_PATH}", "-DANDROID_STL=c++_static", "-DANDROID_TOOLCHAIN=clang", "-DANDROID_ARM_NEON=TRUE" + } + ndk { abiFilters PROP_APP_ABI.split(':') } + } + } + + sourceSets.main { + java.srcDirs "../src", "src" + res.srcDirs "../res", 'res', "${RES_PATH}/proj/res" + jniLibs.srcDirs "../libs", 'libs' + manifest.srcFile "AndroidManifest.xml" + assets.srcDir "${RES_PATH}/data" + jniLibs { + // Vulkan validation layer + // srcDir "${android.ndkDirectory}/sources/third_party/vulkan/src/build-android/jniLibs" + } + } + + externalNativeBuild { + cmake { + version "3.22.1" + path "../CMakeLists.txt" + buildStagingDirectory "${RES_PATH}/proj/build" + } + } + + signingConfigs { + + release { + if (project.hasProperty("RELEASE_STORE_FILE") && !RELEASE_STORE_FILE.isEmpty()) { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + } + } + } + + buildTypes { + release { + debuggable false + jniDebuggable false + renderscriptDebuggable false + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.release + } + + externalNativeBuild { + cmake { + // switch HIDE_SYMBOLS to OFF to skip compilation flag `-fvisibility=hidden` + arguments "-DHIDE_SYMBOLS=ON" + } + } + + if (!Boolean.parseBoolean(PROP_IS_DEBUG)) { + getIsDefault().set(true) + } + + } + + debug { + debuggable true + jniDebuggable true + renderscriptDebuggable true + // resValue "string", "app_name", "${PROP_APP_NAME}-dbg" + // applicationIdSuffix ".debug" + } + } +} + +dependencies { + implementation fileTree(dir: '../libs', include: ['*.jar','*.aar']) + implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) + implementation fileTree(dir: "${COCOS_ENGINE_PATH}/cocos/platform/android/java/libs", include: ['*.jar']) + implementation project(':libservice') + implementation project(':libcocos') + if (Boolean.parseBoolean(PROP_ENABLE_INPUTSDK)) { + implementation 'com.google.android.libraries.play.games:inputmapping:1.1.0-beta' + implementation "org.jetbrains.kotlin:kotlin-stdlib:1.4.10" + } +} diff --git a/templates/google-play/template/app/proguard-rules.pro b/templates/google-play/template/app/proguard-rules.pro new file mode 100644 index 00000000000..fc884b76762 --- /dev/null +++ b/templates/google-play/template/app/proguard-rules.pro @@ -0,0 +1,54 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\developSoftware\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Proguard Cocos2d-x-lite for release +-keep public class com.cocos.** { *; } +-dontwarn com.cocos.** + +# Proguard Apache HTTP for release +-keep class org.apache.http.** { *; } +-dontwarn org.apache.http.** + +# Proguard okhttp for release +-keep class okhttp3.** { *; } +-dontwarn okhttp3.** + +-keep class okio.** { *; } +-dontwarn okio.** + +# Proguard Android Webivew for release. you can comment if you are not using a webview +-keep public class android.net.http.SslError +-keep public class android.webkit.WebViewClient + +-keep public class com.google.** { *; } + +-dontwarn android.webkit.WebView +-dontwarn android.net.http.SslError +-dontwarn android.webkit.WebViewClient + +# This is generated automatically by the Android Gradle plugin. +-dontwarn android.hardware.BatteryState +-dontwarn android.hardware.lights.Light +-dontwarn android.hardware.lights.LightState$Builder +-dontwarn android.hardware.lights.LightState +-dontwarn android.hardware.lights.LightsManager$LightsSession +-dontwarn android.hardware.lights.LightsManager +-dontwarn android.hardware.lights.LightsRequest$Builder +-dontwarn android.hardware.lights.LightsRequest +-dontwarn android.net.ssl.SSLSockets +-dontwarn android.os.VibratorManager \ No newline at end of file diff --git a/templates/google-play/template/app/src/com/cocos/game/AppActivity.java b/templates/google-play/template/app/src/com/cocos/game/AppActivity.java new file mode 100644 index 00000000000..5aaece88515 --- /dev/null +++ b/templates/google-play/template/app/src/com/cocos/game/AppActivity.java @@ -0,0 +1,125 @@ +/**************************************************************************** +Copyright (c) 2015-2016 Chukong Technologies Inc. +Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. + +http://www.cocos2d-x.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +****************************************************************************/ +package com.cocos.game; + +import android.os.Bundle; +import android.content.Intent; +import android.content.res.Configuration; + +import com.cocos.service.SDKWrapper; +import com.cocos.lib.CocosActivity; + +public class AppActivity extends CocosActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // DO OTHER INITIALIZATION BELOW + SDKWrapper.shared().init(this); + + } + + @Override + protected void onResume() { + super.onResume(); + SDKWrapper.shared().onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + SDKWrapper.shared().onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508 + if (!isTaskRoot()) { + return; + } + SDKWrapper.shared().onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + SDKWrapper.shared().onActivityResult(requestCode, resultCode, data); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + SDKWrapper.shared().onNewIntent(intent); + } + + @Override + protected void onRestart() { + super.onRestart(); + SDKWrapper.shared().onRestart(); + } + + @Override + protected void onStop() { + super.onStop(); + SDKWrapper.shared().onStop(); + } + + @Override + public void onBackPressed() { + SDKWrapper.shared().onBackPressed(); + super.onBackPressed(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + SDKWrapper.shared().onConfigurationChanged(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + SDKWrapper.shared().onRestoreInstanceState(savedInstanceState); + super.onRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + SDKWrapper.shared().onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + protected void onStart() { + SDKWrapper.shared().onStart(); + super.onStart(); + } + + @Override + public void onLowMemory() { + SDKWrapper.shared().onLowMemory(); + super.onLowMemory(); + } +} diff --git a/templates/google-play/template/build-cfg.json b/templates/google-play/template/build-cfg.json new file mode 100644 index 00000000000..fb658e29853 --- /dev/null +++ b/templates/google-play/template/build-cfg.json @@ -0,0 +1,8 @@ +{ + "ndk_module_path" :[ + "${COCOS_ROOT}", + "${COCOS_ROOT}/cocos", + "${COCOS_ROOT}/external" + ], + "copy_resources": [] +} diff --git a/templates/google-play/template/build.gradle b/templates/google-play/template/build.gradle new file mode 100644 index 00000000000..84203e08ee1 --- /dev/null +++ b/templates/google-play/template/build.gradle @@ -0,0 +1,28 @@ +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +buildscript { + + repositories { + google() + mavenCentral() + // jcenter() // keeped as anchor, will be removed soon + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.2' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +allprojects { + repositories { + google() + mavenCentral() + // jcenter() // keeped as anchor, will be removed soon + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/templates/google-play/template/instantapp/AndroidManifest.xml b/templates/google-play/template/instantapp/AndroidManifest.xml new file mode 100644 index 00000000000..0216ac9da8f --- /dev/null +++ b/templates/google-play/template/instantapp/AndroidManifest.xml @@ -0,0 +1,53 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/templates/google-play/template/instantapp/build.gradle b/templates/google-play/template/instantapp/build.gradle new file mode 100644 index 00000000000..4336667f2c4 --- /dev/null +++ b/templates/google-play/template/instantapp/build.gradle @@ -0,0 +1,100 @@ +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'com.android.application' + +RES_PATH = RES_PATH.replace("\\", "/") +COCOS_ENGINE_PATH = COCOS_ENGINE_PATH.replace("\\", "/") +buildDir = "${RES_PATH}/proj/build/instantapp" + +android { + compileSdkVersion PROP_COMPILE_SDK_VERSION.toInteger() + buildToolsVersion PROP_BUILD_TOOLS_VERSION + ndkPath PROP_NDK_PATH + namespace APPLICATION_ID + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + defaultConfig { + minSdkVersion PROP_MIN_SDK_VERSION + targetSdkVersion PROP_TARGET_SDK_VERSION + versionCode 1 + versionName "1.0" + + externalNativeBuild { + cmake { + targets "cocos" + arguments "-DRES_DIR=${RES_PATH}", "-DCOCOS_X_PATH=${COCOS_ENGINE_PATH}","-DANDROID_STL=c++_static", "-DANDROID_TOOLCHAIN=clang", "-DANDROID_ARM_NEON=TRUE", "-DANDROID_LD=gold" + cppFlags "-frtti -fexceptions -fsigned-char -DANDROID_INSTANT=1" + } + ndk { abiFilters PROP_APP_ABI.split(':') } + } + } + + sourceSets.main { + java.srcDirs "../src", "src" + res.srcDirs "../res", 'res', "${RES_PATH}/proj/res" + jniLibs.srcDirs "../libs", 'libs' + manifest.srcFile "AndroidManifest.xml" + assets.srcDir "${RES_PATH}/data" + } + + externalNativeBuild { + cmake { + version "3.22.1" + path "../CMakeLists.txt" + buildStagingDirectory "${RES_PATH}/proj/build" + } + } + + signingConfigs { + + release { + if (project.hasProperty("RELEASE_STORE_FILE") && !RELEASE_STORE_FILE.isEmpty()) { + storeFile file(RELEASE_STORE_FILE) + storePassword RELEASE_STORE_PASSWORD + keyAlias RELEASE_KEY_ALIAS + keyPassword RELEASE_KEY_PASSWORD + } + } + } + + buildTypes { + release { + debuggable false + jniDebuggable false + renderscriptDebuggable false + minifyEnabled true + shrinkResources true + proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' + if (project.hasProperty("RELEASE_STORE_FILE")) { + signingConfig signingConfigs.release + } + + externalNativeBuild { + cmake { + // switch HIDE_SYMBOLS to OFF to skip compilation flag `-fvisibility=hidden` + arguments "-DHIDE_SYMBOLS=ON" + } + } + } + + debug { + debuggable true + jniDebuggable true + renderscriptDebuggable true + // resValue "string", "app_name", "${PROP_APP_NAME}-dbg" + // applicationIdSuffix ".debug" + } + } +} + +dependencies { + implementation fileTree(dir: '../libs', include: ['*.jar','*.aar']) + implementation fileTree(dir: 'libs', include: ['*.jar','*.aar']) + implementation fileTree(dir: "${COCOS_ENGINE_PATH}/cocos/platform/android/java/libs", include: ['*.jar']) + implementation project(':libservice') + implementation project(':libcocos') +} diff --git a/templates/google-play/template/instantapp/proguard-rules.pro b/templates/google-play/template/instantapp/proguard-rules.pro new file mode 100644 index 00000000000..fc884b76762 --- /dev/null +++ b/templates/google-play/template/instantapp/proguard-rules.pro @@ -0,0 +1,54 @@ +# Add project specific ProGuard rules here. +# By default, the flags in this file are appended to flags specified +# in E:\developSoftware\Android\SDK/tools/proguard/proguard-android.txt +# You can edit the include path and order by changing the proguardFiles +# directive in build.gradle. +# +# For more details, see +# http://developer.android.com/guide/developing/tools/proguard.html + +# Add any project specific keep options here: + +# If your project uses WebView with JS, uncomment the following +# and specify the fully qualified class name to the JavaScript interface +# class: +#-keepclassmembers class fqcn.of.javascript.interface.for.webview { +# public *; +#} + +# Proguard Cocos2d-x-lite for release +-keep public class com.cocos.** { *; } +-dontwarn com.cocos.** + +# Proguard Apache HTTP for release +-keep class org.apache.http.** { *; } +-dontwarn org.apache.http.** + +# Proguard okhttp for release +-keep class okhttp3.** { *; } +-dontwarn okhttp3.** + +-keep class okio.** { *; } +-dontwarn okio.** + +# Proguard Android Webivew for release. you can comment if you are not using a webview +-keep public class android.net.http.SslError +-keep public class android.webkit.WebViewClient + +-keep public class com.google.** { *; } + +-dontwarn android.webkit.WebView +-dontwarn android.net.http.SslError +-dontwarn android.webkit.WebViewClient + +# This is generated automatically by the Android Gradle plugin. +-dontwarn android.hardware.BatteryState +-dontwarn android.hardware.lights.Light +-dontwarn android.hardware.lights.LightState$Builder +-dontwarn android.hardware.lights.LightState +-dontwarn android.hardware.lights.LightsManager$LightsSession +-dontwarn android.hardware.lights.LightsManager +-dontwarn android.hardware.lights.LightsRequest$Builder +-dontwarn android.hardware.lights.LightsRequest +-dontwarn android.net.ssl.SSLSockets +-dontwarn android.os.VibratorManager \ No newline at end of file diff --git a/templates/google-play/template/instantapp/src/com/cocos/game/InstantActivity.java b/templates/google-play/template/instantapp/src/com/cocos/game/InstantActivity.java new file mode 100644 index 00000000000..318e8398d31 --- /dev/null +++ b/templates/google-play/template/instantapp/src/com/cocos/game/InstantActivity.java @@ -0,0 +1,125 @@ +/**************************************************************************** +Copyright (c) 2015-2016 Chukong Technologies Inc. +Copyright (c) 2017-2018 Xiamen Yaji Software Co., Ltd. + +http://www.cocos2d-x.org + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. +****************************************************************************/ +package com.cocos.game; + +import android.os.Bundle; +import android.content.Intent; +import android.content.res.Configuration; + +import com.cocos.service.SDKWrapper; +import com.cocos.lib.CocosActivity; + +public class InstantActivity extends CocosActivity { + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + // DO OTHER INITIALIZATION BELOW + SDKWrapper.shared().init(this); + + } + + @Override + protected void onResume() { + super.onResume(); + SDKWrapper.shared().onResume(); + } + + @Override + protected void onPause() { + super.onPause(); + SDKWrapper.shared().onPause(); + } + + @Override + protected void onDestroy() { + super.onDestroy(); + // Workaround in https://stackoverflow.com/questions/16283079/re-launch-of-activity-on-home-button-but-only-the-first-time/16447508 + if (!isTaskRoot()) { + return; + } + SDKWrapper.shared().onDestroy(); + } + + @Override + protected void onActivityResult(int requestCode, int resultCode, Intent data) { + super.onActivityResult(requestCode, resultCode, data); + SDKWrapper.shared().onActivityResult(requestCode, resultCode, data); + } + + @Override + protected void onNewIntent(Intent intent) { + super.onNewIntent(intent); + SDKWrapper.shared().onNewIntent(intent); + } + + @Override + protected void onRestart() { + super.onRestart(); + SDKWrapper.shared().onRestart(); + } + + @Override + protected void onStop() { + super.onStop(); + SDKWrapper.shared().onStop(); + } + + @Override + public void onBackPressed() { + SDKWrapper.shared().onBackPressed(); + super.onBackPressed(); + } + + @Override + public void onConfigurationChanged(Configuration newConfig) { + SDKWrapper.shared().onConfigurationChanged(newConfig); + super.onConfigurationChanged(newConfig); + } + + @Override + protected void onRestoreInstanceState(Bundle savedInstanceState) { + SDKWrapper.shared().onRestoreInstanceState(savedInstanceState); + super.onRestoreInstanceState(savedInstanceState); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + SDKWrapper.shared().onSaveInstanceState(outState); + super.onSaveInstanceState(outState); + } + + @Override + protected void onStart() { + SDKWrapper.shared().onStart(); + super.onStart(); + } + + @Override + public void onLowMemory() { + SDKWrapper.shared().onLowMemory(); + super.onLowMemory(); + } +} diff --git a/templates/google-play/template/res/mipmap-hdpi/ic_launcher.png b/templates/google-play/template/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..888c0d56abef8d4ca9123954777b6f820c967b6f GIT binary patch literal 6780 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 literal 0 HcmV?d00001 diff --git a/templates/google-play/template/res/mipmap-xxhdpi/ic_launcher.png b/templates/google-play/template/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000000000000000000000000000000000000..2f1e5a6b3776489f893d8ed4768ae9fe356b4df8 GIT binary patch literal 16236 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‹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 literal 0 HcmV?d00001 diff --git a/templates/google-play/template/res/values/strings.xml b/templates/google-play/template/res/values/strings.xml new file mode 100644 index 00000000000..8542005550c --- /dev/null +++ b/templates/google-play/template/res/values/strings.xml @@ -0,0 +1,2 @@ + + diff --git a/tsconfig.json b/tsconfig.json index 273a16fd24f..369cb10f46b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -55,6 +55,7 @@ "exports/**/*.ts", "editor/exports/**/*.ts", "typedoc-index.ts", - "pal/**/*.ts" + "pal/**/*.ts", + "vendor/**/*.ts" ] } diff --git a/typedoc-index.ts b/typedoc-index.ts index f5f38d1b343..c25b3950dbe 100644 --- a/typedoc-index.ts +++ b/typedoc-index.ts @@ -29,3 +29,4 @@ export * from './exports/video'; export * from './exports/webview'; export * from './exports/sorting'; export * from './exports/xr'; +export * from './exports/vendor-google'; diff --git a/vendor/google/billing/billing.ts b/vendor/google/billing/billing.ts new file mode 100644 index 00000000000..c31f64ad209 --- /dev/null +++ b/vendor/google/billing/billing.ts @@ -0,0 +1,829 @@ +/**************************************************************************** + Copyright (c) 2024 Xiamen Yaji Software Co., Ltd. + + http://www.cocos.com + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*****************************************************************************/ +import { JSB } from 'internal:constants'; +import { EventTarget } from '../../../cocos/core/event'; + +interface BillingEventMap { + [google.BillingEventType.BILLING_SETUP_FINISHED]: (result: google.BillingResult) => void, + [google.BillingEventType.BILLING_SERVICE_DISCONNECTED]: () => void, + [google.BillingEventType.PRODUCT_DETAILS_RESPONSE]: + (result: google.BillingResult, productDetailsList: google.ProductDetails[]) => void, + [google.BillingEventType.PURCHASES_UPDATED]: (result: google.BillingResult, purchases: google.Purchase[]) => void, + [google.BillingEventType.CONSUME_RESPONSE]: (result: google.BillingResult, purchaseToken: string) => void, + [google.BillingEventType.ACKNOWLEDGE_PURCHASES_RESPONSE]: (result: google.BillingResult) => void + [google.BillingEventType.QUERY_PURCHASES_RESPONSE]: (result: google.BillingResult, purchases: google.Purchase[]) => void, + [google.BillingEventType.BILLING_CONFIG_RESPONSE]: (result: google.BillingResult, config: google.BillingConfig) => void + [google.BillingEventType.ALTERNATIVE_BILLING_ONLY_TOKEN_RESPONSE]: + (result: google.BillingResult, alternativeBillingOnlyReportingDetails: google.AlternativeBillingOnlyReportingDetails) => void + [google.BillingEventType.EXTERNAL_OFFER_REPORTING_DETAILS_RESPONSE]: + (result: google.BillingResult, externalOfferReportingDetails: google.ExternalOfferReportingDetails) => void + [google.BillingEventType.ALTERNATIVE_BILLING_ONLY_AVAILABILITY_RESPONSE]: (result: google.BillingResult) => void + [google.BillingEventType.EXTERNAL_OFFER_AVAILABILITY_RESPONSE]: (result: google.BillingResult) => void + [google.BillingEventType.ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG_RESPONSE]: (result: google.BillingResult) => void + [google.BillingEventType.EXTERNAL_OFFER_INFORMATION_DIALOG_RESPONSE]: (result: google.BillingResult) => void + [google.BillingEventType.IN_APP_MESSAGE_RESPONSE]: (result: google.InAppMessageResult) => void +} + +class Billing { + private _eventTarget: EventTarget = new EventTarget(); + constructor () { + if (!JSB || !jsb.googleBilling) { + return; + } + jsb.onBillingSetupFinished = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.BILLING_SETUP_FINISHED, result); + }; + + jsb.onBillingServiceDisconnected = (): void => { + this._eventTarget.emit(google.BillingEventType.BILLING_SERVICE_DISCONNECTED); + }; + + jsb.onProductDetailsResponse = ( + result: google.BillingResult, + productDetailsList: google.ProductDetails[], + ): void => { + this._eventTarget.emit(google.BillingEventType.PRODUCT_DETAILS_RESPONSE, result, productDetailsList); + }; + + jsb.onPurchasesUpdated = ( + result: google.BillingResult, + purchaseList: google.Purchase[], + ): void => { + this._eventTarget.emit(google.BillingEventType.PURCHASES_UPDATED, result, purchaseList); + }; + + jsb.onConsumeResponse = ( + result: google.BillingResult, + purchaseToken: string, + ): void => { + this._eventTarget.emit(google.BillingEventType.CONSUME_RESPONSE, result, purchaseToken); + }; + + jsb.onAcknowledgePurchaseResponse = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.ACKNOWLEDGE_PURCHASES_RESPONSE, result); + }; + + jsb.onQueryPurchasesResponse = ( + result: google.BillingResult, + purchaseList: google.Purchase[], + ): void => { + this._eventTarget.emit(google.BillingEventType.QUERY_PURCHASES_RESPONSE, result, purchaseList); + }; + + jsb.onBillingConfigResponse = (result: google.BillingResult, config: google.BillingConfig): void => { + this._eventTarget.emit(google.BillingEventType.BILLING_CONFIG_RESPONSE, result, config); + }; + + jsb.onAlternativeBillingOnlyTokenResponse = ( + result: google.BillingResult, + details: google.AlternativeBillingOnlyReportingDetails, + ): void => { + this._eventTarget.emit(google.BillingEventType.ALTERNATIVE_BILLING_ONLY_TOKEN_RESPONSE, result, details); + }; + + jsb.onExternalOfferReportingDetailsResponse = (result: google.BillingResult, details: google.ExternalOfferReportingDetails): void => { + this._eventTarget.emit(google.BillingEventType.EXTERNAL_OFFER_REPORTING_DETAILS_RESPONSE, result, details); + }; + + jsb.onAlternativeBillingOnlyAvailabilityResponse = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.ALTERNATIVE_BILLING_ONLY_AVAILABILITY_RESPONSE, result); + }; + + jsb.onExternalOfferAvailabilityResponse = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.EXTERNAL_OFFER_AVAILABILITY_RESPONSE, result); + }; + + jsb.onAlternativeBillingOnlyInformationDialogResponse = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG_RESPONSE, result); + }; + + jsb.onExternalOfferInformationDialogResponse = (result: google.BillingResult): void => { + this._eventTarget.emit(google.BillingEventType.EXTERNAL_OFFER_INFORMATION_DIALOG_RESPONSE, result); + }; + + jsb.onInAppMessageResponse = (result: google.InAppMessageResult): void => { + this._eventTarget.emit(google.BillingEventType.IN_APP_MESSAGE_RESPONSE, result); + }; + } + + /** + * @en Starts up BillingClient setup process asynchronously. + * @zh 异步启动 BillingClient 设置过程。 + */ + public startConnection (): void { + jsb.googleBilling?.startConnection(); + } + + /** + * @en Closes the connection and releases all held resources such as service connections. + * @zh 关闭连接并释放所有持有的资源,例如服务连接。 + */ + public endConnection (): void { + jsb.googleBilling?.endConnection(); + } + + /** + * @en Get the current billing client connection state. + * @zh 获取当前billing客户端连接状态。 + */ + public getConnectionState (): number { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.getConnectionState(); + } + return google.ConnectionState.DISCONNECTED; + } + + /** + * @en Checks if the client is currently connected to the service, so that requests to other methods will succeed. + Returns true if the client is currently connected to the service, false otherwise. + * @zh 检查客户端当前是否连接到服务,以便对其他方法的请求能够成功。 + 如果客户端当前已连接到服务,则返回 true,否则返回 false。 + */ + public isReady (): boolean { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.isReady(); + } + return false; + } + + /** + * @en Performs a network query the details of products available for sale in your app. + * @zh 执行网络查询您的应用中可供销售的产品的详细信息。 + * @param productId @zh 产品ID。 @en product id. + * @param productType @zh 产品类型。 @en product type. + * + */ + public queryProductDetailsParams (productId: string[], productType: google.ProductType): void { + jsb.googleBilling?.queryProductDetailsParams(productId, productType); + } + + /** + * @en Initiates the billing flow for an in-app purchase or subscription. + * @zh 启动应用内购买或订阅的计费流程。 + * @param productDetails @zh 产品详情。 @en product details. + * @param selectedOfferToken @zh 选择提供的token。 @en selected offer token. + */ + public launchBillingFlow (productDetails: google.ProductDetails[], selectedOfferToken: string | null): void { + jsb.googleBilling?.launchBillingFlow(productDetails, selectedOfferToken); + } + + /** + * @en Consumes a given in-app product. + * @zh 消费指定的应用内产品。 + * @param purchase @zh 已经购买的产品。 @en Purchased Products. + */ + public consumePurchases (purchase: google.Purchase[]): void { + jsb.googleBilling?.consumePurchases(purchase); + } + + /** + * @en Acknowledges in-app purchases. + * @zh 确认应用内购买。 + * @param purchase @zh 已经购买的产品。 @en Purchased Products. + */ + public acknowledgePurchase (purchase: google.Purchase[]): void { + jsb.googleBilling?.acknowledgePurchase(purchase); + } + + /** + * @en Returns purchases details for currently owned items bought within your app. + * @zh 返回您应用内当前拥有的购买商品的购买详情。 + * @param productType @zh 产品类型 @en Product type. + */ + public queryPurchasesAsync (productType: google.ProductType): void { + jsb.googleBilling?.queryPurchasesAsync(productType); + } + + /** + * @en Gets the billing config, which stores configuration used to perform billing operations. + * @zh 获取计费配置,其中存储用于执行计费操作的配置。 + */ + public getBillingConfigAsync (): void { + jsb.googleBilling?.getBillingConfigAsync(); + } + + /** + * @en Creates alternative billing only purchase details that can be used to report a transaction made + * via alternative billing without user choice to use Google Play billing. + * @zh 创建仅限替代结算的购买详情,可用于报告通过替代结算进行的交易,而无需用户选择使用 Google Play 结算。 + */ + public createAlternativeBillingOnlyReportingDetailsAsync (): void { + jsb.googleBilling?.createAlternativeBillingOnlyReportingDetailsAsync(); + } + + /** + * @en Checks the availability of offering alternative billing without user choice to use Google Play billing. + * @zh 检查是否可以提供替代结算方式,而无需用户选择使用 Google Play 结算方式。 + */ + public isAlternativeBillingOnlyAvailableAsync (): void { + jsb.googleBilling?.isAlternativeBillingOnlyAvailableAsync(); + } + + /** + * @en Creates purchase details that can be used to report a transaction made via external offer. + * @zh 创建可用于报告通过外部报价进行的交易的购买详情。 + */ + public createExternalOfferReportingDetailsAsync (): void { + jsb.googleBilling?.createExternalOfferReportingDetailsAsync(); + } + + /** + * @en Checks the availability of providing external offer. + * @zh 检查提供外部报价的可用性。 + */ + public isExternalOfferAvailableAsync (): void { + jsb.googleBilling?.isExternalOfferAvailableAsync(); + } + + /** + * @en Checks if the specified feature or capability is supported by the Play Store. + * @zh 检查 Play Store 是否支持指定的功能。 + * @param feature @zh 功能特性 @en feature. + */ + public isFeatureSupported (feature: string): google.BillingResult | null { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.isFeatureSupported(feature); + } + return null; + } + + /** + * @en Shows the alternative billing only information dialog on top of the calling app. + * @zh 在调用应用程序顶部显示仅显示备用计费信息对话框。 + */ + public showAlternativeBillingOnlyInformationDialog (): google.BillingResult | null { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.showAlternativeBillingOnlyInformationDialog(); + } + return null; + } + + /** + * @en Shows the external offer information dialog on top of the calling app. + * @zh 在调用应用程序顶部显示外部优惠信息对话框。 + */ + public showExternalOfferInformationDialog (): google.BillingResult | null { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.showExternalOfferInformationDialog(); + } + return null; + } + + /** + * @en Overlays billing related messages on top of the calling app. + * @zh 在调用应用程序上叠加与计费相关的消息。 + */ + public showInAppMessages (): google.BillingResult | null { + if (jsb.googleBilling) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-return + return jsb.googleBilling.showInAppMessages(); + } + return null; + } + + public on (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] { + this._eventTarget.on(type, callback, target); + return callback; + } + public once (type: K, callback: BillingEventMap[K], target?: unknown): BillingEventMap[K] { + this._eventTarget.once(type, callback, target); + return callback; + } + public off (eventType: K, callback?: BillingEventMap[K], target?: any): void { + this._eventTarget.off(eventType, callback, target); + } +} + +const gpBilling = new Billing(); + +export namespace google { + /** + * @en Google Play Billing event type + * @zh Google Play Billing事件类型 + */ + export enum BillingEventType { + /** + * @en + * Called to notify that setup is complete. + * + * @zh + * 当安装已经完成时触发。 + */ + BILLING_SETUP_FINISHED = 'billing_setup_finished', + /** + * @en + * Called to notify that the connection to the billing service was lost. + * + * @zh + * 当Billing服务连接断开时触发。 + */ + BILLING_SERVICE_DISCONNECTED = 'billing_service_disconnected', + /** + * @en + * Listen to this event to get notifications of purchase updates. + * + * @zh + * 监听这个事件可以获取购买更新。 + */ + PURCHASES_UPDATED = 'purchases_updated', + /** + * @en + * Called to notify that query product details operation has finished. + * + * @zh + * 查询产品详细信息操作完成时触发。 + */ + PRODUCT_DETAILS_RESPONSE = 'product_details_response', + /** + * @en + * Called to notify that the query purchases operation has finished. + * + * @zh + * 查询购买操作完成时触发。 + */ + QUERY_PURCHASES_RESPONSE = 'query_purchases_response', + /** + * @en + * Called to notify that a consume operation has finished. + * + * @zh + * 消费操作完成时触发。 + */ + CONSUME_RESPONSE = 'consume_response', + /** + * @en + * Called to notify that an acknowledge purchase operation has finished. + * + * @zh + * 确认购买操作完成时触发。 + */ + ACKNOWLEDGE_PURCHASES_RESPONSE = 'acknowledge_purchases_response', + /** + * @en + * Called to notify when the get billing config flow has finished. + * + * @zh + * 获取Billing配置流程完成时触发。 + */ + BILLING_CONFIG_RESPONSE = 'billing_config_response', + /** + * @en + * Called to receive the results from createAlternativeBillingOnlyReportingDetailsAsync when it is finished. + * + * @zh + * 当调用createAlternativeBillingOnlyReportingDetailsAsync接口完成时触发,可以接收调用结果。 + */ + ALTERNATIVE_BILLING_ONLY_TOKEN_RESPONSE = 'alternative_billing_only_token_response', + /** + * @en + * Called to receive the results from createExternalOfferReportingDetailsAsync when it is finished. + * + * @zh + * 当调用createExternalOfferReportingDetailsAsync接口完成时触发,可以接收调用结果。 + */ + EXTERNAL_OFFER_REPORTING_DETAILS_RESPONSE = 'external_offer_reporting_details_response', + /** + * @en + * Called to receive the results from BillingClient#isAlternativeBillingOnlyAvailableAsync when it is finished. + * + * @zh + * 当调用BillingClient#isAlternativeBillingOnlyAvailableAsync接口完成时触发,可以接收调用结果。 + */ + ALTERNATIVE_BILLING_ONLY_AVAILABILITY_RESPONSE = 'alternative_billing_only_availability_response', + /** + * @en + * Called to receive the results from BillingClient#isExternalOfferAvailableAsync when it is finished. + * + * @zh + * 当调用BillingClient#isExternalOfferAvailableAsync接口完成时触发,可以接收调用结果。 + */ + EXTERNAL_OFFER_AVAILABILITY_RESPONSE = 'external_offer_availability_response', + /** + * @en + * Called to notify that the alternative billing only dialog flow is finished. + * + * @zh + * 当仅替代Billing对话流程已完成时触发。 + */ + ALTERNATIVE_BILLING_ONLY_INFORMATION_DIALOG_RESPONSE = 'alternative_billing_only_information_dialog_response', + /** + * @en + * Called to notify that the external offer information dialog flow is finished. + * + * @zh + * 当外部报价信息对话流程已完成时触发。 + */ + EXTERNAL_OFFER_INFORMATION_DIALOG_RESPONSE = 'external_offer_information_dialog_response', + /** + * @en + * Called to notify when the in-app messaging flow has finished. + * + * @zh + * 当应用内消息流程完成时触发。 + */ + IN_APP_MESSAGE_RESPONSE = 'in_app_message_response', + + } + + /** + * @en + * Supported Product types. + * + * @zh + * 支持的产品类型。 + */ + export enum ProductType { + /** + * @en + * A Product type for Android apps in-app products. + * + * @zh + * Android 应用内产品的产品类型。 + */ + INAPP = 'inapp', + /** + * @en + * A Product type for Android apps subscriptions. + * + * @zh + * Android 应用程序订阅的产品类型。 + */ + SUBS = 'subs' + } + + /** + * @en + * Possible response codes. + * + * @zh + * 可能的响应代码。 + */ + export enum BillingResponseCode { + /** + * @en + * This field is deprecated. + * See SERVICE_UNAVAILABLE which will be used instead of this code. + * + * @zh + * 这个字段已经废弃。 + * 看看SERVICE_UNAVAILABLE将使用哪一个来代替此代码。 + */ + SERVICE_TIMEOUT = -3, + /** + * @en + * The requested feature is not supported by the Play Store on the current device. + * + * @zh + * 当前设备上的 Play Store 不支持所请求的功能。 + */ + FEATURE_NOT_SUPPORTED = -2, + /** + * @en + * The app is not connected to the Play Store service via the Google Play Billing Library. + * + * @zh + * 该应用未通过 Google Play Billing库连接到 Play Store 服务。 + */ + SERVICE_DISCONNECTED = -1, + /** + * @en + * Success. + * + * @zh + * 成功。 + */ + OK = 0, + /** + * @en + * Transaction was canceled by the user. + * + * @zh + * 交易已被用户取消。 + */ + USER_CANCELED = 1, + /** + * @en + * The service is currently unavailable. + * + * @zh + * 当前设备上的 Play Store 不支持所请求的功能。 + */ + SERVICE_UNAVAILABLE = 2, + /** + * @en + * A user billing error occurred during processing. + * + * @zh + * 处理过程中出现用户billing错误。 + */ + BILLING_UNAVAILABLE = 3, + /** + * @en + * The requested product is not available for purchase. + * + * @zh + * 所请求的产品无法购买。 + */ + ITEM_UNAVAILABLE = 4, + /** + * @en + * Error resulting from incorrect usage of the API. + * + * @zh + * 由于错误使用 API 而导致的错误。 + */ + DEVELOPER_ERROR = 5, + /** + * @en + * Fatal error during the API action. + * + * @zh + * API 操作期间发生致命错误。 + */ + ERROR = 6, + /** + * @en + * The purchase failed because the item is already owned. + * + * @zh + * 购买失败,因为该物品已被拥有。 + */ + ITEM_ALREADY_OWNED = 7, + /** + * @en + * Requested action on the item failed since it is not owned by the user. + * + * @zh + * 由于该项目不属于用户,因此对该项目请求的操作失败。 + */ + ITEM_NOT_OWNED = 8, + /** + * @en + * A network error occurred during the operation. + * + * @zh + * 操作期间发生网络错误。 + */ + NETWORK_ERROR = 12, + } + + /** + * @en + * Recurrence mode of the pricing phase. + * + * @zh + * 定价阶段的复现模式。 + */ + export enum RecurrenceMode { + /** + * @en + * The billing plan payment recurs for infinite billing periods unless cancelled. + * + * @zh + * 除非取消,否则billing计划付款将无限期地重复。 + */ + INFINITE_RECURRING = 1, + /** + * @en + * The billing plan payment recurs for a fixed number of billing period set in billingCycleCount. + * + * @zh + * Billing计划付款将在 billingCycleCount 中设置的固定计费周期内重复发生。 + */ + FINITE_RECURRING = 2, + /** + * @en + * The billing plan payment is a one time charge that does not repeat. + * + * @zh + * Billing计划付款是一次性费用,不会重复。 + */ + NON_RECURRING = 3, + } + + /** + * @en + * Connection state of billing client. + * + * @zh + * Billing client的连接状态 + */ + export enum ConnectionState { + /** + * @en + * This client was not yet connected to billing service or was already closed. + * + * @zh + * 此客户端尚未连接到Billing服务或已关闭。 + */ + DISCONNECTED = 0, + /** + * @en + * This client is currently in process of connecting to billing service. + * + * @zh + * 此客户端目前正在连接到Billing服务。 + */ + CONNECTING = 1, + /** + * @en + * This client is currently connected to billing service. + * + * @zh + * 此客户端当前已连接到Billing服务。 + */ + CONNECTED = 2, + /** + * @en + * This client was already closed and shouldn't be used again. + * + * @zh + * 该客户端已关闭,不应再次使用。 + */ + CLOSED = 3, + } + + /** + * @en + * Features/capabilities supported by isFeatureSupported. + * + * @zh + * 支持的特性/能力isFeatureSupported。 + */ + export enum FeatureType { + /** + * @en + * Alternative billing only. + * + * @zh + * 仅限替代Billing。 + */ + ALTERNATIVE_BILLING_ONLY = 'jjj', + /** + * @en + * Get billing config. + * + * @zh + * 获取计费配置。 + */ + BILLING_CONFIG = 'ggg', + /** + * @en + * Play billing library support for external offer. + * + * @zh + * Play billing库支持外部报价。 + */ + EXTERNAL_OFFER = 'kkk', + /** + * @en + * Show in-app messages. + * + * @zh + * 显示应用内消息。 + */ + IN_APP_MESSAGING = 'bbb', + /** + * @en + * Launch a price change confirmation flow. + * + * @zh + * 启动价格变动确认流程。 + */ + PRICE_CHANGE_CONFIRMATION = 'priceChangeConfirmation', + /** + * @en + * Play billing library support for querying and purchasing. + * + * @zh + * Play Billing库支持查询、购买。 + */ + PRODUCT_DETAILS = 'fff', + /** + * @en + * Purchase/query for subscriptions. + * + * @zh + * 购买/查询订阅。 + */ + SUBSCRIPTIONS = 'subscriptions', + /** + * @en + * Subscriptions update/replace. + * + * @zh + * 订阅更新/替换。 + */ + UBSCRIPTIONS_UPDATE = 'subscriptionsUpdate', + } + + /** + * @en + * Possible purchase states. + * + * @zh + * 可能的购买状态。 + */ + export enum PurchaseState { + /** + * @en + * Purchase is pending and not yet completed to be processed by your app. + * + * @zh + * 购买处于待处理状态且尚未完成,无法由您的应用程序处理。 + */ + PENDING = 2, + /** + * @en + * Purchase is completed.. + * + * @zh + * 购买完成。 + */ + PURCHASED = 1, + /** + * @en + * Purchase with unknown state. + * + * @zh + * 未知状态 + */ + UNSPECIFIED_STATE = 0, + } + + /** + * @en + * Possible response codes. + * + * @zh + * InAppMessage可能的影响代码。 + */ + export enum InAppMessageResponseCode { + /** + * @en + * The flow has finished and there is no action needed from developers. + * + * @zh + * 流程已完成,开发人员无需采取任何行动。 + */ + NO_ACTION_NEEDED = 0, + /** + * @en + * The subscription status changed. + * + * @zh + * 订阅状态已改变。 + */ + SUBSCRIPTION_STATUS_UPDATED = 1, + } + + export type BillingResult = jsb.BillingResult; + export type OneTimePurchaseOfferDetails = jsb.OneTimePurchaseOfferDetails; + export type InstallmentPlanDetails = jsb.InstallmentPlanDetails; + export type PricingPhase = jsb.PricingPhase; + export type SubscriptionOfferDetails = jsb.SubscriptionOfferDetails; + export type ProductDetails = jsb.ProductDetails; + export type AccountIdentifiers = jsb.AccountIdentifiers; + export type PendingPurchaseUpdate = jsb.PendingPurchaseUpdate; + export type Purchase = jsb.Purchase; + export type BillingConfig = jsb.BillingConfig; + export type AlternativeBillingOnlyReportingDetails = jsb.AlternativeBillingOnlyReportingDetails; + export type ExternalOfferReportingDetails = jsb.ExternalOfferReportingDetails; + export type InAppMessageResult = jsb.InAppMessageResult; + + /** + * @en + * Interface for Google Play blling module. + * + * @zh + * Google Play blling模块的接口。 + * + */ + export const billing = gpBilling; +} diff --git a/vendor/google/index.ts b/vendor/google/index.ts new file mode 100644 index 00000000000..51359d6f468 --- /dev/null +++ b/vendor/google/index.ts @@ -0,0 +1,25 @@ +/* + Copyright (c) 2024 Xiamen Yaji Software Co., Ltd. + + https://www.cocos.com/ + + Permission is hereby granted, free of charge, to any person obtaining a copy + of this software and associated documentation files (the "Software"), to deal + in the Software without restriction, including without limitation the rights to + use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies + of the Software, and to permit persons to whom the Software is furnished to do so, + subject to the following conditions: + + The above copyright notice and this permission notice shall be included in + all copies or substantial portions of the Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN + THE SOFTWARE. +*/ + +export * from './billing/billing'; From 5ffedf858a1adbd51c9abfac3172ce6a64a32e29 Mon Sep 17 00:00:00 2001 From: hyde zhou Date: Fri, 29 Nov 2024 15:34:46 +0800 Subject: [PATCH 09/13] break circular dependency (#17991) --- native/cocos/scene/RenderScene.cpp | 2 -- native/cocos/scene/RenderScene.h | 1 - 2 files changed, 3 deletions(-) diff --git a/native/cocos/scene/RenderScene.cpp b/native/cocos/scene/RenderScene.cpp index a01c95f8671..f31babe063d 100644 --- a/native/cocos/scene/RenderScene.cpp +++ b/native/cocos/scene/RenderScene.cpp @@ -118,13 +118,11 @@ RenderScene::~RenderScene() = default; void RenderScene::activate() { const auto *sceneData = Root::getInstance()->getPipeline()->getPipelineSceneData(); _octree = sceneData->getOctree(); - _rayTracing->activate(); } bool RenderScene::initialize(const IRenderSceneInfo &info) { _name = info.name; _lodStateCache = ccnew LodStateCache(this); - _rayTracing = ccnew raytracing::RayTracing(this); return true; } diff --git a/native/cocos/scene/RenderScene.h b/native/cocos/scene/RenderScene.h index 13f6be6ca9c..109d8926096 100644 --- a/native/cocos/scene/RenderScene.h +++ b/native/cocos/scene/RenderScene.h @@ -131,7 +131,6 @@ class RenderScene : public RefCounted { uint64_t _modelId{0}; IntrusivePtr _mainLight; IntrusivePtr _lodStateCache; - IntrusivePtr _rayTracing; ccstd::vector> _models; ccstd::vector> _cameras; ccstd::vector> _directionalLights; From 2aafef2d60e9c2ad0c23a3e25406bf6f9d60f70b Mon Sep 17 00:00:00 2001 From: James Chen Date: Mon, 2 Dec 2024 14:48:12 +0800 Subject: [PATCH 10/13] Fix that assets in scene configured with `autorelease` could not be auto released after switching scene. It's a memory leak from v3.8.1. (#17993) --- cocos/scene-graph/node.jsb.ts | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/cocos/scene-graph/node.jsb.ts b/cocos/scene-graph/node.jsb.ts index 8fd82f1dd5e..4b99c37f42b 100644 --- a/cocos/scene-graph/node.jsb.ts +++ b/cocos/scene-graph/node.jsb.ts @@ -1171,6 +1171,14 @@ Object.defineProperty(nodeProto, 'scene', { } }); +Object.defineProperty(nodeProto, 'id', { + configurable: true, + enumerable: true, + set(id) { + this._id = id; + } +}); + nodeProto.rotate = function (rot: Quat, ns?: NodeSpace): void { _tempFloatArray[1] = rot.x; _tempFloatArray[2] = rot.y; From fb4d2b07ab397f52a1c899d9c527613c66d13d2f Mon Sep 17 00:00:00 2001 From: James Chen Date: Tue, 3 Dec 2024 09:35:25 +0800 Subject: [PATCH 11/13] Fix memory leaks of NSObjects on macOS if gfx device thread is disabled. (#17997) --- native/cocos/platform/mac/MacPlatform.mm | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/native/cocos/platform/mac/MacPlatform.mm b/native/cocos/platform/mac/MacPlatform.mm index 722b358f2ed..4d0b0ab84df 100644 --- a/native/cocos/platform/mac/MacPlatform.mm +++ b/native/cocos/platform/mac/MacPlatform.mm @@ -79,12 +79,16 @@ of this software and associated engine source code (the "Software"), a limited, int32_t MacPlatform::loop(void) { #if CC_EDITOR - runTask(); + @autoreleasepool { + runTask(); + } return 1; #else while(!_readyToExit) { - pollEvent(); - runTask(); + @autoreleasepool { + pollEvent(); + runTask(); + } } onDestroy(); return 0; @@ -95,7 +99,9 @@ of this software and associated engine source code (the "Software"), a limited, #if defined(CC_SERVER_MODE) cocos_main(argc, argv); while (true) { - runTask(); + @autoreleasepool { + runTask(); + } } return 0; #else From b4a5a7c090f837f6e3c3e777a5ab413d780143aa Mon Sep 17 00:00:00 2001 From: James Chen Date: Tue, 3 Dec 2024 19:46:50 +0800 Subject: [PATCH 12/13] [v3.8.5] Fix light probe could not work. (#17999) It's a bug starts from v3.8.5 branch: https://github.com/cocos/cocos-engine/pull/17703 --- cocos/core/math/vec3.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/cocos/core/math/vec3.ts b/cocos/core/math/vec3.ts index 8533bede8b8..5767065eec4 100644 --- a/cocos/core/math/vec3.ts +++ b/cocos/core/math/vec3.ts @@ -955,7 +955,11 @@ export class Vec3 extends ValueType { * @returns Returns `true` when the components of both vectors are equal within the specified range of error; otherwise it returns `false`. */ public equals (other: Vec3, epsilon = EPSILON): boolean { - return Vec3.equals(this, other, epsilon); + return ( + abs(this.x - other.x) <= epsilon + && abs(this.y - other.y) <= epsilon + && abs(this.z - other.z) <= epsilon + ); } /** From 0ec0ec3597004fffafcf649b9faf086cdd104b9b Mon Sep 17 00:00:00 2001 From: James Chen Date: Thu, 5 Dec 2024 09:17:16 +0800 Subject: [PATCH 13/13] fixed #18000: [v3.8.5][bug] Nested tween with different target in parallel action could not work correctly. (#18001) * fixed #18000: [v3.8.5][bug] Nested tween with different target in parallel action could not work correctly. * Update action-interval.ts * Reset the length of the temporary action array to zero after being used. --- cocos/tween/actions/action-instant.ts | 10 +- cocos/tween/actions/action-interval.ts | 28 +++-- cocos/tween/actions/action.ts | 83 ++++++++------- cocos/tween/tween-action.ts | 8 +- cocos/tween/tween.ts | 40 ++++---- tests/tween/tween.test.ts | 135 ++++++++++++++++++++++++- 6 files changed, 223 insertions(+), 81 deletions(-) diff --git a/cocos/tween/actions/action-instant.ts b/cocos/tween/actions/action-instant.ts index 69f4fe81a18..f0cbaf2bb60 100644 --- a/cocos/tween/actions/action-instant.ts +++ b/cocos/tween/actions/action-instant.ts @@ -72,7 +72,7 @@ export abstract class ActionInstant extends FiniteTimeAction { */ export class Show extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -112,7 +112,7 @@ export function show (): Show { */ export class Hide extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -152,7 +152,7 @@ export function hide (): Hide { */ export class ToggleVisibility extends ActionInstant { update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; const _renderComps = target.getComponentsInChildren(Renderer); for (let i = 0; i < _renderComps.length; ++i) { @@ -204,7 +204,7 @@ export class RemoveSelf extends ActionInstant { } update (_dt: number): void { - const target = (this.workerTarget ?? this.target) as T; + const target = this._getWorkerTarget(); if (!target) return; target.removeFromParent(); if (this._isNeedCleanUp) { @@ -302,7 +302,7 @@ export class CallFunc extends ActionInstant { */ execute (): void { if (this._callback) { - const target = (this.workerTarget ?? this.target) as Target; + const target = this._getWorkerTarget() as Target; this._callback.call(this._callbackThis, target, this._data); } } diff --git a/cocos/tween/actions/action-interval.ts b/cocos/tween/actions/action-interval.ts index ca63c9f3bd7..d3c4bbed8f9 100644 --- a/cocos/tween/actions/action-interval.ts +++ b/cocos/tween/actions/action-interval.ts @@ -28,7 +28,7 @@ import { FiniteTimeAction } from './action'; import { macro, logID, errorID } from '../../core'; import { ActionInstant } from './action-instant'; -import type { TweenUpdateCallback } from '../tween'; +import type { Tween, TweenUpdateCallback } from '../tween'; // Extra action for making a Sequence or Spawn when only adding one action to it. class DummyAction extends FiniteTimeAction { @@ -351,16 +351,20 @@ export class Sequence extends ActionInterval { return action; } - updateWorkerTarget (workerTarget: T): void { + updateOwner (owner: Tween): void { if (this._actions.length < 2) { return; } - this._actions[1].workerTarget = workerTarget; const actionOne = this._actions[0]; + const actionTwo = this._actions[1]; + + if (!actionTwo._owner) { + actionTwo._owner = owner; + } if (actionOne instanceof Sequence || actionOne instanceof Spawn) { - actionOne.updateWorkerTarget(workerTarget); - } else { - actionOne.workerTarget = workerTarget; + actionOne.updateOwner(owner); + } else if (!actionOne._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + actionOne._owner = owner; } } @@ -833,16 +837,18 @@ export class Spawn extends ActionInterval { return this; } - updateWorkerTarget (workerTarget: T): void { + updateOwner (owner: Tween): void { if (!this._one || !this._two) { return; } - this._two.workerTarget = workerTarget; + if (!this._two._owner) { + this._two._owner = owner; + } const one = this._one; if (one instanceof Spawn || one instanceof Sequence) { - one.updateWorkerTarget(workerTarget); - } else { - one.workerTarget = workerTarget; + one.updateOwner(owner); + } else if (!one._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + one._owner = owner; } } diff --git a/cocos/tween/actions/action.ts b/cocos/tween/actions/action.ts index b7dbcbcbc87..1695bf6ed34 100644 --- a/cocos/tween/actions/action.ts +++ b/cocos/tween/actions/action.ts @@ -25,6 +25,8 @@ THE SOFTWARE. */ +import type { Tween } from '../tween'; + export enum ActionEnum { /** * @en Default Action tag. @@ -54,44 +56,9 @@ export abstract class Action { protected target: unknown = null; /** - * The `workerTarget` was added from Cocos Creator 3.8.4 and it's used for nest `Tween` functionality. - * It stores the target of sub-tween and its value may be different from `target`. - * - * Example 1: - * ```ts - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }).start(); - * // target and original target are both `node`, workerTarget is `null`. - * ``` - * - * Example 2: - * ```ts - * tween(node).parallel( // ----- Root tween - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 - * tween(node).to(1, { position: new Vec3(10, 10, 10) }) // ----- Sub Tween 2 - * ).start(); - * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. - * // The `target` and `originalTarget` of all internal actions are `node`. - * // Actions in root tween: workerTarget = null - * // Actions in sub tween 1: workerTarget = node - * // Actions in sub tween 2: workerTarget = node - * ``` - * - * Example 3: - * ```ts - * tween(node).parallel( // ----- Root tween - * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 - * tween(node.getComponent(UITransform)).to(1, { // ----- Sub Tween 2 - * contentSize: new Size(10, 10) - * }) - * ).start(); - * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. - * // The `target` and `originalTarget` of all internal actions are `node`. - * // Actions in root tween: workerTarget = null - * // Actions in sub tween 1: workerTarget = node - * // Actions in sub tween 2: workerTarget = node's UITransform component - * ``` + * The tween who owns this action. */ - public workerTarget: unknown = null; + public _owner: Tween | null = null; protected tag = ActionEnum.TAG_INVALID; @@ -177,6 +144,48 @@ export abstract class Action { this.originalTarget = originalTarget; } + /** + * Return the worker target of the current action applys on. + * + * Example 1: + * ```ts + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }).start(); + * // target and original target are both `node`, _getWorkerTarget returns `null`. + * ``` + * + * Example 2: + * ```ts + * tween(node).parallel( // ----- Root tween + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 + * tween(node).to(1, { position: new Vec3(10, 10, 10) }) // ----- Sub Tween 2 + * ).start(); + * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. + * // The `target` and `originalTarget` of all internal actions are `node`. + * // Actions in root tween: _getWorkerTarget returns `node`, + * // Actions in sub tween 1: _getWorkerTarget returns `node`, + * // Actions in sub tween 2: _getWorkerTarget returns `node`. + * ``` + * + * Example 3: + * ```ts + * tween(node).parallel( // ----- Root tween + * tween(node).to(1, { scale: new Vec3(2, 2, 2) }), // ----- Sub tween 1 + * tween(node.getComponent(UITransform)).to(1, { // ----- Sub Tween 2 + * contentSize: new Size(10, 10) + * }) + * ).start(); + * // Note that only root tween is started here. We call tweens in `parallel`/`sequence` sub tweens. + * // The `target` and `originalTarget` of all internal actions are `node`. + * // Actions in root tween: workerTarget = `node`, + * // Actions in sub tween 1: workerTarget = `node`, + * // Actions in sub tween 2: workerTarget = `node`'s UITransform component. + * ``` + */ + protected _getWorkerTarget (): T | null { + const workerTarget: T | null = this._owner?.getTarget(); + return (workerTarget ?? this.target) as T; + } + /** * @en get tag number. * @zh 获取用于识别动作的标签。 diff --git a/cocos/tween/tween-action.ts b/cocos/tween/tween-action.ts index 4d38913f865..f549ffd58c0 100644 --- a/cocos/tween/tween-action.ts +++ b/cocos/tween/tween-action.ts @@ -205,7 +205,7 @@ export class TweenAction extends ActionInterval { clone (): TweenAction { const action = new TweenAction(this._duration, this._originProps, this._opts); action._reversed = this._reversed; - action.workerTarget = this.workerTarget; + action._owner = this._owner; action._id = this._id; this._cloneDecoration(action); return action; @@ -220,7 +220,7 @@ export class TweenAction extends ActionInterval { const action = new TweenAction(this._duration, this._originProps, this._opts); this._cloneDecoration(action); action._reversed = !this._reversed; - action.workerTarget = this.workerTarget; + action._owner = this._owner; return action; } @@ -229,7 +229,7 @@ export class TweenAction extends ActionInterval { if (!isEqual) return; super.startWithTarget(target); - const workerTarget = (this.workerTarget ?? this.target) as T; + const workerTarget = this._getWorkerTarget(); if (!workerTarget) return; const relative = !!this._opts.relative; const props = this._props; @@ -355,7 +355,7 @@ export class TweenAction extends ActionInterval { } update (t: number): void { - const workerTarget = (this.workerTarget ?? this.target) as T; + const workerTarget = this._getWorkerTarget(); if (!workerTarget) return; if (!this._opts) return; diff --git a/cocos/tween/tween.ts b/cocos/tween/tween.ts index a97101cce5f..3bd5f5c9328 100644 --- a/cocos/tween/tween.ts +++ b/cocos/tween/tween.ts @@ -233,7 +233,7 @@ export class Tween { if (action) { reversedAction = action.reverse(); - reversedAction.workerTarget = t._target; + reversedAction._owner = t; } else { warnID(16391, `${actionId}`); } @@ -259,17 +259,17 @@ export class Tween { */ private insertAction (other: FiniteTimeAction): Tween { const action = other.clone(); - this.updateWorkerTargetForAction(action); + this.updateOwnerForAction(action); this._actions.push(action); return this; } - private updateWorkerTargetForAction (action: Action | null): void { + private updateOwnerForAction (action: Action | null): void { if (!action) return; if (action instanceof Sequence || action instanceof Spawn) { - action.updateWorkerTarget(this._target); - } else { - action.workerTarget = this._target; + action.updateOwner(this); + } else if (!action._owner) { // action's owner should never be changed, so only set owner when it's not set yet. + action._owner = this; } } @@ -284,12 +284,6 @@ export class Tween { */ target (target: U): Tween { (this as unknown as Tween)._target = target; - - for (let i = 0, len = this._actions.length; i < len; ++i) { - const action = this._actions[i]; - this.updateWorkerTargetForAction(action); - } - return this as unknown as Tween; } @@ -834,12 +828,12 @@ export class Tween { TweenSystem.instance.ActionManager.resumeTarget(target); } - private _union (updateWorkerTarget: boolean): Sequence | null { + private _union (needUpdateOwner: boolean): Sequence | null { const actions = this._actions; if (actions.length === 0) return null; const action = sequence(actions); - if (updateWorkerTarget) { - this.updateWorkerTargetForAction(action); + if (needUpdateOwner) { + this.updateOwnerForAction(action); } return action; } @@ -857,29 +851,33 @@ export class Tween { return action; } - private static readonly _tmp_args: FiniteTimeAction[] = []; + private static readonly _tmpArgs: FiniteTimeAction[] = []; private static _tweenToActions (args: Tween[]): void { - const tmp_args = Tween._tmp_args; - tmp_args.length = 0; + const tmpArgs = Tween._tmpArgs; + tmpArgs.length = 0; for (let l = args.length, i = 0; i < l; i++) { const t = args[i]; const action = t._union(true); if (action) { action.setSpeed(t._timeScale); - tmp_args.push(action); + tmpArgs.push(action); } } } private static _wrappedSequence (args: Tween[]): Sequence | null { Tween._tweenToActions(args); - return sequence(Tween._tmp_args); + const ret = sequence(Tween._tmpArgs); + this._tmpArgs.length = 0; + return ret; } private static _wrappedParallel (args: Tween[]): Spawn | null { Tween._tweenToActions(args); - return spawn(Tween._tmp_args); + const ret = spawn(Tween._tmpArgs); + this._tmpArgs.length = 0; + return ret; } } legacyCC.Tween = Tween; diff --git a/tests/tween/tween.test.ts b/tests/tween/tween.test.ts index 323b5a06953..8459964d3a1 100644 --- a/tests/tween/tween.test.ts +++ b/tests/tween/tween.test.ts @@ -6,8 +6,7 @@ import { game, director } from "../../cocos/game"; import { UITransform } from "../../cocos/2d/framework/ui-transform"; import { Canvas } from "../../cocos/2d/framework/canvas"; import { Batcher2D } from "../../cocos/2d/renderer/batcher-2d"; -import { Label, UIOpacity } from "../../cocos/2d"; -import { Sprite } from "../../cocos/2d"; +import { UIOpacity, Sprite } from "../../cocos/2d"; function isSizeEqualTo(a: Size, b: Size) { return approx(a.width, b.width) && approx(a.height, b.height); @@ -239,6 +238,136 @@ test('Test different target in sequence', function() { director.unregisterSystem(sys); }); +test('Test different target in sequence nesting parallel', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node = new Node('TestNode'); + const uiOpacity = node.addComponent(UIOpacity) as UIOpacity; + node.parent = scene; + + tween(node) + .sequence( + tween(node) + .to(1, {scale: v3(2, 2, 2)}), + tween(node) + .parallel( + tween(uiOpacity) + .to(1, { opacity: 0 }), + tween(node) + .to(1, { scale: v3(1, 1, 1) }) + ) + ) + .start(); + + runFrames(1); // Kick off + + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(255); + runFrames(30); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(255/2); + runFrames(30); + expect(node.scale.equals(new Vec3(1, 1, 1))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(0); + + // test end + director.unregisterSystem(sys); +}); + +test('Test different target in parallel nesting sequence', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node = new Node('TestNode'); + const uiOpacity = node.addComponent(UIOpacity) as UIOpacity; + node.parent = scene; + + tween(node) + .parallel( + tween(node).sequence( + tween(node) + .to(1, { scale: v3(2, 2, 2) }), + tween(node) + .to(1, { position: v3(100, 100, 0) }) + ), + tween(uiOpacity) + .to(2, { opacity: 0 }), + ) + .start(); + + runFrames(1); // Kick off + + runFrames(30); + expect(node.scale.equals(new Vec3(1.5, 1.5, 1.5))).toBeTruthy(); + expect(node.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + runFrames(30); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + expect(node.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + + expect(uiOpacity.opacity).toBeCloseTo(255/2); + + runFrames(60); + expect(node.scale.equals(new Vec3(2, 2, 2))).toBeTruthy(); + expect(node.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + expect(uiOpacity.opacity).toBeCloseTo(0); + + // test end + director.unregisterSystem(sys); +}); + +test('Test different target in sequence nesting parallel and re-target', function() { + const sys = new TweenSystem(); + (TweenSystem.instance as any) = sys; + director.registerSystem(TweenSystem.ID, sys, System.Priority.MEDIUM); + + const scene = new Scene('test'); + director.runSceneImmediate(scene); + + const node1 = new Node('TestNode1'); + node1.parent = scene; + + const node2 = new Node('TestNode2'); + node2.parent = scene; + + const node3 = new Node('TestNode3'); + node3.parent = scene; + + tween(node1) + .sequence( + tween(node1) + .to(1, { position: v3(100, 100, 0) }).target(node3), + ) + .target(node2) + .to(1, { position: v3(200, 200, 200) }) + .start(); + + runFrames(1); // Kick off + runFrames(60); + expect(node1.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node2.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node3.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + + runFrames(60); + expect(node1.position.equals(new Vec3(0, 0, 0))).toBeTruthy(); + expect(node2.position.equals(new Vec3(200, 200, 200))).toBeTruthy(); + expect(node3.position.equals(new Vec3(100, 100, 0))).toBeTruthy(); + + // test end + director.unregisterSystem(sys); +}); + test('Test different target in then', function() { // @ts-expect-error director.root!._batcher = new Batcher2D(director.root!); @@ -4811,4 +4940,4 @@ test('parallel with two call tween', function () { expect(cb2).toBeCalledTimes(1); director.unregisterSystem(sys); -}); \ No newline at end of file +});