2022-09-20 15:50:01 +00:00
package models
import (
"encoding/xml"
"fmt"
"github.com/ByteArena/box2d"
"github.com/golang/protobuf/proto"
"github.com/gorilla/websocket"
"go.uber.org/zap"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
. "server/common"
"server/common/utils"
pb "server/pb_output"
2022-09-24 04:01:50 +00:00
"strings"
2022-09-20 15:50:01 +00:00
"sync"
"sync/atomic"
"time"
)
const (
UPSYNC_MSG_ACT_HB_PING = int32 ( 1 )
UPSYNC_MSG_ACT_PLAYER_CMD = int32 ( 2 )
UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK = int32 ( 3 )
DOWNSYNC_MSG_ACT_HB_REQ = int32 ( 1 )
DOWNSYNC_MSG_ACT_INPUT_BATCH = int32 ( 2 )
DOWNSYNC_MSG_ACT_ROOM_FRAME = int32 ( 3 )
)
const (
2022-09-24 04:01:50 +00:00
MAGIC_ROOM_DOWNSYNC_FRAME_ID_BATTLE_READY_TO_START = - 1
MAGIC_ROOM_DOWNSYNC_FRAME_ID_BATTLE_START = 0
2022-09-20 15:50:01 +00:00
MAGIC_ROOM_DOWNSYNC_FRAME_ID_PLAYER_ADDED_AND_ACKED = - 98
MAGIC_ROOM_DOWNSYNC_FRAME_ID_PLAYER_READDED_AND_ACKED = - 97
MAGIC_JOIN_INDEX_DEFAULT = 0
MAGIC_JOIN_INDEX_INVALID = - 1
)
const (
// You can equivalently use the `GroupIndex` approach, but the more complicated and general purpose approach is used deliberately here. Reference http://www.aurelienribon.com/post/2011-07-box2d-tutorial-collision-filtering.
COLLISION_CATEGORY_CONTROLLED_PLAYER = ( 1 << 1 )
COLLISION_CATEGORY_TREASURE = ( 1 << 2 )
COLLISION_CATEGORY_TRAP = ( 1 << 3 )
COLLISION_CATEGORY_TRAP_BULLET = ( 1 << 4 )
COLLISION_CATEGORY_BARRIER = ( 1 << 5 )
COLLISION_CATEGORY_PUMPKIN = ( 1 << 6 )
COLLISION_CATEGORY_SPEED_SHOES = ( 1 << 7 )
COLLISION_MASK_FOR_CONTROLLED_PLAYER = ( COLLISION_CATEGORY_TREASURE | COLLISION_CATEGORY_TRAP | COLLISION_CATEGORY_TRAP_BULLET | COLLISION_CATEGORY_SPEED_SHOES )
COLLISION_MASK_FOR_TREASURE = ( COLLISION_CATEGORY_CONTROLLED_PLAYER )
COLLISION_MASK_FOR_TRAP = ( COLLISION_CATEGORY_CONTROLLED_PLAYER )
COLLISION_MASK_FOR_TRAP_BULLET = ( COLLISION_CATEGORY_CONTROLLED_PLAYER )
COLLISION_MASK_FOR_BARRIER = ( COLLISION_CATEGORY_PUMPKIN )
COLLISION_MASK_FOR_PUMPKIN = ( COLLISION_CATEGORY_BARRIER )
COLLISION_MASK_FOR_SPEED_SHOES = ( COLLISION_CATEGORY_CONTROLLED_PLAYER )
)
var DIRECTION_DECODER = [ ] [ ] int32 {
{ 0 , 0 } ,
{ 0 , + 1 } ,
{ 0 , - 1 } ,
{ + 2 , 0 } ,
{ - 2 , 0 } ,
{ + 2 , + 1 } ,
{ - 2 , - 1 } ,
{ + 2 , - 1 } ,
{ - 2 , + 1 } ,
{ + 2 , 0 } ,
{ - 2 , 0 } ,
{ 0 , + 1 } ,
{ 0 , - 1 } ,
}
type RoomBattleState struct {
IDLE int32
WAITING int32
PREPARE int32
IN_BATTLE int32
STOPPING_BATTLE_FOR_SETTLEMENT int32
IN_SETTLEMENT int32
IN_DISMISSAL int32
}
type BattleStartCbType func ( )
type SignalToCloseConnCbType func ( customRetCode int , customRetMsg string )
// A single instance containing only "named constant integers" to be shared by all threads.
var RoomBattleStateIns RoomBattleState
func InitRoomBattleStateIns ( ) {
RoomBattleStateIns = RoomBattleState {
IDLE : 0 ,
WAITING : - 1 ,
PREPARE : 10000000 ,
IN_BATTLE : 10000001 ,
STOPPING_BATTLE_FOR_SETTLEMENT : 10000002 ,
IN_SETTLEMENT : 10000003 ,
IN_DISMISSAL : 10000004 ,
}
}
func calRoomScore ( inRoomPlayerCount int32 , roomPlayerCnt int , currentRoomBattleState int32 ) float32 {
x := float32 ( inRoomPlayerCount ) / float32 ( roomPlayerCnt )
d := ( x - 0.5 )
d2 := d * d
return - 7.8125 * d2 + 5.0 - float32 ( currentRoomBattleState )
}
type Room struct {
Id int32
Capacity int
Players map [ int32 ] * Player
/ * *
* The following ` PlayerDownsyncSessionDict ` is NOT individually put
* under ` type Player struct ` for a reason .
*
* Upon each connection establishment , a new instance ` player Player ` is created for the given ` playerId ` .
* To be specific , if
* - that ` playerId == 42 ` accidentally reconnects in just several milliseconds after a passive disconnection , e . g . due to bad wireless signal strength , and
* - that ` type Player struct ` contains a ` DownsyncSession ` field
*
* , then we might have to
* - clean up ` previousPlayerInstance.DownsyncSession `
* - initialize ` currentPlayerInstance.DownsyncSession `
*
* to avoid chaotic flaws .
*
* Moreover , during the invocation of ` PlayerSignalToCloseDict ` , the ` Player ` instance is supposed to be deallocated ( though not synchronously ) .
* /
PlayerDownsyncSessionDict map [ int32 ] * websocket . Conn
PlayerSignalToCloseDict map [ int32 ] SignalToCloseConnCbType
Score float32
State int32
Index int
Tick int32
ServerFPS int32
BattleDurationNanos int64
EffectivePlayerCount int32
DismissalWaitGroup sync . WaitGroup
Treasures map [ int32 ] * Treasure
Traps map [ int32 ] * Trap
GuardTowers map [ int32 ] * GuardTower
Bullets map [ int32 ] * Bullet
SpeedShoes map [ int32 ] * SpeedShoe
Barriers map [ int32 ] * Barrier
Pumpkins map [ int32 ] * Pumpkin
AccumulatedLocalIdForBullets int32
CollidableWorld * box2d . B2World
AllPlayerInputsBuffer * RingBuffer
LastAllConfirmedInputFrameId int32
LastAllConfirmedInputFrameIdWithChange int32
LastAllConfirmedInputList [ ] uint64
InputDelayFrames int32
InputScaleFrames uint32 // inputDelayedAndScaledFrameId = ((originalFrameId - InputDelayFrames) >> InputScaleFrames)
JoinIndexBooleanArr [ ] bool
StageName string
StageDiscreteW int32
StageDiscreteH int32
StageTileW int32
StageTileH int32
RawBattleStrToVec2DListMap StrToVec2DListMap
RawBattleStrToPolygon2DListMap StrToPolygon2DListMap
}
func ( pR * Room ) onTreasurePickedUp ( contactingPlayer * Player , contactingTreasure * Treasure ) {
if _ , existent := pR . Treasures [ contactingTreasure . LocalIdInBattle ] ; existent {
Logger . Info ( "Player has picked up treasure:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "contactingPlayer.Id" , contactingPlayer . Id ) , zap . Any ( "contactingTreasure.LocalIdInBattle" , contactingTreasure . LocalIdInBattle ) )
pR . CollidableWorld . DestroyBody ( contactingTreasure . CollidableBody )
pR . Treasures [ contactingTreasure . LocalIdInBattle ] = & Treasure { Removed : true }
pR . Players [ contactingPlayer . Id ] . Score += contactingTreasure . Score
}
}
const (
PLAYER_DEFAULT_SPEED = 200 // Hardcoded
ADD_SPEED = 100 // Hardcoded
)
func ( pR * Room ) onSpeedShoePickedUp ( contactingPlayer * Player , contactingSpeedShoe * SpeedShoe , nowMillis int64 ) {
if _ , existent := pR . SpeedShoes [ contactingSpeedShoe . LocalIdInBattle ] ; existent && contactingPlayer . AddSpeedAtGmtMillis == - 1 {
Logger . Info ( "Player has picked up a SpeedShoe:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "contactingPlayer.Id" , contactingPlayer . Id ) , zap . Any ( "contactingSpeedShoe.LocalIdInBattle" , contactingSpeedShoe . LocalIdInBattle ) )
pR . CollidableWorld . DestroyBody ( contactingSpeedShoe . CollidableBody )
pR . SpeedShoes [ contactingSpeedShoe . LocalIdInBattle ] = & SpeedShoe {
Removed : true ,
RemovedAtFrameId : pR . Tick ,
}
pR . Players [ contactingPlayer . Id ] . Speed += ADD_SPEED
pR . Players [ contactingPlayer . Id ] . AddSpeedAtGmtMillis = nowMillis
}
}
func ( pR * Room ) onBulletCrashed ( contactingPlayer * Player , contactingBullet * Bullet , nowMillis int64 , maxMillisToFreezePerPlayer int64 ) {
if _ , existent := pR . Bullets [ contactingBullet . LocalIdInBattle ] ; existent {
pR . CollidableWorld . DestroyBody ( contactingBullet . CollidableBody )
pR . Bullets [ contactingBullet . LocalIdInBattle ] = & Bullet {
Removed : true ,
RemovedAtFrameId : pR . Tick ,
}
if contactingPlayer != nil {
if maxMillisToFreezePerPlayer > ( nowMillis - pR . Players [ contactingPlayer . Id ] . FrozenAtGmtMillis ) {
// Deliberately doing nothing. -- YFLu, 2019-09-04.
} else {
pR . Players [ contactingPlayer . Id ] . Speed = 0
pR . Players [ contactingPlayer . Id ] . FrozenAtGmtMillis = nowMillis
pR . Players [ contactingPlayer . Id ] . AddSpeedAtGmtMillis = - 1
//Logger.Info("Player has picked up bullet:", zap.Any("roomId", pR.Id), zap.Any("contactingPlayer.Id", contactingPlayer.Id), zap.Any("contactingBullet.LocalIdInBattle", contactingBullet.LocalIdInBattle), zap.Any("pR.Players[contactingPlayer.Id].Speed", pR.Players[contactingPlayer.Id].Speed))
}
}
}
}
func ( pR * Room ) onPumpkinEncounterPlayer ( pumpkin * Pumpkin , player * Player ) {
Logger . Info ( "pumpkin has caught the player: " , zap . Any ( "pumpkinId" , pumpkin . LocalIdInBattle ) , zap . Any ( "playerId" , player . Id ) )
}
func ( pR * Room ) updateScore ( ) {
pR . Score = calRoomScore ( pR . EffectivePlayerCount , pR . Capacity , pR . State )
}
func ( pR * Room ) AddPlayerIfPossible ( pPlayerFromDbInit * Player , session * websocket . Conn , signalToCloseConnOfThisPlayer SignalToCloseConnCbType ) bool {
playerId := pPlayerFromDbInit . Id
// TODO: Any thread-safety concern for accessing "pR" here?
if RoomBattleStateIns . IDLE != pR . State && RoomBattleStateIns . WAITING != pR . State {
Logger . Warn ( "AddPlayerIfPossible error, roomState:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "roomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return false
}
if _ , existent := pR . Players [ playerId ] ; existent {
Logger . Warn ( "AddPlayerIfPossible error, existing in the room.PlayersDict:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "roomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return false
}
defer pR . onPlayerAdded ( playerId )
pPlayerFromDbInit . AckingFrameId = 0
pPlayerFromDbInit . AckingInputFrameId = - 1
pPlayerFromDbInit . LastSentInputFrameId = - 1
pPlayerFromDbInit . BattleState = PlayerBattleStateIns . ADDED_PENDING_BATTLE_COLLIDER_ACK
pPlayerFromDbInit . FrozenAtGmtMillis = - 1 // Hardcoded temporarily.
pPlayerFromDbInit . Speed = PLAYER_DEFAULT_SPEED // Hardcoded temporarily.
pPlayerFromDbInit . AddSpeedAtGmtMillis = - 1 // Hardcoded temporarily.
pR . Players [ playerId ] = pPlayerFromDbInit
pR . PlayerDownsyncSessionDict [ playerId ] = session
pR . PlayerSignalToCloseDict [ playerId ] = signalToCloseConnOfThisPlayer
return true
}
func ( pR * Room ) ReAddPlayerIfPossible ( pTmpPlayerInstance * Player , session * websocket . Conn , signalToCloseConnOfThisPlayer SignalToCloseConnCbType ) bool {
playerId := pTmpPlayerInstance . Id
// TODO: Any thread-safety concern for accessing "pR" and "pEffectiveInRoomPlayerInstance" here?
if RoomBattleStateIns . PREPARE != pR . State && RoomBattleStateIns . WAITING != pR . State && RoomBattleStateIns . IN_BATTLE != pR . State && RoomBattleStateIns . IN_SETTLEMENT != pR . State && RoomBattleStateIns . IN_DISMISSAL != pR . State {
Logger . Warn ( "ReAddPlayerIfPossible error due to roomState:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "roomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return false
}
if _ , existent := pR . Players [ playerId ] ; ! existent {
Logger . Warn ( "ReAddPlayerIfPossible error due to player nonexistent for room:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "roomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return false
}
/ *
* WARNING : The "pTmpPlayerInstance *Player" used here is a temporarily constructed
* instance from "<proj-root>/battle_srv/ws/serve.go" , which is NOT the same as "pR.Players[pTmpPlayerInstance.Id]" .
* -- YFLu
* /
defer pR . onPlayerReAdded ( playerId )
pR . PlayerDownsyncSessionDict [ playerId ] = session
pR . PlayerSignalToCloseDict [ playerId ] = signalToCloseConnOfThisPlayer
pEffectiveInRoomPlayerInstance := pR . Players [ playerId ]
pEffectiveInRoomPlayerInstance . AckingFrameId = 0
pEffectiveInRoomPlayerInstance . AckingInputFrameId = - 1
pEffectiveInRoomPlayerInstance . LastSentInputFrameId = - 1
pEffectiveInRoomPlayerInstance . BattleState = PlayerBattleStateIns . READDED_PENDING_BATTLE_COLLIDER_ACK
Logger . Warn ( "ReAddPlayerIfPossible finished." , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "roomEffectivePlayerCount" , pR . EffectivePlayerCount ) , zap . Any ( "player AckingFrameId" , pEffectiveInRoomPlayerInstance . AckingFrameId ) , zap . Any ( "player AckingInputFrameId" , pEffectiveInRoomPlayerInstance . AckingInputFrameId ) )
return true
}
func ( pR * Room ) refreshColliders ( ) {
/ *
"BarrierCollider" s are NOT added to the "colliders in B2World of the current battle" , thus NOT involved in server - side collision detection !
-- YFLu , 2019 - 0 9 - 04
* /
gravity := box2d . MakeB2Vec2 ( 0.0 , 0.0 )
world := box2d . MakeB2World ( gravity )
world . SetContactFilter ( & box2d . B2ContactFilter { } )
pR . CollidableWorld = & world
Logger . Info ( "Begins `refreshColliders` for players:" , zap . Any ( "roomId" , pR . Id ) )
for _ , player := range pR . Players {
var bdDef box2d . B2BodyDef
colliderOffset := box2d . MakeB2Vec2 ( 0 , 0 ) // Matching that of client-side setting.
bdDef = box2d . MakeB2BodyDef ( )
bdDef . Type = box2d . B2BodyType . B2_dynamicBody
bdDef . Position . Set ( player . X + colliderOffset . X , player . Y + colliderOffset . Y )
b2Body := pR . CollidableWorld . CreateBody ( & bdDef )
b2CircleShape := box2d . MakeB2CircleShape ( )
b2CircleShape . M_radius = 32 // Matching that of client-side setting.
fd := box2d . MakeB2FixtureDef ( )
fd . Shape = & b2CircleShape
fd . Filter . CategoryBits = COLLISION_CATEGORY_CONTROLLED_PLAYER
fd . Filter . MaskBits = COLLISION_MASK_FOR_CONTROLLED_PLAYER
fd . Density = 0.0
b2Body . CreateFixtureFromDef ( & fd )
player . CollidableBody = b2Body
b2Body . SetUserData ( player )
}
Logger . Info ( "Ends `refreshColliders` for players:" , zap . Any ( "roomId" , pR . Id ) )
Logger . Info ( "Begins `refreshColliders` for treasures:" , zap . Any ( "roomId" , pR . Id ) )
for _ , treasure := range pR . Treasures {
var bdDef box2d . B2BodyDef
bdDef . Type = box2d . B2BodyType . B2_dynamicBody
bdDef = box2d . MakeB2BodyDef ( )
bdDef . Position . Set ( treasure . PickupBoundary . Anchor . X , treasure . PickupBoundary . Anchor . Y )
b2Body := pR . CollidableWorld . CreateBody ( & bdDef )
pointsCount := len ( treasure . PickupBoundary . Points )
b2Vertices := make ( [ ] box2d . B2Vec2 , pointsCount )
for vIndex , v2 := range treasure . PickupBoundary . Points {
b2Vertices [ vIndex ] = v2 . ToB2Vec2 ( )
}
b2PolygonShape := box2d . MakeB2PolygonShape ( )
b2PolygonShape . Set ( b2Vertices , pointsCount )
fd := box2d . MakeB2FixtureDef ( )
fd . Shape = & b2PolygonShape
fd . Filter . CategoryBits = COLLISION_CATEGORY_TREASURE
fd . Filter . MaskBits = COLLISION_MASK_FOR_TREASURE
fd . Density = 0.0
b2Body . CreateFixtureFromDef ( & fd )
treasure . CollidableBody = b2Body
b2Body . SetUserData ( treasure )
}
Logger . Info ( "Ends `refreshColliders` for treasures:" , zap . Any ( "roomId" , pR . Id ) )
Logger . Info ( "Begins `refreshColliders` for towers:" , zap . Any ( "roomId" , pR . Id ) )
for _ , tower := range pR . GuardTowers {
// Logger.Info("Begins `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower.LocalIdInBattle", tower.LocalIdInBattle), zap.Any("tower.X", tower.X), zap.Any("tower.Y", tower.Y), zap.Any("tower.PickupBoundary", tower.PickupBoundary), zap.Any("tower.PickupBoundary.Points", tower.PickupBoundary.Points), zap.Any("tower.WidthInB2World", tower.WidthInB2World), zap.Any("tower.HeightInB2World", tower.HeightInB2World), zap.Any("roomId", pR.Id))
var bdDef box2d . B2BodyDef
bdDef . Type = box2d . B2BodyType . B2_dynamicBody
bdDef = box2d . MakeB2BodyDef ( )
bdDef . Position . Set ( tower . PickupBoundary . Anchor . X , tower . PickupBoundary . Anchor . Y )
b2Body := pR . CollidableWorld . CreateBody ( & bdDef )
// Logger.Info("Checks#1 `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
pointsCount := len ( tower . PickupBoundary . Points )
b2Vertices := make ( [ ] box2d . B2Vec2 , pointsCount )
for vIndex , v2 := range tower . PickupBoundary . Points {
b2Vertices [ vIndex ] = v2 . ToB2Vec2 ( )
}
// Logger.Info("Checks#2 `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
b2PolygonShape := box2d . MakeB2PolygonShape ( )
// Logger.Info("Checks#3 `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
b2PolygonShape . Set ( b2Vertices , pointsCount )
// Logger.Info("Checks#4 `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
fd := box2d . MakeB2FixtureDef ( )
fd . Shape = & b2PolygonShape
fd . Filter . CategoryBits = COLLISION_CATEGORY_TRAP
fd . Filter . MaskBits = COLLISION_MASK_FOR_TRAP
fd . Density = 0.0
b2Body . CreateFixtureFromDef ( & fd )
// Logger.Info("Checks#5 `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
tower . CollidableBody = b2Body
b2Body . SetUserData ( tower )
// Logger.Info("Ends `refreshColliders` for single tower:", zap.Any("k-th", k), zap.Any("tower", tower), zap.Any("roomId", pR.Id))
}
Logger . Info ( "Ends `refreshColliders` for towers:" , zap . Any ( "roomId" , pR . Id ) )
listener := RoomBattleContactListener {
name : "TreasureHunterX" ,
room : pR ,
}
/ *
* Setting a "ContactListener" for "pR.CollidableWorld"
* will only trigger corresponding callbacks in the
* SAME GOROUTINE of "pR.CollidableWorld.Step(...)" according
* to "https://github.com/ByteArena/box2d/blob/master/DynamicsB2World.go" and
* "https://github.com/ByteArena/box2d/blob/master/DynamicsB2Contact.go" .
*
* The invocation - chain involves "Step -> SolveTOI -> B2ContactUpdate -> [BeginContact, EndContact, PreSolve]" .
* /
pR . CollidableWorld . SetContactListener ( listener )
}
func calculateDiffFrame ( currentFrame * pb . RoomDownsyncFrame , lastFrame * pb . RoomDownsyncFrame ) * pb . RoomDownsyncFrame {
if lastFrame == nil {
return currentFrame
}
diffFrame := & pb . RoomDownsyncFrame {
Id : currentFrame . Id ,
RefFrameId : lastFrame . Id ,
Players : currentFrame . Players ,
SentAt : currentFrame . SentAt ,
CountdownNanos : currentFrame . CountdownNanos ,
Bullets : currentFrame . Bullets ,
Treasures : make ( map [ int32 ] * pb . Treasure , 0 ) ,
Traps : make ( map [ int32 ] * pb . Trap , 0 ) ,
SpeedShoes : make ( map [ int32 ] * pb . SpeedShoe , 0 ) ,
GuardTowers : make ( map [ int32 ] * pb . GuardTower , 0 ) ,
}
for k , last := range lastFrame . Treasures {
if last . Removed {
diffFrame . Treasures [ k ] = last
continue
}
curr , ok := currentFrame . Treasures [ k ]
if ! ok {
diffFrame . Treasures [ k ] = & pb . Treasure { Removed : true }
Logger . Info ( "A treasure is removed." , zap . Any ( "diffFrame.id" , diffFrame . Id ) , zap . Any ( "treasure.LocalIdInBattle" , curr . LocalIdInBattle ) )
continue
}
if ok , v := diffTreasure ( last , curr ) ; ok {
diffFrame . Treasures [ k ] = v
}
}
for k , last := range lastFrame . Bullets {
curr , ok := currentFrame . Bullets [ k ]
/ *
* The use of ' bullet . RemovedAtFrameId ' implies that you SHOULDN ' T create a record ' & Bullet { Removed : true } ' here after it ' s already deleted from ' room . Bullets ' . Same applies for ` Traps ` and ` SpeedShoes ` .
*
* -- YFLu
* /
if false == ok {
diffFrame . Bullets [ k ] = & pb . Bullet { Removed : true }
// Logger.Info("A bullet is removed.", zap.Any("diffFrame.id", diffFrame.Id), zap.Any("bullet.LocalIdInBattle", lastFrame.Bullets[k].LocalIdInBattle))
continue
}
if ok , v := diffBullet ( last , curr ) ; ok {
diffFrame . Bullets [ k ] = v
}
}
for k , last := range lastFrame . Traps {
curr , ok := currentFrame . Traps [ k ]
if false == ok {
continue
}
if ok , v := diffTrap ( last , curr ) ; ok {
diffFrame . Traps [ k ] = v
}
}
for k , last := range lastFrame . SpeedShoes {
curr , ok := currentFrame . SpeedShoes [ k ]
if false == ok {
continue
}
if ok , v := diffSpeedShoe ( last , curr ) ; ok {
diffFrame . SpeedShoes [ k ] = v
}
}
return diffFrame
}
func diffTreasure ( last * pb . Treasure , curr * pb . Treasure ) ( bool , * pb . Treasure ) {
treature := & pb . Treasure { }
t := false
if last . Score != curr . Score {
treature . Score = curr . Score
t = true
}
if last . X != curr . X {
treature . X = curr . X
t = true
}
if last . Y != curr . Y {
treature . Y = curr . Y
t = true
}
return t , treature
}
func diffTrap ( last * pb . Trap , curr * pb . Trap ) ( bool , * pb . Trap ) {
trap := & pb . Trap { }
t := false
if last . X != curr . X {
trap . X = curr . X
t = true
}
if last . Y != curr . Y {
trap . Y = curr . Y
t = true
}
return t , trap
}
func diffSpeedShoe ( last * pb . SpeedShoe , curr * pb . SpeedShoe ) ( bool , * pb . SpeedShoe ) {
speedShoe := & pb . SpeedShoe { }
t := false
if last . X != curr . X {
speedShoe . X = curr . X
t = true
}
if last . Y != curr . Y {
speedShoe . Y = curr . Y
t = true
}
return t , speedShoe
}
func diffBullet ( last * pb . Bullet , curr * pb . Bullet ) ( bool , * pb . Bullet ) {
t := true
return t , curr
}
func ( pR * Room ) ChooseStage ( ) error {
/ *
* We use the verb "refresh" here to imply that upon invocation of this function , all colliders will be recovered if they were destroyed in the previous battle .
*
* -- YFLu , 2019 - 0 9 - 04
* /
pwd , err := os . Getwd ( )
ErrFatal ( err )
rand . Seed ( time . Now ( ) . Unix ( ) )
2022-09-24 04:01:50 +00:00
stageNameList := [ ] string { /*"pacman" ,*/ "richsoil" }
2022-09-20 15:50:01 +00:00
chosenStageIndex := rand . Int ( ) % len ( stageNameList ) // Hardcoded temporarily. -- YFLu
pR . StageName = stageNameList [ chosenStageIndex ]
relativePathForAllStages := "../frontend/assets/resources/map"
relativePathForChosenStage := fmt . Sprintf ( "%s/%s" , relativePathForAllStages , pR . StageName )
pTmxMapIns := & TmxMap { }
absDirPathContainingDirectlyTmxFile := filepath . Join ( pwd , relativePathForChosenStage )
absTmxFilePath := fmt . Sprintf ( "%s/map.tmx" , absDirPathContainingDirectlyTmxFile )
if ! filepath . IsAbs ( absTmxFilePath ) {
panic ( "Tmx filepath must be absolute!" )
}
byteArr , err := ioutil . ReadFile ( absTmxFilePath )
if nil != err {
panic ( err )
}
err = xml . Unmarshal ( byteArr , pTmxMapIns )
if nil != err {
panic ( err )
}
// Obtain the content of `gidBoundariesMapInB2World`.
gidBoundariesMapInB2World := make ( map [ int ] StrToPolygon2DListMap , 0 )
for _ , tileset := range pTmxMapIns . Tilesets {
relativeTsxFilePath := fmt . Sprintf ( "%s/%s" , filepath . Join ( pwd , relativePathForChosenStage ) , tileset . Source ) // Note that "TmxTileset.Source" can be a string of "relative path".
absTsxFilePath , err := filepath . Abs ( relativeTsxFilePath )
if nil != err {
panic ( err )
}
if ! filepath . IsAbs ( absTsxFilePath ) {
panic ( "Filepath must be absolute!" )
}
byteArrOfTsxFile , err := ioutil . ReadFile ( absTsxFilePath )
if nil != err {
panic ( err )
}
DeserializeTsxToColliderDict ( pTmxMapIns , byteArrOfTsxFile , int ( tileset . FirstGid ) , gidBoundariesMapInB2World )
}
stageDiscreteW , stageDiscreteH , stageTileW , stageTileH , toRetStrToVec2DListMap , toRetStrToPolygon2DListMap , err := ParseTmxLayersAndGroups ( pTmxMapIns , gidBoundariesMapInB2World )
if nil != err {
panic ( err )
}
pR . StageDiscreteW = stageDiscreteW
pR . StageDiscreteH = stageDiscreteH
pR . StageTileW = stageTileW
pR . StageTileH = stageTileH
pR . RawBattleStrToVec2DListMap = toRetStrToVec2DListMap
pR . RawBattleStrToPolygon2DListMap = toRetStrToPolygon2DListMap
// Refresh "Treasure" data for RoomDownsyncFrame.
lowScoreTreasurePolygon2DList := * ( toRetStrToPolygon2DListMap [ "LowScoreTreasure" ] )
highScoreTreasurePolygon2DList := * ( toRetStrToPolygon2DListMap [ "HighScoreTreasure" ] )
var treasureLocalIdInBattle int32 = 0
for _ , polygon2D := range lowScoreTreasurePolygon2DList {
/ *
// For debug-printing only.
Logger . Info ( "ChooseStage printing polygon2D for lowScoreTreasurePolygon2DList" , zap . Any ( "treasureLocalIdInBattle" , treasureLocalIdInBattle ) , zap . Any ( "polygon2D.Anchor" , polygon2D . Anchor ) , zap . Any ( "polygon2D.Points" , polygon2D . Points ) )
* /
theTreasure := & Treasure {
Id : 0 ,
LocalIdInBattle : treasureLocalIdInBattle ,
Score : LOW_SCORE_TREASURE_SCORE ,
Type : LOW_SCORE_TREASURE_TYPE ,
X : polygon2D . Anchor . X ,
Y : polygon2D . Anchor . Y ,
PickupBoundary : polygon2D ,
}
pR . Treasures [ theTreasure . LocalIdInBattle ] = theTreasure
treasureLocalIdInBattle ++
}
for _ , polygon2D := range highScoreTreasurePolygon2DList {
/ *
// For debug-printing only.
Logger . Info ( "ChooseStage printing polygon2D for highScoreTreasurePolygon2DList" , zap . Any ( "treasureLocalIdInBattle" , treasureLocalIdInBattle ) , zap . Any ( "polygon2D.Anchor" , polygon2D . Anchor ) , zap . Any ( "polygon2D.Points" , polygon2D . Points ) )
* /
theTreasure := & Treasure {
Id : 0 ,
LocalIdInBattle : treasureLocalIdInBattle ,
Score : HIGH_SCORE_TREASURE_SCORE ,
Type : HIGH_SCORE_TREASURE_TYPE ,
X : polygon2D . Anchor . X ,
Y : polygon2D . Anchor . Y ,
PickupBoundary : polygon2D ,
}
pR . Treasures [ theTreasure . LocalIdInBattle ] = theTreasure
treasureLocalIdInBattle ++
}
// Refresh "GuardTower" data for RoomDownsyncFrame.
guardTowerPolygon2DList := * ( toRetStrToPolygon2DListMap [ "GuardTower" ] )
var guardTowerLocalIdInBattle int32 = 0
for _ , polygon2D := range guardTowerPolygon2DList {
/ *
// For debug-printing only.
Logger . Info ( "ChooseStage printing polygon2D for guardTowerPolygon2DList" , zap . Any ( "guardTowerLocalIdInBattle" , guardTowerLocalIdInBattle ) , zap . Any ( "polygon2D.Anchor" , polygon2D . Anchor ) , zap . Any ( "polygon2D.Points" , polygon2D . Points ) , zap . Any ( "pR.GuardTowers" , pR . GuardTowers ) )
* /
var inRangePlayers InRangePlayerCollection
pInRangePlayers := & inRangePlayers
pInRangePlayers = pInRangePlayers . Init ( 10 )
theGuardTower := & GuardTower {
Id : 0 ,
LocalIdInBattle : guardTowerLocalIdInBattle ,
X : polygon2D . Anchor . X ,
Y : polygon2D . Anchor . Y ,
PickupBoundary : polygon2D ,
InRangePlayers : pInRangePlayers ,
LastAttackTick : utils . UnixtimeNano ( ) ,
WidthInB2World : float64 ( polygon2D . TmxObjectWidth ) ,
HeightInB2World : float64 ( polygon2D . TmxObjectHeight ) ,
}
pR . GuardTowers [ theGuardTower . LocalIdInBattle ] = theGuardTower
guardTowerLocalIdInBattle ++
}
return nil
}
func ( pR * Room ) ConvertToInputFrameId ( originalFrameId int32 , inputDelayFrames int32 ) int32 {
if originalFrameId < inputDelayFrames {
return 0
}
return ( ( originalFrameId - inputDelayFrames ) >> pR . InputScaleFrames )
}
func ( pR * Room ) EncodeUpsyncCmd ( upsyncCmd * pb . InputFrameUpsync ) uint64 {
var ret uint64 = 0
// There're 13 possible directions, occupying the first 4 bits, no need to shift
ret += uint64 ( upsyncCmd . EncodedDir )
return ret
}
func ( pR * Room ) CanPopSt ( refLowerInputFrameId int32 ) bool {
rb := pR . AllPlayerInputsBuffer
if rb . Cnt <= 0 {
return false
}
if rb . StFrameId <= refLowerInputFrameId {
// already delayed too much
return true
}
return false
}
func ( pR * Room ) AllPlayerInputsBufferString ( ) string {
s := make ( [ ] string , 0 )
2022-09-24 04:01:50 +00:00
s = append ( s , fmt . Sprintf ( "{lastAllConfirmedInputFrameId: %v, lastAllConfirmedInputFrameIdWithChange: %v}" , pR . LastAllConfirmedInputFrameId , pR . LastAllConfirmedInputFrameIdWithChange ) )
2022-09-20 15:50:01 +00:00
for playerId , player := range pR . Players {
2022-09-24 04:01:50 +00:00
s = append ( s , fmt . Sprintf ( "{playerId: %v, ackingFrameId: %v, ackingInputFrameId: %v, lastSentInputFrameId: %v}" , playerId , player . AckingFrameId , player . AckingInputFrameId , player . LastSentInputFrameId ) )
}
2022-09-20 15:50:01 +00:00
for i := pR . AllPlayerInputsBuffer . StFrameId ; i < pR . AllPlayerInputsBuffer . EdFrameId ; i ++ {
tmp := pR . AllPlayerInputsBuffer . GetByFrameId ( i )
if nil == tmp {
break
}
f := tmp . ( * pb . InputFrameDownsync )
s = append ( s , fmt . Sprintf ( "{inputFrameId: %v, inputList: %v, confirmedList: %v}" , f . InputFrameId , f . InputList , f . ConfirmedList ) )
}
return strings . Join ( s , "\n" )
}
func ( pR * Room ) StartBattle ( ) {
if RoomBattleStateIns . WAITING != pR . State {
Logger . Warn ( "[StartBattle] Battle not started after all players' battle state checked!" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) )
return
}
// Always instantiates a new channel and let the old one die out due to not being retained by any root reference.
nanosPerFrame := 1000000000 / int64 ( pR . ServerFPS )
pR . Tick = 0
// Refresh "Colliders" for server-side contact listening of B2World.
pR . refreshColliders ( )
/ * *
* Will be triggered from a goroutine which executes the critical ` Room.AddPlayerIfPossible ` , thus the ` battleMainLoop ` should be detached .
* All of the consecutive stages , e . g . settlement , dismissal , should share the same goroutine with ` battleMainLoop ` .
* /
battleMainLoop := func ( ) {
defer func ( ) {
2022-09-24 04:01:50 +00:00
if r := recover ( ) ; r != nil {
Logger . Error ( "battleMainLoop, recovery spot#1, recovered from: " , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "panic" , r ) )
}
2022-09-20 15:50:01 +00:00
Logger . Info ( "The `battleMainLoop` is stopped for:" , zap . Any ( "roomId" , pR . Id ) )
pR . onBattleStoppedForSettlement ( )
} ( )
battleMainLoopStartedNanos := utils . UnixtimeNano ( )
2022-09-24 04:01:50 +00:00
totalElapsedNanos := int64 ( 0 )
2022-09-20 15:50:01 +00:00
Logger . Info ( "The `battleMainLoop` is started for:" , zap . Any ( "roomId" , pR . Id ) )
for {
2022-09-24 04:01:50 +00:00
if 0 == pR . Tick {
2022-09-20 15:50:01 +00:00
// The legacy frontend code needs this "kickoffFrame" to remove the "ready to start 3-2-1" panel
kickoffFrame := pb . RoomDownsyncFrame {
Id : pR . Tick ,
2022-09-24 04:01:50 +00:00
RefFrameId : MAGIC_ROOM_DOWNSYNC_FRAME_ID_BATTLE_START ,
2022-09-20 15:50:01 +00:00
Players : toPbPlayers ( pR . Players ) ,
Treasures : toPbTreasures ( pR . Treasures ) ,
Traps : toPbTraps ( pR . Traps ) ,
Bullets : toPbBullets ( pR . Bullets ) ,
SpeedShoes : toPbSpeedShoes ( pR . SpeedShoes ) ,
GuardTowers : toPbGuardTowers ( pR . GuardTowers ) ,
SentAt : utils . UnixtimeMilli ( ) ,
CountdownNanos : ( pR . BattleDurationNanos - totalElapsedNanos ) ,
}
for playerId , player := range pR . Players {
if swapped := atomic . CompareAndSwapInt32 ( & player . BattleState , PlayerBattleStateIns . ACTIVE , PlayerBattleStateIns . ACTIVE ) ; ! swapped {
/ *
[ WARNING ] DON ' T send anything into "DedicatedForwardingChanForPlayer" if the player is disconnected , because it could jam the channel and cause significant delay upon "battle recovery for reconnected player" .
* /
continue
}
pR . sendSafely ( kickoffFrame , playerId )
}
}
if totalElapsedNanos > pR . BattleDurationNanos {
Logger . Info ( fmt . Sprintf ( "The `battleMainLoop` is stopped:\n%v" , pR . AllPlayerInputsBufferString ( ) ) )
pR . StopBattleForSettlement ( )
}
if swapped := atomic . CompareAndSwapInt32 ( & pR . State , RoomBattleStateIns . IN_BATTLE , RoomBattleStateIns . IN_BATTLE ) ; ! swapped {
return
}
stCalculation := utils . UnixtimeNano ( )
refInputFrameId := int32 ( 999999999 ) // Hardcoded as a max reference.
for playerId , _ := range pR . Players {
thatId := atomic . LoadInt32 ( & ( pR . Players [ playerId ] . AckingInputFrameId ) )
if thatId > refInputFrameId {
continue
}
refInputFrameId = thatId
}
for pR . CanPopSt ( refInputFrameId ) {
// _ = pR.AllPlayerInputsBuffer.Pop()
f := pR . AllPlayerInputsBuffer . Pop ( ) . ( * pb . 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 . Info ( "inputFrame lifecycle#5[popped]:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "refInputFrameId" , refInputFrameId ) , zap . Any ( "inputFrameId" , f . InputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
}
}
lastAllConfirmedInputFrameIdWithChange := atomic . LoadInt32 ( & ( pR . LastAllConfirmedInputFrameIdWithChange ) )
for playerId , player := range pR . Players {
if swapped := atomic . CompareAndSwapInt32 ( & player . BattleState , PlayerBattleStateIns . ACTIVE , PlayerBattleStateIns . ACTIVE ) ; ! swapped {
/ *
[ WARNING ] DON ' T send anything into "DedicatedForwardingChanForPlayer" if the player is disconnected , because it could jam the channel and cause significant delay upon "battle recovery for reconnected player" .
* /
continue
}
toSendInputFrames := make ( [ ] * pb . InputFrameDownsync , 0 , pR . AllPlayerInputsBuffer . Cnt )
// [WARNING] Websocket is TCP-based, thus no need to re-send a previously sent inputFrame to a same player!
anchorInputFrameId := atomic . LoadInt32 ( & ( pR . Players [ playerId ] . LastSentInputFrameId ) )
candidateToSendInputFrameId := anchorInputFrameId + 1
// [WARNING] EDGE CASE HERE: Upon initialization, all of "lastAllConfirmedInputFrameId", "lastAllConfirmedInputFrameIdWithChange" and "anchorInputFrameId" are "-1", thus "candidateToSendInputFrameId" starts with "0", however "inputFrameId: 0" might not have been all confirmed!
debugSendingInputFrameId := int32 ( - 1 )
for candidateToSendInputFrameId <= lastAllConfirmedInputFrameIdWithChange {
tmp := pR . AllPlayerInputsBuffer . GetByFrameId ( candidateToSendInputFrameId )
if nil == tmp {
break
}
f := tmp . ( * pb . InputFrameDownsync )
if pR . inputFrameIdDebuggable ( candidateToSendInputFrameId ) {
debugSendingInputFrameId = candidateToSendInputFrameId
Logger . Info ( "inputFrame lifecycle#3[sending]:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "refInputFrameId" , refInputFrameId ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "playerAnchorInputFrameId" , anchorInputFrameId ) , zap . Any ( "playerAckingInputFrameId" , player . AckingInputFrameId ) , zap . Any ( "inputFrameId" , candidateToSendInputFrameId ) , zap . Any ( "inputFrameId-doublecheck" , f . InputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) , zap . Any ( "ConfirmedList" , f . ConfirmedList ) )
}
toSendInputFrames = append ( toSendInputFrames , f )
candidateToSendInputFrameId ++
}
if 0 >= len ( toSendInputFrames ) {
continue
}
pR . sendSafely ( toSendInputFrames , playerId )
atomic . StoreInt32 ( & ( pR . Players [ playerId ] . LastSentInputFrameId ) , candidateToSendInputFrameId - 1 )
if - 1 != debugSendingInputFrameId {
Logger . Info ( "inputFrame lifecycle#4[sent]:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "refInputFrameId" , refInputFrameId ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "playerAckingInputFrameId" , player . AckingInputFrameId ) , zap . Any ( "inputFrameId" , debugSendingInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
}
}
2022-09-24 04:01:50 +00:00
pR . Tick ++
2022-09-20 15:50:01 +00:00
now := utils . UnixtimeNano ( )
elapsedInCalculation := now - stCalculation
totalElapsedNanos = ( now - battleMainLoopStartedNanos )
// Logger.Info("Elapsed time statistics:", zap.Any("roomId", pR.Id), zap.Any("elapsedInCalculation", elapsedInCalculation), zap.Any("totalElapsedNanos", totalElapsedNanos))
time . Sleep ( time . Duration ( nanosPerFrame - elapsedInCalculation ) )
}
}
pR . onBattlePrepare ( func ( ) {
pR . onBattleStarted ( ) // NOTE: Deliberately not using `defer`.
go battleMainLoop ( )
} )
}
func ( pR * Room ) OnBattleCmdReceived ( pReq * pb . WsReq ) {
if swapped := atomic . CompareAndSwapInt32 ( & pR . State , RoomBattleStateIns . IN_BATTLE , RoomBattleStateIns . IN_BATTLE ) ; ! swapped {
return
}
playerId := pReq . PlayerId
indiceInJoinIndexBooleanArr := uint32 ( pReq . JoinIndex - 1 )
inputFrameUpsyncBatch := pReq . InputFrameUpsyncBatch
ackingFrameId := pReq . AckingFrameId
ackingInputFrameId := pReq . AckingInputFrameId
for _ , inputFrameUpsync := range inputFrameUpsyncBatch {
if _ , existent := pR . Players [ playerId ] ; ! existent {
Logger . Warn ( "upcmd player doesn't exist:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) )
return
}
if swapped := atomic . CompareAndSwapInt32 ( & ( pR . Players [ playerId ] . AckingFrameId ) , pR . Players [ playerId ] . AckingFrameId , ackingFrameId ) ; ! swapped {
panic ( fmt . Sprintf ( "Failed to update AckingFrameId to %v for roomId=%v, playerId=%v" , ackingFrameId , pR . Id , playerId ) )
}
if swapped := atomic . CompareAndSwapInt32 ( & ( pR . Players [ playerId ] . AckingInputFrameId ) , pR . Players [ playerId ] . AckingInputFrameId , ackingInputFrameId ) ; ! swapped {
panic ( fmt . Sprintf ( "Failed to update AckingInputFrameId to %v for roomId=%v, playerId=%v" , ackingInputFrameId , pR . Id , playerId ) )
}
clientInputFrameId := inputFrameUpsync . InputFrameId
if clientInputFrameId < pR . AllPlayerInputsBuffer . StFrameId {
Logger . Warn ( "Obsolete inputFrameUpsync:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "clientInputFrameId" , clientInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
return
}
var joinMask uint64 = ( 1 << indiceInJoinIndexBooleanArr )
encodedInput := pR . EncodeUpsyncCmd ( inputFrameUpsync )
if clientInputFrameId >= pR . AllPlayerInputsBuffer . EdFrameId {
// The outer-if branching is for reducing an extra get-and-set operation, which is now placed in the else-branch.
for clientInputFrameId >= pR . AllPlayerInputsBuffer . EdFrameId {
newInputList := make ( [ ] uint64 , len ( pR . Players ) )
newInputList [ indiceInJoinIndexBooleanArr ] = encodedInput
pR . AllPlayerInputsBuffer . Put ( & pb . InputFrameDownsync {
InputFrameId : pR . AllPlayerInputsBuffer . EdFrameId ,
InputList : newInputList ,
ConfirmedList : joinMask , // by now only the current player has confirmed this input frame
} )
if pR . inputFrameIdDebuggable ( clientInputFrameId ) {
Logger . Info ( "inputFrame lifecycle#1[inserted]" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "inputFrameId" , clientInputFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
}
}
} else {
tmp2 := pR . AllPlayerInputsBuffer . GetByFrameId ( clientInputFrameId )
if nil == tmp2 {
// This shouldn't happen due to the previous 2 checks
Logger . Warn ( "Mysterious error getting an input frame:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "clientInputFrameId" , clientInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
return
}
inputFrameDownsync := tmp2 . ( * pb . InputFrameDownsync )
oldConfirmedList := inputFrameDownsync . ConfirmedList
if ( oldConfirmedList & joinMask ) > 0 {
Logger . Warn ( "Cmd already confirmed but getting set attempt:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "clientInputFrameId" , clientInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
return
}
// In Golang 1.12, there's no "compare-and-swap primitive" on a custom struct (or it's pointer, unless it's an unsafe pointer https://pkg.go.dev/sync/atomic@go1.12#CompareAndSwapPointer). Although CAS on custom struct is possible in Golang 1.19 https://pkg.go.dev/sync/atomic@go1.19.1#Value.CompareAndSwap, using a single word is still faster whenever possible.
if swapped := atomic . CompareAndSwapUint64 ( & inputFrameDownsync . InputList [ indiceInJoinIndexBooleanArr ] , uint64 ( 0 ) , encodedInput ) ; ! swapped {
if encodedInput > 0 {
Logger . Warn ( "Failed input CAS:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "clientInputFrameId" , clientInputFrameId ) )
}
return
}
newConfirmedList := ( oldConfirmedList | joinMask )
if swapped := atomic . CompareAndSwapUint64 ( & ( inputFrameDownsync . ConfirmedList ) , oldConfirmedList , newConfirmedList ) ; ! swapped {
if encodedInput > 0 {
Logger . Warn ( "Failed confirm CAS:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "clientInputFrameId" , clientInputFrameId ) )
}
return
}
totPlayerCnt := uint32 ( len ( pR . Players ) )
allConfirmedMask := uint64 ( ( 1 << totPlayerCnt ) - 1 ) // TODO: What if a player is disconnected backthen?
if allConfirmedMask == newConfirmedList {
if false == pR . equalInputLists ( inputFrameDownsync . InputList , pR . LastAllConfirmedInputList ) {
atomic . StoreInt32 ( & ( pR . LastAllConfirmedInputFrameIdWithChange ) , clientInputFrameId ) // [WARNING] Different from the CAS in "battleMainLoop", it's safe to just update "pR.LastAllConfirmedInputFrameIdWithChange" here, because only monotonic increment is possible here!
Logger . Info ( "Key inputFrame change" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "inputFrameId" , clientInputFrameId ) , zap . Any ( "lastInputFrameId" , pR . LastAllConfirmedInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) , zap . Any ( "newInputList" , inputFrameDownsync . InputList ) , zap . Any ( "lastInputList" , pR . LastAllConfirmedInputList ) )
}
atomic . StoreInt32 ( & ( pR . LastAllConfirmedInputFrameId ) , clientInputFrameId ) // [WARNING] It's IMPORTANT that "pR.LastAllConfirmedInputFrameId" is NOT NECESSARILY CONSECUTIVE, i.e. if one of the players disconnects and reconnects within a considerable amount of frame delays!
2022-09-24 04:01:50 +00:00
for i , v := range inputFrameDownsync . InputList {
// To avoid potential misuse of pointers
pR . LastAllConfirmedInputList [ i ] = v
}
2022-09-20 15:50:01 +00:00
if pR . inputFrameIdDebuggable ( clientInputFrameId ) {
Logger . Info ( "inputFrame lifecycle#2[allconfirmed]" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "inputFrameId" , clientInputFrameId ) , zap . Any ( "StFrameId" , pR . AllPlayerInputsBuffer . StFrameId ) , zap . Any ( "EdFrameId" , pR . AllPlayerInputsBuffer . EdFrameId ) )
}
}
}
}
}
func ( pR * Room ) equalInputLists ( lhs [ ] uint64 , rhs [ ] uint64 ) bool {
if len ( lhs ) != len ( rhs ) {
return false
}
for i , _ := range lhs {
if lhs [ i ] != rhs [ i ] {
return false
}
}
return true
}
func ( pR * Room ) StopBattleForSettlement ( ) {
if RoomBattleStateIns . IN_BATTLE != pR . State {
return
}
pR . State = RoomBattleStateIns . STOPPING_BATTLE_FOR_SETTLEMENT
Logger . Info ( "Stopping the `battleMainLoop` for:" , zap . Any ( "roomId" , pR . Id ) )
pR . Tick ++
for playerId , _ := range pR . Players {
assembledFrame := pb . RoomDownsyncFrame {
Id : pR . Tick ,
2022-09-26 02:36:46 +00:00
RefFrameId : pR . Tick , // Hardcoded for now.
2022-09-20 15:50:01 +00:00
Players : toPbPlayers ( pR . Players ) ,
SentAt : utils . UnixtimeMilli ( ) ,
CountdownNanos : - 1 , // TODO: Replace this magic constant!
Treasures : toPbTreasures ( pR . Treasures ) ,
Traps : toPbTraps ( pR . Traps ) ,
}
pR . sendSafely ( assembledFrame , playerId )
}
// Note that `pR.onBattleStoppedForSettlement` will be called by `battleMainLoop`.
}
func ( pR * Room ) onBattleStarted ( ) {
if RoomBattleStateIns . PREPARE != pR . State {
return
}
pR . State = RoomBattleStateIns . IN_BATTLE
pR . updateScore ( )
}
func ( pR * Room ) onBattlePrepare ( cb BattleStartCbType ) {
if RoomBattleStateIns . WAITING != pR . State {
Logger . Warn ( "[onBattlePrepare] Battle not started after all players' battle state checked!" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) )
return
}
pR . State = RoomBattleStateIns . PREPARE
Logger . Info ( "Battle state transitted to RoomBattleStateIns.PREPARE for:" , zap . Any ( "roomId" , pR . Id ) )
playerMetas := make ( map [ int32 ] * pb . PlayerMeta , 0 )
for _ , player := range pR . Players {
playerMetas [ player . Id ] = & pb . PlayerMeta {
Id : player . Id ,
Name : player . Name ,
DisplayName : player . DisplayName ,
Avatar : player . Avatar ,
JoinIndex : player . JoinIndex ,
}
}
battleReadyToStartFrame := pb . RoomDownsyncFrame {
2022-09-24 04:01:50 +00:00
Id : pR . Tick ,
Players : toPbPlayers ( pR . Players ) ,
SentAt : utils . UnixtimeMilli ( ) ,
RefFrameId : MAGIC_ROOM_DOWNSYNC_FRAME_ID_BATTLE_READY_TO_START ,
PlayerMetas : playerMetas ,
CountdownNanos : pR . BattleDurationNanos ,
2022-09-20 15:50:01 +00:00
}
Logger . Info ( "Sending out frame for RoomBattleState.PREPARE " , zap . Any ( "battleReadyToStartFrame" , battleReadyToStartFrame ) )
for _ , player := range pR . Players {
pR . sendSafely ( battleReadyToStartFrame , player . Id )
}
battlePreparationNanos := int64 ( 6000000000 )
preparationLoop := func ( ) {
defer func ( ) {
Logger . Info ( "The `preparationLoop` is stopped for:" , zap . Any ( "roomId" , pR . Id ) )
cb ( )
} ( )
preparationLoopStartedNanos := utils . UnixtimeNano ( )
totalElapsedNanos := int64 ( 0 )
for {
if totalElapsedNanos > battlePreparationNanos {
break
}
now := utils . UnixtimeNano ( )
totalElapsedNanos = ( now - preparationLoopStartedNanos )
time . Sleep ( time . Duration ( battlePreparationNanos - totalElapsedNanos ) )
}
}
go preparationLoop ( )
}
func ( pR * Room ) onBattleStoppedForSettlement ( ) {
if RoomBattleStateIns . STOPPING_BATTLE_FOR_SETTLEMENT != pR . State {
return
}
defer func ( ) {
pR . onSettlementCompleted ( )
} ( )
pR . State = RoomBattleStateIns . IN_SETTLEMENT
Logger . Info ( "The room is in settlement:" , zap . Any ( "roomId" , pR . Id ) )
// TODO: Some settlement labor.
}
func ( pR * Room ) onSettlementCompleted ( ) {
pR . Dismiss ( )
}
func ( pR * Room ) Dismiss ( ) {
if RoomBattleStateIns . IN_SETTLEMENT != pR . State {
return
}
pR . State = RoomBattleStateIns . IN_DISMISSAL
if 0 < len ( pR . Players ) {
Logger . Info ( "The room is in dismissal:" , zap . Any ( "roomId" , pR . Id ) )
for playerId , _ := range pR . Players {
Logger . Info ( "Adding 1 to pR.DismissalWaitGroup:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) )
pR . DismissalWaitGroup . Add ( 1 )
pR . expelPlayerForDismissal ( playerId )
pR . DismissalWaitGroup . Done ( )
Logger . Info ( "Decremented 1 to pR.DismissalWaitGroup:" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) )
}
pR . DismissalWaitGroup . Wait ( )
}
pR . onDismissed ( )
}
func ( pR * Room ) onDismissed ( ) {
// Always instantiates new HeapRAM blocks and let the old blocks die out due to not being retained by any root reference.
pR . Players = make ( map [ int32 ] * Player )
pR . Treasures = make ( map [ int32 ] * Treasure )
pR . Traps = make ( map [ int32 ] * Trap )
pR . GuardTowers = make ( map [ int32 ] * GuardTower )
pR . Bullets = make ( map [ int32 ] * Bullet )
pR . SpeedShoes = make ( map [ int32 ] * SpeedShoe )
pR . PlayerDownsyncSessionDict = make ( map [ int32 ] * websocket . Conn )
pR . PlayerSignalToCloseDict = make ( map [ int32 ] SignalToCloseConnCbType )
pR . LastAllConfirmedInputFrameId = - 1
pR . LastAllConfirmedInputFrameIdWithChange = - 1
pR . LastAllConfirmedInputList = make ( [ ] uint64 , pR . Capacity )
for indice , _ := range pR . JoinIndexBooleanArr {
pR . JoinIndexBooleanArr [ indice ] = false
}
pR . AllPlayerInputsBuffer = NewRingBuffer ( 1024 )
pR . ChooseStage ( )
pR . EffectivePlayerCount = 0
// [WARNING] It's deliberately ordered such that "pR.State = RoomBattleStateIns.IDLE" is put AFTER all the refreshing operations above.
pR . State = RoomBattleStateIns . IDLE
pR . updateScore ( )
Logger . Info ( "The room is completely dismissed:" , zap . Any ( "roomId" , pR . Id ) )
}
func ( pR * Room ) Unicast ( toPlayerId int32 , msg interface { } ) {
// TODO
}
func ( pR * Room ) Broadcast ( msg interface { } ) {
// TODO
}
func ( pR * Room ) expelPlayerDuringGame ( playerId int32 ) {
defer pR . onPlayerExpelledDuringGame ( playerId )
}
func ( pR * Room ) expelPlayerForDismissal ( playerId int32 ) {
pR . onPlayerExpelledForDismissal ( playerId )
}
func ( pR * Room ) onPlayerExpelledDuringGame ( playerId int32 ) {
pR . onPlayerLost ( playerId )
}
func ( pR * Room ) onPlayerExpelledForDismissal ( playerId int32 ) {
pR . onPlayerLost ( playerId )
Logger . Info ( "onPlayerExpelledForDismissal:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "nowRoomBattleState" , pR . State ) , zap . Any ( "nowRoomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
}
func ( pR * Room ) OnPlayerDisconnected ( playerId int32 ) {
defer func ( ) {
if r := recover ( ) ; r != nil {
Logger . Error ( "Room OnPlayerDisconnected, recovery spot#1, recovered from: " , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "panic" , r ) )
}
} ( )
if _ , existent := pR . Players [ playerId ] ; existent {
switch pR . Players [ playerId ] . BattleState {
case PlayerBattleStateIns . DISCONNECTED :
case PlayerBattleStateIns . LOST :
case PlayerBattleStateIns . EXPELLED_DURING_GAME :
case PlayerBattleStateIns . EXPELLED_IN_DISMISSAL :
Logger . Info ( "Room OnPlayerDisconnected[early return #1]:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "playerBattleState" , pR . Players [ playerId ] . BattleState ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "nowRoomBattleState" , pR . State ) , zap . Any ( "nowRoomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return
}
} else {
// Not even the "pR.Players[playerId]" exists.
Logger . Info ( "Room OnPlayerDisconnected[early return #2]:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "nowRoomBattleState" , pR . State ) , zap . Any ( "nowRoomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
return
}
switch pR . State {
case RoomBattleStateIns . WAITING :
pR . onPlayerLost ( playerId )
delete ( pR . Players , playerId ) // Note that this statement MUST be put AFTER `pR.onPlayerLost(...)` to avoid nil pointer exception.
if 0 == pR . EffectivePlayerCount {
pR . State = RoomBattleStateIns . IDLE
}
pR . updateScore ( )
Logger . Info ( "Player disconnected while room is at RoomBattleStateIns.WAITING:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "nowRoomBattleState" , pR . State ) , zap . Any ( "nowRoomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
default :
pR . Players [ playerId ] . BattleState = PlayerBattleStateIns . DISCONNECTED
pR . clearPlayerNetworkSession ( playerId ) // Still need clear the network session pointers, because "OnPlayerDisconnected" is only triggered from "signalToCloseConnOfThisPlayer" in "ws/serve.go", when the same player reconnects the network session pointers will be re-assigned
Logger . Info ( "Player disconnected from room:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "playerBattleState" , pR . Players [ playerId ] . BattleState ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "nowRoomBattleState" , pR . State ) , zap . Any ( "nowRoomEffectivePlayerCount" , pR . EffectivePlayerCount ) )
}
}
func ( pR * Room ) onPlayerLost ( playerId int32 ) {
defer func ( ) {
if r := recover ( ) ; r != nil {
Logger . Error ( "Room OnPlayerLost, recovery spot, recovered from: " , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "panic" , r ) )
}
} ( )
if player , existent := pR . Players [ playerId ] ; existent {
player . BattleState = PlayerBattleStateIns . LOST
pR . clearPlayerNetworkSession ( playerId )
pR . EffectivePlayerCount --
indiceInJoinIndexBooleanArr := int ( player . JoinIndex - 1 )
if ( 0 <= indiceInJoinIndexBooleanArr ) && ( indiceInJoinIndexBooleanArr < len ( pR . JoinIndexBooleanArr ) ) {
pR . JoinIndexBooleanArr [ indiceInJoinIndexBooleanArr ] = false
} else {
Logger . Warn ( "Room OnPlayerLost, pR.JoinIndexBooleanArr is out of range: " , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "indiceInJoinIndexBooleanArr" , indiceInJoinIndexBooleanArr ) , zap . Any ( "len(pR.JoinIndexBooleanArr)" , len ( pR . JoinIndexBooleanArr ) ) )
}
player . JoinIndex = MAGIC_JOIN_INDEX_INVALID
Logger . Info ( "Room OnPlayerLost: " , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "resulted pR.JoinIndexBooleanArr" , pR . JoinIndexBooleanArr ) )
}
}
func ( pR * Room ) clearPlayerNetworkSession ( playerId int32 ) {
if _ , y := pR . PlayerDownsyncSessionDict [ playerId ] ; y {
Logger . Info ( "sending termination symbol for:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) )
delete ( pR . PlayerDownsyncSessionDict , playerId )
delete ( pR . PlayerSignalToCloseDict , playerId )
}
}
func ( pR * Room ) onPlayerAdded ( playerId int32 ) {
pR . EffectivePlayerCount ++
if 1 == pR . EffectivePlayerCount {
pR . State = RoomBattleStateIns . WAITING
}
for index , value := range pR . JoinIndexBooleanArr {
if false == value {
pR . Players [ playerId ] . JoinIndex = int32 ( index ) + 1
pR . JoinIndexBooleanArr [ index ] = true
// Lazily assign the initial position of "Player" for "RoomDownsyncFrame".
playerPosList := * ( pR . RawBattleStrToVec2DListMap [ "PlayerStartingPos" ] )
if index > len ( playerPosList ) {
panic ( fmt . Sprintf ( "onPlayerAdded error, index >= len(playerPosList), roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v" , pR . Id , playerId , pR . State , pR . EffectivePlayerCount ) )
}
playerPos := playerPosList [ index ]
if nil == playerPos {
panic ( fmt . Sprintf ( "onPlayerAdded error, nil == playerPos, roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v" , pR . Id , playerId , pR . State , pR . EffectivePlayerCount ) )
}
pR . Players [ playerId ] . X = playerPos . X
pR . Players [ playerId ] . Y = playerPos . Y
break
}
}
pR . updateScore ( )
Logger . Info ( "onPlayerAdded:" , zap . Any ( "playerId" , playerId ) , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "joinIndex" , pR . Players [ playerId ] . JoinIndex ) , zap . Any ( "EffectivePlayerCount" , pR . EffectivePlayerCount ) , zap . Any ( "resulted pR.JoinIndexBooleanArr" , pR . JoinIndexBooleanArr ) , zap . Any ( "RoomBattleState" , pR . State ) )
}
func ( pR * Room ) onPlayerReAdded ( playerId int32 ) {
/ *
* [ WARNING ]
*
* If a player quits at "RoomBattleState.WAITING" , then his / her re - joining will always invoke ` AddPlayerIfPossible(...) ` . Therefore , this
* function will only be invoked for players who quit the battle at ">RoomBattleState.WAITING" and re - join at "RoomBattleState.IN_BATTLE" , during which the ` pR.JoinIndexBooleanArr ` doesn ' t change .
* /
Logger . Info ( "Room got `onPlayerReAdded` invoked," , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "resulted pR.JoinIndexBooleanArr" , pR . JoinIndexBooleanArr ) )
pR . updateScore ( )
}
func ( pR * Room ) OnPlayerBattleColliderAcked ( playerId int32 ) bool {
pPlayer , ok := pR . Players [ playerId ]
if false == ok {
return false
}
playerMetas := make ( map [ int32 ] * pb . PlayerMeta , 0 )
for _ , player := range pR . Players {
playerMetas [ player . Id ] = & pb . PlayerMeta {
Id : player . Id ,
Name : player . Name ,
DisplayName : player . DisplayName ,
Avatar : player . Avatar ,
JoinIndex : player . JoinIndex ,
}
}
var playerAckedFrame pb . RoomDownsyncFrame
switch pPlayer . BattleState {
case PlayerBattleStateIns . ADDED_PENDING_BATTLE_COLLIDER_ACK :
playerAckedFrame = pb . RoomDownsyncFrame {
Id : pR . Tick ,
Players : toPbPlayers ( pR . Players ) ,
SentAt : utils . UnixtimeMilli ( ) ,
RefFrameId : MAGIC_ROOM_DOWNSYNC_FRAME_ID_PLAYER_ADDED_AND_ACKED ,
PlayerMetas : playerMetas ,
}
case PlayerBattleStateIns . READDED_PENDING_BATTLE_COLLIDER_ACK :
playerAckedFrame = pb . RoomDownsyncFrame {
Id : pR . Tick ,
Players : toPbPlayers ( pR . Players ) ,
SentAt : utils . UnixtimeMilli ( ) ,
RefFrameId : MAGIC_ROOM_DOWNSYNC_FRAME_ID_PLAYER_READDED_AND_ACKED ,
PlayerMetas : playerMetas ,
}
default :
}
for _ , player := range pR . Players {
/ *
[ WARNING ]
This ` playerAckedFrame ` is the first ever "RoomDownsyncFrame" for every "PersistentSessionClient on the frontend" , and it goes right after each "BattleColliderInfo" .
By making use of the sequential nature of each ws session , all later "RoomDownsyncFrame" s generated after ` pRoom.StartBattle() ` will be put behind this ` playerAckedFrame ` .
* /
pR . sendSafely ( playerAckedFrame , player . Id )
}
pPlayer . BattleState = PlayerBattleStateIns . ACTIVE
Logger . Info ( "OnPlayerBattleColliderAcked" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "roomState" , pR . State ) , zap . Any ( "playerId" , playerId ) , zap . Any ( "capacity" , pR . Capacity ) , zap . Any ( "len(players)" , len ( pR . Players ) ) )
if pR . Capacity == len ( pR . Players ) {
allAcked := true
for _ , p := range pR . Players {
if PlayerBattleStateIns . ACTIVE != p . BattleState {
Logger . Info ( "unexpectedly got an inactive player" , zap . Any ( "roomId" , pR . Id ) , zap . Any ( "playerId" , p . Id ) , zap . Any ( "battleState" , p . BattleState ) )
allAcked = false
break
}
}
if true == allAcked {
pR . StartBattle ( ) // WON'T run if the battle state is not in WAITING.
}
}
pR . updateScore ( )
return true
}
func ( pR * Room ) sendSafely ( s interface { } , playerId int32 ) {
defer func ( ) {
if r := recover ( ) ; r != nil {
pR . PlayerSignalToCloseDict [ playerId ] ( Constants . RetCode . UnknownError , fmt . Sprintf ( "%v" , r ) )
}
} ( )
var resp * pb . WsResp = nil
switch v := s . ( type ) {
case pb . RoomDownsyncFrame :
roomDownsyncFrame := s . ( pb . RoomDownsyncFrame )
resp = & pb . WsResp {
Ret : int32 ( Constants . RetCode . Ok ) ,
EchoedMsgId : int32 ( 0 ) ,
Act : DOWNSYNC_MSG_ACT_ROOM_FRAME ,
Rdf : & roomDownsyncFrame ,
}
case [ ] * pb . InputFrameDownsync :
toSendFrames := s . ( [ ] * pb . InputFrameDownsync )
resp = & pb . WsResp {
Ret : int32 ( Constants . RetCode . Ok ) ,
EchoedMsgId : int32 ( 0 ) ,
Act : DOWNSYNC_MSG_ACT_INPUT_BATCH ,
InputFrameDownsyncBatch : toSendFrames ,
}
default :
panic ( fmt . Sprintf ( "Unknown downsync message type, roomId=%v, playerId=%v, roomState=%v, v=%v" , pR . Id , playerId , v ) )
}
theBytes , marshalErr := proto . Marshal ( resp )
if nil != marshalErr {
panic ( fmt . Sprintf ( "Error marshaling downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v" , pR . Id , playerId , pR . State , pR . EffectivePlayerCount ) )
}
if err := pR . PlayerDownsyncSessionDict [ playerId ] . WriteMessage ( websocket . BinaryMessage , theBytes ) ; nil != err {
panic ( fmt . Sprintf ( "Error sending downsync message: roomId=%v, playerId=%v, roomState=%v, roomEffectivePlayerCount=%v" , pR . Id , playerId , pR . State , pR . EffectivePlayerCount ) )
}
}
func ( pR * Room ) inputFrameIdDebuggable ( inputFrameId int32 ) bool {
return 0 == ( inputFrameId % 10 )
}
type RoomBattleContactListener struct {
name string
room * Room
}
// Implementing the GolangBox2d contact listeners [begins].
/ * *
* Note that the execution of these listeners is within the SAME GOROUTINE as that of "`battleMainLoop` in the same room" .
* See the comments in ` Room.refreshContactListener() ` for details .
* /
func ( l RoomBattleContactListener ) BeginContact ( contact box2d . B2ContactInterface ) {
var pTower * GuardTower
var pPlayer * Player
switch v := contact . GetNodeA ( ) . Other . GetUserData ( ) . ( type ) {
case * GuardTower :
pTower = v
case * Player :
pPlayer = v
default :
//
}
switch v := contact . GetNodeB ( ) . Other . GetUserData ( ) . ( type ) {
case * GuardTower :
pTower = v
case * Player :
pPlayer = v
default :
}
if pTower != nil && pPlayer != nil {
pTower . InRangePlayers . AppendPlayer ( pPlayer )
}
}
func ( l RoomBattleContactListener ) EndContact ( contact box2d . B2ContactInterface ) {
var pTower * GuardTower
var pPlayer * Player
switch v := contact . GetNodeA ( ) . Other . GetUserData ( ) . ( type ) {
case * GuardTower :
pTower = v
case * Player :
pPlayer = v
default :
}
switch v := contact . GetNodeB ( ) . Other . GetUserData ( ) . ( type ) {
case * GuardTower :
pTower = v
case * Player :
pPlayer = v
default :
}
if pTower != nil && pPlayer != nil {
pTower . InRangePlayers . RemovePlayerById ( pPlayer . Id )
}
}
func ( l RoomBattleContactListener ) PreSolve ( contact box2d . B2ContactInterface , oldManifold box2d . B2Manifold ) {
//fmt.Printf("I am PreSolve %s\n", l.name);
}
func ( l RoomBattleContactListener ) PostSolve ( contact box2d . B2ContactInterface , impulse * box2d . B2ContactImpulse ) {
//fmt.Printf("PostSolve %s\n", l.name);
}
// Implementing the GolangBox2d contact listeners [ends].