import Utils from "../Pay/Utils"; import DailyTaskProgressTracker from "./DailyTaskProgressTracker"; /** 任务内存缓存及串行同步。UI 读取缓存,进度保存与领奖共用同一条请求队列。 */ export default class DailyTaskSync { private static loaded = false; private static uid = ""; private static offset = 0; private static revision = 0; private static savedRevision = 0; private static resets: { [taskId: string]: number } = {}; private static queue: Array<(done: () => void) => void> = []; private static busy = false; private static readers: Array<(response: any) => void> = null; private static paused = false; private static needsRefresh = false; private static hiddenAt = 0; static now(): number { return Date.now() + this.offset; } static isExpired(): boolean { const data = DailyTaskProgressTracker.getTaskData(); return !!data && [data.dayEndsAt, data.weekEndsAt] .some(end => Number(end) > 0 && this.now() >= Number(end)); } static cached(): any { const data = DailyTaskProgressTracker.getTaskData(); return this.loaded && this.uid === this.currentUid() && !this.needsRefresh && !this.isExpired() && data && Number(data.dayEndsAt) > this.now() && Number(data.weekEndsAt) > this.now() ? data : null; } static markDirty(): void { this.revision++; this.notify(); } static markReset(taskId: string): void { this.resets[taskId] = this.revision + 1; } static read(callback: (response: any) => void, force = false): void { const cache = this.cached(); if (!force && cache) { callback(this.success(cache)); return; } if (this.readers) { this.readers.push(callback); return; } this.readers = [callback]; this.enqueue(done => { this.saveDirty(saved => { if (saved.code !== 1) { this.finishRead(saved, done); return; } const snapshot = this.snapshot(); const revision = this.revision; const requestUid = this.currentUid(); Utils.getDailyQuestInfo(response => { if (requestUid !== this.currentUid()) response = this.failure("登录用户已变更,请重新获取任务"); if (this.validResponse(response)) { this.needsRefresh = false; this.apply(response.data.task, snapshot, revision); response.data.task = DailyTaskProgressTracker.getTaskData(); if (!this.cached()) { this.needsRefresh = true; response = this.failure("任务已过期或缺少刷新时间,请重试"); } } else { this.needsRefresh = true; response = response && response.code !== 1 ? response : this.failure("任务数据无效,请重试"); } this.finishRead(response, done); }); }); }); } static save(callback: (response: any) => void = () => {}): void { this.enqueue(done => this.saveDirty(response => { done(); callback(response); })); } static claim(id: string, callback: (response: any) => void, scope: "daily" | "weekly" = "daily"): void { if (this.paused) { callback(this.failure("任务测试已暂停,请先恢复任务同步")); return; } // 结算在线进度;其保存请求和本次领奖将按顺序执行。 DailyTaskProgressTracker.flushNow(false); this.enqueue(done => { this.saveDirty(saved => { if (saved.code !== 1) { done(); callback(saved); return; } if (!this.cached()) { done(); this.read(() => callback(this.failure("任务数据已刷新,请重新领取")), true); return; } const snapshot = this.snapshot(); const revision = this.revision; const taskData = DailyTaskProgressTracker.getTaskData(); const item = scope === "weekly" ? taskData.weeklyRewards.find(item => item.rewardId === id) : taskData.dailyTasks.find(item => item.taskId === id); if (!item || item.state !== 1) { done(); callback(this.failure("任务状态已变化,请查看最新任务")); return; } const requestUid = this.currentUid(); Utils.getDailyQuestReward(id, response => { if (requestUid !== this.currentUid()) response = this.failure("登录用户已变更"); if (this.validResponse(response)) { this.apply(response.data.task, snapshot, revision); response.data.task = DailyTaskProgressTracker.getTaskData(); } else { this.needsRefresh = true; response = response && response.code !== 1 ? response : this.failure("领奖返回数据异常,请刷新任务后重试"); } // 先让领奖界面消费结果,再执行后续排队的同步。 try { callback(response); } finally { done(); } }, scope); }); }); } static background(): void { this.hiddenAt = Date.now(); } static networkChanged(connected: boolean): void { if (!connected) this.needsRefresh = true; else this.foreground(); } static foreground(): void { if (!this.loaded || this.paused) return; const longAbsence = this.hiddenAt > 0 && Date.now() - this.hiddenAt >= 5 * 60 * 1000; this.hiddenAt = 0; if (longAbsence || !this.cached()) this.read(() => {}, true); } /** 微信开发者工具控制台:pause -> Postman 修改 -> refresh -> resume。 */ static debug(action: "pause" | "refresh" | "resume" | "status", callback: (response: any) => void): void { if (cc.fx.GameTool.getWechatGameVersion() !== "开发版") { callback(this.failure("任务调试仅限开发版")); return; } if (action === "status") { callback({ code: 1, data: { paused: this.paused, busy: this.busy, cached: !!this.cached(), expiresAt: this.expiration() } }); return; } if ((action === "refresh" || action === "resume") && this.paused && this.revision !== this.savedRevision && !this.isExpired()) { callback(this.failure("暂停前的进度尚未保存成功,请重新执行 pause")); return; } if (action === "pause") { // 先结算,再暂停新事件;等待已发送请求和现有进度保存完成后才允许改服务器。 DailyTaskProgressTracker.flushNow(false); this.paused = true; DailyTaskProgressTracker.setDebugPaused(true); this.enqueue(done => this.saveDirty(response => { done(); callback(response); }, true)); } else if (action === "refresh") { this.read(callback, true); } else if (action === "resume") { this.read(response => { if (response.code === 1) { this.paused = false; DailyTaskProgressTracker.setDebugPaused(false); } callback(response); }, true); } else callback(this.failure("未知任务调试操作")); } private static saveDirty(callback: (response: any) => void, allowPaused = false): void { const data = DailyTaskProgressTracker.getTaskData(); if (!data || this.revision === this.savedRevision || (this.paused && !allowPaused) || this.isExpired() || (this.loaded && this.uid !== this.currentUid())) { callback(this.success(data)); return; } const snapshot = this.snapshot(); const revision = this.revision; const requestUid = this.currentUid(); Utils.setDailyQuestInfo(response => { if (requestUid !== this.currentUid()) response = this.failure("登录用户已变更"); if (!this.validResponse(response)) { this.needsRefresh = true; callback(response && response.code !== 1 ? response : this.failure("任务进度保存失败")); return; } this.savedRevision = revision; this.needsRefresh = false; DailyTaskProgressTracker.confirmSaved(response.data.task); this.apply(response.data.task, snapshot, revision); // 保存期间新增的事件仍保留在缓存中,继续提交后才允许领奖/强制读取。 this.saveDirty(callback, allowPaused); }, snapshot); } private static apply(data: any, snapshot: any, revision: number): void { const current = DailyTaskProgressTracker.getTaskData(); const accountChanged = this.loaded && this.uid !== this.currentUid(); const sameDay = !accountChanged && snapshot && current && data.dayKey === snapshot.dayKey && data.dayKey === current.dayKey; if (sameDay) { data.dailyTasks.forEach(task => { const before = snapshot.dailyTasks.find(item => item.taskId === task.taskId); const local = current.dailyTasks.find(item => item.taskId === task.taskId); if (!before || !local || task.state === 2) return; if (local.state === 2 && before.state !== 2) { task.state = 2; task.value = local.value; return; } const delta = Number(local.value) - Number(before.value); const reset = (this.resets[task.taskId] || 0) > revision; if (!delta && local.state === before.state && !reset) return; // 连胜可被清零,不能简单地取进度最大值。 task.value = Math.max(0, Math.min(task.target, reset || delta < 0 ? local.value : Number(task.value) + delta)); task.state = task.value >= task.target ? 1 : 0; }); } else if (accountChanged || current && data.dayKey !== current.dayKey) { this.revision = this.savedRevision = 0; this.resets = {}; DailyTaskProgressTracker.changePeriod(accountChanged); } this.uid = this.currentUid(); this.loaded = true; if (Number(data.serverNow) > 0) this.offset = Number(data.serverNow) - Date.now(); DailyTaskProgressTracker.setTaskData(data); this.notify(); } private static finishRead(response: any, done: () => void): void { const readers = this.readers; this.readers = null; try { readers.forEach(callback => callback(response)); } finally { done(); } } private static enqueue(operation: (done: () => void) => void): void { this.queue.push(operation); this.pump(); } private static pump(): void { if (this.busy || !this.queue.length) return; this.busy = true; this.queue.shift()(() => { this.busy = false; this.pump(); }); } private static snapshot(): any { const data = DailyTaskProgressTracker.getTaskData(); return data ? JSON.parse(JSON.stringify(data)) : null; } private static currentUid(): string { return cc.fx.StorageMessage.getStorage("uid") || cc.fx.GameConfig.GM_INFO.uid || ""; } private static validResponse(response: any): boolean { const data = response && response.code === 1 && response.data && response.data.task; return DailyTaskProgressTracker.isValidTaskData(data) && Array.isArray(data.weeklyRewards); } private static success(task: any): any { return { code: 1, data: { task } }; } private static failure(msg: string): any { return { code: 0, msg }; } private static expiration(): number { const data = DailyTaskProgressTracker.getTaskData(); return data ? Math.min(Number(data.dayEndsAt) || 0, Number(data.weekEndsAt) || 0) : 0; } private static notify(): void { if (cc.director && cc.director.emit) cc.director.emit("daily-tasks-changed"); } }