diff --git a/battle_srv/models/ringbuf.go b/battle_srv/models/ringbuf.go index 572e4d1..295c9f5 100644 --- a/battle_srv/models/ringbuf.go +++ b/battle_srv/models/ringbuf.go @@ -21,6 +21,10 @@ func NewRingBuffer(n int32) *RingBuffer { } func (rb *RingBuffer) Put(pItem interface{}) { + for rb.Cnt >= rb.N-1 { + // Make room for the new element + rb.Pop() + } rb.Eles[rb.Ed] = pItem rb.EdFrameId++ rb.Cnt++ @@ -69,5 +73,8 @@ func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} { } func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} { + if frameId >= rb.EdFrameId { + return nil + } return rb.GetByOffset(frameId - rb.StFrameId) } diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index aea745e..7a8a7bb 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -335,10 +335,6 @@ func (pR *Room) ConvertToGeneratingRenderFrameId(inputFrameId int32) int32 { return (inputFrameId << pR.InputScaleFrames) } -func (pR *Room) ConvertToJustBeforeNextGeneratingRenderFrameId(inputFrameId int32) int32 { - return (inputFrameId << pR.InputScaleFrames) + (1 << pR.InputScaleFrames) - 1 -} - func (pR *Room) ConvertToFirstUsedRenderFrameId(inputFrameId int32, inputDelayFrames int32) int32 { return ((inputFrameId << pR.InputScaleFrames) + inputDelayFrames) } @@ -413,7 +409,6 @@ func (pR *Room) StartBattle() { pR.onBattleStoppedForSettlement() }() - battleStartedAtNanos := utils.UnixtimeNano() pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano() Logger.Info("The `battleMainLoop` is started for:", zap.Any("roomId", pR.Id)) @@ -422,11 +417,7 @@ func (pR *Room) StartBattle() { elapsedNanosSinceLastFrameIdTriggered := stCalculation - pR.LastRenderFrameIdTriggeredAt if elapsedNanosSinceLastFrameIdTriggered < pR.dilutedRollbackEstimatedDtNanos { - totalElapsedNanos := (stCalculation - battleStartedAtNanos) - serverFpsByFar := float64(pR.RenderFrameId) * float64(1000000000) / float64(totalElapsedNanos) - Logger.Info(fmt.Sprintf("Avoiding too fast frame@roomId=%v, renderFrameId=%v, totalElapsedNanos=%v, serverFpsByFar=%v: elapsedNanosSinceLastFrameIdTriggered=%v", pR.Id, pR.RenderFrameId, totalElapsedNanos, serverFpsByFar, elapsedNanosSinceLastFrameIdTriggered)) - time.Sleep(time.Duration(pR.dilutedRollbackEstimatedDtNanos - elapsedNanosSinceLastFrameIdTriggered)) - continue + Logger.Info(fmt.Sprintf("renderFrameId=%v@roomId=%v: Is backend running too fast? elapsedNanosSinceLastFrameIdTriggered=%v", pR.RenderFrameId, pR.Id, elapsedNanosSinceLastFrameIdTriggered)) } if pR.RenderFrameId > pR.BattleDurationFrames { @@ -461,7 +452,7 @@ func (pR *Room) StartBattle() { Upon resync, it's still possible that "refRenderFrameId < frontend.chaserRenderFrameId" -- and this is allowed. */ - refRenderFrameId := pR.ConvertToJustBeforeNextGeneratingRenderFrameId(upperToSendInputFrameId) + refRenderFrameId := pR.ConvertToGeneratingRenderFrameId(upperToSendInputFrameId + 1) // for the frontend to jump immediately into generating & upsyncing the next input frame, thus getting rid of "resync avalanche" dynamicsDuration := int64(0) if pR.BackendDynamicsEnabled { @@ -480,10 +471,13 @@ func (pR *Room) StartBattle() { } for playerId, player := range pR.Players { - if swapped := atomic.CompareAndSwapInt32(&player.BattleState, PlayerBattleStateIns.ACTIVE, PlayerBattleStateIns.ACTIVE); !swapped { - // [WARNING] DON'T send anything if the player is not yet active, because it could jam the channel and cause significant delay upon "battle recovery for reconnected player". + + currPlayerBattleState := atomic.LoadInt32(&(player.BattleState)) + if PlayerBattleStateIns.DISCONNECTED == currPlayerBattleState || PlayerBattleStateIns.LOST == currPlayerBattleState { + // [WARNING] DON'T try to send any message to an inactive player! continue } + if 0 == pR.RenderFrameId { kickoffFrame := pR.RenderFrameBuffer.GetByFrameId(0).(*RoomDownsyncFrame) pR.sendSafely(kickoffFrame, nil, DOWNSYNC_MSG_ACT_BATTLE_START, playerId) @@ -531,14 +525,15 @@ func (pR *Room) StartBattle() { 2. reconnection */ shouldResync1 := (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId) - shouldResync2 := (0 < (unconfirmedMask & uint64(1< players = 2; int64 countdownNanos = 3; repeated MeleeBullet meleeBullets = 4; // I don't know how to mimic inheritance/composition in protobuf by far, thus using an array for each type of bullet as a compromise + uint64 backendUnconfirmedMask = 5; // Indexed by "joinIndex", same compression concern as stated in InputFrameDownsync } diff --git a/frontend/assets/scenes/login.fire b/frontend/assets/scenes/login.fire index b62a942..76a6bfa 100644 --- a/frontend/assets/scenes/login.fire +++ b/frontend/assets/scenes/login.fire @@ -440,7 +440,7 @@ "array": [ 0, 0, - 342.9460598986377, + 216.50635094610968, 0, 0, 0, diff --git a/frontend/assets/scripts/AttackingCharacter.js b/frontend/assets/scripts/AttackingCharacter.js index 9a365e7..ce05c8b 100644 --- a/frontend/assets/scripts/AttackingCharacter.js +++ b/frontend/assets/scripts/AttackingCharacter.js @@ -96,7 +96,7 @@ cc.Class({ _interruptPlayingAnimAndPlayNewAnimDragonBones(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, underlyingAnimationCtrl, playingAnimName) { if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) { // No "framesToRecover" - console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, to: ${JSON.stringify(rdfPlayer)}`); + //console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`); underlyingAnimationCtrl.gotoAndPlayByFrame(newAnimName, 0, -1); } else { const animationData = underlyingAnimationCtrl._animations[newAnimName]; @@ -112,7 +112,7 @@ cc.Class({ _interruptPlayingAnimAndPlayNewAnimFrameAnim(rdfPlayer, prevRdfPlayer, newCharacterState, newAnimName, playingAnimName) { if (window.ATK_CHARACTER_STATE_INTERRUPT_WAIVE_SET.has(newCharacterState)) { // No "framesToRecover" - console.warn(`#FrameAnim JoinIndex=${rdfPlayer.joinIndex}, playing new ${newAnimName} from the beginning: while the playing anim is ${playingAnimName}, player rdf changed from: ${null == prevRdfPlayer ? null : JSON.stringify(prevRdfPlayer)}, to: ${JSON.stringify(rdfPlayer)}`); + //console.warn(`#DragonBones JoinIndex=${rdfPlayer.joinIndex}, ${playingAnimName} -> ${newAnimName}`); this.animComp.play(newAnimName, 0); return; } diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index cf56c94..b0d0826 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -110,7 +110,7 @@ cc.Class({ while (0 < self.recentRenderCache.cnt && self.recentRenderCache.stFrameId < minToKeepRenderFrameId) { self.recentRenderCache.pop(); } - const ret = self.recentRenderCache.setByFrameId(rdf, rdf.id); + const [ret, oldStFrameId, oldEdFrameId] = self.recentRenderCache.setByFrameId(rdf, rdf.id); return ret; }, @@ -123,9 +123,12 @@ cc.Class({ while (0 < self.recentInputCache.cnt && self.recentInputCache.stFrameId < minToKeepInputFrameId) { self.recentInputCache.pop(); } - const ret = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId); - if (-1 < self.lastAllConfirmedInputFrameId && self.recentInputCache.stFrameId > self.lastAllConfirmedInputFrameId) { - console.error("Invalid input cache dumped! lastAllConfirmedRenderFrameId=", self.lastAllConfirmedRenderFrameId, ", lastAllConfirmedInputFrameId=", self.lastAllConfirmedInputFrameId, ", recentRenderCache=", self._stringifyRecentRenderCache(false), ", recentInputCache=", self._stringifyRecentInputCache(false)); + const [ret, oldStFrameId, oldEdFrameId] = self.recentInputCache.setByFrameId(inputFrameDownsync, inputFrameDownsync.inputFrameId); + if (window.RING_BUFF_NON_CONSECUTIVE_SET == ret) { + throw `Failed to dump input cache#1! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; + } + if (window.RING_BUFF_FAILED_TO_SET == ret) { + throw `Failed to dump input cache#2 (maybe recentInputCache too small)! inputFrameDownsync.inputFrameId=${inputFrameDownsync.inputFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; } return ret; }, @@ -153,7 +156,7 @@ cc.Class({ null == self.ctrl || null == self.selfPlayerInfo ) { - return [null, null]; + throw `noDelayInputFrameId=${inputFrameId} couldn't be generated: recentInputCache=${self._stringifyRecentInputCache(false)}`; } const joinIndex = self.selfPlayerInfo.joinIndex; @@ -163,19 +166,24 @@ cc.Class({ // If "forceConfirmation" is active on backend, we shouldn't override the already downsynced "inputFrameDownsync"s. const existingInputFrame = self.recentInputCache.getByFrameId(inputFrameId); if (null != existingInputFrame && self._allConfirmed(existingInputFrame.confirmedList)) { + console.log(`noDelayInputFrameId=${inputFrameId} already exists in recentInputCache and is all-confirmed: recentInputCache=${self._stringifyRecentInputCache(false)}`); return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]]; } const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); const currSelfInput = self.ctrl.getEncodedInput(); prefabbedInputList[(joinIndex - 1)] = currSelfInput; - const prefabbedInputFrameDownsync = { + const prefabbedInputFrameDownsync = window.pb.protos.InputFrameDownsync.create({ inputFrameId: inputFrameId, inputList: prefabbedInputList, confirmedList: (1 << (self.selfPlayerInfo.joinIndex - 1)) - }; + }); self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" + if (inputFrameId >= self.recentInputCache.edFrameId) { + throw `noDelayInputFrameId=${inputFrameId} seems not properly dumped #1: recentInputCache=${self._stringifyRecentInputCache(false)}`; + } + return [previousSelfInput, currSelfInput]; }, @@ -203,7 +211,7 @@ cc.Class({ for (let i = batchInputFrameIdSt; i <= latestLocalInputFrameId; ++i) { const inputFrameDownsync = self.recentInputCache.getByFrameId(i); if (null == inputFrameDownsync) { - console.error("sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=", i, ": latestLocalInputFrameId=", latestLocalInputFrameId, ", recentInputCache=", self._stringifyRecentInputCache(false)); + console.error(`sendInputFrameUpsyncBatch: recentInputCache is NOT having inputFrameId=i: latestLocalInputFrameId=${latestLocalInputFrameId}, recentInputCache=${self._stringifyRecentInputCache(false)}`); } else { const inputFrameUpsync = { inputFrameId: i, @@ -225,6 +233,9 @@ cc.Class({ }).finish(); window.sendSafely(reqData); self.lastUpsyncInputFrameId = latestLocalInputFrameId; + if (self.lastUpsyncInputFrameId >= self.recentInputCache.edFrameId) { + throw `noDelayInputFrameId=${self.lastUpsyncInputFrameId} == latestLocalInputFrameId=${latestLocalInputFrameId} seems not properly dumped #2: recentInputCache=${self._stringifyRecentInputCache(false)}`; + } }, onEnable() { @@ -413,7 +424,9 @@ cc.Class({ /** Init required prefab ended. */ window.handleBattleColliderInfo = function(parsedBattleColliderInfo) { + // TODO: Upon reconnection, the backend might have already been sending down data that'd trigger "onRoomDownsyncFrame & onInputFrameDownsyncBatch", but frontend could reject those data due to "battleState != PlayerBattleState.ACTIVE". Object.assign(self, parsedBattleColliderInfo); + self.tooFastDtIntervalMillis = 0.5 * self.rollbackEstimatedDtMillis; const tiledMapIns = self.node.getComponent(cc.TiledMap); @@ -574,14 +587,18 @@ cc.Class({ onRoomDownsyncFrame(rdf) { // This function is also applicable to "re-joining". const self = window.mapIns; - if (rdf.id < self.lastAllConfirmedRenderFrameId) { - return window.RING_BUFF_FAILED_TO_SET; + if (!self.recentRenderCache) { + return; } + if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) { + return; + } + const shouldForceDumping1 = (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id); + const shouldForceDumping2 = (rdf.id > self.renderFrameId + self.renderFrameIdLagTolerance); - const dumpRenderCacheRet = self.dumpToRenderCache(rdf); + const dumpRenderCacheRet = (shouldForceDumping1 || shouldForceDumping2) ? self.dumpToRenderCache(rdf) : window.RING_BUFF_CONSECUTIVE_SET; if (window.RING_BUFF_FAILED_TO_SET == dumpRenderCacheRet) { - console.error("Something is wrong while setting the RingBuffer by frameId!"); - return dumpRenderCacheRet; + throw `Failed to dump render cache#1 (maybe recentRenderCache too small)! rdf.id=${rdf.id}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; } if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START < rdf.id && window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet) { /* @@ -592,13 +609,7 @@ cc.Class({ return dumpRenderCacheRet; } - // The logic below applies to ( || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) - if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) { - console.log('On battle started! renderFrameId=', rdf.id); - } else { - console.log('On battle resynced! renderFrameId=', rdf.id); - } - + // The logic below applies to (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id || window.RING_BUFF_NON_CONSECUTIVE_SET == dumpRenderCacheRet) const players = rdf.players; self._initPlayerRichInfoDict(players); @@ -612,11 +623,22 @@ cc.Class({ if (null == self.renderFrameId || self.renderFrameId <= rdf.id) { // In fact, not having "window.RING_BUFF_CONSECUTIVE_SET == dumpRenderCacheRet" should already imply that "self.renderFrameId <= rdf.id", but here we double check and log the anomaly + + if (window.MAGIC_ROOM_DOWNSYNC_FRAME_ID.BATTLE_START == rdf.id) { + console.log('On battle started! renderFrameId=', rdf.id); + } else { + console.warn(`Got resync@localRenderFrameId=${self.renderFrameId} -> rdf.id=${rdf.id} & rdf.backendUnconfirmedMask=${rdf.backendUnconfirmedMask}, @lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${self.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}`); + } + self.renderFrameId = rdf.id; self.lastRenderFrameIdTriggeredAt = performance.now(); // In this case it must be true that "rdf.id > chaserRenderFrameId >= lastAllConfirmedRenderFrameId". self.lastAllConfirmedRenderFrameId = rdf.id; self.chaserRenderFrameId = rdf.id; + const candidateLastAllConfirmedInputFrame = self._convertToInputFrameId(rdf.id - 1, self.inputDelayFrames); + if (self.lastAllConfirmedInputFrame < candidateLastAllConfirmedInputFrame) { + self.lastAllConfirmedInputFrame = candidateLastAllConfirmedInputFrame; + } const canvasNode = self.canvasNode; self.ctrl = canvasNode.getComponent("TouchEventsManager"); @@ -651,8 +673,10 @@ cc.Class({ onInputFrameDownsyncBatch(batch) { const self = this; - if (ALL_BATTLE_STATES.IN_BATTLE != self.battleState - && ALL_BATTLE_STATES.IN_SETTLEMENT != self.battleState) { + if (!self.recentInputCache) { + return; + } + if (ALL_BATTLE_STATES.IN_SETTLEMENT == self.battleState) { return; } @@ -663,6 +687,7 @@ cc.Class({ if (inputFrameDownsyncId < self.lastAllConfirmedInputFrameId) { continue; } + self.lastAllConfirmedInputFrameId = inputFrameDownsyncId; const localInputFrame = self.recentInputCache.getByFrameId(inputFrameDownsyncId); if (null != localInputFrame && @@ -672,7 +697,6 @@ cc.Class({ ) { firstPredictedYetIncorrectInputFrameId = inputFrameDownsyncId; } - self.lastAllConfirmedInputFrameId = inputFrameDownsyncId; // [WARNING] Take all "inputFrameDownsync" from backend as all-confirmed, it'll be later checked by "rollbackAndChase". inputFrameDownsync.confirmedList = (1 << self.playerRichInfoDict.size) - 1; self.dumpToInputCache(inputFrameDownsync); @@ -716,7 +740,7 @@ cc.Class({ logBattleStats() { const self = this; let s = []; - s.push(`Battle stats: renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}`); + s.push(`Battle stats: renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`); for (let i = self.recentInputCache.stFrameId; i < self.recentInputCache.edFrameId; ++i) { const inputFrameDownsync = self.recentInputCache.getByFrameId(i); @@ -761,7 +785,8 @@ cc.Class({ const [wx, wy] = self.virtualGridToWorldPos(vx, vy); newPlayerNode.setPosition(wx, wy); playerScriptIns.mapNode = self.node; - const colliderWidth = playerDownsyncInfo.colliderRadius * 2, colliderHeight = playerDownsyncInfo.colliderRadius * 3; + const colliderWidth = playerDownsyncInfo.colliderRadius * 2, + colliderHeight = playerDownsyncInfo.colliderRadius * 3; const [x0, y0] = self.virtualGridToPolygonColliderAnchorPos(vx, vy, colliderWidth, colliderHeight), pts = [[0, 0], [colliderWidth, 0], [colliderWidth, colliderHeight], [0, colliderHeight]]; @@ -783,7 +808,8 @@ cc.Class({ const self = this; if (ALL_BATTLE_STATES.IN_BATTLE == self.battleState) { const elapsedMillisSinceLastFrameIdTriggered = performance.now() - self.lastRenderFrameIdTriggeredAt; - if (elapsedMillisSinceLastFrameIdTriggered < (self.rollbackEstimatedDtMillis)) { + if (elapsedMillisSinceLastFrameIdTriggered < self.tooFastDtIntervalMillis) { + // [WARNING] We should avoid a frontend ticking too fast to prevent cheating, as well as ticking too slow to cause a "resync avalanche" that impacts user experience! // console.debug("Avoiding too fast frame@renderFrameId=", self.renderFrameId, ": elapsedMillisSinceLastFrameIdTriggered=", elapsedMillisSinceLastFrameIdTriggered); return; } @@ -822,17 +848,13 @@ cc.Class({ */ // [WARNING] Don't try to get "prevRdf(i.e. renderFrameId == latest-1)" by "self.recentRenderCache.getByFrameId(...)" here, as the cache might have been updated by asynchronous "onRoomDownsyncFrame(...)" calls! self.applyRoomDownsyncFrameDynamics(rdf, prevRdf); + ++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!! + self.lastRenderFrameIdTriggeredAt = performance.now(); let t3 = performance.now(); } catch (err) { console.error("Error during Map.update", err); + self.onBattleStopped(); // TODO: Popup to ask player to refresh browser } finally { - // Update countdown - self.countdownNanos = self.battleDurationNanos - self.renderFrameId * self.rollbackEstimatedDtNanos; - if (self.countdownNanos <= 0) { - self.onBattleStopped(self.playerRichInfoDict); - return; - } - const countdownSeconds = parseInt(self.countdownNanos / 1000000000); if (isNaN(countdownSeconds)) { console.warn(`countdownSeconds is NaN for countdownNanos == ${self.countdownNanos}.`); @@ -840,8 +862,6 @@ cc.Class({ if (null != self.countdownLabel) { self.countdownLabel.string = countdownSeconds; } - ++self.renderFrameId; // [WARNING] It's important to increment the renderFrameId AFTER all the operations above!!! - self.lastRenderFrameIdTriggeredAt = performance.now(); } } }, @@ -967,15 +987,21 @@ cc.Class({ playerRichInfo.scriptIns.updateSpeed(immediatePlayerInfo.speed); playerRichInfo.scriptIns.updateCharacterAnim(immediatePlayerInfo, prevRdfPlayer, false); } + + // Update countdown + self.countdownNanos = self.battleDurationNanos - self.renderFrameId * self.rollbackEstimatedDtNanos; + if (self.countdownNanos <= 0) { + self.onBattleStopped(self.playerRichInfoDict); + } }, getCachedInputFrameDownsyncWithPrediction(inputFrameId) { const self = this; - let inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId); - if (null != inputFrameDownsync && -1 != self.lastAllConfirmedInputFrameId && inputFrameId > self.lastAllConfirmedInputFrameId) { - const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId); + const inputFrameDownsync = self.recentInputCache.getByFrameId(inputFrameId); + const lastAllConfirmedInputFrame = self.recentInputCache.getByFrameId(self.lastAllConfirmedInputFrameId); + if (null != inputFrameDownsync && null != lastAllConfirmedInputFrame && inputFrameId > self.lastAllConfirmedInputFrameId) { for (let i = 0; i < inputFrameDownsync.inputList.length; ++i) { - if (i == self.selfPlayerInfo.joinIndex - 1) continue; + if (i == (self.selfPlayerInfo.joinIndex - 1)) continue; inputFrameDownsync.inputList[i] = (lastAllConfirmedInputFrame.inputList[i] & 15); // Don't predict attack input! } } @@ -1007,11 +1033,7 @@ cc.Class({ }; } - const toRet = { - id: currRenderFrame.id + 1, - players: nextRenderFramePlayers, - meleeBullets: [] - }; + const nextRenderFrameMeleeBullets = []; const bulletPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order @@ -1104,7 +1126,7 @@ cc.Class({ collisionSysMap.delete(collisionBulletIndex); } if (removedBulletsAtCurrFrame.has(collisionBulletIndex)) continue; - toRet.meleeBullets.push(meleeBullet); + nextRenderFrameMeleeBullets.push(meleeBullet); } // Process player inputs @@ -1145,7 +1167,7 @@ cc.Class({ punch.offenderJoinIndex = joinIndex; punch.offenderPlayerId = playerId; punch.originatedRenderFrameId = currRenderFrame.id; - toRet.meleeBullets.push(punch); + nextRenderFrameMeleeBullets.push(punch); // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}: ${self._stringifyRecentInputCache(true)}`); // console.log(`A rising-edge of meleeBullet is created at renderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.inputFrameId}`); @@ -1198,7 +1220,11 @@ cc.Class({ } - return toRet; + return window.pb.protos.RoomDownsyncFrame.create({ + id: currRenderFrame.id + 1, + players: nextRenderFramePlayers, + meleeBullets: nextRenderFrameMeleeBullets, + }); }, rollbackAndChase(renderFrameIdSt, renderFrameIdEd, collisionSys, collisionSysMap, isChasing) { @@ -1206,31 +1232,24 @@ cc.Class({ This function eventually calculates a "RoomDownsyncFrame" where "RoomDownsyncFrame.id == renderFrameIdEd" if not interruptted. */ const self = this; - let prevLatestRdf = null; - let latestRdf = self.recentRenderCache.getByFrameId(renderFrameIdSt); // typed "RoomDownsyncFrame" - if (null == latestRdf) { - console.error(`Couldn't find renderFrameId=${renderFrameIdSt}, to rollback, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`); - return [prevLatestRdf, latestRdf]; - } + let i = renderFrameIdSt, + prevLatestRdf = null, + latestRdf = null; - if (renderFrameIdSt >= renderFrameIdEd) { - return [prevLatestRdf, latestRdf]; - } - - for (let i = renderFrameIdSt; i < renderFrameIdEd; ++i) { - const currRenderFrame = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"! - if (null == currRenderFrame) { + do { + latestRdf = self.recentRenderCache.getByFrameId(i); // typed "RoomDownsyncFrame"; [WARNING] When "true == isChasing", this function can be interruptted by "onRoomDownsyncFrame(rdf)" asynchronously anytime, making this line return "null"! + if (null == latestRdf) { console.warn(`Couldn't find renderFrame for i=${i} to rollback, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, might've been interruptted by onRoomDownsyncFrame`); return [prevLatestRdf, latestRdf]; } const j = self._convertToInputFrameId(i, self.inputDelayFrames); const delayedInputFrame = self.getCachedInputFrameDownsyncWithPrediction(j); if (null == delayedInputFrame) { - console.warn(`Failed to get cached delayedInputFrame for i=${i}, j=${j}, self.renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}`); - return [prevLatestRdf, latestRdf]; + // Shouldn't happen! + throw `Failed to get cached delayedInputFrame for i=${i}, j=${j}, renderFrameId=${self.renderFrameId}, lastAllConfirmedRenderFrameId=${self.lastAllConfirmedRenderFrameId}, lastUpsyncInputFrameId=${self.lastUpsyncInputFrameId}, lastAllConfirmedInputFrameId=${self.lastAllConfirmedInputFrameId}, chaserRenderFrameId=${self.chaserRenderFrameId}; recentRenderCache=${self._stringifyRecentRenderCache(false)}, recentInputCache=${self._stringifyRecentInputCache(false)}`; } prevLatestRdf = latestRdf; - latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, currRenderFrame, collisionSys, collisionSysMap); + latestRdf = self.applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputFrame, prevLatestRdf, collisionSys, collisionSysMap); if ( self._allConfirmed(delayedInputFrame.confirmedList) && @@ -1249,7 +1268,8 @@ cc.Class({ self.chaserRenderFrameId = latestRdf.id; } self.dumpToRenderCache(latestRdf); - } + ++i; + } while (i < renderFrameIdEd); return [prevLatestRdf, latestRdf]; }, diff --git a/frontend/assets/scripts/RingBuffer.js b/frontend/assets/scripts/RingBuffer.js index 52c1ea0..f8d4094 100644 --- a/frontend/assets/scripts/RingBuffer.js +++ b/frontend/assets/scripts/RingBuffer.js @@ -13,6 +13,10 @@ var RingBuffer = function(capacity) { }; RingBuffer.prototype.put = function(item) { + while (this.cnt >= this.n - 1) { + // Make room for the new element + this.pop(); + } this.eles[this.ed] = item this.edFrameId++; this.cnt++; @@ -61,40 +65,41 @@ RingBuffer.prototype.getArrIdxByOffset = function(offsetFromSt) { }; RingBuffer.prototype.getByFrameId = function(frameId) { + if (frameId >= this.edFrameId) return null; const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId); return (null == arrIdx ? null : this.eles[arrIdx]); }; // [WARNING] During a battle, frontend could receive non-consecutive frames (either renderFrame or inputFrame) due to resync, the buffer should handle these frames properly. RingBuffer.prototype.setByFrameId = function(item, frameId) { + const oldStFrameId = this.stFrameId, + oldEdFrameId = this.edFrameId; if (frameId < this.stFrameId) { - console.error("Invalid putByFrameId#1: stFrameId=", this.stFrameId, ", edFrameId=", this.edFrameId, ", incoming item=", item); - return window.RING_BUFF_FAILED_TO_SET; + return [window.RING_BUFF_FAILED_TO_SET, oldStFrameId, oldEdFrameId]; } - const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId); - if (null != arrIdx) { - this.eles[arrIdx] = item; - return window.RING_BUFF_CONSECUTIVE_SET; + // By now "this.stFrameId <= frameId" + + if (this.edFrameId > frameId) { + const arrIdx = this.getArrIdxByOffset(frameId - this.stFrameId); + if (null != arrIdx) { + this.eles[arrIdx] = item; + return [window.RING_BUFF_CONSECUTIVE_SET, oldStFrameId, oldEdFrameId]; + } } - // When "null == arrIdx", should it still be deemed consecutive if "frameId == edFrameId" prior to the reset? + // By now "this.edFrameId <= frameId" let ret = window.RING_BUFF_CONSECUTIVE_SET; if (this.edFrameId < frameId) { this.st = this.ed = 0; this.stFrameId = this.edFrameId = frameId; this.cnt = 0; ret = window.RING_BUFF_NON_CONSECUTIVE_SET; + } else { + // this.edFrameId == frameId + this.put(item); } - this.eles[this.ed] = item - this.edFrameId++; - this.cnt++; - this.ed++; - if (this.ed >= this.n) { - this.ed -= this.n; // Deliberately not using "%" operator for performance concern - } - - return ret; + return [ret, oldStFrameId, oldEdFrameId]; }; module.exports = RingBuffer; diff --git a/frontend/assets/scripts/WsSessionMgr.js b/frontend/assets/scripts/WsSessionMgr.js index 30cb349..98646c5 100644 --- a/frontend/assets/scripts/WsSessionMgr.js +++ b/frontend/assets/scripts/WsSessionMgr.js @@ -177,8 +177,6 @@ ${JSON.stringify(resp, null, 2)}`); return; } const inputFrameIdConsecutive = (resp.inputFrameDownsyncBatch[0].inputFrameId == mapIns.lastAllConfirmedInputFrameId + 1); - const renderFrameIdConsecutive = (resp.rdf.id <= mapIns.renderFrameId + mapIns.renderFrameIdLagTolerance); - console.warn(`Got resync@localRenderFrameId=${mapIns.renderFrameId}, @lastAllConfirmedRenderFrameId=${mapIns.lastAllConfirmedRenderFrameId}, @lastAllConfirmedInputFrameId=${mapIns.lastAllConfirmedInputFrameId}, @chaserRenderFrameId=${mapIns.chaserRenderFrameId}, @localRecentInputCache=${mapIns._stringifyRecentInputCache(false)}, inputFrameIdConsecutive=${inputFrameIdConsecutive}, renderFrameIdConsecutive=${renderFrameIdConsecutive}`); // The following order of execution is important mapIns.onRoomDownsyncFrame(resp.rdf); mapIns.onInputFrameDownsyncBatch(resp.inputFrameDownsyncBatch); diff --git a/frontend/assets/scripts/modules/room_downsync_frame_proto_bundle.forcemsg.js b/frontend/assets/scripts/modules/room_downsync_frame_proto_bundle.forcemsg.js index 070c8f7..9b5e86e 100644 --- a/frontend/assets/scripts/modules/room_downsync_frame_proto_bundle.forcemsg.js +++ b/frontend/assets/scripts/modules/room_downsync_frame_proto_bundle.forcemsg.js @@ -5082,6 +5082,7 @@ $root.protos = (function() { * @property {Object.|null} [players] RoomDownsyncFrame players * @property {number|Long|null} [countdownNanos] RoomDownsyncFrame countdownNanos * @property {Array.|null} [meleeBullets] RoomDownsyncFrame meleeBullets + * @property {number|Long|null} [backendUnconfirmedMask] RoomDownsyncFrame backendUnconfirmedMask */ /** @@ -5133,6 +5134,14 @@ $root.protos = (function() { */ RoomDownsyncFrame.prototype.meleeBullets = $util.emptyArray; + /** + * RoomDownsyncFrame backendUnconfirmedMask. + * @member {number|Long} backendUnconfirmedMask + * @memberof protos.RoomDownsyncFrame + * @instance + */ + RoomDownsyncFrame.prototype.backendUnconfirmedMask = $util.Long ? $util.Long.fromBits(0,0,true) : 0; + /** * Creates a new RoomDownsyncFrame instance using the specified properties. * @function create @@ -5169,6 +5178,8 @@ $root.protos = (function() { if (message.meleeBullets != null && message.meleeBullets.length) for (var i = 0; i < message.meleeBullets.length; ++i) $root.protos.MeleeBullet.encode(message.meleeBullets[i], writer.uint32(/* id 4, wireType 2 =*/34).fork()).ldelim(); + if (message.backendUnconfirmedMask != null && Object.hasOwnProperty.call(message, "backendUnconfirmedMask")) + writer.uint32(/* id 5, wireType 0 =*/40).uint64(message.backendUnconfirmedMask); return writer; }; @@ -5240,6 +5251,10 @@ $root.protos = (function() { message.meleeBullets.push($root.protos.MeleeBullet.decode(reader, reader.uint32())); break; } + case 5: { + message.backendUnconfirmedMask = reader.uint64(); + break; + } default: reader.skipType(tag & 7); break; @@ -5304,6 +5319,9 @@ $root.protos = (function() { return "meleeBullets." + error; } } + if (message.backendUnconfirmedMask != null && message.hasOwnProperty("backendUnconfirmedMask")) + if (!$util.isInteger(message.backendUnconfirmedMask) && !(message.backendUnconfirmedMask && $util.isInteger(message.backendUnconfirmedMask.low) && $util.isInteger(message.backendUnconfirmedMask.high))) + return "backendUnconfirmedMask: integer|Long expected"; return null; }; @@ -5350,6 +5368,15 @@ $root.protos = (function() { message.meleeBullets[i] = $root.protos.MeleeBullet.fromObject(object.meleeBullets[i]); } } + if (object.backendUnconfirmedMask != null) + if ($util.Long) + (message.backendUnconfirmedMask = $util.Long.fromValue(object.backendUnconfirmedMask)).unsigned = true; + else if (typeof object.backendUnconfirmedMask === "string") + message.backendUnconfirmedMask = parseInt(object.backendUnconfirmedMask, 10); + else if (typeof object.backendUnconfirmedMask === "number") + message.backendUnconfirmedMask = object.backendUnconfirmedMask; + else if (typeof object.backendUnconfirmedMask === "object") + message.backendUnconfirmedMask = new $util.LongBits(object.backendUnconfirmedMask.low >>> 0, object.backendUnconfirmedMask.high >>> 0).toNumber(true); return message; }; @@ -5377,6 +5404,11 @@ $root.protos = (function() { object.countdownNanos = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; } else object.countdownNanos = options.longs === String ? "0" : 0; + if ($util.Long) { + var long = new $util.Long(0, 0, true); + object.backendUnconfirmedMask = options.longs === String ? long.toString() : options.longs === Number ? long.toNumber() : long; + } else + object.backendUnconfirmedMask = options.longs === String ? "0" : 0; } if (message.id != null && message.hasOwnProperty("id")) object.id = message.id; @@ -5396,6 +5428,11 @@ $root.protos = (function() { for (var j = 0; j < message.meleeBullets.length; ++j) object.meleeBullets[j] = $root.protos.MeleeBullet.toObject(message.meleeBullets[j], options); } + if (message.backendUnconfirmedMask != null && message.hasOwnProperty("backendUnconfirmedMask")) + if (typeof message.backendUnconfirmedMask === "number") + object.backendUnconfirmedMask = options.longs === String ? String(message.backendUnconfirmedMask) : message.backendUnconfirmedMask; + else + object.backendUnconfirmedMask = options.longs === String ? $util.Long.prototype.toString.call(message.backendUnconfirmedMask) : options.longs === Number ? new $util.LongBits(message.backendUnconfirmedMask.low >>> 0, message.backendUnconfirmedMask.high >>> 0).toNumber(true) : message.backendUnconfirmedMask; return object; };