diff --git a/battle_srv/models/room.go b/battle_srv/models/room.go index 4ae10d5..6ed12ab 100644 --- a/battle_srv/models/room.go +++ b/battle_srv/models/room.go @@ -448,12 +448,11 @@ func (pR *Room) StartBattle() { pR.prefabInputFrameDownsync(noDelayInputFrameId) } + pR.markConfirmationIfApplicable() unconfirmedMask := uint64(0) if pR.BackendDynamicsEnabled { // Force setting all-confirmed of buffered inputFrames periodically unconfirmedMask = pR.forceConfirmationIfApplicable() - } else { - pR.markConfirmationIfApplicable() } upperToSendInputFrameId := atomic.LoadInt32(&(pR.LastAllConfirmedInputFrameId)) @@ -486,6 +485,7 @@ func (pR *Room) StartBattle() { refRenderFrameId = pR.CurDynamicsRenderFrameId } } + 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 disconnected, because it could jam the channel and cause significant delay upon "battle recovery for reconnected player". @@ -500,7 +500,7 @@ func (pR *Room) StartBattle() { candidateToSendInputFrameId := pR.Players[playerId].LastSentInputFrameId + 1 if candidateToSendInputFrameId < pR.InputsBuffer.StFrameId { // [WARNING] As "player.LastSentInputFrameId <= lastAllConfirmedInputFrameIdWithChange" for each iteration, and "lastAllConfirmedInputFrameIdWithChange <= lastAllConfirmedInputFrameId" where the latter is used to "applyInputFrameDownsyncDynamics" and then evict "pR.InputsBuffer", thus there's a very high possibility that "player.LastSentInputFrameId" is already evicted. - // Logger.Debug(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false))) + Logger.Warn(fmt.Sprintf("LastSentInputFrameId already popped: roomId=%v, playerId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, candidateToSendInputFrameId-1, player.AckingInputFrameId, pR.InputsBufferString(false))) candidateToSendInputFrameId = pR.InputsBuffer.StFrameId } @@ -526,7 +526,6 @@ func (pR *Room) StartBattle() { if 0 >= len(toSendInputFrames) { // [WARNING] When sending DOWNSYNC_MSG_ACT_FORCED_RESYNC, there MUST BE accompanying "toSendInputFrames" for calculating "refRenderFrameId"! - if MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId { Logger.Warn(fmt.Sprintf("Not sending due to empty toSendInputFrames: roomId=%v, playerId=%v, refRenderFrameId=%v, upperToSendInputFrameId=%v, lastSentInputFrameId=%v, playerAckingInputFrameId=%v", pR.Id, playerId, refRenderFrameId, upperToSendInputFrameId, player.LastSentInputFrameId, player.AckingInputFrameId)) } @@ -535,8 +534,9 @@ func (pR *Room) StartBattle() { indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1) var joinMask uint64 = (1 << indiceInJoinIndexBooleanArr) - if pR.BackendDynamicsEnabled && (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || 0 < (unconfirmedMask&joinMask)) { - // [WARNING] Even upon "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED", it could be true that "0 == (unconfirmedMask & joinMask)"! + shouldResyncForSlowerClocker := (0 < (unconfirmedMask & joinMask)) // This condition is critical, if we don't send resync upon this condition, the player with a slower frontend clock might never get its input synced + if pR.BackendDynamicsEnabled && (MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED == player.LastSentInputFrameId || shouldResyncForSlowerClocker) { + // [WARNING] Even upon "MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED", it could be true that "0 == ()"! tmp := pR.RenderFrameBuffer.GetByFrameId(refRenderFrameId) if nil == tmp { panic(fmt.Sprintf("Required refRenderFrameId=%v for roomId=%v, playerId=%v, candidateToSendInputFrameId=%v doesn't exist! InputsBuffer=%v, RenderFrameBuffer=%v", refRenderFrameId, pR.Id, playerId, candidateToSendInputFrameId, pR.InputsBufferString(false), pR.RenderFrameBufferString())) @@ -550,28 +550,35 @@ func (pR *Room) StartBattle() { } } - // Evict no longer required "RenderFrameBuffer" - for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) { - _ = pR.RenderFrameBuffer.Pop() + if pR.BackendDynamicsEnabled { + // Evict no longer required "RenderFrameBuffer" + for pR.RenderFrameBuffer.N < pR.RenderFrameBuffer.Cnt || (0 < pR.RenderFrameBuffer.Cnt && pR.RenderFrameBuffer.StFrameId < refRenderFrameId) { + _ = pR.RenderFrameBuffer.Pop() + } } toApplyInputFrameId := pR.ConvertToInputFrameId(refRenderFrameId, pR.InputDelayFrames) - if false == pR.BackendDynamicsEnabled { - // When "false == pR.BackendDynamicsEnabled", the variable "refRenderFrameId" is not well defined - minLastSentInputFrameId := int32(math.MaxInt32) - for _, player := range pR.Players { - if player.LastSentInputFrameId >= minLastSentInputFrameId { - continue - } - minLastSentInputFrameId = player.LastSentInputFrameId + /* + [WARNING] + The following updates to "toApplyInputFrameId" is necessary because + 1. When "false == pR.BackendDynamicsEnabled", the variable "refRenderFrameId" is not well defined; + 2. When "true == pR.BackendDynamicsEnabled", the initial value of "toApplyInputFrameId" might be too big and thus making frontends unable to receive consecutive all-confirmed inputFrameDownsync sequence - which is what we want during a battle if no one disconnects. + */ + minLastSentInputFrameId := int32(math.MaxInt32) + for _, player := range pR.Players { + if player.LastSentInputFrameId >= minLastSentInputFrameId { + continue } + minLastSentInputFrameId = player.LastSentInputFrameId + } + if minLastSentInputFrameId < toApplyInputFrameId { toApplyInputFrameId = minLastSentInputFrameId } for pR.InputsBuffer.N < pR.InputsBuffer.Cnt || (0 < pR.InputsBuffer.Cnt && pR.InputsBuffer.StFrameId < toApplyInputFrameId) { f := pR.InputsBuffer.Pop().(*InputFrameDownsync) if pR.inputFrameIdDebuggable(f.InputFrameId) { // Popping of an "inputFrame" would be AFTER its being all being confirmed, because it requires the "inputFrame" to be all acked - Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false))) + Logger.Debug("inputFrame lifecycle#4[popped]:", zap.Any("roomId", pR.Id), zap.Any("inputFrameId", f.InputFrameId), zap.Any("minLastSentInputFrameId", minLastSentInputFrameId), zap.Any("toApplyInputFrameId", toApplyInputFrameId), zap.Any("InputsBuffer", pR.InputsBufferString(false))) } } @@ -1109,7 +1116,6 @@ func (pR *Room) markConfirmationIfApplicable() { inputFrameDownsync.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr) } - // Force confirmation of "inputFrame2" if allConfirmedMask == inputFrameDownsync.ConfirmedList { pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1) } else { @@ -1140,24 +1146,12 @@ func (pR *Room) forceConfirmationIfApplicable() uint64 { if nil == tmp { panic(fmt.Sprintf("inputFrameId2=%v doesn't exist for roomId=%v, this is abnormal because the server should prefab inputFrameDownsync in a most advanced pace, check the prefab logic! InputsBuffer=%v", inputFrameId2, pR.Id, pR.InputsBufferString(false))) } - inputFrame2 := tmp.(*InputFrameDownsync) - for _, player := range pR.Players { - // Enrich by already arrived player upsync commands - bufIndex := pR.toDiscreteInputsBufferIndex(inputFrame2.InputFrameId, player.JoinIndex) - tmp, loaded := pR.DiscreteInputsBuffer.LoadAndDelete(bufIndex) - if !loaded { - continue - } - inputFrameUpsync := tmp.(*InputFrameUpsync) - indiceInJoinIndexBooleanArr := uint32(player.JoinIndex - 1) - inputFrame2.InputList[indiceInJoinIndexBooleanArr] = pR.EncodeUpsyncCmd(inputFrameUpsync) - inputFrame2.ConfirmedList |= (1 << indiceInJoinIndexBooleanArr) - } totPlayerCnt := uint32(pR.Capacity) allConfirmedMask := uint64((1 << totPlayerCnt) - 1) // Force confirmation of "inputFrame2" + inputFrame2 := tmp.(*InputFrameDownsync) oldConfirmedList := inputFrame2.ConfirmedList unconfirmedMask := (oldConfirmedList ^ allConfirmedMask) inputFrame2.ConfirmedList = allConfirmedMask @@ -1244,9 +1238,6 @@ func (pR *Room) applyInputFrameDownsyncDynamicsOnSingleRenderFrame(delayedInputF effPushbacks[joinIndex-1].X, effPushbacks[joinIndex-1].Y = float64(0), float64(0) currPlayerDownsync := currRenderFrame.Players[playerId] encodedInput := inputList[joinIndex-1] - if 0 == encodedInput { - continue - } decodedInput := DIRECTION_DECODER[encodedInput] newVx := (currPlayerDownsync.VirtualGridX + (decodedInput[0] + decodedInput[0]*currPlayerDownsync.Speed)) newVy := (currPlayerDownsync.VirtualGridY + (decodedInput[1] + decodedInput[1]*currPlayerDownsync.Speed)) diff --git a/frontend/assets/scripts/Map.js b/frontend/assets/scripts/Map.js index f0969b0..9b00b67 100644 --- a/frontend/assets/scripts/Map.js +++ b/frontend/assets/scripts/Map.js @@ -174,9 +174,16 @@ cc.Class({ } const joinIndex = self.selfPlayerInfo.joinIndex; - const discreteDir = self.ctrl.getDiscretizedDirection(); const previousInputFrameDownsyncWithPrediction = self.getCachedInputFrameDownsyncWithPrediction(inputFrameId); + const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]); + + // 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)) { + return [previousSelfInput, existingInputFrame.inputList[joinIndex - 1]]; + } const prefabbedInputList = (null == previousInputFrameDownsyncWithPrediction ? new Array(self.playerRichInfoDict.size).fill(0) : previousInputFrameDownsyncWithPrediction.inputList.slice()); + const discreteDir = self.ctrl.getDiscretizedDirection(); prefabbedInputList[(joinIndex - 1)] = discreteDir.encodedIdx; const prefabbedInputFrameDownsync = { inputFrameId: inputFrameId, @@ -186,7 +193,6 @@ cc.Class({ self.dumpToInputCache(prefabbedInputFrameDownsync); // A prefabbed inputFrame, would certainly be adding a new inputFrame to the cache, because server only downsyncs "all-confirmed inputFrames" - const previousSelfInput = (null == previousInputFrameDownsyncWithPrediction ? null : previousInputFrameDownsyncWithPrediction.inputList[joinIndex - 1]); return [previousSelfInput, discreteDir.encodedIdx]; }, @@ -664,6 +670,8 @@ 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); } @@ -1000,9 +1008,10 @@ cc.Class({ if (null != delayedInputFrame) { const inputList = delayedInputFrame.inputList; - const effPushbacks = new Array(inputList.length).fill([0.0, 0.0]); // Guaranteed determinism regardless of traversal order + const effPushbacks = new Array(self.playerRichInfoArr.length); // Guaranteed determinism regardless of traversal order for (let j in self.playerRichInfoArr) { const joinIndex = parseInt(j) + 1; + effPushbacks[joinIndex - 1] = [0.0, 0.0]; const playerId = self.playerRichInfoArr[j].id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); @@ -1010,9 +1019,6 @@ cc.Class({ const encodedInput = inputList[joinIndex - 1]; const decodedInput = self.ctrl.decodeDirection(encodedInput); - if (0 == decodedInput.dx && 0 == decodedInput.dy) { - continue; - } // console.log(`Got non-zero inputs for playerId=${playerId}, decodedInput=${JSON.stringify(decodedInput)} @currRenderFrame.id=${currRenderFrame.id}, delayedInputFrame.id=${delayedInputFrame.id}`); /* @@ -1020,7 +1026,7 @@ cc.Class({ */ const newVx = player.virtualGridX + (decodedInput.dx + player.speed * decodedInput.dx); const newVy = player.virtualGridY + (decodedInput.dy + player.speed * decodedInput.dy); - const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[j]); + const newCpos = self.virtualGridToPlayerColliderPos(newVx, newVy, self.playerRichInfoArr[joinIndex - 1]); playerCollider.x = newCpos[0]; playerCollider.y = newCpos[1]; // Update directions and thus would eventually update moving animation accordingly @@ -1041,11 +1047,9 @@ cc.Class({ // Test if the player collides with the wall if (!playerCollider.collides(potential, result)) continue; // Push the player out of the wall - effPushbacks[j][0] += result.overlap * result.overlap_x; - effPushbacks[j][1] += result.overlap * result.overlap_y; + effPushbacks[joinIndex - 1][0] += result.overlap * result.overlap_x; + effPushbacks[joinIndex - 1][1] += result.overlap * result.overlap_y; } - - const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x, playerCollider.y, self.playerRichInfoArr[j]); } for (let j in self.playerRichInfoArr) { @@ -1053,7 +1057,7 @@ cc.Class({ const playerId = self.playerRichInfoArr[j].id; const collisionPlayerIndex = self.collisionPlayerIndexPrefix + joinIndex; const playerCollider = collisionSysMap.get(collisionPlayerIndex); - const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[j][0], playerCollider.y - effPushbacks[j][1], self.playerRichInfoArr[j]); + const newVpos = self.playerColliderAnchorToVirtualGridPos(playerCollider.x - effPushbacks[joinIndex - 1][0], playerCollider.y - effPushbacks[joinIndex - 1][1], self.playerRichInfoArr[j]); nextRenderFramePlayers[playerId].virtualGridX = newVpos[0]; nextRenderFramePlayers[playerId].virtualGridY = newVpos[1]; }