mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-21 14:37:44 +00:00
Compare commits
6 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
b9827f8430 | ||
|
91d16b1cc4 | ||
|
b19868920a | ||
|
be5200663c | ||
|
7b878ff947 | ||
|
c78c480f99 |
@@ -25,9 +25,16 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"net"
|
"net"
|
||||||
|
// _ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
/*
|
||||||
|
// Only used for profiling
|
||||||
|
go func() {
|
||||||
|
http.ListenAndServe("0.0.0.0:6060", nil)
|
||||||
|
}()
|
||||||
|
*/
|
||||||
MustParseConfig()
|
MustParseConfig()
|
||||||
MustParseConstants()
|
MustParseConstants()
|
||||||
storage.Init()
|
storage.Init()
|
||||||
|
@@ -47,8 +47,7 @@ type Player struct {
|
|||||||
TutorialStage int `db:"tutorial_stage"`
|
TutorialStage int `db:"tutorial_stage"`
|
||||||
|
|
||||||
// other in-battle info fields
|
// other in-battle info fields
|
||||||
LastReceivedInputFrameId int32
|
LastConsecutiveRecvInputFrameId int32
|
||||||
LastUdpReceivedInputFrameId int32
|
|
||||||
LastSentInputFrameId int32
|
LastSentInputFrameId int32
|
||||||
AckingFrameId int32
|
AckingFrameId int32
|
||||||
AckingInputFrameId int32
|
AckingInputFrameId int32
|
||||||
|
@@ -136,7 +136,7 @@ type Room struct {
|
|||||||
EffectivePlayerCount int32
|
EffectivePlayerCount int32
|
||||||
DismissalWaitGroup sync.WaitGroup
|
DismissalWaitGroup sync.WaitGroup
|
||||||
InputsBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
|
InputsBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
|
||||||
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputList, player.LastReceivedInputFrameId, player.LastUdpReceivedInputFrameId]
|
InputsBufferLock sync.Mutex // Guards [InputsBuffer, LatestPlayerUpsyncedInputFrameId, LastAllConfirmedInputFrameId, LastAllConfirmedInputList, LastAllConfirmedInputFrameIdWithChange, LastIndividuallyConfirmedInputFrameId, LastIndividuallyConfirmedInputList, player.LastConsecutiveRecvInputFrameId]
|
||||||
RenderFrameBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
|
RenderFrameBuffer *resolv.RingBuffer // Indices are STRICTLY consecutive
|
||||||
LatestPlayerUpsyncedInputFrameId int32
|
LatestPlayerUpsyncedInputFrameId int32
|
||||||
LastAllConfirmedInputFrameId int32
|
LastAllConfirmedInputFrameId int32
|
||||||
@@ -157,6 +157,8 @@ type Room struct {
|
|||||||
TmxPolygonsMap StrToPolygon2DListMap
|
TmxPolygonsMap StrToPolygon2DListMap
|
||||||
|
|
||||||
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
|
rdfIdToActuallyUsedInput map[int32]*pb.InputFrameDownsync
|
||||||
|
allowUpdateInputFrameInPlaceUponDynamics bool
|
||||||
|
LastIndividuallyConfirmedInputFrameId []int32
|
||||||
LastIndividuallyConfirmedInputList []uint64
|
LastIndividuallyConfirmedInputList []uint64
|
||||||
|
|
||||||
BattleUdpTunnelLock sync.Mutex
|
BattleUdpTunnelLock sync.Mutex
|
||||||
@@ -194,8 +196,7 @@ func (pR *Room) AddPlayerIfPossible(pPlayerFromDbInit *Player, speciesId int, se
|
|||||||
pPlayerFromDbInit.AckingFrameId = -1
|
pPlayerFromDbInit.AckingFrameId = -1
|
||||||
pPlayerFromDbInit.AckingInputFrameId = -1
|
pPlayerFromDbInit.AckingInputFrameId = -1
|
||||||
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
pPlayerFromDbInit.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
||||||
pPlayerFromDbInit.LastReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
pPlayerFromDbInit.LastConsecutiveRecvInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
||||||
pPlayerFromDbInit.LastUdpReceivedInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
|
||||||
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
|
pPlayerFromDbInit.BattleState = PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK
|
||||||
|
|
||||||
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
|
pPlayerFromDbInit.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
|
||||||
@@ -237,7 +238,7 @@ func (pR *Room) ReAddPlayerIfPossible(pTmpPlayerInstance *Player, session *webso
|
|||||||
pEffectiveInRoomPlayerInstance.AckingFrameId = -1
|
pEffectiveInRoomPlayerInstance.AckingFrameId = -1
|
||||||
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
|
pEffectiveInRoomPlayerInstance.AckingInputFrameId = -1
|
||||||
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
|
pEffectiveInRoomPlayerInstance.LastSentInputFrameId = MAGIC_LAST_SENT_INPUT_FRAME_ID_READDED
|
||||||
// [WARNING] DON'T reset "player.LastReceivedInputFrameId" & "player.LastUdpReceivedInputFrameId" upon reconnection!
|
// [WARNING] DON'T reset "player.LastConsecutiveRecvInputFrameId" & "pR.LastIndividuallyConfirmedInputFrameId[...]" upon reconnection!
|
||||||
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
|
pEffectiveInRoomPlayerInstance.BattleState = PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK
|
||||||
|
|
||||||
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
|
pEffectiveInRoomPlayerInstance.ColliderRadius = DEFAULT_PLAYER_RADIUS // Hardcoded
|
||||||
@@ -483,7 +484,7 @@ func (pR *Room) StartBattle() {
|
|||||||
*/
|
*/
|
||||||
totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt
|
totalElapsedNanos := utils.UnixtimeNano() - battleStartedAt
|
||||||
nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling
|
nextRenderFrameId := int32((totalElapsedNanos + pR.dilutedRollbackEstimatedDtNanos - 1) / pR.dilutedRollbackEstimatedDtNanos) // fast ceiling
|
||||||
toSleepNanos := int64(0)
|
toSleepNanos := int64(pR.dilutedRollbackEstimatedDtNanos >> 1) // Sleep half-frame time by default
|
||||||
if nextRenderFrameId > pR.RenderFrameId {
|
if nextRenderFrameId > pR.RenderFrameId {
|
||||||
if 0 == pR.RenderFrameId {
|
if 0 == pR.RenderFrameId {
|
||||||
// It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames
|
// It's important to send kickoff frame iff "0 == pR.RenderFrameId && nextRenderFrameId > pR.RenderFrameId", otherwise it might send duplicate kickoff frames
|
||||||
@@ -515,7 +516,7 @@ func (pR *Room) StartBattle() {
|
|||||||
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
|
pR.LastRenderFrameIdTriggeredAt = utils.UnixtimeNano()
|
||||||
|
|
||||||
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
|
elapsedInCalculation := (utils.UnixtimeNano() - stCalculation)
|
||||||
toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation // don't sleep if "nextRenderFrame == pR.RenderFrameId"
|
toSleepNanos = pR.dilutedRollbackEstimatedDtNanos - elapsedInCalculation
|
||||||
if elapsedInCalculation > pR.RollbackEstimatedDtNanos {
|
if elapsedInCalculation > pR.RollbackEstimatedDtNanos {
|
||||||
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos))
|
Logger.Warn(fmt.Sprintf("SLOW FRAME! Elapsed time statistics: roomId=%v, room.RenderFrameId=%v, elapsedInCalculation=%v ns, dynamicsDuration=%v ns, RollbackEstimatedDtNanos=%v, dilutedRollbackEstimatedDtNanos=%v", pR.Id, pR.RenderFrameId, elapsedInCalculation, dynamicsDuration, pR.RollbackEstimatedDtNanos, pR.dilutedRollbackEstimatedDtNanos))
|
||||||
}
|
}
|
||||||
@@ -544,13 +545,13 @@ func (pR *Room) StartBattle() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
|
// [WARNING] DON'T put a "default" block here! Otherwise "for { select {... default: } }" pattern would NEVER block on empty channel and thus consume a lot of CPU time unnecessarily!
|
||||||
case inputsBufferSnapshot := <-playerDownsyncChan:
|
case inputsBufferSnapshot := <-playerDownsyncChan:
|
||||||
pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync)
|
pR.downsyncToSinglePlayer(playerId, player, inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, inputsBufferSnapshot.ToSendInputFrameDownsyncs, inputsBufferSnapshot.ShouldForceResync)
|
||||||
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d)#2", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, playerId))
|
//Logger.Info(fmt.Sprintf("Sent inputsBufferSnapshot(refRenderFrameId:%d, unconfirmedMask:%v) to for (roomId: %d, playerId:%d)#2", inputsBufferSnapshot.RefRenderFrameId, inputsBufferSnapshot.UnconfirmedMask, pR.Id, playerId))
|
||||||
case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan:
|
case inputsBufferSnapshot2 := <-playerSecondaryDownsyncChan:
|
||||||
pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex)
|
pR.downsyncPeerInputFrameUpsyncToSinglePlayer(playerId, player, inputsBufferSnapshot2.ToSendInputFrameDownsyncs, inputsBufferSnapshot2.PeerJoinIndex)
|
||||||
//Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId))
|
//Logger.Info(fmt.Sprintf("Sent secondary inputsBufferSnapshot to for (roomId: %d, playerId:%d)#2", pR.Id, playerId))
|
||||||
default:
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -804,6 +805,11 @@ func (pR *Room) OnDismissed() {
|
|||||||
pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
|
pR.RenderFrameBuffer = resolv.NewRingBuffer(pR.RenderCacheSize)
|
||||||
pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
|
pR.InputsBuffer = resolv.NewRingBuffer((pR.RenderCacheSize >> 1) + 1)
|
||||||
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
|
pR.rdfIdToActuallyUsedInput = make(map[int32]*pb.InputFrameDownsync)
|
||||||
|
pR.allowUpdateInputFrameInPlaceUponDynamics = true
|
||||||
|
pR.LastIndividuallyConfirmedInputFrameId = make([]int32, pR.Capacity)
|
||||||
|
for i := 0; i < pR.Capacity; i++ {
|
||||||
|
pR.LastIndividuallyConfirmedInputFrameId[i] = MAGIC_LAST_SENT_INPUT_FRAME_ID_NORMAL_ADDED
|
||||||
|
}
|
||||||
pR.LastIndividuallyConfirmedInputList = make([]uint64, pR.Capacity)
|
pR.LastIndividuallyConfirmedInputList = make([]uint64, pR.Capacity)
|
||||||
|
|
||||||
pR.LatestPlayerUpsyncedInputFrameId = -1
|
pR.LatestPlayerUpsyncedInputFrameId = -1
|
||||||
@@ -817,7 +823,7 @@ func (pR *Room) OnDismissed() {
|
|||||||
|
|
||||||
pR.collisionHolder = resolv.NewCollision()
|
pR.collisionHolder = resolv.NewCollision()
|
||||||
pR.effPushbacks = make([]*battle.Vec2D, pR.Capacity)
|
pR.effPushbacks = make([]*battle.Vec2D, pR.Capacity)
|
||||||
for i := 0; i < len(pR.effPushbacks); i++ {
|
for i := 0; i < pR.Capacity; i++ {
|
||||||
pR.effPushbacks[i] = &battle.Vec2D{X: 0, Y: 0}
|
pR.effPushbacks[i] = &battle.Vec2D{X: 0, Y: 0}
|
||||||
}
|
}
|
||||||
pR.hardPushbackNormsArr = make([][]*battle.Vec2D, pR.Capacity)
|
pR.hardPushbackNormsArr = make([][]*battle.Vec2D, pR.Capacity)
|
||||||
@@ -1190,9 +1196,9 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
|
|||||||
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#1: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
|
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#1: roomId=%v, playerId=%v, clientInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, pR.InputsBufferString(false)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if clientInputFrameId < player.LastReceivedInputFrameId {
|
if clientInputFrameId < player.LastConsecutiveRecvInputFrameId {
|
||||||
// [WARNING] It's important for correctness that we use "player.LastReceivedInputFrameId" instead of "player.LastUdpReceivedInputFrameId" here!
|
// [WARNING] It's important for correctness that we use "player.LastConsecutiveRecvInputFrameId" instead of "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" here!
|
||||||
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastReceivedInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastReceivedInputFrameId, pR.InputsBufferString(false)))
|
Logger.Debug(fmt.Sprintf("Omitting obsolete inputFrameUpsync#2: roomId=%v, playerId=%v, clientInputFrameId=%v, playerLastConsecutiveRecvInputFrameId=%v, InputsBuffer=%v", pR.Id, playerId, clientInputFrameId, player.LastConsecutiveRecvInputFrameId, pR.InputsBufferString(false)))
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if clientInputFrameId > pR.InputsBuffer.EdFrameId {
|
if clientInputFrameId > pR.InputsBuffer.EdFrameId {
|
||||||
@@ -1208,19 +1214,19 @@ func (pR *Room) markConfirmationIfApplicable(inputFrameUpsyncBatch []*pb.InputFr
|
|||||||
/*
|
/*
|
||||||
[WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
|
[WARNING] We have to distinguish whether or not the incoming batch is from UDP here, otherwise "pR.LatestPlayerUpsyncedInputFrameId - pR.LastAllConfirmedInputFrameId" might become unexpectedly large in case of "UDP packet loss + slow ws session"!
|
||||||
|
|
||||||
Moreover, only ws session upsyncs should advance "player.LastReceivedInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
|
Moreover, only ws session upsyncs should advance "player.LastConsecutiveRecvInputFrameId" & "pR.LatestPlayerUpsyncedInputFrameId".
|
||||||
|
|
||||||
Kindly note that the updates of "player.LastReceivedInputFrameId" could be discrete before and after reconnection.
|
Kindly note that the updates of "player.LastConsecutiveRecvInputFrameId" could be discrete before and after reconnection.
|
||||||
*/
|
*/
|
||||||
player.LastReceivedInputFrameId = clientInputFrameId
|
player.LastConsecutiveRecvInputFrameId = clientInputFrameId
|
||||||
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
|
if clientInputFrameId > pR.LatestPlayerUpsyncedInputFrameId {
|
||||||
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
|
pR.LatestPlayerUpsyncedInputFrameId = clientInputFrameId
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if clientInputFrameId > player.LastUdpReceivedInputFrameId {
|
if clientInputFrameId > pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] {
|
||||||
// No need to update "player.LastUdpReceivedInputFrameId" only when "true == fromUDP", we should keep "player.LastUdpReceivedInputFrameId >= player.LastReceivedInputFrameId" at any moment.
|
// No need to update "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1]" only when "true == fromUDP", we should keep "pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] >= player.LastConsecutiveRecvInputFrameId" at any moment.
|
||||||
player.LastUdpReceivedInputFrameId = clientInputFrameId
|
pR.LastIndividuallyConfirmedInputFrameId[player.JoinIndex-1] = clientInputFrameId
|
||||||
// It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block.
|
// It's safe (in terms of getting an eventually correct "RenderFrameBuffer") to put the following update of "pR.LastIndividuallyConfirmedInputList" which is ONLY used for prediction in "InputsBuffer" out of "false == fromUDP" block.
|
||||||
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
|
pR.LastIndividuallyConfirmedInputList[player.JoinIndex-1] = inputFrameUpsync.Encoded
|
||||||
}
|
}
|
||||||
@@ -1299,6 +1305,9 @@ func (pR *Room) forceConfirmationIfApplicable(prevRenderFrameId int32) uint64 {
|
|||||||
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v! InputsBuffer=%v", j, pR.Id, pR.InputsBufferString(false)))
|
panic(fmt.Sprintf("inputFrameId=%v doesn't exist for roomId=%v! InputsBuffer=%v", j, pR.Id, pR.InputsBufferString(false)))
|
||||||
}
|
}
|
||||||
inputFrameDownsync := tmp.(*battle.InputFrameDownsync)
|
inputFrameDownsync := tmp.(*battle.InputFrameDownsync)
|
||||||
|
if pR.allowUpdateInputFrameInPlaceUponDynamics {
|
||||||
|
battle.UpdateInputFrameInPlaceUponDynamics(j, pR.Capacity, inputFrameDownsync.ConfirmedList, inputFrameDownsync.InputList, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, int32(MAGIC_JOIN_INDEX_INVALID))
|
||||||
|
}
|
||||||
unconfirmedMask |= (allConfirmedMask ^ inputFrameDownsync.ConfirmedList)
|
unconfirmedMask |= (allConfirmedMask ^ inputFrameDownsync.ConfirmedList)
|
||||||
inputFrameDownsync.ConfirmedList = allConfirmedMask
|
inputFrameDownsync.ConfirmedList = allConfirmedMask
|
||||||
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
|
pR.onInputFrameDownsyncAllConfirmed(inputFrameDownsync, -1)
|
||||||
@@ -1379,7 +1388,7 @@ func (pR *Room) applyInputFrameDownsyncDynamics(fromRenderFrameId int32, toRende
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame.Id, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr, pR.RenderFrameBuffer, pR.collisionHolder, pR.effPushbacks, pR.hardPushbackNormsArr, pR.jumpedOrNotList, pR.dynamicRectangleColliders)
|
battle.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(pR.InputsBuffer, currRenderFrame.Id, pR.Space, pR.CollisionSysMap, pR.SpaceOffsetX, pR.SpaceOffsetY, pR.CharacterConfigsArr, pR.RenderFrameBuffer, pR.collisionHolder, pR.effPushbacks, pR.hardPushbackNormsArr, pR.jumpedOrNotList, pR.dynamicRectangleColliders, pR.LastIndividuallyConfirmedInputFrameId, pR.LastIndividuallyConfirmedInputList, false, MAGIC_JOIN_INDEX_INVALID) // "allowUpdateInputFrameInPlaceUponDynamics" is instead used when "forceConfirmationIfApplicable"
|
||||||
pR.CurDynamicsRenderFrameId++
|
pR.CurDynamicsRenderFrameId++
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -512,8 +512,7 @@ cc.Class({
|
|||||||
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
window.clearBoundRoomIdInBothVolatileAndPersistentStorage();
|
||||||
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
window.initPersistentSessionClient(self.initAfterWSConnected, null /* Deliberately NOT passing in any `expectedRoomId`. -- YFLu */ );
|
||||||
};
|
};
|
||||||
resultPanelScriptIns.onCloseDelegate = () => {
|
resultPanelScriptIns.onCloseDelegate = () => {};
|
||||||
};
|
|
||||||
|
|
||||||
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
|
self.gameRuleNode = cc.instantiate(self.gameRulePrefab);
|
||||||
self.gameRuleNode.width = self.canvasNode.width;
|
self.gameRuleNode.width = self.canvasNode.width;
|
||||||
@@ -1428,7 +1427,8 @@ othersForcedDownsyncRenderFrame=${JSON.stringify(othersForcedDownsyncRenderFrame
|
|||||||
};
|
};
|
||||||
self.rdfIdToActuallyUsedInput.set(i, inputFrameDownsyncClone);
|
self.rdfIdToActuallyUsedInput.set(i, inputFrameDownsyncClone);
|
||||||
}
|
}
|
||||||
const renderRes = gopkgs.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(self.recentInputCache, i, collisionSys, collisionSysMap, self.spaceOffsetX, self.spaceOffsetY, self.chConfigsOrderedByJoinIndex, self.recentRenderCache, self.collisionHolder, self.effPushbacks, self.hardPushbackNormsArr, self.jumpedOrNotList, self.dynamicRectangleColliders);
|
const allowUpdateInputFrameInPlaceUponDynamics = (!isChasing);
|
||||||
|
const renderRes = gopkgs.ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(self.recentInputCache, i, collisionSys, collisionSysMap, self.spaceOffsetX, self.spaceOffsetY, self.chConfigsOrderedByJoinIndex, self.recentRenderCache, self.collisionHolder, self.effPushbacks, self.hardPushbackNormsArr, self.jumpedOrNotList, self.dynamicRectangleColliders, self.lastIndividuallyConfirmedInputFrameId, self.lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, self.selfPlayerInfo.joinIndex);
|
||||||
const nextRdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i + 1);
|
const nextRdf = gopkgs.GetRoomDownsyncFrame(self.recentRenderCache, i + 1);
|
||||||
|
|
||||||
if (true == isChasing) {
|
if (true == isChasing) {
|
||||||
|
File diff suppressed because one or more lines are too long
@@ -160,7 +160,6 @@ void startRecvLoop(void* arg) {
|
|||||||
|
|
||||||
int uvCloseRet = uv_loop_close(l);
|
int uvCloseRet = uv_loop_close(l);
|
||||||
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
|
CCLOG("UDP recv loop is closed in UvRecvThread, uvCloseRet=%d", uvCloseRet);
|
||||||
uv_mutex_destroy(&recvRingBuffLock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void startSendLoop(void* arg) {
|
void startSendLoop(void* arg) {
|
||||||
@@ -174,7 +173,6 @@ void startSendLoop(void* arg) {
|
|||||||
|
|
||||||
int uvCloseRet = uv_loop_close(l);
|
int uvCloseRet = uv_loop_close(l);
|
||||||
CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet);
|
CCLOG("UDP send loop is closed in UvSendThread, uvCloseRet=%d", uvCloseRet);
|
||||||
uv_mutex_destroy(&sendRingBuffLock);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
int initSendLoop(struct sockaddr const* pUdpAddr) {
|
int initSendLoop(struct sockaddr const* pUdpAddr) {
|
||||||
@@ -189,9 +187,6 @@ int initSendLoop(struct sockaddr const* pUdpAddr) {
|
|||||||
uv_mutex_init(&sendRingBuffLock);
|
uv_mutex_init(&sendRingBuffLock);
|
||||||
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
|
sendRingBuff = new SendRingBuff(maxBuffedMsgs);
|
||||||
|
|
||||||
uv_mutex_init(&recvRingBuffLock);
|
|
||||||
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
|
|
||||||
|
|
||||||
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
|
uv_async_init(sendLoop, &uvSendLoopStopSig, _onUvStopSig);
|
||||||
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
|
uv_async_init(sendLoop, &uvSendLoopTriggerSig, _onUvSthNewToSend);
|
||||||
|
|
||||||
@@ -208,6 +203,9 @@ bool initRecvLoop(struct sockaddr const* pUdpAddr) {
|
|||||||
CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes));
|
CCLOGERROR("Failed to bind recv; recvSockInitRes=%d, recvbindRes=%d, reason=%s", recvSockInitRes, recvbindRes, uv_strerror(recvbindRes));
|
||||||
exit(-1);
|
exit(-1);
|
||||||
}
|
}
|
||||||
|
uv_mutex_init(&recvRingBuffLock);
|
||||||
|
recvRingBuff = new RecvRingBuff(maxBuffedMsgs);
|
||||||
|
|
||||||
uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead);
|
uv_udp_recv_start(udpRecvSocket, _allocBuffer, _onRead);
|
||||||
uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig);
|
uv_async_init(recvLoop, &uvRecvLoopStopSig, _onUvStopSig);
|
||||||
|
|
||||||
@@ -249,6 +247,12 @@ bool DelayNoMore::UdpSession::openUdpSession(int port) {
|
|||||||
bool DelayNoMore::UdpSession::closeUdpSession() {
|
bool DelayNoMore::UdpSession::closeUdpSession() {
|
||||||
CCLOG("About to close udp session and dealloc all resources...");
|
CCLOG("About to close udp session and dealloc all resources...");
|
||||||
|
|
||||||
|
/*
|
||||||
|
[WARNING] It's possible that "closeUdpSession" is called when "openUdpSession" was NEVER CALLED, thus we have to avoid program crash in this case.
|
||||||
|
|
||||||
|
In general one shouldn't just check the state of "sendTid" by whether or not "NULL == sendLoop", but in this particular game, both "openUdpSession" and "closeUdpSession" are only called from "GameThread", no thread-safety concern here, i.e. if "openUdpSession" was ever called earlier, then "sendLoop" wouldn't be NULL when "closeUdpSession" is later called.
|
||||||
|
*/
|
||||||
|
if (NULL != sendLoop) {
|
||||||
uv_async_send(&uvSendLoopStopSig);
|
uv_async_send(&uvSendLoopStopSig);
|
||||||
CCLOG("Signaling UvSendThread to end in GameThread...");
|
CCLOG("Signaling UvSendThread to end in GameThread...");
|
||||||
uv_thread_join(&sendTid);
|
uv_thread_join(&sendTid);
|
||||||
@@ -256,6 +260,14 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
|
|||||||
free(sendLoop);
|
free(sendLoop);
|
||||||
delete sendRingBuff;
|
delete sendRingBuff;
|
||||||
|
|
||||||
|
udpSendSocket = NULL;
|
||||||
|
sendLoop = NULL;
|
||||||
|
sendRingBuff = NULL;
|
||||||
|
|
||||||
|
uv_mutex_destroy(&sendRingBuffLock);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (NULL != recvLoop) {
|
||||||
uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send
|
uv_async_send(&uvRecvLoopStopSig); // The few if not only guaranteed thread safe utility of libuv :) See http://docs.libuv.org/en/v1.x/async.html#c.uv_async_send
|
||||||
CCLOG("Signaling UvRecvThread to end in GameThread...");
|
CCLOG("Signaling UvRecvThread to end in GameThread...");
|
||||||
uv_thread_join(&recvTid);
|
uv_thread_join(&recvTid);
|
||||||
@@ -263,6 +275,13 @@ bool DelayNoMore::UdpSession::closeUdpSession() {
|
|||||||
free(recvLoop);
|
free(recvLoop);
|
||||||
delete recvRingBuff;
|
delete recvRingBuff;
|
||||||
|
|
||||||
|
udpRecvSocket = NULL;
|
||||||
|
recvLoop = NULL;
|
||||||
|
recvRingBuff = NULL;
|
||||||
|
|
||||||
|
uv_mutex_destroy(&recvRingBuffLock);
|
||||||
|
}
|
||||||
|
|
||||||
CCLOG("Closed udp session and dealloc all resources in GameThread...");
|
CCLOG("Closed udp session and dealloc all resources in GameThread...");
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -356,8 +375,10 @@ bool DelayNoMore::UdpSession::pollUdpRecvRingBuff() {
|
|||||||
while (true) {
|
while (true) {
|
||||||
RecvWork f;
|
RecvWork f;
|
||||||
bool res = recvRingBuff->pop(&f);
|
bool res = recvRingBuff->pop(&f);
|
||||||
if (!res) return false;
|
if (!res) {
|
||||||
|
// Deliberately returning "true" here to prevent "jswrapper" from printing "Failed to invoke Xxx..." too frequently
|
||||||
|
return true;
|
||||||
|
}
|
||||||
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
// [WARNING] Declaring "AutoHandleScope" is critical here, otherwise "onUdpMessageCb.toObject()" wouldn't be recognized as a function of the ScriptEngine!
|
||||||
se::AutoHandleScope hs;
|
se::AutoHandleScope hs;
|
||||||
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
// [WARNING] Use of the "ScriptEngine" is only allowed in "GameThread a.k.a. CocosThread"!
|
||||||
|
@@ -76,7 +76,7 @@
|
|||||||
"shelter_z_reducer",
|
"shelter_z_reducer",
|
||||||
"shelter"
|
"shelter"
|
||||||
],
|
],
|
||||||
"last-module-event-record-time": 1676513919950,
|
"last-module-event-record-time": 1677337364473,
|
||||||
"simulator-orientation": false,
|
"simulator-orientation": false,
|
||||||
"simulator-resolution": {
|
"simulator-resolution": {
|
||||||
"height": 640,
|
"height": 640,
|
||||||
|
@@ -490,6 +490,23 @@ func calcHardPushbacksNorms(joinIndex int32, currPlayerDownsync, thatPlayerInNex
|
|||||||
return retCnt
|
return retCnt
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func UpdateInputFrameInPlaceUponDynamics(inputFrameId int32, roomCapacity int, confirmedList uint64, inputList []uint64, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) {
|
||||||
|
for i := 0; i < roomCapacity; i++ {
|
||||||
|
if int32(i+1) == toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics {
|
||||||
|
// On frontend, a "self input" is only confirmed by websocket downsync, which is quite late and might get the "self input" incorrectly overwritten if not excluded here
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if 0 < (confirmedList & (1 << uint32(i))) {
|
||||||
|
// This in-place update on the "inputsBuffer" is only correct when "delayed input for this player is not yet confirmed"
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if lastIndividuallyConfirmedInputFrameId[i] >= inputFrameId {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
inputList[i] = (lastIndividuallyConfirmedInputList[i] & uint64(15))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *resolv.RingBuffer) (int, bool, int32, int32) {
|
func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync, currRenderFrame *RoomDownsyncFrame, chConfig *CharacterConfig, inputsBuffer *resolv.RingBuffer) (int, bool, int32, int32) {
|
||||||
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
|
// returns (patternId, jumpedOrNot, effectiveDx, effectiveDy)
|
||||||
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
|
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
|
||||||
@@ -503,10 +520,13 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
|
|||||||
return PATTERN_ID_UNABLE_TO_OP, false, 0, 0
|
return PATTERN_ID_UNABLE_TO_OP, false, 0, 0
|
||||||
}
|
}
|
||||||
|
|
||||||
delayedInputList := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync).InputList
|
delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
|
||||||
|
delayedInputList := delayedInputFrameDownsync.InputList
|
||||||
|
|
||||||
var delayedInputListForPrevRdf []uint64 = nil
|
var delayedInputListForPrevRdf []uint64 = nil
|
||||||
if 0 < delayedInputFrameIdForPrevRdf {
|
if 0 < delayedInputFrameIdForPrevRdf {
|
||||||
delayedInputListForPrevRdf = inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync).InputList
|
delayedInputFrameDownsyncForPrevRdf := inputsBuffer.GetByFrameId(delayedInputFrameIdForPrevRdf).(*InputFrameDownsync)
|
||||||
|
delayedInputListForPrevRdf = delayedInputFrameDownsyncForPrevRdf.InputList
|
||||||
}
|
}
|
||||||
|
|
||||||
jumpedOrNot := false
|
jumpedOrNot := false
|
||||||
@@ -564,7 +584,7 @@ func deriveOpPattern(currPlayerDownsync, thatPlayerInNextFrame *PlayerDownsync,
|
|||||||
|
|
||||||
The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & dynamicRectangleColliders("player" & "bullet"), which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously.
|
The function "ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame" is creating new heap-memory blocks at 60fps, e.g. nextRenderFramePlayers & nextRenderFrameMeleeBullets & nextRenderFrameFireballBullets & effPushbacks & hardPushbackNorms & jumpedOrNotList & dynamicRectangleColliders("player" & "bullet"), which would induce "possibly performance impacting garbage collections" when many rooms are running simultaneously.
|
||||||
*/
|
*/
|
||||||
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object) bool {
|
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
|
||||||
currRenderFrame := renderFrameBuffer.GetByFrameId(currRenderFrameId).(*RoomDownsyncFrame)
|
currRenderFrame := renderFrameBuffer.GetByFrameId(currRenderFrameId).(*RoomDownsyncFrame)
|
||||||
nextRenderFrameId := currRenderFrameId + 1
|
nextRenderFrameId := currRenderFrameId + 1
|
||||||
roomCapacity := len(currRenderFrame.PlayersArr)
|
roomCapacity := len(currRenderFrame.PlayersArr)
|
||||||
@@ -610,6 +630,17 @@ func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer *resolv.Rin
|
|||||||
|
|
||||||
bulletLocalId := currRenderFrame.BulletLocalIdCounter
|
bulletLocalId := currRenderFrame.BulletLocalIdCounter
|
||||||
// 1. Process player inputs
|
// 1. Process player inputs
|
||||||
|
delayedInputFrameId := ConvertToDelayedInputFrameId(currRenderFrame.Id)
|
||||||
|
|
||||||
|
if 0 < delayedInputFrameId {
|
||||||
|
delayedInputFrameDownsync := inputsBuffer.GetByFrameId(delayedInputFrameId).(*InputFrameDownsync)
|
||||||
|
delayedInputList := delayedInputFrameDownsync.InputList
|
||||||
|
roomCapacity := len(delayedInputList)
|
||||||
|
if allowUpdateInputFrameInPlaceUponDynamics {
|
||||||
|
UpdateInputFrameInPlaceUponDynamics(delayedInputFrameId, roomCapacity, delayedInputFrameDownsync.ConfirmedList, delayedInputList, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
|
for i, currPlayerDownsync := range currRenderFrame.PlayersArr {
|
||||||
chConfig := chConfigsOrderedByJoinIndex[i]
|
chConfig := chConfigsOrderedByJoinIndex[i]
|
||||||
thatPlayerInNextFrame := nextRenderFramePlayers[i]
|
thatPlayerInNextFrame := nextRenderFramePlayers[i]
|
||||||
|
@@ -106,9 +106,9 @@ func GetCharacterConfigsOrderedByJoinIndex(speciesIdList []int) []*js.Object {
|
|||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|
||||||
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object) bool {
|
func ApplyInputFrameDownsyncDynamicsOnSingleRenderFrameJs(inputsBuffer *resolv.RingBuffer, currRenderFrameId int32, collisionSys *resolv.Space, collisionSysMap map[int32]*resolv.Object, collisionSpaceOffsetX, collisionSpaceOffsetY float64, chConfigsOrderedByJoinIndex []*CharacterConfig, renderFrameBuffer *resolv.RingBuffer, collision *resolv.Collision, effPushbacks []*Vec2D, hardPushbackNormsArr [][]*Vec2D, jumpedOrNotList []bool, dynamicRectangleColliders []*resolv.Object, lastIndividuallyConfirmedInputFrameId []int32, lastIndividuallyConfirmedInputList []uint64, allowUpdateInputFrameInPlaceUponDynamics bool, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics int32) bool {
|
||||||
// We need access to all fields of RoomDownsyncFrame for displaying in frontend
|
// We need access to all fields of RoomDownsyncFrame for displaying in frontend
|
||||||
return ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrameId, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex, renderFrameBuffer, collision, effPushbacks, hardPushbackNormsArr, jumpedOrNotList, dynamicRectangleColliders)
|
return ApplyInputFrameDownsyncDynamicsOnSingleRenderFrame(inputsBuffer, currRenderFrameId, collisionSys, collisionSysMap, collisionSpaceOffsetX, collisionSpaceOffsetY, chConfigsOrderedByJoinIndex, renderFrameBuffer, collision, effPushbacks, hardPushbackNormsArr, jumpedOrNotList, dynamicRectangleColliders, lastIndividuallyConfirmedInputFrameId, lastIndividuallyConfirmedInputList, allowUpdateInputFrameInPlaceUponDynamics, toExcludeJoinIndexUpdateInputFrameInPlaceUponDynamics)
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetRoomDownsyncFrame(renderFrameBuffer *resolv.RingBuffer, frameId int32) *js.Object {
|
func GetRoomDownsyncFrame(renderFrameBuffer *resolv.RingBuffer, frameId int32) *js.Object {
|
||||||
|
Reference in New Issue
Block a user