243 lines
12 KiB
TypeScript
243 lines
12 KiB
TypeScript
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");
|
|
}
|
|
}
|