mirror of
https://github.com/genxium/DelayNoMore
synced 2025-10-09 00:26:39 +00:00
Initial commit.
This commit is contained in:
32
battle_srv/Makefile
Normal file
32
battle_srv/Makefile
Normal file
@@ -0,0 +1,32 @@
|
||||
PROJECTNAME=server.exe
|
||||
ROOT_DIR=.
|
||||
all: help
|
||||
|
||||
gen-constants:
|
||||
gojson -pkg common -name constants -input common/constants.json -o common/constants_struct.go
|
||||
sed -i 's/int64/int/g' common/constants_struct.go
|
||||
|
||||
run-test: build
|
||||
ServerEnv=TEST ./$(PROJECTNAME)
|
||||
|
||||
run-test-and-hotreload:
|
||||
ServerEnv=TEST CompileDaemon -log-prefix=false -build="go build" -command="./$(PROJECTNAME)"
|
||||
|
||||
build:
|
||||
go build -o $(ROOT_DIR)/$(PROJECTNAME)
|
||||
|
||||
run-prod: build-prod
|
||||
./$(PROJECTNAME)
|
||||
|
||||
build-prod:
|
||||
go build -ldflags "-s -w -X main.VERSION=$(shell git rev-parse --short HEAD)-$(shell date "+%Y%m%d-%H:%M:%S")" -o $(ROOT_DIR)/$(PROJECTNAME)
|
||||
|
||||
.PHONY: help
|
||||
|
||||
help: Makefile
|
||||
@echo
|
||||
@echo " Choose a command run:"
|
||||
@echo
|
||||
@sed -n 's/^##//p' $< | column -t -s ':' | sed -e 's/^/ /'
|
||||
@echo
|
||||
|
32
battle_srv/api/req_logger.go
Normal file
32
battle_srv/api/req_logger.go
Normal file
@@ -0,0 +1,32 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"github.com/gin-gonic/gin"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
. "server/common"
|
||||
)
|
||||
|
||||
func RequestLogger() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
buf, _ := ioutil.ReadAll(c.Request.Body)
|
||||
rdr1 := ioutil.NopCloser(bytes.NewBuffer(buf))
|
||||
rdr2 := ioutil.NopCloser(bytes.NewBuffer(buf)) // We have to create a new Buffer, because rdr1 will be read.
|
||||
|
||||
if s := readBody(rdr1); s != "" {
|
||||
Logger.Debug(s) // Print the request body.
|
||||
}
|
||||
|
||||
c.Request.Body = rdr2
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
func readBody(reader io.Reader) string {
|
||||
buf := new(bytes.Buffer)
|
||||
buf.ReadFrom(reader)
|
||||
|
||||
s := buf.String()
|
||||
return s
|
||||
}
|
27
battle_srv/api/signal.go
Normal file
27
battle_srv/api/signal.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
)
|
||||
|
||||
const RET = "ret"
|
||||
const PLAYER_ID = "playerId"
|
||||
const TARGET_PLAYER_ID = "targetPlayerId"
|
||||
const TOKEN = "token"
|
||||
|
||||
func CErr(c *gin.Context, err error) {
|
||||
if err != nil {
|
||||
c.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
func HandleRet() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Next()
|
||||
ret := c.GetInt("ret")
|
||||
if ret != 0 {
|
||||
c.JSON(http.StatusOK, gin.H{"ret": ret})
|
||||
}
|
||||
}
|
||||
}
|
723
battle_srv/api/v1/player.go
Normal file
723
battle_srv/api/v1/player.go
Normal file
@@ -0,0 +1,723 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gin-gonic/gin/binding"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"server/api"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
var Player = playerController{}
|
||||
|
||||
type playerController struct {
|
||||
}
|
||||
|
||||
type smsCaptchaReq struct {
|
||||
Num string `json:"phoneNum,omitempty" form:"phoneNum"`
|
||||
CountryCode string `json:"phoneCountryCode,omitempty" form:"phoneCountryCode"`
|
||||
Captcha string `json:"smsLoginCaptcha,omitempty" form:"smsLoginCaptcha"`
|
||||
}
|
||||
|
||||
func (req *smsCaptchaReq) extAuthID() string {
|
||||
return req.CountryCode + req.Num
|
||||
}
|
||||
func (req *smsCaptchaReq) redisKey() string {
|
||||
return "/cuisine/sms/captcha/" + req.extAuthID()
|
||||
}
|
||||
|
||||
type wechatShareConfigReq struct {
|
||||
Url string `form:"url"`
|
||||
}
|
||||
|
||||
func (p *playerController) GetWechatShareConfig(c *gin.Context) {
|
||||
var req wechatShareConfigReq
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
api.CErr(c, err)
|
||||
if err != nil || req.Url == "" {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
config, err := utils.WechatIns.GetJsConfig(req.Url)
|
||||
if err != nil {
|
||||
Logger.Info("err", zap.Any("", err))
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
Config *utils.JsConfig `json:"jsConfig"`
|
||||
}{Constants.RetCode.Ok, config}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (p *playerController) SMSCaptchaGet(c *gin.Context) {
|
||||
var req smsCaptchaReq
|
||||
err := c.ShouldBindQuery(&req)
|
||||
api.CErr(c, err)
|
||||
if err != nil || req.Num == "" || req.CountryCode == "" {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
// Composite a key to access against Redis-server.
|
||||
redisKey := req.redisKey()
|
||||
ttl, err := storage.RedisManagerIns.TTL(redisKey).Result()
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.UnknownError)
|
||||
return
|
||||
}
|
||||
// Redis剩余时长校验
|
||||
if ttl >= ConstVals.Player.CaptchaMaxTTL {
|
||||
Logger.Info("There's an existing SmsCaptcha record in Redis-server: ", zap.String("key", redisKey), zap.Duration("ttl", ttl))
|
||||
c.Set(api.RET, Constants.RetCode.SmsCaptchaRequestedTooFrequently)
|
||||
return
|
||||
}
|
||||
Logger.Info("A new SmsCaptcha record is needed for: ", zap.String("key", redisKey))
|
||||
pass := false
|
||||
var succRet int
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
// 测试环境,优先从数据库校验`player.name`,不通过再走机器人magic name校验
|
||||
player, err := models.GetPlayerByName(req.Num)
|
||||
if nil == err && nil != player {
|
||||
pass = true
|
||||
succRet = Constants.RetCode.IsTestAcc
|
||||
}
|
||||
}
|
||||
|
||||
if !pass {
|
||||
// 机器人magic name校验,不通过再走手机号校验
|
||||
player, err := models.GetPlayerByName(req.Num)
|
||||
if nil == err && nil != player {
|
||||
pass = true
|
||||
succRet = Constants.RetCode.IsBotAcc
|
||||
}
|
||||
}
|
||||
|
||||
if !pass {
|
||||
if RE_PHONE_NUM.MatchString(req.Num) {
|
||||
succRet = Constants.RetCode.Ok
|
||||
pass = true
|
||||
}
|
||||
// Hardecoded 只验证国内手机号格式
|
||||
if req.CountryCode == "86" {
|
||||
if RE_CHINA_PHONE_NUM.MatchString(req.Num) {
|
||||
succRet = Constants.RetCode.Ok
|
||||
pass = true
|
||||
} else {
|
||||
succRet = Constants.RetCode.InvalidRequestParam
|
||||
pass = false
|
||||
}
|
||||
}
|
||||
}
|
||||
if !pass {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
smsCaptchaReq
|
||||
GetSmsCaptchaRespErrorCode int `json:"getSmsCaptchaRespErrorCode"`
|
||||
}{Ret: succRet}
|
||||
var captcha string
|
||||
if ttl >= 0 {
|
||||
// 已有未过期的旧验证码记录,续验证码有效期。
|
||||
storage.RedisManagerIns.Expire(redisKey, ConstVals.Player.CaptchaExpire)
|
||||
captcha = storage.RedisManagerIns.Get(redisKey).Val()
|
||||
if ttl >= ConstVals.Player.CaptchaExpire/4 {
|
||||
if succRet == Constants.RetCode.Ok {
|
||||
getSmsCaptchaRespErrorCode := sendSMSViaVendor(req.Num, req.CountryCode, captcha)
|
||||
if getSmsCaptchaRespErrorCode != 0 {
|
||||
resp.Ret = Constants.RetCode.GetSmsCaptchaRespErrorCode
|
||||
resp.GetSmsCaptchaRespErrorCode = getSmsCaptchaRespErrorCode
|
||||
}
|
||||
}
|
||||
}
|
||||
Logger.Info("Extended ttl of existing SMSCaptcha record in Redis:", zap.String("key", redisKey), zap.String("captcha", captcha))
|
||||
} else {
|
||||
// 校验通过,进行验证码生成处理
|
||||
captcha = strconv.Itoa(utils.Rand.Number(1000, 9999))
|
||||
if succRet == Constants.RetCode.Ok {
|
||||
getSmsCaptchaRespErrorCode := sendSMSViaVendor(req.Num, req.CountryCode, captcha)
|
||||
if getSmsCaptchaRespErrorCode != 0 {
|
||||
resp.Ret = Constants.RetCode.GetSmsCaptchaRespErrorCode
|
||||
resp.GetSmsCaptchaRespErrorCode = getSmsCaptchaRespErrorCode
|
||||
}
|
||||
}
|
||||
storage.RedisManagerIns.Set(redisKey, captcha, ConstVals.Player.CaptchaExpire)
|
||||
Logger.Info("Generated new captcha", zap.String("key", redisKey), zap.String("captcha", captcha))
|
||||
}
|
||||
if succRet == Constants.RetCode.IsTestAcc {
|
||||
resp.Captcha = captcha
|
||||
}
|
||||
if succRet == Constants.RetCode.IsBotAcc {
|
||||
resp.Captcha = captcha
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (p *playerController) SMSCaptchaLogin(c *gin.Context) {
|
||||
var req smsCaptchaReq
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
api.CErr(c, err)
|
||||
if err != nil || req.Num == "" || req.CountryCode == "" || req.Captcha == "" {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
|
||||
redisKey := req.redisKey()
|
||||
captcha := storage.RedisManagerIns.Get(redisKey).Val()
|
||||
Logger.Info("Comparing captchas", zap.String("key", redisKey), zap.String("whats-in-redis", captcha), zap.String("whats-from-req", req.Captcha))
|
||||
if captcha != req.Captcha {
|
||||
c.Set(api.RET, Constants.RetCode.SmsCaptchaNotMatch)
|
||||
return
|
||||
}
|
||||
|
||||
player, err := p.maybeCreateNewPlayer(req)
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
now := utils.UnixtimeMilli()
|
||||
token := utils.TokenGenerator(32)
|
||||
expiresAt := now + 1000*int64(Constants.Player.IntAuthTokenTTLSeconds)
|
||||
playerLogin := models.PlayerLogin{
|
||||
CreatedAt: now,
|
||||
FromPublicIP: models.NewNullString(c.ClientIP()),
|
||||
IntAuthToken: token,
|
||||
PlayerID: int(player.Id),
|
||||
DisplayName: models.NewNullString(player.DisplayName),
|
||||
UpdatedAt: now,
|
||||
}
|
||||
err = playerLogin.Insert()
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
storage.RedisManagerIns.Del(redisKey)
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
Token string `json:"intAuthToken"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
PlayerID int `json:"playerId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Name string `json:"name"`
|
||||
}{Constants.RetCode.Ok, token, expiresAt, int(player.Id), player.DisplayName, player.Name}
|
||||
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type wechatLogin struct {
|
||||
Authcode string `form:"code"`
|
||||
}
|
||||
|
||||
func (p *playerController) WechatLogin(c *gin.Context) {
|
||||
var req wechatLogin
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
api.CErr(c, err)
|
||||
if err != nil || req.Authcode == "" {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
|
||||
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
|
||||
baseInfo, err := utils.WechatIns.GetOauth2Basic(req.Authcode)
|
||||
|
||||
if err != nil {
|
||||
Logger.Info("err", zap.Any("", err))
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
|
||||
userInfo, err := utils.WechatIns.GetMoreInfo(baseInfo.AccessToken, baseInfo.OpenID)
|
||||
|
||||
if err != nil {
|
||||
Logger.Info("err", zap.Any("", err))
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
//fserver不会返回openId
|
||||
userInfo.OpenID = baseInfo.OpenID
|
||||
|
||||
player, err := p.maybeCreatePlayerWechatAuthBinding(userInfo)
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
|
||||
now := utils.UnixtimeMilli()
|
||||
token := utils.TokenGenerator(32)
|
||||
expiresAt := now + 1000*int64(Constants.Player.IntAuthTokenTTLSeconds)
|
||||
playerLogin := models.PlayerLogin{
|
||||
CreatedAt: now,
|
||||
FromPublicIP: models.NewNullString(c.ClientIP()),
|
||||
IntAuthToken: token,
|
||||
PlayerID: int(player.Id),
|
||||
DisplayName: models.NewNullString(player.DisplayName),
|
||||
UpdatedAt: now,
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
}
|
||||
err = playerLogin.Insert()
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
Token string `json:"intAuthToken"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
PlayerID int `json:"playerId"`
|
||||
DisplayName models.NullString `json:"displayName"`
|
||||
Avatar string `json:"avatar"`
|
||||
}{
|
||||
Constants.RetCode.Ok,
|
||||
token, expiresAt,
|
||||
playerLogin.PlayerID,
|
||||
playerLogin.DisplayName,
|
||||
userInfo.HeadImgURL,
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
type wechatGameLogin struct {
|
||||
Authcode string `form:"code"`
|
||||
AvatarUrl string `form:"avatarUrl"`
|
||||
NickName string `form:"nickName"`
|
||||
}
|
||||
|
||||
func (p *playerController) WechatGameLogin(c *gin.Context) {
|
||||
var req wechatGameLogin
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
if nil != err {
|
||||
Logger.Error("WechatGameLogin got an invalid request param error", zap.Error(err))
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
if "" == req.Authcode {
|
||||
Logger.Warn("WechatGameLogin got an invalid request param", zap.Any("req", req))
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
|
||||
//baseInfo ResAccessToken 获取用户授权access_token的返回结果
|
||||
baseInfo, err := utils.WechatGameIns.GetOauth2Basic(req.Authcode)
|
||||
|
||||
if err != nil {
|
||||
Logger.Info("err", zap.Any("", err))
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info("baseInfo", zap.Any(":", baseInfo))
|
||||
//crate new userInfo from client userInfo
|
||||
userInfo := utils.UserInfo{
|
||||
Nickname: req.NickName,
|
||||
HeadImgURL: req.AvatarUrl,
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
Logger.Info("err", zap.Any("", err))
|
||||
c.Set(api.RET, Constants.RetCode.WechatServerError)
|
||||
return
|
||||
}
|
||||
//fserver不会返回openId
|
||||
userInfo.OpenID = baseInfo.OpenID
|
||||
|
||||
player, err := p.maybeCreatePlayerWechatGameAuthBinding(userInfo)
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
|
||||
now := utils.UnixtimeMilli()
|
||||
token := utils.TokenGenerator(32)
|
||||
expiresAt := now + 1000*int64(Constants.Player.IntAuthTokenTTLSeconds)
|
||||
playerLogin := models.PlayerLogin{
|
||||
CreatedAt: now,
|
||||
FromPublicIP: models.NewNullString(c.ClientIP()),
|
||||
IntAuthToken: token,
|
||||
PlayerID: int(player.Id),
|
||||
DisplayName: models.NewNullString(player.DisplayName),
|
||||
UpdatedAt: now,
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
}
|
||||
err = playerLogin.Insert()
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
Token string `json:"intAuthToken"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
PlayerID int `json:"playerId"`
|
||||
DisplayName models.NullString `json:"displayName"`
|
||||
Avatar string `json:"avatar"`
|
||||
}{
|
||||
Constants.RetCode.Ok,
|
||||
token, expiresAt,
|
||||
playerLogin.PlayerID,
|
||||
playerLogin.DisplayName,
|
||||
userInfo.HeadImgURL,
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (p *playerController) IntAuthTokenLogin(c *gin.Context) {
|
||||
token := p.getIntAuthToken(c)
|
||||
if "" == token {
|
||||
return
|
||||
}
|
||||
playerLogin, err := models.GetPlayerLoginByToken(token)
|
||||
api.CErr(c, err)
|
||||
if err != nil || playerLogin == nil {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidToken)
|
||||
return
|
||||
}
|
||||
|
||||
//kobako: 从player获取display name等
|
||||
player, err := models.GetPlayerById(playerLogin.PlayerID)
|
||||
if err != nil {
|
||||
Logger.Error("Get player by id in IntAuthTokenLogin function error: ", zap.Error(err))
|
||||
return
|
||||
}
|
||||
|
||||
expiresAt := playerLogin.UpdatedAt + 1000*int64(Constants.Player.IntAuthTokenTTLSeconds)
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
Token string `json:"intAuthToken"`
|
||||
ExpiresAt int64 `json:"expiresAt"`
|
||||
PlayerID int `json:"playerId"`
|
||||
DisplayName string `json:"displayName"`
|
||||
Avatar string `json:"avatar"`
|
||||
Name string `json:"name"`
|
||||
}{Constants.RetCode.Ok, token, expiresAt,
|
||||
playerLogin.PlayerID, player.DisplayName,
|
||||
playerLogin.Avatar, player.Name,
|
||||
}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (p *playerController) IntAuthTokenLogout(c *gin.Context) {
|
||||
token := p.getIntAuthToken(c)
|
||||
if "" == token {
|
||||
return
|
||||
}
|
||||
err := models.DelPlayerLoginByToken(token)
|
||||
api.CErr(c, err)
|
||||
if err != nil {
|
||||
c.Set(api.RET, Constants.RetCode.UnknownError)
|
||||
return
|
||||
}
|
||||
c.Set(api.RET, Constants.RetCode.Ok)
|
||||
}
|
||||
|
||||
func (p *playerController) FetchProfile(c *gin.Context) {
|
||||
targetPlayerId := c.GetInt(api.TARGET_PLAYER_ID)
|
||||
wallet, err := models.GetPlayerWalletById(targetPlayerId)
|
||||
if err != nil {
|
||||
api.CErr(c, err)
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
player, err := models.GetPlayerById(targetPlayerId)
|
||||
if err != nil {
|
||||
api.CErr(c, err)
|
||||
c.Set(api.RET, Constants.RetCode.MysqlError)
|
||||
return
|
||||
}
|
||||
if wallet == nil || player == nil {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return
|
||||
}
|
||||
resp := struct {
|
||||
Ret int `json:"ret"`
|
||||
TutorialStage int `json:"tutorialStage"`
|
||||
Wallet *models.PlayerWallet `json:"wallet"`
|
||||
}{Constants.RetCode.Ok, player.TutorialStage, wallet}
|
||||
c.JSON(http.StatusOK, resp)
|
||||
}
|
||||
|
||||
func (p *playerController) TokenAuth(c *gin.Context) {
|
||||
var req struct {
|
||||
Token string `form:"intAuthToken"`
|
||||
TargetPlayerId int `form:"targetPlayerId"`
|
||||
}
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
if err == nil {
|
||||
playerLogin, err := models.GetPlayerLoginByToken(req.Token)
|
||||
api.CErr(c, err)
|
||||
if err == nil && playerLogin != nil {
|
||||
c.Set(api.PLAYER_ID, playerLogin.PlayerID)
|
||||
c.Set(api.TARGET_PLAYER_ID, req.TargetPlayerId)
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
}
|
||||
Logger.Debug("TokenAuth Failed", zap.String("token", req.Token))
|
||||
c.Set(api.RET, Constants.RetCode.InvalidToken)
|
||||
c.Abort()
|
||||
}
|
||||
|
||||
// 以下是内部私有函数
|
||||
func (p *playerController) maybeCreateNewPlayer(req smsCaptchaReq) (*models.Player, error) {
|
||||
extAuthID := req.extAuthID()
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
player, err := models.GetPlayerByName(req.Num)
|
||||
if err != nil {
|
||||
Logger.Error("Seeking test env player error:", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
Logger.Info("Got a test env player:", zap.Any("phonenum", req.Num), zap.Any("playerId", player.Id))
|
||||
return player, nil
|
||||
}
|
||||
} else { //正式环境检查是否为bot用户
|
||||
botPlayer, err := models.GetPlayerByName(req.Num)
|
||||
if err != nil {
|
||||
Logger.Error("Seeking bot player error:", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if botPlayer != nil {
|
||||
Logger.Info("Got a bot player:", zap.Any("phonenum", req.Num), zap.Any("playerId", botPlayer.Id))
|
||||
return botPlayer, nil
|
||||
}
|
||||
}
|
||||
|
||||
bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.Sms, extAuthID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bind != nil {
|
||||
player, err := models.GetPlayerById(bind.PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
return player, nil
|
||||
}
|
||||
}
|
||||
now := utils.UnixtimeMilli()
|
||||
player := models.Player{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
return p.createNewPlayer(player, extAuthID, int(Constants.AuthChannel.Sms))
|
||||
|
||||
}
|
||||
|
||||
func (p *playerController) maybeCreatePlayerWechatAuthBinding(userInfo utils.UserInfo) (*models.Player, error) {
|
||||
bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.Wechat, userInfo.OpenID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bind != nil {
|
||||
player, err := models.GetPlayerById(bind.PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
{ //更新玩家姓名及头像
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}
|
||||
return player, nil
|
||||
}
|
||||
}
|
||||
now := utils.UnixtimeMilli()
|
||||
player := models.Player{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DisplayName: userInfo.Nickname,
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
}
|
||||
return p.createNewPlayer(player, userInfo.OpenID, int(Constants.AuthChannel.Wechat))
|
||||
}
|
||||
|
||||
func (p *playerController) maybeCreatePlayerWechatGameAuthBinding(userInfo utils.UserInfo) (*models.Player, error) {
|
||||
bind, err := models.GetPlayerAuthBinding(Constants.AuthChannel.WechatGame, userInfo.OpenID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if bind != nil {
|
||||
player, err := models.GetPlayerById(bind.PlayerID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if player != nil {
|
||||
{ //更新玩家姓名及头像
|
||||
updateInfo := models.Player{
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
DisplayName: userInfo.Nickname,
|
||||
}
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
ok, err := models.Update(tx, player.Id, &updateInfo)
|
||||
if err != nil && ok != true {
|
||||
return nil, err
|
||||
} else {
|
||||
tx.Commit()
|
||||
}
|
||||
}
|
||||
return player, nil
|
||||
}
|
||||
}
|
||||
now := utils.UnixtimeMilli()
|
||||
player := models.Player{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
DisplayName: userInfo.Nickname,
|
||||
Avatar: userInfo.HeadImgURL,
|
||||
}
|
||||
return p.createNewPlayer(player, userInfo.OpenID, int(Constants.AuthChannel.WechatGame))
|
||||
}
|
||||
|
||||
func (p *playerController) createNewPlayer(player models.Player, extAuthID string, channel int) (*models.Player, error) {
|
||||
Logger.Debug("createNewPlayer", zap.String("extAuthID", extAuthID))
|
||||
now := utils.UnixtimeMilli()
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
err := player.Insert(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
playerAuthBinding := models.PlayerAuthBinding{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Channel: channel,
|
||||
ExtAuthID: extAuthID,
|
||||
PlayerID: int(player.Id),
|
||||
}
|
||||
err = playerAuthBinding.Insert(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
wallet := models.PlayerWallet{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: int(player.Id),
|
||||
}
|
||||
err = wallet.Insert(tx)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tx.Commit()
|
||||
return &player, nil
|
||||
}
|
||||
|
||||
type intAuthTokenReq struct {
|
||||
Token string `form:"intAuthToken,omitempty"`
|
||||
}
|
||||
|
||||
func (p *playerController) getIntAuthToken(c *gin.Context) string {
|
||||
var req intAuthTokenReq
|
||||
err := c.ShouldBindWith(&req, binding.FormPost)
|
||||
api.CErr(c, err)
|
||||
if err != nil || "" == req.Token {
|
||||
c.Set(api.RET, Constants.RetCode.InvalidRequestParam)
|
||||
return ""
|
||||
}
|
||||
return req.Token
|
||||
}
|
||||
|
||||
type tel struct {
|
||||
Mobile string `json:"mobile"`
|
||||
Nationcode string `json:"nationcode"`
|
||||
}
|
||||
|
||||
type captchaReq struct {
|
||||
Ext string `json:"ext"`
|
||||
Extend string `json:"extend"`
|
||||
Params *[2]string `json:"params"`
|
||||
Sig string `json:"sig"`
|
||||
Sign string `json:"sign"`
|
||||
Tel *tel `json:"tel"`
|
||||
Time int64 `json:"time"`
|
||||
Tpl_id int `json:"tpl_id"`
|
||||
}
|
||||
|
||||
func sendSMSViaVendor(mobile string, nationcode string, captchaCode string) int {
|
||||
tel := &tel{
|
||||
Mobile: mobile,
|
||||
Nationcode: nationcode,
|
||||
}
|
||||
var captchaExpireMin string
|
||||
//短信有效期hardcode
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
//测试环境下有效期为20秒 先hardcode了
|
||||
captchaExpireMin = "0.5"
|
||||
} else {
|
||||
captchaExpireMin = strconv.Itoa(int(ConstVals.Player.CaptchaExpire) / 60000000000)
|
||||
}
|
||||
params := [2]string{captchaCode, captchaExpireMin}
|
||||
appkey := "41a5142feff0b38ade02ea12deee9741" // TODO: Should read from config file!
|
||||
rand := strconv.Itoa(utils.Rand.Number(1000, 9999))
|
||||
now := utils.UnixtimeSec()
|
||||
|
||||
hash := sha256.New()
|
||||
hash.Write([]byte("appkey=" + appkey + "&random=" + rand + "&time=" + strconv.FormatInt(now, 10) + "&mobile=" + mobile))
|
||||
md := hash.Sum(nil)
|
||||
sig := hex.EncodeToString(md)
|
||||
|
||||
reqData := &captchaReq{
|
||||
Ext: "",
|
||||
Extend: "",
|
||||
Params: ¶ms,
|
||||
Sig: sig,
|
||||
Sign: "洛克互娱",
|
||||
Tel: tel,
|
||||
Time: now,
|
||||
Tpl_id: 207399,
|
||||
}
|
||||
reqDataString, err := json.Marshal(reqData)
|
||||
req := bytes.NewBuffer([]byte(reqDataString))
|
||||
if err != nil {
|
||||
Logger.Info("json marshal", zap.Any("err:", err))
|
||||
return -1
|
||||
}
|
||||
resp, err := http.Post("https://yun.tim.qq.com/v5/tlssmssvr/sendsms?sdkappid=1400150185&random="+rand,
|
||||
"application/json",
|
||||
req)
|
||||
if err != nil {
|
||||
Logger.Info("resp", zap.Any("err:", err))
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
respBody, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
Logger.Info("body", zap.Any("response body err:", err))
|
||||
}
|
||||
type bodyStruct struct {
|
||||
Result int `json:"result"`
|
||||
}
|
||||
var body bodyStruct
|
||||
json.Unmarshal(respBody, &body)
|
||||
return body.Result
|
||||
}
|
144
battle_srv/api/v1/player_test.go
Normal file
144
battle_srv/api/v1/player_test.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package v1
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/url"
|
||||
. "server/common"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/hashicorp/go-cleanhttp"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
)
|
||||
|
||||
var httpClient = cleanhttp.DefaultPooledClient()
|
||||
|
||||
func fakeSMSCaptchReq(num string) smsCaptchReq {
|
||||
if num == "" {
|
||||
num = "15625296200"
|
||||
}
|
||||
return smsCaptchReq{
|
||||
Num: num,
|
||||
CountryCode: "086",
|
||||
}
|
||||
}
|
||||
|
||||
type dbTestPlayer struct {
|
||||
Name string `db:"name"`
|
||||
MagicPhoneCountryCode string `db:"magic_phone_country_code"`
|
||||
MagicPhoneNum string `db:"magic_phone_num"`
|
||||
}
|
||||
|
||||
type smsCaptchaGetResp struct {
|
||||
Ret int `json:"ret"`
|
||||
Captcha string `json:"smsLoginCaptcha"`
|
||||
}
|
||||
|
||||
func init() {
|
||||
MustParseConfig()
|
||||
MustParseConstants()
|
||||
}
|
||||
|
||||
// 添加ServerEnv=TEST可以缩减sleep时间
|
||||
// battle_srv$ ServerEnv=TEST go test --count=1 -v server/api/v1 -run SMSCaptchaGet*
|
||||
|
||||
func Test_SMSCaptchaGet_frequentlyAndValidSend(t *testing.T) {
|
||||
req := fakeSMSCaptchReq("")
|
||||
resp := mustDoSmsCaptchaGetReq(req, t)
|
||||
if resp.Ret != Constants.RetCode.Ok {
|
||||
t.Fail()
|
||||
}
|
||||
time.Sleep(time.Second * 1)
|
||||
resp = mustDoSmsCaptchaGetReq(req, t)
|
||||
if resp.Ret != Constants.RetCode.SmsCaptchaRequestedTooFrequently {
|
||||
t.Fail()
|
||||
}
|
||||
t.Log("Sleep in a period of sms valid resend seconds")
|
||||
period := Constants.Player.SmsValidResendPeriodSeconds
|
||||
time.Sleep(time.Duration(period) * time.Second)
|
||||
t.Log("Sleep finished")
|
||||
resp = mustDoSmsCaptchaGetReq(req, t)
|
||||
if resp.Ret != Constants.RetCode.Ok {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SMSCaptchaGet_illegalPhone(t *testing.T) {
|
||||
resp := mustDoSmsCaptchaGetReq(fakeSMSCaptchReq("fake"), t)
|
||||
if resp.Ret != Constants.RetCode.InvalidRequestParam {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SMSCaptchaGet_testAcc(t *testing.T) {
|
||||
player, err := getTestPlayer()
|
||||
if err == nil && player != nil {
|
||||
resp := mustDoSmsCaptchaGetReq(fakeSMSCaptchReq(player.Name), t)
|
||||
if resp.Ret != Constants.RetCode.IsTestAcc {
|
||||
t.Fail()
|
||||
}
|
||||
} else {
|
||||
t.Skip("请准备test_env.sqlite和先插入测试用户数据")
|
||||
}
|
||||
}
|
||||
|
||||
func Test_SMSCaptchaGet_expired(t *testing.T) {
|
||||
req := fakeSMSCaptchReq("")
|
||||
resp := mustDoSmsCaptchaGetReq(req, t)
|
||||
if resp.Ret != Constants.RetCode.Ok {
|
||||
t.Fail()
|
||||
}
|
||||
|
||||
t.Log("Sleep in a period of sms expired seconds")
|
||||
period := Constants.Player.SmsExpiredSeconds
|
||||
time.Sleep(time.Duration(period) * time.Second)
|
||||
t.Log("Sleep finished")
|
||||
|
||||
req = fakeSMSCaptchReq("")
|
||||
resp = mustDoSmsCaptchaGetReq(req, t)
|
||||
if resp.Ret != Constants.RetCode.Ok {
|
||||
t.Fail()
|
||||
}
|
||||
}
|
||||
|
||||
func mustDoSmsCaptchaGetReq(req smsCaptchReq, t *testing.T) smsCaptchaGetResp {
|
||||
api := "http://localhost:9992/api/player/v1/SmsCaptcha/get?"
|
||||
parameters := url.Values{}
|
||||
parameters.Add("phoneNum", req.Num)
|
||||
parameters.Add("phoneCountryCode", req.CountryCode)
|
||||
resp, err := httpClient.Get(api + parameters.Encode())
|
||||
if resp != nil {
|
||||
defer resp.Body.Close()
|
||||
}
|
||||
if err != nil {
|
||||
t.Error("mustDoSmsCaptchaGetReq http err", err)
|
||||
t.FailNow()
|
||||
}
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("code!=200, resp code", resp.StatusCode)
|
||||
t.FailNow()
|
||||
}
|
||||
var respJson smsCaptchaGetResp
|
||||
err = json.NewDecoder(resp.Body).Decode(&respJson)
|
||||
if err != nil {
|
||||
t.Error("mustDoSmsCaptchaGetReq json decode err", err)
|
||||
t.FailNow()
|
||||
}
|
||||
return respJson
|
||||
}
|
||||
|
||||
func getTestPlayer() (*dbTestPlayer, error) {
|
||||
db, err := sqlx.Connect("sqlite3", Conf.General.TestEnvSQLitePath)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer db.Close()
|
||||
p := new(dbTestPlayer)
|
||||
err = db.Get(p, "SELECT name, magic_phone_country_code, magic_phone_num FROM test_player limit 1")
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, err
|
||||
}
|
15
battle_srv/check_daemon.sh
Normal file
15
battle_srv/check_daemon.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
|
||||
basedir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
|
||||
PID_FILE="$basedir/treasure-hunter.pid"
|
||||
if [ -f $PID_FILE ]; then
|
||||
pid=$( cat "$PID_FILE" )
|
||||
if [ -z $pid ]; then
|
||||
echo "There's no pid stored in $PID_FILE."
|
||||
else
|
||||
ps aux | grep "$pid"
|
||||
fi
|
||||
else
|
||||
echo "There's no PidFile $PID_FILE."
|
||||
fi
|
155
battle_srv/common/conf.go
Normal file
155
battle_srv/common/conf.go
Normal file
@@ -0,0 +1,155 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 隐式导入
|
||||
var Conf *config
|
||||
|
||||
const (
|
||||
APP_NAME = "server"
|
||||
SERVER_ENV_PROD = "PROD"
|
||||
SERVER_ENV_TEST = "TEST"
|
||||
)
|
||||
|
||||
type generalConf struct {
|
||||
AppRoot string `json:"-"`
|
||||
ConfDir string `json:"-"`
|
||||
TestEnvSQLitePath string `json:"-"`
|
||||
PreConfSQLitePath string `json:"-"`
|
||||
ServerEnv string `json:"-"`
|
||||
}
|
||||
|
||||
type mysqlConf struct {
|
||||
DSN string `json:"-"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
Dbname string `json:"dbname"`
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
}
|
||||
|
||||
type sioConf struct {
|
||||
HostAndPort string `json:"hostAndPort"`
|
||||
}
|
||||
|
||||
type botServerConf struct {
|
||||
SecondsBeforeSummoning int `json:"secondsBeforeSummoning"`
|
||||
Protocol string `json:"protocol"`
|
||||
Host string `json:"host"`
|
||||
Port int `json:"port"`
|
||||
SymmetricKey string `json:"symmetricKey"`
|
||||
}
|
||||
|
||||
type redisConf struct {
|
||||
Dbname int `json:"dbname"`
|
||||
Host string `json:"host"`
|
||||
Password string `json:"password"`
|
||||
Port int `json:"port"`
|
||||
}
|
||||
|
||||
type config struct {
|
||||
General *generalConf
|
||||
MySQL *mysqlConf
|
||||
Sio *sioConf
|
||||
Redis *redisConf
|
||||
BotServer *botServerConf
|
||||
}
|
||||
|
||||
func MustParseConfig() {
|
||||
Conf = &config{
|
||||
General: new(generalConf),
|
||||
MySQL: new(mysqlConf),
|
||||
Sio: new(sioConf),
|
||||
Redis: new(redisConf),
|
||||
BotServer: new(botServerConf),
|
||||
}
|
||||
execPath, err := os.Executable()
|
||||
ErrFatal(err)
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
Logger.Debug("os.GetWd", zap.String("pwd", pwd))
|
||||
ErrFatal(err)
|
||||
|
||||
appRoot := pwd
|
||||
confDir := filepath.Join(appRoot, "configs")
|
||||
Logger.Debug("conf", zap.String("dir", confDir))
|
||||
if isNotExist(confDir) {
|
||||
appRoot = filepath.Dir(execPath)
|
||||
confDir = filepath.Join(appRoot, "configs")
|
||||
Logger.Debug("conf", zap.String("dir", confDir))
|
||||
if isNotExist(confDir) {
|
||||
i := strings.LastIndex(pwd, "battle_srv")
|
||||
if i == -1 {
|
||||
Logger.Fatal("无法找到配置目录,cp -rn configs.template configs,并配置相关参数,再启动")
|
||||
}
|
||||
appRoot = pwd[:(i + 10)]
|
||||
confDir = filepath.Join(appRoot, "configs")
|
||||
Logger.Debug("conf", zap.String("dir", confDir))
|
||||
if isNotExist(confDir) {
|
||||
Logger.Fatal("无法找到配置目录,cp -rn configs.template configs,并配置相关参数,再启动")
|
||||
}
|
||||
}
|
||||
}
|
||||
Conf.General.AppRoot = appRoot
|
||||
testEnvSQLitePath := filepath.Join(confDir, "test_env.sqlite")
|
||||
if !isNotExist(testEnvSQLitePath) {
|
||||
Conf.General.TestEnvSQLitePath = testEnvSQLitePath
|
||||
}
|
||||
preConfSQLitePath := filepath.Join(confDir, "pre_conf_data.sqlite")
|
||||
if !isNotExist(preConfSQLitePath) {
|
||||
Conf.General.PreConfSQLitePath = preConfSQLitePath
|
||||
}
|
||||
Conf.General.ConfDir = confDir
|
||||
Conf.General.ServerEnv = os.Getenv("ServerEnv")
|
||||
|
||||
loadJSON("mysql.json", Conf.MySQL)
|
||||
setMySQLDSNURL(Conf.MySQL)
|
||||
loadJSON("sio.json", Conf.Sio)
|
||||
loadJSON("redis.json", Conf.Redis)
|
||||
loadJSON("bot_server.json", Conf.BotServer)
|
||||
}
|
||||
|
||||
func setMySQLDSNURL(c *mysqlConf) {
|
||||
var dsn = fmt.Sprintf("%s:%s@tcp(%s:%d)/%s",
|
||||
c.Username, c.Password, c.Host, c.Port, c.Dbname)
|
||||
c.DSN = dsn
|
||||
|
||||
}
|
||||
|
||||
func loadJSON(fp string, v interface{}) {
|
||||
if !filepath.IsAbs(fp) {
|
||||
fp = filepath.Join(Conf.General.ConfDir, fp)
|
||||
}
|
||||
_, err := os.Stat(fp)
|
||||
ErrFatal(err)
|
||||
|
||||
fd, err := os.Open(fp)
|
||||
ErrFatal(err)
|
||||
defer fd.Close()
|
||||
Logger.Info("Opened json file successfully.", zap.String("fp", fp))
|
||||
err = json.NewDecoder(fd).Decode(v)
|
||||
ErrFatal(err)
|
||||
Logger.Info("Loaded json file successfully.", zap.String("fp", fp))
|
||||
}
|
||||
|
||||
// Please only use this auxiliary function before server is fully started up, but not afterwards (启动过程可以使用,运行时不准使用).
|
||||
func ErrFatal(err error) {
|
||||
if err != nil {
|
||||
Logger.Fatal("ErrFatal", zap.NamedError("err", err))
|
||||
}
|
||||
}
|
||||
|
||||
func isNotExist(p string) bool {
|
||||
if _, err := os.Stat(p); err != nil {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
}
|
67
battle_srv/common/constants.json
Normal file
67
battle_srv/common/constants.json
Normal file
@@ -0,0 +1,67 @@
|
||||
{
|
||||
"RET_CODE": {
|
||||
"__comment__":"基础",
|
||||
"OK": 9000,
|
||||
"UNKNOWN_ERROR": 9001,
|
||||
"INVALID_REQUEST_PARAM": 9002,
|
||||
"IS_TEST_ACC": 9003,
|
||||
"MYSQL_ERROR": 9004,
|
||||
"NONEXISTENT_ACT": 9005,
|
||||
"LACK_OF_DIAMOND": 9006,
|
||||
"LACK_OF_GOLD": 9007,
|
||||
"LACK_OF_ENERGY": 9008,
|
||||
"NONEXISTENT_ACT_HANDLER": 9009,
|
||||
"LOCALLY_NO_AVAILABLE_ROOM": 9010,
|
||||
"LOCALLY_NO_SPECIFIED_ROOM": 9011,
|
||||
"PLAYER_NOT_ADDABLE_TO_ROOM": 9012,
|
||||
"PLAYER_NOT_READDABLE_TO_ROOM": 9013,
|
||||
"PLAYER_NOT_FOUND": 9014,
|
||||
"PLAYER_CHEATING": 9015,
|
||||
"WECHAT_SERVER_ERROR": 9016,
|
||||
"IS_BOT_ACC": 9017,
|
||||
|
||||
"__comment__":"SMS",
|
||||
"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 5001,
|
||||
"SMS_CAPTCHA_NOT_MATCH": 5002,
|
||||
"INVALID_TOKEN": 2001,
|
||||
|
||||
"DUPLICATED": 2002,
|
||||
"INCORRECT_HANDLE": 2004,
|
||||
|
||||
"INCORRECT_PASSWORD": 2006,
|
||||
"INCORRECT_CAPTCHA": 2007,
|
||||
"INVALID_EMAIL_LITERAL": 2008,
|
||||
"NO_ASSOCIATED_EMAIL": 2009,
|
||||
"SEND_EMAIL_TIMEOUT": 2010,
|
||||
"INCORRECT_PHONE_COUNTRY_CODE": 2011,
|
||||
"NEW_HANDLE_CONFLICT": 2013,
|
||||
"FAILED_TO_UPDATE": 2014,
|
||||
"FAILED_TO_DELETE": 2015,
|
||||
"FAILED_TO_CREATE": 2016,
|
||||
"INCORRECT_PHONE_NUMBER": 2018,
|
||||
"INSUFFICIENT_MEM_TO_ALLOCATE_CONNECTION": 3001,
|
||||
"PASSWORD_RESET_CODE_GENERATION_PER_EMAIL_TOO_FREQUENTLY": 4000,
|
||||
"TRADE_CREATION_TOO_FREQUENTLY": 4002,
|
||||
"MAP_NOT_UNLOCKED": 4003,
|
||||
|
||||
"GET_SMS_CAPTCHA_RESP_ERROR_CODE": 5003,
|
||||
"NOT_IMPLEMENTED_YET": 65535
|
||||
},
|
||||
"AUTH_CHANNEL": {
|
||||
"SMS": 0,
|
||||
"WECHAT": 1,
|
||||
"WECHAT_GAME": 2
|
||||
},
|
||||
"PLAYER": {
|
||||
"DIAMOND": 1,
|
||||
"GOLD": 2,
|
||||
"ENERGY": 3,
|
||||
"SMS_EXPIRED_SECONDS":120,
|
||||
"SMS_VALID_RESEND_PERIOD_SECONDS":30,
|
||||
"INT_AUTH_TOKEN_TTL_SECONDS":604800
|
||||
},
|
||||
"WS": {
|
||||
"INTERVAL_TO_PING": 2000,
|
||||
"WILL_KICK_IF_INACTIVE_FOR": 6000
|
||||
}
|
||||
}
|
38
battle_srv/common/constants_loader.go
Normal file
38
battle_srv/common/constants_loader.go
Normal file
@@ -0,0 +1,38 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
|
||||
"github.com/imdario/mergo"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// 隐式导入
|
||||
var Constants *constants
|
||||
|
||||
func MustParseConstants() {
|
||||
fp := filepath.Join(Conf.General.AppRoot, "common/constants.json")
|
||||
if isNotExist(fp) {
|
||||
Logger.Fatal("common/constants.json文件不存在")
|
||||
}
|
||||
Constants = new(constants)
|
||||
loadJSON(fp, Constants)
|
||||
|
||||
Logger.Debug("Conf.General.ServerEnv", zap.String("env", Conf.General.ServerEnv))
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
fp = filepath.Join(Conf.General.AppRoot, "common/constants_test.json")
|
||||
if !isNotExist(fp) {
|
||||
testConstants := new(constants)
|
||||
loadJSON(fp, testConstants)
|
||||
//Logger.Debug(spew.Sdump(Constants))
|
||||
//Logger.Debug(spew.Sdump(testConstants))
|
||||
err := mergo.Merge(testConstants, Constants)
|
||||
ErrFatal(err)
|
||||
Constants = testConstants
|
||||
//Logger.Debug("mergo.Merge", zap.Error(err))
|
||||
//Logger.Debug(spew.Sdump(testConstants))
|
||||
}
|
||||
}
|
||||
constantsPost()
|
||||
// Logger.Debug("const", zap.Int("IntAuthTokenTTLSeconds", Constants.Player.IntAuthTokenTTLSeconds))
|
||||
}
|
64
battle_srv/common/constants_struct.go
Normal file
64
battle_srv/common/constants_struct.go
Normal file
@@ -0,0 +1,64 @@
|
||||
package common
|
||||
|
||||
type constants struct {
|
||||
AuthChannel struct {
|
||||
Sms int `json:"SMS"`
|
||||
Wechat int `json:"WECHAT"`
|
||||
WechatGame int `json:"WECHAT_GAME"`
|
||||
} `json:"AUTH_CHANNEL"`
|
||||
Player struct {
|
||||
Diamond int `json:"DIAMOND"`
|
||||
Energy int `json:"ENERGY"`
|
||||
Gold int `json:"GOLD"`
|
||||
IntAuthTokenTTLSeconds int `json:"INT_AUTH_TOKEN_TTL_SECONDS"`
|
||||
SmsExpiredSeconds int `json:"SMS_EXPIRED_SECONDS"`
|
||||
SmsValidResendPeriodSeconds int `json:"SMS_VALID_RESEND_PERIOD_SECONDS"`
|
||||
} `json:"PLAYER"`
|
||||
RetCode struct {
|
||||
Duplicated int `json:"DUPLICATED"`
|
||||
FailedToCreate int `json:"FAILED_TO_CREATE"`
|
||||
FailedToDelete int `json:"FAILED_TO_DELETE"`
|
||||
FailedToUpdate int `json:"FAILED_TO_UPDATE"`
|
||||
IncorrectCaptcha int `json:"INCORRECT_CAPTCHA"`
|
||||
IncorrectHandle int `json:"INCORRECT_HANDLE"`
|
||||
IncorrectPassword int `json:"INCORRECT_PASSWORD"`
|
||||
IncorrectPhoneCountryCode int `json:"INCORRECT_PHONE_COUNTRY_CODE"`
|
||||
IncorrectPhoneNumber int `json:"INCORRECT_PHONE_NUMBER"`
|
||||
InsufficientMemToAllocateConnection int `json:"INSUFFICIENT_MEM_TO_ALLOCATE_CONNECTION"`
|
||||
InvalidEmailLiteral int `json:"INVALID_EMAIL_LITERAL"`
|
||||
InvalidRequestParam int `json:"INVALID_REQUEST_PARAM"`
|
||||
InvalidToken int `json:"INVALID_TOKEN"`
|
||||
IsTestAcc int `json:"IS_TEST_ACC"`
|
||||
IsBotAcc int `json:"IS_BOT_ACC"`
|
||||
LackOfDiamond int `json:"LACK_OF_DIAMOND"`
|
||||
LackOfEnergy int `json:"LACK_OF_ENERGY"`
|
||||
LackOfGold int `json:"LACK_OF_GOLD"`
|
||||
MapNotUnlocked int `json:"MAP_NOT_UNLOCKED"`
|
||||
MysqlError int `json:"MYSQL_ERROR"`
|
||||
GetSmsCaptchaRespErrorCode int `json:"GET_SMS_CAPTCHA_RESP_ERROR_CODE"`
|
||||
NewHandleConflict int `json:"NEW_HANDLE_CONFLICT"`
|
||||
NonexistentAct int `json:"NONEXISTENT_ACT"`
|
||||
NonexistentActHandler int `json:"NONEXISTENT_ACT_HANDLER"`
|
||||
LocallyNoAvailableRoom int `json:"LOCALLY_NO_AVAILABLE_ROOM"`
|
||||
LocallyNoSpecifiedRoom int `json:"LOCALLY_NO_SPECIFIED_ROOM"`
|
||||
PlayerNotAddableToRoom int `json:"PLAYER_NOT_ADDABLE_TO_ROOM"`
|
||||
PlayerNotReAddableToRoom int `json:"PLAYER_NOT_READDABLE_TO_ROOM"`
|
||||
PlayerNotFound int `json:"PLAYER_NOT_FOUND"`
|
||||
PlayerCheating int `json:"PLAYER_CHEATING"`
|
||||
NotImplementedYet int `json:"NOT_IMPLEMENTED_YET"`
|
||||
NoAssociatedEmail int `json:"NO_ASSOCIATED_EMAIL"`
|
||||
Ok int `json:"OK"`
|
||||
PasswordResetCodeGenerationPerEmailTooFrequently int `json:"PASSWORD_RESET_CODE_GENERATION_PER_EMAIL_TOO_FREQUENTLY"`
|
||||
SendEmailTimeout int `json:"SEND_EMAIL_TIMEOUT"`
|
||||
SmsCaptchaNotMatch int `json:"SMS_CAPTCHA_NOT_MATCH"`
|
||||
SmsCaptchaRequestedTooFrequently int `json:"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY"`
|
||||
TradeCreationTooFrequently int `json:"TRADE_CREATION_TOO_FREQUENTLY"`
|
||||
UnknownError int `json:"UNKNOWN_ERROR"`
|
||||
WechatServerError int `json:"WECHAT_SERVER_ERROR"`
|
||||
Comment string `json:"__comment__"`
|
||||
} `json:"RET_CODE"`
|
||||
Ws struct {
|
||||
IntervalToPing int `json:"INTERVAL_TO_PING"`
|
||||
WillKickIfInactiveFor int `json:"WILL_KICK_IF_INACTIVE_FOR"`
|
||||
} `json:"WS"`
|
||||
}
|
7
battle_srv/common/constants_test.json
Normal file
7
battle_srv/common/constants_test.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"PLAYER": {
|
||||
"SMS_EXPIRED_SECONDS":30,
|
||||
"SMS_VALID_RESEND_PERIOD_SECONDS":8,
|
||||
"INT_AUTH_TOKEN_TTL_SECONDS":1800
|
||||
}
|
||||
}
|
23
battle_srv/common/logger.go
Normal file
23
battle_srv/common/logger.go
Normal file
@@ -0,0 +1,23 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"go.uber.org/zap"
|
||||
"go.uber.org/zap/zapcore"
|
||||
)
|
||||
|
||||
var Logger *zap.Logger
|
||||
var LoggerConfig zap.Config
|
||||
|
||||
func init() {
|
||||
LoggerConfig = zap.NewDevelopmentConfig()
|
||||
LoggerConfig.Level.SetLevel(zap.InfoLevel)
|
||||
LoggerConfig.Development = false
|
||||
LoggerConfig.Sampling = &zap.SamplingConfig{
|
||||
Initial: 100,
|
||||
Thereafter: 100,
|
||||
}
|
||||
LoggerConfig.EncoderConfig.EncodeLevel = zapcore.CapitalColorLevelEncoder
|
||||
var err error
|
||||
Logger, err = LoggerConfig.Build()
|
||||
ErrFatal(err)
|
||||
}
|
35
battle_srv/common/utils/rand.go
Normal file
35
battle_srv/common/utils/rand.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
crypto_rand "crypto/rand"
|
||||
"encoding/hex"
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var Rand *privateRand
|
||||
|
||||
type privateRand struct {
|
||||
*rand.Rand
|
||||
}
|
||||
|
||||
func init() {
|
||||
Rand = &privateRand{rand.New(rand.NewSource(time.Now().UnixNano()))}
|
||||
}
|
||||
|
||||
func (p *privateRand) Number(numberRange ...int) int {
|
||||
nr := 0
|
||||
if len(numberRange) > 1 {
|
||||
nr = 1
|
||||
nr = p.Intn(numberRange[1]-numberRange[0]) + numberRange[0]
|
||||
} else {
|
||||
nr = p.Intn(numberRange[0])
|
||||
}
|
||||
return nr
|
||||
}
|
||||
|
||||
func TokenGenerator(len int) string {
|
||||
b := make([]byte, len/2)
|
||||
crypto_rand.Read(b)
|
||||
return hex.EncodeToString(b)
|
||||
}
|
19
battle_srv/common/utils/time.go
Normal file
19
battle_srv/common/utils/time.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package utils
|
||||
|
||||
import "time"
|
||||
|
||||
func UnixtimeNano() int64 {
|
||||
return time.Now().UnixNano()
|
||||
}
|
||||
|
||||
func UnixtimeMicro() int64 {
|
||||
return time.Now().UnixNano() / 1000
|
||||
}
|
||||
|
||||
func UnixtimeMilli() int64 {
|
||||
return time.Now().UnixNano() / 1000000
|
||||
}
|
||||
|
||||
func UnixtimeSec() int64 {
|
||||
return time.Now().Unix()
|
||||
}
|
283
battle_srv/common/utils/wechat.go
Normal file
283
battle_srv/common/utils/wechat.go
Normal file
@@ -0,0 +1,283 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/sha1"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"go.uber.org/zap"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/http"
|
||||
. "server/common"
|
||||
. "server/configs"
|
||||
"sort"
|
||||
"time"
|
||||
)
|
||||
|
||||
var WechatIns *wechat
|
||||
var WechatGameIns *wechat
|
||||
|
||||
func InitWechat(conf WechatConfig) {
|
||||
WechatIns = NewWechatIns(&conf, Constants.AuthChannel.Wechat)
|
||||
}
|
||||
|
||||
func InitWechatGame(conf WechatConfig) {
|
||||
WechatGameIns = NewWechatIns(&conf, Constants.AuthChannel.WechatGame)
|
||||
}
|
||||
|
||||
func NewWechatIns(conf *WechatConfig, channel int) *wechat {
|
||||
newWechat := &wechat{
|
||||
config: conf,
|
||||
channel: channel,
|
||||
}
|
||||
return newWechat
|
||||
}
|
||||
|
||||
const ()
|
||||
|
||||
type wechat struct {
|
||||
config *WechatConfig
|
||||
channel int
|
||||
}
|
||||
|
||||
// CommonError 微信返回的通用错误json
|
||||
type CommonError struct {
|
||||
ErrCode int64 `json:"errcode"`
|
||||
ErrMsg string `json:"errmsg"`
|
||||
}
|
||||
|
||||
// ResAccessToken 获取用户授权access_token的返回结果
|
||||
type resAccessToken struct {
|
||||
CommonError
|
||||
|
||||
AccessToken string `json:"access_token"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
OpenID string `json:"openid"`
|
||||
Scope string `json:"scope"`
|
||||
}
|
||||
|
||||
// Config 返回给用户jssdk配置信息
|
||||
type JsConfig struct {
|
||||
AppID string `json:"app_id"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
NonceStr string `json:"nonce_str"`
|
||||
Signature string `json:"signature"`
|
||||
}
|
||||
|
||||
// resTicket 请求jsapi_tikcet返回结果
|
||||
type resTicket struct {
|
||||
CommonError
|
||||
|
||||
Ticket string `json:"ticket"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
func (w *wechat) GetJsConfig(uri string) (config *JsConfig, err error) {
|
||||
config = new(JsConfig)
|
||||
var ticketStr string
|
||||
ticketStr, err = w.getTicket()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
nonceStr := randomStr(16)
|
||||
timestamp := UnixtimeSec()
|
||||
str := fmt.Sprintf("jsapi_ticket=%s&noncestr=%s×tamp=%d&url=%s", ticketStr, nonceStr, timestamp, uri)
|
||||
sigStr := signature(str)
|
||||
|
||||
config.AppID = w.config.AppID
|
||||
config.NonceStr = nonceStr
|
||||
config.Timestamp = timestamp
|
||||
config.Signature = sigStr
|
||||
return
|
||||
}
|
||||
|
||||
//TODO add cache, getTicket 获取jsapi_ticket
|
||||
func (w *wechat) getTicket() (ticketStr string, err error) {
|
||||
var ticket resTicket
|
||||
ticket, err = w.getTicketFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
ticketStr = ticket.Ticket
|
||||
return
|
||||
}
|
||||
|
||||
func (w *wechat) GetOauth2Basic(authcode string) (result resAccessToken, err error) {
|
||||
var accessTokenURL string
|
||||
if w.channel == Constants.AuthChannel.WechatGame {
|
||||
accessTokenURL = w.config.ApiProtocol + "://" + w.config.ApiGateway + "/sns/jscode2session?appid=%s&secret=%s&js_code=%s&grant_type=authorization_code"
|
||||
}
|
||||
if w.channel == Constants.AuthChannel.Wechat {
|
||||
accessTokenURL = w.config.ApiProtocol + "://" + w.config.ApiGateway + "/sns/oauth2/access_token?appid=%s&secret=%s&code=%s&grant_type=authorization_code"
|
||||
}
|
||||
urlStr := fmt.Sprintf(accessTokenURL, w.config.AppID, w.config.AppSecret, authcode)
|
||||
Logger.Info("urlStr", zap.Any(":", urlStr))
|
||||
response, err := get(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
Logger.Info("GetOauth2Basic marshal error", zap.Any("err", err))
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetOauth2Basic error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//UserInfo 用户授权获取到用户信息
|
||||
type UserInfo struct {
|
||||
CommonError
|
||||
OpenID string `json:"openid"`
|
||||
Nickname string `json:"nickname"`
|
||||
Sex int32 `json:"sex"`
|
||||
Province string `json:"province"`
|
||||
City string `json:"city"`
|
||||
Country string `json:"country"`
|
||||
HeadImgURL string `json:"headimgurl"`
|
||||
Privilege []string `json:"privilege"`
|
||||
Unionid string `json:"unionid"`
|
||||
}
|
||||
|
||||
func (w *wechat) GetMoreInfo(accessToken string, openId string) (result UserInfo, err error) {
|
||||
userInfoURL := w.config.ApiProtocol + "://" + w.config.ApiGateway + "/sns/userinfo?appid=%s&access_token=%s&openid=%s&lang=zh_CN"
|
||||
urlStr := fmt.Sprintf(userInfoURL, w.config.AppID, accessToken, openId)
|
||||
response, err := get(urlStr)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = json.Unmarshal(response, &result)
|
||||
if err != nil {
|
||||
Logger.Info("GetMoreInfo marshal error", zap.Any("err", err))
|
||||
return
|
||||
}
|
||||
if result.ErrCode != 0 {
|
||||
err = fmt.Errorf("GetMoreInfo error : errcode=%v , errmsg=%v", result.ErrCode, result.ErrMsg)
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
//HTTPGet get 请求
|
||||
func get(uri string) ([]byte, error) {
|
||||
response, err := http.Get(uri)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer response.Body.Close()
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||
}
|
||||
body, err := ioutil.ReadAll(response.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return body, err
|
||||
}
|
||||
|
||||
//PostJSON post json 数据请求
|
||||
func post(uri string, obj interface{}) ([]byte, error) {
|
||||
jsonData, err := json.Marshal(obj)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003c"), []byte("<"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u003e"), []byte(">"), -1)
|
||||
jsonData = bytes.Replace(jsonData, []byte("\\u0026"), []byte("&"), -1)
|
||||
|
||||
body := bytes.NewBuffer(jsonData)
|
||||
response, err := http.Post(uri, "application/json;charset=utf-8", body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("http get error : uri=%v , statusCode=%v", uri, response.StatusCode)
|
||||
}
|
||||
return ioutil.ReadAll(response.Body)
|
||||
}
|
||||
|
||||
//Signature sha1签名
|
||||
func signature(params ...string) string {
|
||||
sort.Strings(params)
|
||||
h := sha1.New()
|
||||
for _, s := range params {
|
||||
io.WriteString(h, s)
|
||||
}
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
//RandomStr 随机生成字符串
|
||||
func randomStr(length int) string {
|
||||
str := "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
||||
bytes := []byte(str)
|
||||
result := []byte{}
|
||||
r := rand.New(rand.NewSource(time.Now().UnixNano()))
|
||||
for i := 0; i < length; i++ {
|
||||
result = append(result, bytes[r.Intn(len(bytes))])
|
||||
}
|
||||
return string(result)
|
||||
}
|
||||
|
||||
//getTicketFromServer 强制从服务器中获取ticket
|
||||
func (w *wechat) getTicketFromServer() (ticket resTicket, err error) {
|
||||
var accessToken string
|
||||
accessToken, err = w.getAccessTokenFromServer()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
getTicketURL := w.config.ApiProtocol + "://" + w.config.ApiGateway + "/cgi-bin/ticket/getticket?access_token=%s&type=jsapi"
|
||||
var response []byte
|
||||
url := fmt.Sprintf(getTicketURL, accessToken)
|
||||
response, err = get(url)
|
||||
err = json.Unmarshal(response, &ticket)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if ticket.ErrCode != 0 {
|
||||
err = fmt.Errorf("getTicket Error : errcode=%d , errmsg=%s", ticket.ErrCode, ticket.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
//jsAPITicketCacheKey := fmt.Sprintf("jsapi_ticket_%s", w.config.AppID)
|
||||
//expires := ticket.ExpiresIn - 1500
|
||||
//set
|
||||
//err = js.Cache.Set(jsAPITicketCacheKey, ticket.Ticket, time.Duration(expires)*time.Second)
|
||||
return
|
||||
}
|
||||
|
||||
//GetAccessTokenFromServer 强制从微信服务器获取token
|
||||
func (w *wechat) getAccessTokenFromServer() (accessToken string, err error) {
|
||||
AccessTokenURL := w.config.ApiProtocol + "://" + w.config.ApiGateway + "/cgi-bin/token"
|
||||
url := fmt.Sprintf("%s?grant_type=client_credential&appid=%s&secret=%s", AccessTokenURL, w.config.AppID, w.config.AppSecret)
|
||||
var body []byte
|
||||
body, err = get(url)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
var r resAccessToken
|
||||
err = json.Unmarshal(body, &r)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
if r.ErrMsg != "" {
|
||||
err = fmt.Errorf("get access_token error : errcode=%v , errormsg=%v", r.ErrCode, r.ErrMsg)
|
||||
return
|
||||
}
|
||||
|
||||
//accessTokenCacheKey := fmt.Sprintf("access_token_%s", w.config.AppID)
|
||||
//expires := r.ExpiresIn - 1500
|
||||
//set to redis err = ctx.Cache.Set(accessTokenCacheKey, r.AccessToken, time.Duration(expires)*time.Second)
|
||||
accessToken = r.AccessToken
|
||||
return
|
||||
}
|
31
battle_srv/common/vals.go
Normal file
31
battle_srv/common/vals.go
Normal file
@@ -0,0 +1,31 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"time"
|
||||
)
|
||||
|
||||
var (
|
||||
RE_PHONE_NUM = regexp.MustCompile(`^\+?[0-9]{8,14}$`)
|
||||
RE_SMS_CAPTCHA_CODE = regexp.MustCompile(`^[0-9]{4}$`)
|
||||
RE_CHINA_PHONE_NUM = regexp.MustCompile(`^(13[0-9]|14[5|7]|15[0|1|2|3|5|6|7|8|9]|18[0|1|2|3|5|6|7|8|9])\d{8}$`)
|
||||
)
|
||||
|
||||
// 隐式导入
|
||||
var ConstVals = &struct {
|
||||
Player struct {
|
||||
CaptchaExpire time.Duration
|
||||
CaptchaMaxTTL time.Duration
|
||||
}
|
||||
Ws struct {
|
||||
WillKickIfInactiveFor time.Duration
|
||||
}
|
||||
}{}
|
||||
|
||||
func constantsPost() {
|
||||
ConstVals.Player.CaptchaExpire = time.Duration(Constants.Player.SmsExpiredSeconds) * time.Second
|
||||
ConstVals.Player.CaptchaMaxTTL = ConstVals.Player.CaptchaExpire -
|
||||
time.Duration(Constants.Player.SmsValidResendPeriodSeconds)*time.Second
|
||||
|
||||
ConstVals.Ws.WillKickIfInactiveFor = time.Duration(Constants.Ws.WillKickIfInactiveFor) * time.Millisecond
|
||||
}
|
7
battle_srv/configs.template/bot_server.json
Normal file
7
battle_srv/configs.template/bot_server.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"secondsBeforeSummoning": 10,
|
||||
"protocol": "http",
|
||||
"host": "localhost",
|
||||
"port": 15351,
|
||||
"symmetricKey": ""
|
||||
}
|
7
battle_srv/configs.template/mysql.json
Normal file
7
battle_srv/configs.template/mysql.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 3306,
|
||||
"dbname": "tsrht",
|
||||
"username": "root",
|
||||
"password": "root"
|
||||
}
|
BIN
battle_srv/configs.template/pre_conf_data.sqlite
Normal file
BIN
battle_srv/configs.template/pre_conf_data.sqlite
Normal file
Binary file not shown.
6
battle_srv/configs.template/redis.json
Normal file
6
battle_srv/configs.template/redis.json
Normal file
@@ -0,0 +1,6 @@
|
||||
{
|
||||
"host": "localhost",
|
||||
"port": 6379,
|
||||
"dbname": 0,
|
||||
"password": ""
|
||||
}
|
3
battle_srv/configs.template/sio.json
Normal file
3
battle_srv/configs.template/sio.json
Normal file
@@ -0,0 +1,3 @@
|
||||
{
|
||||
"hostAndPort": "0.0.0.0:9992"
|
||||
}
|
BIN
battle_srv/configs.template/test_env.sqlite
Normal file
BIN
battle_srv/configs.template/test_env.sqlite
Normal file
Binary file not shown.
33
battle_srv/configs.template/wechat.go
Normal file
33
battle_srv/configs.template/wechat.go
Normal file
@@ -0,0 +1,33 @@
|
||||
package configs
|
||||
|
||||
type WechatConfig struct {
|
||||
ApiProtocol string
|
||||
ApiGateway string
|
||||
AppID string
|
||||
AppSecret string
|
||||
}
|
||||
|
||||
// Fserver
|
||||
var WechatConfigIns = WechatConfig{
|
||||
ApiProtocol: "http",
|
||||
ApiGateway: "119.29.236.44:8089",
|
||||
AppID: "wx5432dc1d6164d4e",
|
||||
AppSecret: "secret1",
|
||||
}
|
||||
|
||||
/*
|
||||
// Production
|
||||
var WechatConfigIns = WechatConfig {
|
||||
ApiProtocol: "https",
|
||||
ApiGateway: "api.weixin.qq.com",
|
||||
AppID: "wxe7063ab415266544",
|
||||
AppSecret: "secret1",
|
||||
}
|
||||
*/
|
||||
|
||||
var WechatGameConfigIns = WechatConfig{
|
||||
ApiProtocol: "https",
|
||||
ApiGateway: "api.weixin.qq.com",
|
||||
AppID: "wxf497c910a2a25edc",
|
||||
AppSecret: "secret1",
|
||||
}
|
144
battle_srv/env_tools/load_pre_conf.go
Normal file
144
battle_srv/env_tools/load_pre_conf.go
Normal file
@@ -0,0 +1,144 @@
|
||||
package env_tools
|
||||
|
||||
import (
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.uber.org/zap"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
)
|
||||
|
||||
func LoadPreConf() {
|
||||
Logger.Info(`Merging PreConfSQLite data into MySQL`,
|
||||
zap.String("PreConfSQLitePath", Conf.General.PreConfSQLitePath))
|
||||
db, err := sqlx.Connect("sqlite3", Conf.General.PreConfSQLitePath)
|
||||
ErrFatal(err)
|
||||
defer db.Close()
|
||||
|
||||
loadPreConfToMysql(db)
|
||||
|
||||
// --kobako
|
||||
maybeCreateNewPlayerFromBotTable(db, "bot_player")
|
||||
}
|
||||
|
||||
type dbBotPlayer struct {
|
||||
Name string `db:"name"`
|
||||
MagicPhoneCountryCode string `db:"magic_phone_country_code"`
|
||||
MagicPhoneNum string `db:"magic_phone_num"`
|
||||
DisplayName string `db:"display_name"`
|
||||
}
|
||||
|
||||
func loadPreConfToMysql(db *sqlx.DB) {
|
||||
tbs := []string{}
|
||||
loadSqlite(db, tbs)
|
||||
}
|
||||
|
||||
func loadSqlite(db *sqlx.DB, tbs []string) {
|
||||
for _, v := range tbs {
|
||||
result, err := storage.MySQLManagerIns.Exec("truncate " + v)
|
||||
ErrFatal(err)
|
||||
Logger.Info("truncate", zap.Any("truncate "+v, result))
|
||||
query, args, err := sq.Select("*").From(v).ToSql()
|
||||
if err != nil {
|
||||
Logger.Info("loadSql ToSql error", zap.Any("err", err))
|
||||
}
|
||||
rows, err := db.Queryx(query, args...)
|
||||
if err != nil {
|
||||
Logger.Info("loadSql query error", zap.Any("err", err))
|
||||
}
|
||||
createMysqlData(rows, v)
|
||||
}
|
||||
}
|
||||
|
||||
func createMysqlData(rows *sqlx.Rows, v string) {
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer Logger.Info("Loaded table " + v + " from PreConfSQLite successfully.")
|
||||
switch v {
|
||||
// TODO
|
||||
}
|
||||
err := tx.Commit()
|
||||
if err != nil {
|
||||
defer tx.Rollback()
|
||||
Logger.Info(v+" load", zap.Any("tx.commit error", err))
|
||||
}
|
||||
}
|
||||
|
||||
//加上tableName参数, 用于pre_conf_data.sqlite里bot_player表的复用 --kobako
|
||||
func maybeCreateNewPlayerFromBotTable(db *sqlx.DB, tableName string) {
|
||||
var ls []*dbBotPlayer
|
||||
err := db.Select(&ls, "SELECT name, magic_phone_country_code, magic_phone_num, display_name FROM "+tableName)
|
||||
ErrFatal(err)
|
||||
names := make([]string, len(ls), len(ls))
|
||||
for i, v := range ls {
|
||||
names[i] = v.Name
|
||||
}
|
||||
sql := "SELECT name FROM `player` WHERE name in (?)"
|
||||
query, args, err := sqlx.In(sql, names)
|
||||
ErrFatal(err)
|
||||
query = storage.MySQLManagerIns.Rebind(query)
|
||||
// existNames := make([]string, len(ls), len(ls))
|
||||
var existPlayers []*models.Player
|
||||
err = storage.MySQLManagerIns.Select(&existPlayers, query, args...)
|
||||
ErrFatal(err)
|
||||
|
||||
for _, botPlayer := range ls {
|
||||
var flag bool
|
||||
for _, v := range existPlayers {
|
||||
if botPlayer.Name == v.Name {
|
||||
// 已有数据,合并处理
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flag {
|
||||
// 找不到,新增
|
||||
Logger.Debug("create", zap.Any(tableName, botPlayer))
|
||||
err := createNewBotPlayer(botPlayer)
|
||||
if err != nil {
|
||||
Logger.Warn("createNewPlayer from"+tableName, zap.NamedError("createNewPlayerErr", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createNewBotPlayer(p *dbBotPlayer) error {
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
now := utils.UnixtimeMilli()
|
||||
player := models.Player{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Name: p.Name,
|
||||
DisplayName: p.DisplayName,
|
||||
}
|
||||
err := player.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playerAuthBinding := models.PlayerAuthBinding{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Channel: int(Constants.AuthChannel.Sms),
|
||||
ExtAuthID: p.MagicPhoneCountryCode + p.MagicPhoneNum,
|
||||
PlayerID: int(player.Id),
|
||||
}
|
||||
err = playerAuthBinding.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wallet := models.PlayerWallet{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: int(player.Id),
|
||||
}
|
||||
err = wallet.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
102
battle_srv/env_tools/test_env_db.go
Normal file
102
battle_srv/env_tools/test_env_db.go
Normal file
@@ -0,0 +1,102 @@
|
||||
package env_tools
|
||||
|
||||
import (
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
|
||||
"github.com/jmoiron/sqlx"
|
||||
_ "github.com/mattn/go-sqlite3"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func MergeTestPlayerAccounts() {
|
||||
fp := Conf.General.TestEnvSQLitePath
|
||||
Logger.Info(`Initializing TestPlayerAccounts in runtime MySQLServer from SQLite file:`, zap.String("fp", fp))
|
||||
db, err := sqlx.Connect("sqlite3", fp)
|
||||
ErrFatal(err)
|
||||
defer db.Close()
|
||||
maybeCreateNewPlayer(db, "test_player")
|
||||
}
|
||||
|
||||
type dbTestPlayer struct {
|
||||
Name string `db:"name"`
|
||||
MagicPhoneCountryCode string `db:"magic_phone_country_code"`
|
||||
MagicPhoneNum string `db:"magic_phone_num"`
|
||||
}
|
||||
|
||||
func maybeCreateNewPlayer(db *sqlx.DB, tableName string) {
|
||||
var ls []*dbTestPlayer
|
||||
err := db.Select(&ls, "SELECT name, magic_phone_country_code, magic_phone_num FROM "+tableName)
|
||||
ErrFatal(err)
|
||||
names := make([]string, len(ls), len(ls))
|
||||
for i, v := range ls {
|
||||
names[i] = v.Name
|
||||
}
|
||||
sql := "SELECT name FROM `player` WHERE name in (?)"
|
||||
query, args, err := sqlx.In(sql, names)
|
||||
ErrFatal(err)
|
||||
query = storage.MySQLManagerIns.Rebind(query)
|
||||
// existNames := make([]string, len(ls), len(ls))
|
||||
var existPlayers []*models.Player
|
||||
err = storage.MySQLManagerIns.Select(&existPlayers, query, args...)
|
||||
ErrFatal(err)
|
||||
|
||||
for _, testPlayer := range ls {
|
||||
var flag bool
|
||||
for _, v := range existPlayers {
|
||||
if testPlayer.Name == v.Name {
|
||||
// 已有数据,合并处理
|
||||
flag = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !flag {
|
||||
// 找不到,新增
|
||||
Logger.Debug("create", zap.Any(tableName, testPlayer))
|
||||
err := createNewPlayer(testPlayer)
|
||||
if err != nil {
|
||||
Logger.Warn("createNewPlayer from"+tableName, zap.NamedError("createNewPlayerErr", err))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func createNewPlayer(p *dbTestPlayer) error {
|
||||
tx := storage.MySQLManagerIns.MustBegin()
|
||||
defer tx.Rollback()
|
||||
now := utils.UnixtimeMilli()
|
||||
player := models.Player{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Name: p.Name,
|
||||
}
|
||||
err := player.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
playerAuthBinding := models.PlayerAuthBinding{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
Channel: int(Constants.AuthChannel.Sms),
|
||||
ExtAuthID: p.MagicPhoneCountryCode + p.MagicPhoneNum,
|
||||
PlayerID: int(player.Id),
|
||||
}
|
||||
err = playerAuthBinding.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
wallet := models.PlayerWallet{
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
ID: int(player.Id),
|
||||
}
|
||||
err = wallet.Insert(tx)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
tx.Commit()
|
||||
return nil
|
||||
}
|
37
battle_srv/go.mod
Normal file
37
battle_srv/go.mod
Normal file
@@ -0,0 +1,37 @@
|
||||
module server
|
||||
|
||||
require (
|
||||
github.com/ByteArena/box2d v1.0.2
|
||||
github.com/ChimeraCoder/gojson v1.0.0 // indirect
|
||||
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414
|
||||
github.com/davecgh/go-spew v1.1.1
|
||||
github.com/fatih/color v1.7.0 // indirect
|
||||
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 // indirect
|
||||
github.com/gin-gonic/gin v1.3.0
|
||||
github.com/githubnemo/CompileDaemon v1.0.0 // indirect
|
||||
github.com/go-redis/redis v6.13.2+incompatible
|
||||
github.com/go-sql-driver/mysql v1.4.0
|
||||
github.com/golang/protobuf v1.5.2
|
||||
github.com/google/go-cmp v0.5.9 // indirect
|
||||
github.com/gorilla/websocket v1.2.0
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186
|
||||
github.com/howeyc/fsnotify v0.9.0 // indirect
|
||||
github.com/imdario/mergo v0.3.6
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e
|
||||
github.com/mattn/go-colorable v0.0.9 // indirect
|
||||
github.com/mattn/go-isatty v0.0.3 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.9.0
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967
|
||||
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713
|
||||
github.com/ugorji/go v1.1.1 // indirect
|
||||
go.uber.org/atomic v1.3.2 // indirect
|
||||
go.uber.org/multierr v1.1.0 // indirect
|
||||
go.uber.org/zap v1.9.1
|
||||
google.golang.org/protobuf v1.28.1
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 // indirect
|
||||
gopkg.in/yaml.v2 v2.2.1 // indirect
|
||||
)
|
84
battle_srv/go.sum
Normal file
84
battle_srv/go.sum
Normal file
@@ -0,0 +1,84 @@
|
||||
github.com/ByteArena/box2d v1.0.2 h1:f7f9KEQWhCs1n516DMLzi5w6u0MeeE78Mes4fWMcj9k=
|
||||
github.com/ByteArena/box2d v1.0.2/go.mod h1:LzEuxY9iCz+tskfWCY3o0ywYBRafDDugdSj+/YGI6sE=
|
||||
github.com/ChimeraCoder/gojson v1.0.0 h1:gAYKGTV+xfQ4+l/4C/nazPbiQDUidG0G3ukAJnE7LNE=
|
||||
github.com/ChimeraCoder/gojson v1.0.0/go.mod h1:nYbTQlu6hv8PETM15J927yM0zGj3njIldp72UT1MqSw=
|
||||
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414 h1:VHdYPA0V0YgL97gdjHevN6IVPRX+fOoNMqcYvUAzwNU=
|
||||
github.com/Masterminds/squirrel v0.0.0-20180815162352-8a7e65843414/go.mod h1:xnKTFzjGUiZtiOagBsfnvomW+nJg2usB1ZpordQWqNM=
|
||||
github.com/Pallinder/go-randomdata v0.0.0-20180616180521-15df0648130a h1:0OnS8GR4uI3nau+f/juCZlAq+zCrsHXRJlENrUQ4eU8=
|
||||
github.com/Pallinder/go-randomdata v0.0.0-20180616180521-15df0648130a/go.mod h1:yHmJgulpD2Nfrm0cR9tI/+oAgRqCQQixsA8HyRZfV9Y=
|
||||
github.com/Tarliton/collision2d v0.0.0-20160527013055-f7a088279920 h1:0IrTIOtqYxunLFS7/NZtdHDXifo0sjOnw2y2EhvDIFE=
|
||||
github.com/Tarliton/collision2d v0.0.0-20160527013055-f7a088279920/go.mod h1:xQkVqGFqY6zzdXxydhfUfJQfmSIAyx0XavFZgQK0F5o=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
||||
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be h1:2dYuM6B1e0D4RetEP5sHh4OZkBAvNiLPaqB0h+y6BJU=
|
||||
github.com/gin-contrib/cors v0.0.0-20180514151808-6f0a820f94be/go.mod h1:cw+u9IsAkC16e42NtYYVCLsHYXE98nB3M7Dr9mLSeH4=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7 h1:AzN37oI0cOS+cougNAV9szl6CVoj2RYwzS3DpUQNtlY=
|
||||
github.com/gin-contrib/sse v0.0.0-20170109093832-22d885f9ecc7/go.mod h1:VJ0WA2NBN22VlZ2dKZQPAPnyWw5XTlK1KymzLKsr59s=
|
||||
github.com/gin-gonic/gin v1.3.0 h1:kCmZyPklC0gVdL728E6Aj20uYBJV93nj/TkwBTKhFbs=
|
||||
github.com/gin-gonic/gin v1.3.0/go.mod h1:7cKuhb5qV2ggCFctp2fJQ+ErvciLZrIeoOSOm6mUr7Y=
|
||||
github.com/githubnemo/CompileDaemon v1.0.0 h1:F4nrVyPOxva6gJIw320fwfaFaPl8ZuEk95RZOy9Q4eM=
|
||||
github.com/githubnemo/CompileDaemon v1.0.0/go.mod h1:lE3EXX1td33uhlkFLp+ImWY9qBaoRcDeA3neh4m8ic0=
|
||||
github.com/go-redis/redis v6.13.2+incompatible h1:kfEWSpgBs4XmuzGg7nYPqhQejjzU9eKdIL0PmE2TtRY=
|
||||
github.com/go-redis/redis v6.13.2+incompatible/go.mod h1:NAIEuMOZ/fxfXJIrKDQDz8wamY7mA7PouImQ2Jvg6kA=
|
||||
github.com/go-sql-driver/mysql v1.4.0 h1:7LxgVwFb2hIQtMm87NdgAVfXjnt4OePseqT1tKx+opk=
|
||||
github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w=
|
||||
github.com/golang/protobuf v1.1.0 h1:0iH4Ffd/meGoXqF2lSAhZHt8X+cPgkfn/cb6Cce5Vpc=
|
||||
github.com/golang/protobuf v1.1.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw=
|
||||
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/googollee/go-engine.io v0.0.0-20180611083002-3c3145340e67 h1:CTWsAFYrbWv+NEyoIG+Q5XGZPOEhJ66cuT2QKsz8PgU=
|
||||
github.com/googollee/go-engine.io v0.0.0-20180611083002-3c3145340e67/go.mod h1:MBpz1MS3P4HtRcBpQU4HcjvWXZ9q+JWacMEh2/BFYbg=
|
||||
github.com/googollee/go-socket.io v0.0.0-20180611075005-f12da5711bc6 h1:jyykn2uqd82u413JcoyghmxJakOdbdl8PJ8JAG4JVCQ=
|
||||
github.com/googollee/go-socket.io v0.0.0-20180611075005-f12da5711bc6/go.mod h1:ftBGBMhSYToR5oV4ImIPKvAIsNaTkLC+tTvoNafqxlQ=
|
||||
github.com/gorilla/websocket v1.2.0 h1:VJtLvh6VQym50czpZzx07z/kw9EgAxI3x1ZB8taTMQQ=
|
||||
github.com/gorilla/websocket v1.2.0/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186 h1:URgjUo+bs1KwatoNbwG0uCO4dHN4r1jsp4a5AGgHRjo=
|
||||
github.com/hashicorp/go-cleanhttp v0.0.0-20171218145408-d5fe4b57a186/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80=
|
||||
github.com/howeyc/fsnotify v0.9.0 h1:0gtV5JmOKH4A8SsFxG2BczSeXWWPvcMT0euZt5gDAxY=
|
||||
github.com/howeyc/fsnotify v0.9.0/go.mod h1:41HzSPxBGeFRQKEEwgh49TRw/nKBsYZ2cF1OzPjSJsA=
|
||||
github.com/imdario/mergo v0.3.6 h1:xTNEAn+kxVO7dTZGu0CegyqKZmoWFI0rF8UxjlB2d28=
|
||||
github.com/imdario/mergo v0.3.6/go.mod h1:2EnlNZ0deacrJVfApfmtdGgDfMuh/nq6Ok1EcJh5FfA=
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0 h1:5B0uxl2lzNRVkJVg+uGHxWtRt4C0Wjc6kJKo5XYx8xE=
|
||||
github.com/jmoiron/sqlx v0.0.0-20180614180643-0dae4fefe7c0/go.mod h1:IiEW3SEiiErVyFdH8NTuWjSifiEQKUoyK3LNqr2kCHU=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw=
|
||||
github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk=
|
||||
github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e h1:9MlwzLdW7QSDrhDjFlsEYmxpFyIoXmYRon3dt0io31k=
|
||||
github.com/logrusorgru/aurora v0.0.0-20181002194514-a7b3b318ed4e/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||
github.com/mattn/go-colorable v0.0.9 h1:UVL0vNpWh04HeJXV0KLcaT7r06gOH2l4OW6ddYRUIY4=
|
||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
||||
github.com/mattn/go-isatty v0.0.3 h1:ns/ykhmWi7G9O+8a448SecJU3nSMBXJfqQkl0upE1jI=
|
||||
github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0 h1:pDRiWfl+++eC2FEFRy6jXmQlvp4Yh3z1MJKg4UeYM/4=
|
||||
github.com/mattn/go-sqlite3 v1.9.0/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967 h1:x7xEyJDP7Hv3LVgvWhzioQqbC/KtuUhTigKlH/8ehhE=
|
||||
github.com/robfig/cron v0.0.0-20180505203441-b41be1df6967/go.mod h1:JGuDeoQd7Z6yL4zQhZ3OPEVHB7fL6Ka6skscFHfmt2k=
|
||||
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713 h1:knaxjm6QMbUMNvuaSnJZmw0gRX4V/79JVUQiziJGM84=
|
||||
github.com/thoas/go-funk v0.0.0-20180716193722-1060394a7713/go.mod h1:mlR+dHGb+4YgXkf13rkQTuzrneeHANxOm6+ZnEV9HsA=
|
||||
github.com/ugorji/go v1.1.1 h1:gmervu+jDMvXTbcHQ0pd2wee85nEoE0BsVyEuzkfK8w=
|
||||
github.com/ugorji/go v1.1.1/go.mod h1:hnLbHMwcvSihnDhEfx2/BzKp2xb0Y+ErdfYcrs9tkJQ=
|
||||
go.uber.org/atomic v1.3.2 h1:2Oa65PReHzfn29GpvgsYwloV9AVFHPDk8tYxt2c2tr4=
|
||||
go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE=
|
||||
go.uber.org/multierr v1.1.0 h1:HoEmRHQPVSqub6w2z2d2EOVs2fjyFRGyofhKuyDq0QI=
|
||||
go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0=
|
||||
go.uber.org/zap v1.9.1 h1:XCJQEf3W6eZaVwhRBof6ImoYGJSITeKWsyeh3HFu/5o=
|
||||
go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8=
|
||||
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||
google.golang.org/protobuf v1.28.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w=
|
||||
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2 h1:lFB4DoMU6B626w8ny76MV7VX6W2VHct2GVOI3xgiMrQ=
|
||||
gopkg.in/go-playground/validator.v8 v8.18.2/go.mod h1:RX2a/7Ha8BgOhfk7j780h4/u/RRjR0eouCJSH80/M2Y=
|
||||
gopkg.in/yaml.v2 v2.2.1 h1:mUhvW9EsL+naU5Q3cakzfE91YhliOondGd6ZrsDBHQE=
|
||||
gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
16
battle_srv/logrotate/treasure-hunter
Normal file
16
battle_srv/logrotate/treasure-hunter
Normal file
@@ -0,0 +1,16 @@
|
||||
# This file should be put as "/etc/logrotate.d/treasure-hunter".
|
||||
# Manual usage Example
|
||||
# - logrotate -f /etc/logrotate.d/treasure-hunter
|
||||
|
||||
/var/log/treasure-hunter.log {
|
||||
rotate 12
|
||||
size 32M
|
||||
hourly
|
||||
missingok
|
||||
notifempty
|
||||
compress
|
||||
delaycompress
|
||||
copytruncate
|
||||
create 0640 ubuntu ubuntu
|
||||
su ubuntu ubuntu
|
||||
}
|
117
battle_srv/main.go
Normal file
117
battle_srv/main.go
Normal file
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
"server/api"
|
||||
"server/api/v1"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/configs"
|
||||
"server/env_tools"
|
||||
"server/models"
|
||||
"server/storage"
|
||||
"server/ws"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/gin-contrib/cors"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/robfig/cron"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func main() {
|
||||
MustParseConfig()
|
||||
MustParseConstants()
|
||||
storage.Init()
|
||||
env_tools.LoadPreConf()
|
||||
utils.InitWechat(configs.WechatConfigIns)
|
||||
utils.InitWechatGame(configs.WechatGameConfigIns)
|
||||
if Conf.General.ServerEnv == SERVER_ENV_TEST {
|
||||
env_tools.MergeTestPlayerAccounts()
|
||||
}
|
||||
models.InitRoomHeapManager()
|
||||
startScheduler()
|
||||
router := gin.Default()
|
||||
setRouter(router)
|
||||
|
||||
srv := &http.Server{
|
||||
Addr: fmt.Sprintf("%s", Conf.Sio.HostAndPort),
|
||||
Handler: router,
|
||||
}
|
||||
/*
|
||||
* To disable "Keep-Alive" of http/1.0 clients, thus avoid confusing results when inspecting leaks by `netstat`.
|
||||
*
|
||||
* -- YFLu
|
||||
*/
|
||||
srv.SetKeepAlivesEnabled(false)
|
||||
go func() {
|
||||
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||
Logger.Fatal("Error launching the service:", zap.Error(err))
|
||||
}
|
||||
Logger.Info("Listening and serving HTTP on", zap.Any("Conf.Sio.HostAndPort", Conf.Sio.HostAndPort))
|
||||
}()
|
||||
var gracefulStop = make(chan os.Signal)
|
||||
signal.Notify(gracefulStop, syscall.SIGTERM)
|
||||
signal.Notify(gracefulStop, syscall.SIGINT)
|
||||
sig := <-gracefulStop
|
||||
Logger.Info("Shutdown Server ...")
|
||||
Logger.Info("caught sig", zap.Any("sig", sig))
|
||||
Logger.Info("Wait for 5 second to finish processing")
|
||||
clean()
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
|
||||
defer cancel()
|
||||
if err := srv.Shutdown(ctx); err != nil {
|
||||
Logger.Fatal("Server Shutdown:", zap.Error(err))
|
||||
}
|
||||
Logger.Info("Server exiting")
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
func clean() {
|
||||
Logger.Info("About to clean up the resources occupied by this server-process.")
|
||||
if storage.MySQLManagerIns != nil {
|
||||
storage.MySQLManagerIns.Close()
|
||||
}
|
||||
if Logger != nil {
|
||||
Logger.Sync()
|
||||
}
|
||||
}
|
||||
|
||||
func setRouter(router *gin.Engine) {
|
||||
f := func(c *gin.Context) {
|
||||
c.JSON(http.StatusOK, gin.H{"ping": "pong"})
|
||||
}
|
||||
router.Use(cors.Default())
|
||||
router.StaticFS("/asset", http.Dir(filepath.Join(Conf.General.AppRoot, "asset")))
|
||||
router.GET("/ping", f)
|
||||
router.GET("/tsrht", ws.Serve)
|
||||
|
||||
apiRouter := router.Group("/api")
|
||||
{
|
||||
apiRouter.Use(api.HandleRet(), api.RequestLogger())
|
||||
apiRouter.POST("/player/v1/IntAuthToken/login", v1.Player.IntAuthTokenLogin)
|
||||
apiRouter.POST("/player/v1/IntAuthToken/logout", v1.Player.IntAuthTokenLogout)
|
||||
apiRouter.GET("/player/v1/SmsCaptcha/get", v1.Player.SMSCaptchaGet)
|
||||
apiRouter.POST("/player/v1/SmsCaptcha/login", v1.Player.SMSCaptchaLogin)
|
||||
apiRouter.POST("/player/v1/wechat/login", v1.Player.WechatLogin)
|
||||
apiRouter.POST("/player/v1/wechat/jsconfig", v1.Player.GetWechatShareConfig)
|
||||
apiRouter.POST("/player/v1/wechatGame/login", v1.Player.WechatGameLogin)
|
||||
|
||||
authRouter := func(method string, url string, handler gin.HandlerFunc) {
|
||||
apiRouter.Handle(method, url, v1.Player.TokenAuth, handler)
|
||||
}
|
||||
authRouter(http.MethodPost, "/player/v1/profile/fetch", v1.Player.FetchProfile)
|
||||
}
|
||||
}
|
||||
|
||||
func startScheduler() {
|
||||
c := cron.New()
|
||||
//c.AddFunc("*/1 * * * * *", FuncName)
|
||||
c.Start()
|
||||
}
|
13
battle_srv/models/barrier.go
Normal file
13
battle_srv/models/barrier.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ByteArena/box2d"
|
||||
)
|
||||
|
||||
type Barrier struct {
|
||||
X float64
|
||||
Y float64
|
||||
Type uint32
|
||||
Boundary *Polygon2D
|
||||
CollidableBody *box2d.B2Body
|
||||
}
|
19
battle_srv/models/bullet.go
Normal file
19
battle_srv/models/bullet.go
Normal file
@@ -0,0 +1,19 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ByteArena/box2d"
|
||||
)
|
||||
|
||||
type Bullet struct {
|
||||
LocalIdInBattle int32 `json:"-"`
|
||||
LinearSpeed float64 `json:"-"`
|
||||
X float64 `json:"-"`
|
||||
Y float64 `json:"-"`
|
||||
Removed bool `json:"-"`
|
||||
Dir *Direction `json:"-"`
|
||||
StartAtPoint *Vec2D `json:"-"`
|
||||
EndAtPoint *Vec2D `json:"-"`
|
||||
DamageBoundary *Polygon2D `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
RemovedAtFrameId int32 `json:"-"`
|
||||
}
|
81
battle_srv/models/helper.go
Normal file
81
battle_srv/models/helper.go
Normal file
@@ -0,0 +1,81 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
. "server/common"
|
||||
"server/storage"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func exist(t string, cond sq.Eq) (bool, error) {
|
||||
c, err := getCount(t, cond)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return c >= 1, nil
|
||||
}
|
||||
|
||||
func getCount(t string, cond sq.Eq) (int, error) {
|
||||
query, args, err := sq.Select("count(1)").From(t).Where(cond).ToSql()
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
//Logger.Debug("getCount", zap.String("sql", query), zap.Any("args", args))
|
||||
var c int
|
||||
err = storage.MySQLManagerIns.Get(&c, query, args...)
|
||||
return c, err
|
||||
}
|
||||
|
||||
func insert(t string, cols []string, vs []interface{}) (sql.Result, error) {
|
||||
query, args, err := sq.Insert(t).Columns(cols...).Values(vs...).ToSql()
|
||||
Logger.Debug("txInsert", zap.String("sql", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := storage.MySQLManagerIns.Exec(query, args...)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func txInsert(tx *sqlx.Tx, t string, cols []string, vs []interface{}) (sql.Result, error) {
|
||||
query, args, err := sq.Insert(t).Columns(cols...).Values(vs...).ToSql()
|
||||
//Logger.Debug("txInsert", zap.String("sql", query))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result, err := tx.Exec(query, args...)
|
||||
return result, err
|
||||
}
|
||||
|
||||
func getFields(t string, fields []string, cond sq.Eq, dest interface{}) error {
|
||||
query, args, err := sq.Select(fields...).From(t).Where(cond).Limit(1).ToSql()
|
||||
Logger.Debug("getFields", zap.String("sql", query), zap.Any("args", args))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = storage.MySQLManagerIns.Get(dest, query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func getObj(t string, cond sq.Eq, dest interface{}) error {
|
||||
query, args, err := sq.Select("*").From(t).Where(cond).Limit(1).ToSql()
|
||||
Logger.Debug("getObj", zap.String("sql", query), zap.Any("args", args))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = storage.MySQLManagerIns.Get(dest, query, args...)
|
||||
return err
|
||||
}
|
||||
|
||||
func getList(t string, cond sq.Eq, dest interface{}) error {
|
||||
query, args, err := sq.Select("*").From(t).Where(cond).ToSql()
|
||||
Logger.Debug("getList", zap.String("sql", query), zap.Any("args", args))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
err = storage.MySQLManagerIns.Select(dest, query, args...)
|
||||
//Logger.Debug("getList", zap.Error(err))
|
||||
return err
|
||||
}
|
164
battle_srv/models/in_range_player_collection.go
Normal file
164
battle_srv/models/in_range_player_collection.go
Normal file
@@ -0,0 +1,164 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "github.com/logrusorgru/aurora"
|
||||
)
|
||||
|
||||
type InRangePlayerCollection struct {
|
||||
MaxSize int `json:"-"`
|
||||
CurrentSize int `json:"-"`
|
||||
CurrentNodePointer *InRangePlayerNode `json:"-"`
|
||||
InRangePlayerMap map[int32]*InRangePlayerNode `json:"-"`
|
||||
}
|
||||
|
||||
func (p *InRangePlayerCollection) Init(maxSize int) *InRangePlayerCollection {
|
||||
p = &InRangePlayerCollection{
|
||||
MaxSize: maxSize,
|
||||
CurrentSize: 0,
|
||||
CurrentNodePointer: nil,
|
||||
InRangePlayerMap: make(map[int32]*InRangePlayerNode),
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func (p *InRangePlayerCollection) Print() {
|
||||
fmt.Println(Cyan(fmt.Sprintf("{ \n MaxSize: %d, \n CurrentSize: %d, \n }", p.MaxSize, p.CurrentSize)))
|
||||
}
|
||||
|
||||
func (p *InRangePlayerCollection) AppendPlayer(player *Player) *InRangePlayerNode {
|
||||
if nil != p.InRangePlayerMap[player.Id] { //如果该玩家已存在返回nil
|
||||
return nil
|
||||
} else {
|
||||
//p.CurrentSize
|
||||
size := p.CurrentSize + 1
|
||||
if size > p.MaxSize { //超出守护塔的承载范围
|
||||
fmt.Println(Red(fmt.Sprintf("Error: InRangePlayerCollection overflow, MaxSize: %d, NowSize: %d", p.MaxSize, size)))
|
||||
return nil
|
||||
}
|
||||
p.CurrentSize = size
|
||||
|
||||
node := InRangePlayerNode{
|
||||
Prev: nil,
|
||||
Next: nil,
|
||||
player: player,
|
||||
}
|
||||
|
||||
p.InRangePlayerMap[player.Id] = &node
|
||||
|
||||
{ //p.CurrentNodePointer
|
||||
if p.CurrentNodePointer == nil { //刚init好的情况
|
||||
p.CurrentNodePointer = &node
|
||||
} else { //加到最后面相当于循环链表prepend
|
||||
p.CurrentNodePointer.PrependNode(&node)
|
||||
}
|
||||
}
|
||||
|
||||
return &node
|
||||
}
|
||||
}
|
||||
|
||||
func (p *InRangePlayerCollection) RemovePlayerById(playerId int32) {
|
||||
nodePointer := p.InRangePlayerMap[playerId]
|
||||
|
||||
delete(p.InRangePlayerMap, playerId)
|
||||
|
||||
{ //p.CurrentNodePointer
|
||||
if p.CurrentNodePointer == nodePointer { //如果正准备攻击这个玩家,将指针移动到Next
|
||||
p.CurrentNodePointer = nodePointer.Next
|
||||
}
|
||||
}
|
||||
|
||||
//Remove from the linked list
|
||||
nodePointer.RemoveFromLink()
|
||||
|
||||
p.CurrentSize = p.CurrentSize - 1
|
||||
}
|
||||
|
||||
func (p *InRangePlayerCollection) NextPlayerToAttack() *InRangePlayerNode {
|
||||
if p.CurrentNodePointer.Next != nil {
|
||||
p.CurrentNodePointer = p.CurrentNodePointer.Next
|
||||
} else {
|
||||
//继续攻击当前玩家
|
||||
}
|
||||
return p.CurrentNodePointer
|
||||
}
|
||||
|
||||
//TODO: 完成重构
|
||||
|
||||
/// Doubly circular linked list Implement
|
||||
type InRangePlayerNode struct {
|
||||
Prev *InRangePlayerNode
|
||||
Next *InRangePlayerNode
|
||||
player *Player
|
||||
}
|
||||
|
||||
func (node *InRangePlayerNode) AppendNode(newNode *InRangePlayerNode) *InRangePlayerNode {
|
||||
if node == nil {
|
||||
return newNode
|
||||
} else if node.Next == nil && node.Prev == nil {
|
||||
node.Prev = newNode
|
||||
node.Next = newNode
|
||||
newNode.Prev = node
|
||||
newNode.Next = node
|
||||
return node
|
||||
} else {
|
||||
oldNext := node.Next
|
||||
node.Next = newNode
|
||||
newNode.Next = oldNext
|
||||
oldNext.Prev = newNode
|
||||
newNode.Prev = node
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
func (node *InRangePlayerNode) PrependNode(newNode *InRangePlayerNode) *InRangePlayerNode {
|
||||
if node == nil { //没有节点的情况
|
||||
return newNode
|
||||
} else if node.Next == nil && node.Prev == nil { //单个节点的情况
|
||||
node.Prev = newNode
|
||||
node.Next = newNode
|
||||
newNode.Prev = node
|
||||
newNode.Next = node
|
||||
return node
|
||||
} else {
|
||||
oldPrev := node.Prev
|
||||
node.Prev = newNode
|
||||
newNode.Prev = oldPrev
|
||||
oldPrev.Next = newNode
|
||||
newNode.Next = node
|
||||
return node
|
||||
}
|
||||
}
|
||||
|
||||
func (node *InRangePlayerNode) RemoveFromLink() {
|
||||
if node == nil {
|
||||
return
|
||||
} else if node.Next == nil && node.Prev == nil {
|
||||
node = nil //Wait for GC
|
||||
} else {
|
||||
prev := node.Prev
|
||||
next := node.Next
|
||||
prev.Next = next
|
||||
next.Prev = prev
|
||||
node = nil
|
||||
}
|
||||
}
|
||||
|
||||
func (node *InRangePlayerNode) Print() {
|
||||
if node == nil {
|
||||
fmt.Println("No player in range")
|
||||
} else if node.Next == nil && node.Prev == nil {
|
||||
fmt.Println(Red(node.player.Id))
|
||||
} else {
|
||||
now := node.Next
|
||||
fmt.Printf("%d ", Red(node.player.Id))
|
||||
for node != now {
|
||||
fmt.Printf("%d ", Green(now.player.Id))
|
||||
now = now.Next
|
||||
}
|
||||
fmt.Println("")
|
||||
}
|
||||
}
|
||||
|
||||
/// End Doubly circular linked list Implement
|
132
battle_srv/models/math.go
Normal file
132
battle_srv/models/math.go
Normal file
@@ -0,0 +1,132 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"github.com/ByteArena/box2d"
|
||||
"math"
|
||||
)
|
||||
|
||||
// Use type `float64` for json unmarshalling of numbers.
|
||||
type Direction struct {
|
||||
Dx int32 `json:"dx,omitempty"`
|
||||
Dy int32 `json:"dy,omitempty"`
|
||||
}
|
||||
|
||||
type Vec2D struct {
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
}
|
||||
|
||||
func CreateVec2DFromB2Vec2(b2V2 box2d.B2Vec2) *Vec2D {
|
||||
return &Vec2D{
|
||||
X: b2V2.X,
|
||||
Y: b2V2.Y,
|
||||
}
|
||||
}
|
||||
|
||||
func (v2 *Vec2D) ToB2Vec2() box2d.B2Vec2 {
|
||||
return box2d.MakeB2Vec2(v2.X, v2.Y)
|
||||
}
|
||||
|
||||
type Polygon2D struct {
|
||||
Anchor *Vec2D `json:"-"` // This "Polygon2D.Anchor" is used to be assigned to "B2BodyDef.Position", which in turn is used as the position of the FIRST POINT of the polygon.
|
||||
Points []*Vec2D `json:"-"`
|
||||
|
||||
/*
|
||||
When used to represent a "polyline directly drawn in a `Tmx file`", we can initialize both "Anchor" and "Points" simultaneously.
|
||||
|
||||
Yet when used to represent a "polyline drawn in a `Tsx file`", we have to first initialize "Points w.r.t. center of the tile-rectangle", and then "Anchor(initially nil) of the tile positioned in the `Tmx file`".
|
||||
|
||||
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for more information.
|
||||
*/
|
||||
|
||||
/*
|
||||
[WARNING] Used to cache "`TileWidth & TileHeight` of a Tsx file" only.
|
||||
*/
|
||||
TileWidth int
|
||||
TileHeight int
|
||||
|
||||
/*
|
||||
[WARNING] Used to cache "`Width & TileHeight` of an object in Tmx file" only.
|
||||
*/
|
||||
TmxObjectWidth float64
|
||||
TmxObjectHeight float64
|
||||
}
|
||||
|
||||
func MoveDynamicBody(body *box2d.B2Body, pToTargetPos *box2d.B2Vec2, inSeconds float64) {
|
||||
if body.GetType() != box2d.B2BodyType.B2_dynamicBody {
|
||||
return
|
||||
}
|
||||
body.SetTransform(*pToTargetPos, 0.0)
|
||||
body.SetLinearVelocity(box2d.MakeB2Vec2(0.0, 0.0))
|
||||
body.SetAngularVelocity(0.0)
|
||||
}
|
||||
|
||||
func PrettyPrintFixture(fix *box2d.B2Fixture) {
|
||||
fmt.Printf("\t\tfriction:\t%v\n", fix.M_friction)
|
||||
fmt.Printf("\t\trestitution:\t%v\n", fix.M_restitution)
|
||||
fmt.Printf("\t\tdensity:\t%v\n", fix.M_density)
|
||||
fmt.Printf("\t\tisSensor:\t%v\n", fix.M_isSensor)
|
||||
fmt.Printf("\t\tfilter.categoryBits:\t%d\n", fix.M_filter.CategoryBits)
|
||||
fmt.Printf("\t\tfilter.maskBits:\t%d\n", fix.M_filter.MaskBits)
|
||||
fmt.Printf("\t\tfilter.groupIndex:\t%d\n", fix.M_filter.GroupIndex)
|
||||
|
||||
switch fix.M_shape.GetType() {
|
||||
case box2d.B2Shape_Type.E_circle:
|
||||
{
|
||||
s := fix.M_shape.(*box2d.B2CircleShape)
|
||||
fmt.Printf("\t\tb2CircleShape shape: {\n")
|
||||
fmt.Printf("\t\t\tradius:\t%v\n", s.M_radius)
|
||||
fmt.Printf("\t\t\toffset:\t%v\n", s.M_p)
|
||||
fmt.Printf("\t\t}\n")
|
||||
}
|
||||
break
|
||||
|
||||
case box2d.B2Shape_Type.E_polygon:
|
||||
{
|
||||
s := fix.M_shape.(*box2d.B2PolygonShape)
|
||||
fmt.Printf("\t\tb2PolygonShape shape: {\n")
|
||||
for i := 0; i < s.M_count; i++ {
|
||||
fmt.Printf("\t\t\t%v\n", s.M_vertices[i])
|
||||
}
|
||||
fmt.Printf("\t\t}\n")
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
func PrettyPrintBody(body *box2d.B2Body) {
|
||||
bodyIndex := body.M_islandIndex
|
||||
|
||||
fmt.Printf("{\n")
|
||||
fmt.Printf("\tHeapRAM addr:\t%p\n", body)
|
||||
fmt.Printf("\ttype:\t%d\n", body.M_type)
|
||||
fmt.Printf("\tposition:\t%v\n", body.GetPosition())
|
||||
fmt.Printf("\tangle:\t%v\n", body.M_sweep.A)
|
||||
fmt.Printf("\tlinearVelocity:\t%v\n", body.GetLinearVelocity())
|
||||
fmt.Printf("\tangularVelocity:\t%v\n", body.GetAngularVelocity())
|
||||
fmt.Printf("\tlinearDamping:\t%v\n", body.M_linearDamping)
|
||||
fmt.Printf("\tangularDamping:\t%v\n", body.M_angularDamping)
|
||||
fmt.Printf("\tallowSleep:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_autoSleepFlag)
|
||||
fmt.Printf("\tawake:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_awakeFlag)
|
||||
fmt.Printf("\tfixedRotation:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_fixedRotationFlag)
|
||||
fmt.Printf("\tbullet:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_bulletFlag)
|
||||
fmt.Printf("\tactive:\t%d\n", body.M_flags&box2d.B2Body_Flags.E_activeFlag)
|
||||
fmt.Printf("\tgravityScale:\t%v\n", body.M_gravityScale)
|
||||
fmt.Printf("\tislandIndex:\t%v\n", bodyIndex)
|
||||
fmt.Printf("\tfixtures: {\n")
|
||||
for f := body.M_fixtureList; f != nil; f = f.M_next {
|
||||
PrettyPrintFixture(f)
|
||||
}
|
||||
fmt.Printf("\t}\n")
|
||||
fmt.Printf("}\n")
|
||||
}
|
||||
|
||||
func Distance(pt1 *Vec2D, pt2 *Vec2D) float64 {
|
||||
dx := pt1.X - pt2.X
|
||||
dy := pt1.Y - pt2.Y
|
||||
return math.Sqrt(dx*dx + dy*dy)
|
||||
}
|
203
battle_srv/models/pb_type_convert.go
Normal file
203
battle_srv/models/pb_type_convert.go
Normal file
@@ -0,0 +1,203 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
pb "server/pb_output"
|
||||
)
|
||||
|
||||
func toPbVec2D(modelInstance *Vec2D) *pb.Vec2D {
|
||||
toRet := &pb.Vec2D{
|
||||
X: modelInstance.X,
|
||||
Y: modelInstance.Y,
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPolygon2D(modelInstance *Polygon2D) *pb.Polygon2D {
|
||||
toRet := &pb.Polygon2D{
|
||||
Anchor: toPbVec2D(modelInstance.Anchor),
|
||||
Points: make([]*pb.Vec2D, len(modelInstance.Points)),
|
||||
}
|
||||
for index, p := range modelInstance.Points {
|
||||
toRet.Points[index] = toPbVec2D(p)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbVec2DList(modelInstance *Vec2DList) *pb.Vec2DList {
|
||||
toRet := &pb.Vec2DList{
|
||||
Vec2DList: make([]*pb.Vec2D, len(*modelInstance)),
|
||||
}
|
||||
for k, v := range *modelInstance {
|
||||
toRet.Vec2DList[k] = toPbVec2D(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPolygon2DList(modelInstance *Polygon2DList) *pb.Polygon2DList {
|
||||
toRet := &pb.Polygon2DList{
|
||||
Polygon2DList: make([]*pb.Polygon2D, len(*modelInstance)),
|
||||
}
|
||||
for k, v := range *modelInstance {
|
||||
toRet.Polygon2DList[k] = toPbPolygon2D(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func ToPbStrToBattleColliderInfo(intervalToPing int32, willKickIfInactiveFor int32, boundRoomId int32, stageName string, modelInstance1 StrToVec2DListMap, modelInstance2 StrToPolygon2DListMap, stageDiscreteW int32, stageDiscreteH int32, stageTileW int32, stageTileH int32) *pb.BattleColliderInfo {
|
||||
toRet := &pb.BattleColliderInfo{
|
||||
IntervalToPing: intervalToPing,
|
||||
WillKickIfInactiveFor: willKickIfInactiveFor,
|
||||
BoundRoomId: boundRoomId,
|
||||
StageName: stageName,
|
||||
StrToVec2DListMap: make(map[string]*pb.Vec2DList, 0),
|
||||
StrToPolygon2DListMap: make(map[string]*pb.Polygon2DList, 0),
|
||||
StageDiscreteW: stageDiscreteW,
|
||||
StageDiscreteH: stageDiscreteH,
|
||||
StageTileW: stageTileW,
|
||||
StageTileH: stageTileH,
|
||||
}
|
||||
for k, v := range modelInstance1 {
|
||||
toRet.StrToVec2DListMap[k] = toPbVec2DList(v)
|
||||
}
|
||||
for k, v := range modelInstance2 {
|
||||
toRet.StrToPolygon2DListMap[k] = toPbPolygon2DList(v)
|
||||
}
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbPlayers(modelInstances map[int32]*Player) map[int32]*pb.Player {
|
||||
toRet := make(map[int32]*pb.Player, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.Player{
|
||||
Id: last.Id,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Dir: &pb.Direction{
|
||||
Dx: last.Dir.Dx,
|
||||
Dy: last.Dir.Dy,
|
||||
},
|
||||
Speed: last.Speed,
|
||||
BattleState: last.BattleState,
|
||||
Score: last.Score,
|
||||
Removed: last.Removed,
|
||||
JoinIndex: last.JoinIndex,
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbTreasures(modelInstances map[int32]*Treasure) map[int32]*pb.Treasure {
|
||||
toRet := make(map[int32]*pb.Treasure, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.Treasure{
|
||||
Id: last.Id,
|
||||
LocalIdInBattle: last.LocalIdInBattle,
|
||||
Score: last.Score,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Removed: last.Removed,
|
||||
Type: last.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbTraps(modelInstances map[int32]*Trap) map[int32]*pb.Trap {
|
||||
toRet := make(map[int32]*pb.Trap, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.Trap{
|
||||
Id: last.Id,
|
||||
LocalIdInBattle: last.LocalIdInBattle,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Removed: last.Removed,
|
||||
Type: last.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbBullets(modelInstances map[int32]*Bullet) map[int32]*pb.Bullet {
|
||||
toRet := make(map[int32]*pb.Bullet, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
if nil == last.StartAtPoint || nil == last.EndAtPoint {
|
||||
continue
|
||||
}
|
||||
toRet[k] = &pb.Bullet{
|
||||
LocalIdInBattle: last.LocalIdInBattle,
|
||||
LinearSpeed: last.LinearSpeed,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Removed: last.Removed,
|
||||
StartAtPoint: &pb.Vec2D{
|
||||
X: last.StartAtPoint.X,
|
||||
Y: last.StartAtPoint.Y,
|
||||
},
|
||||
EndAtPoint: &pb.Vec2D{
|
||||
X: last.EndAtPoint.X,
|
||||
Y: last.EndAtPoint.Y,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbSpeedShoes(modelInstances map[int32]*SpeedShoe) map[int32]*pb.SpeedShoe {
|
||||
toRet := make(map[int32]*pb.SpeedShoe, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.SpeedShoe{
|
||||
Id: last.Id,
|
||||
LocalIdInBattle: last.LocalIdInBattle,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Removed: last.Removed,
|
||||
Type: last.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
||||
|
||||
func toPbGuardTowers(modelInstances map[int32]*GuardTower) map[int32]*pb.GuardTower {
|
||||
toRet := make(map[int32]*pb.GuardTower, 0)
|
||||
if nil == modelInstances {
|
||||
return toRet
|
||||
}
|
||||
|
||||
for k, last := range modelInstances {
|
||||
toRet[k] = &pb.GuardTower{
|
||||
Id: last.Id,
|
||||
LocalIdInBattle: last.LocalIdInBattle,
|
||||
X: last.X,
|
||||
Y: last.Y,
|
||||
Removed: last.Removed,
|
||||
Type: last.Type,
|
||||
}
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
142
battle_srv/models/player.go
Normal file
142
battle_srv/models/player.go
Normal file
@@ -0,0 +1,142 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"fmt"
|
||||
"github.com/ByteArena/box2d"
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type PlayerBattleState struct {
|
||||
ADDED_PENDING_BATTLE_COLLIDER_ACK int32
|
||||
READDED_PENDING_BATTLE_COLLIDER_ACK int32
|
||||
ACTIVE int32
|
||||
DISCONNECTED int32
|
||||
LOST int32
|
||||
EXPELLED_DURING_GAME int32
|
||||
EXPELLED_IN_DISMISSAL int32
|
||||
}
|
||||
|
||||
var PlayerBattleStateIns PlayerBattleState
|
||||
|
||||
func InitPlayerBattleStateIns() {
|
||||
PlayerBattleStateIns = PlayerBattleState{
|
||||
ADDED_PENDING_BATTLE_COLLIDER_ACK: 0,
|
||||
READDED_PENDING_BATTLE_COLLIDER_ACK: 1,
|
||||
ACTIVE: 2,
|
||||
DISCONNECTED: 3,
|
||||
LOST: 4,
|
||||
EXPELLED_DURING_GAME: 5,
|
||||
EXPELLED_IN_DISMISSAL: 6,
|
||||
}
|
||||
}
|
||||
|
||||
type Player struct {
|
||||
Id int32 `json:"id,omitempty" db:"id"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Dir *Direction `json:"dir,omitempty"`
|
||||
Speed int32 `json:"speed,omitempty"`
|
||||
BattleState int32 `json:"battleState,omitempty"`
|
||||
LastMoveGmtMillis int32 `json:"lastMoveGmtMillis,omitempty"`
|
||||
Score int32 `json:"score,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
JoinIndex int32
|
||||
|
||||
Name string `json:"name,omitempty" db:"name"`
|
||||
DisplayName string `json:"displayName,omitempty" db:"display_name"`
|
||||
Avatar string `json:"avatar,omitempty"`
|
||||
|
||||
FrozenAtGmtMillis int64 `json:"-" db:"-"`
|
||||
AddSpeedAtGmtMillis int64 `json:"-" db:"-"`
|
||||
CreatedAt int64 `json:"-" db:"created_at"`
|
||||
UpdatedAt int64 `json:"-" db:"updated_at"`
|
||||
DeletedAt NullInt64 `json:"-" db:"deleted_at"`
|
||||
TutorialStage int `json:"-" db:"tutorial_stage"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
AckingFrameId int32 `json:"ackingFrameId"`
|
||||
AckingInputFrameId int32 `json:"-"`
|
||||
LastSentInputFrameId int32 `json:"-"`
|
||||
}
|
||||
|
||||
func ExistPlayerByName(name string) (bool, error) {
|
||||
return exist("player", sq.Eq{"name": name, "deleted_at": nil})
|
||||
}
|
||||
|
||||
func GetPlayerByName(name string) (*Player, error) {
|
||||
return getPlayer(sq.Eq{"name": name, "deleted_at": nil})
|
||||
}
|
||||
|
||||
func GetPlayerById(id int) (*Player, error) {
|
||||
return getPlayer(sq.Eq{"id": id, "deleted_at": nil})
|
||||
}
|
||||
|
||||
func getPlayer(cond sq.Eq) (*Player, error) {
|
||||
var p Player
|
||||
err := getObj("player", cond, &p)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
p.Dir = &Direction{
|
||||
Dx: 0,
|
||||
Dy: 0,
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func (p *Player) Insert(tx *sqlx.Tx) error {
|
||||
result, err := txInsert(tx, "player", []string{"name", "display_name", "created_at", "updated_at", "avatar"},
|
||||
[]interface{}{p.Name, p.DisplayName, p.CreatedAt, p.UpdatedAt, p.Avatar})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.Id = int32(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func Update(tx *sqlx.Tx, id int32, p *Player) (bool, error) {
|
||||
query, args, err := sq.Update("player").
|
||||
Set("display_name", p.DisplayName).
|
||||
Set("avatar", p.Avatar).
|
||||
Where(sq.Eq{"id": id}).ToSql()
|
||||
|
||||
fmt.Println(query)
|
||||
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
fmt.Println("ERRRRRRR: ")
|
||||
fmt.Println(err)
|
||||
return false, err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return rowsAffected >= 1, nil
|
||||
}
|
||||
|
||||
func UpdatePlayerTutorialStage(tx *sqlx.Tx, id int) (bool, error) {
|
||||
query, args, err := sq.Update("player").
|
||||
Set("tutorial_stage", 1).
|
||||
Where(sq.Eq{"tutorial_stage": 0, "id": id}).ToSql()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
result, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return rowsAffected >= 1, nil
|
||||
}
|
35
battle_srv/models/player_auth_binding.go
Normal file
35
battle_srv/models/player_auth_binding.go
Normal file
@@ -0,0 +1,35 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
)
|
||||
|
||||
type PlayerAuthBinding struct {
|
||||
Channel int `db:"channel"`
|
||||
CreatedAt int64 `db:"created_at"`
|
||||
DeletedAt NullInt64 `db:"deleted_at"`
|
||||
ExtAuthID string `db:"ext_auth_id"`
|
||||
PlayerID int `db:"player_id"`
|
||||
UpdatedAt int64 `db:"updated_at"`
|
||||
}
|
||||
|
||||
func (p *PlayerAuthBinding) Insert(tx *sqlx.Tx) error {
|
||||
_, err := txInsert(tx, "player_auth_binding", []string{"channel", "created_at", "ext_auth_id",
|
||||
"player_id", "updated_at"},
|
||||
[]interface{}{p.Channel, p.CreatedAt, p.ExtAuthID, p.PlayerID, p.UpdatedAt})
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPlayerAuthBinding(channel int, extAuthID string) (*PlayerAuthBinding, error) {
|
||||
var p PlayerAuthBinding
|
||||
err := getObj("player_auth_binding",
|
||||
sq.Eq{"channel": channel, "ext_auth_id": extAuthID, "deleted_at": nil},
|
||||
&p)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &p, nil
|
||||
}
|
110
battle_srv/models/player_login.go
Normal file
110
battle_srv/models/player_login.go
Normal file
@@ -0,0 +1,110 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
"server/storage"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
)
|
||||
|
||||
type PlayerLogin struct {
|
||||
CreatedAt int64 `db:"created_at"`
|
||||
DeletedAt NullInt64 `db:"deleted_at"`
|
||||
DisplayName NullString `db:"display_name"`
|
||||
Avatar string `db:"avatar"`
|
||||
FromPublicIP NullString `db:"from_public_ip"`
|
||||
ID int `db:"id"`
|
||||
IntAuthToken string `db:"int_auth_token"`
|
||||
PlayerID int `db:"player_id"`
|
||||
UpdatedAt int64 `db:"updated_at"`
|
||||
}
|
||||
|
||||
func (p *PlayerLogin) Insert() error {
|
||||
result, err := insert("player_login", []string{"created_at", "display_name",
|
||||
"from_public_ip", "int_auth_token", "player_id", "updated_at", "avatar"},
|
||||
[]interface{}{p.CreatedAt, p.DisplayName, p.FromPublicIP, p.IntAuthToken,
|
||||
p.PlayerID, p.UpdatedAt, p.Avatar})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
id, err := result.LastInsertId()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
p.ID = int(id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func GetPlayerLoginByToken(token string) (*PlayerLogin, error) {
|
||||
var p PlayerLogin
|
||||
err := getObj("player_login",
|
||||
sq.Eq{"int_auth_token": token, "deleted_at": nil},
|
||||
&p)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func GetPlayerLoginByPlayerId(playerId int) (*PlayerLogin, error) {
|
||||
var p PlayerLogin
|
||||
err := getObj("player_login",
|
||||
sq.Eq{"player_id": playerId, "deleted_at": nil},
|
||||
&p)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func GetPlayerIdByToken(token string) (int, error) {
|
||||
var p PlayerLogin
|
||||
err := getFields("player_login", []string{"player_id"},
|
||||
sq.Eq{"int_auth_token": token, "deleted_at": nil},
|
||||
&p)
|
||||
if err == sql.ErrNoRows {
|
||||
return 0, nil
|
||||
}
|
||||
return p.PlayerID, nil
|
||||
}
|
||||
|
||||
// TODO 封装到helper
|
||||
func DelPlayerLoginByToken(token string) error {
|
||||
query, args, err := sq.Update("player_login").Set("deleted_at", utils.UnixtimeMilli()).
|
||||
Where(sq.Eq{"int_auth_token": token}).ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
//Logger.Debug(query, args)
|
||||
_, err = storage.MySQLManagerIns.Exec(query, args...)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func EnsuredPlayerLoginByToken(id int, token string) (bool, error) {
|
||||
return exist("player_login", sq.Eq{"int_auth_token": token, "deleted_at": nil, "player_id": id})
|
||||
}
|
||||
|
||||
func EnsuredPlayerLoginById(id int) (bool, error) {
|
||||
return exist("player_login", sq.Eq{"player_id": id, "deleted_at": nil})
|
||||
}
|
||||
|
||||
func CleanExpiredPlayerLoginToken() error {
|
||||
now := utils.UnixtimeMilli()
|
||||
max := now - int64(Constants.Player.IntAuthTokenTTLSeconds*1000)
|
||||
|
||||
query, args, err := sq.Update("player_login").Set("deleted_at", now).
|
||||
Where(sq.LtOrEq{"created_at": max}).ToSql()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = storage.MySQLManagerIns.Exec(query, args...)
|
||||
return err
|
||||
}
|
129
battle_srv/models/player_wallet.go
Normal file
129
battle_srv/models/player_wallet.go
Normal file
@@ -0,0 +1,129 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"errors"
|
||||
. "server/common"
|
||||
"server/common/utils"
|
||||
|
||||
sq "github.com/Masterminds/squirrel"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PlayerWallet struct {
|
||||
CreatedAt int64 `json:"-" db:"created_at"`
|
||||
DeletedAt NullInt64 `json:"-" db:"deleted_at"`
|
||||
Gem int `json:"gem" db:"gem"`
|
||||
ID int `json:"-" db:"id"`
|
||||
UpdatedAt int64 `json:"-" db:"updated_at"`
|
||||
}
|
||||
|
||||
func (p *PlayerWallet) Insert(tx *sqlx.Tx) error {
|
||||
_, err := txInsert(tx, "player_wallet", []string{"id", "created_at", "updated_at"},
|
||||
[]interface{}{p.ID, p.CreatedAt, p.UpdatedAt})
|
||||
return err
|
||||
}
|
||||
|
||||
func GetPlayerWalletById(id int) (*PlayerWallet, error) {
|
||||
var p PlayerWallet
|
||||
err := getObj("player_wallet", sq.Eq{"id": id, "deleted_at": nil}, &p)
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return &p, nil
|
||||
}
|
||||
|
||||
func CostPlayerWallet(tx *sqlx.Tx, id int, currency int, val int) (int, error) {
|
||||
var column string
|
||||
switch currency {
|
||||
case Constants.Player.Diamond:
|
||||
column = "diamond"
|
||||
case Constants.Player.Energy:
|
||||
column = "energy"
|
||||
case Constants.Player.Gold:
|
||||
column = "gold"
|
||||
}
|
||||
if column == "" {
|
||||
Logger.Debug("CostPlayerWallet Error Currency",
|
||||
zap.Int("currency", currency), zap.Int("val", val))
|
||||
return Constants.RetCode.MysqlError, errors.New("error currency")
|
||||
}
|
||||
|
||||
now := utils.UnixtimeMilli()
|
||||
query, args, err := sq.Update("player_wallet").
|
||||
Set(column, sq.Expr(column+"-?", val)).Set("updated_at", now).
|
||||
Where(sq.Eq{"id": id, "deleted_at": nil}).
|
||||
Where(sq.GtOrEq{column: val}).ToSql()
|
||||
|
||||
Logger.Debug("CostPlayerWallet", zap.String("sql", query), zap.Any("args", args))
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
result, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
ok := rowsAffected >= 1
|
||||
Logger.Debug("CostPlayerWallet", zap.Int64("rowsAffected", rowsAffected),
|
||||
zap.Bool("cost", ok))
|
||||
if !ok {
|
||||
var ret int
|
||||
switch currency {
|
||||
case Constants.Player.Diamond:
|
||||
ret = Constants.RetCode.LackOfDiamond
|
||||
case Constants.Player.Energy:
|
||||
ret = Constants.RetCode.LackOfEnergy
|
||||
case Constants.Player.Gold:
|
||||
ret = Constants.RetCode.LackOfGold
|
||||
}
|
||||
return ret, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func AddPlayerWallet(tx *sqlx.Tx, id int, currency int, val int) (int, error) {
|
||||
var column string
|
||||
switch currency {
|
||||
case Constants.Player.Diamond:
|
||||
column = "diamond"
|
||||
case Constants.Player.Energy:
|
||||
column = "energy"
|
||||
case Constants.Player.Gold:
|
||||
column = "gold"
|
||||
}
|
||||
if column == "" {
|
||||
Logger.Debug("CostPlayerWallet Error Currency",
|
||||
zap.Int("currency", currency), zap.Int("val", val))
|
||||
return Constants.RetCode.MysqlError, errors.New("error currency")
|
||||
}
|
||||
|
||||
now := utils.UnixtimeMilli()
|
||||
query, args, err := sq.Update("player_wallet").
|
||||
Set(column, sq.Expr(column+"+?", val)).Set("updated_at", now).
|
||||
Where(sq.Eq{"id": id, "deleted_at": nil}).ToSql()
|
||||
|
||||
Logger.Debug("AddPlayerWallet", zap.String("sql", query), zap.Any("args", args))
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
result, err := tx.Exec(query, args...)
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
rowsAffected, err := result.RowsAffected()
|
||||
if err != nil {
|
||||
return Constants.RetCode.MysqlError, err
|
||||
}
|
||||
ok := rowsAffected >= 1
|
||||
Logger.Debug("AddPlayerWallet", zap.Int64("rowsAffected", rowsAffected),
|
||||
zap.Bool("add", ok))
|
||||
if !ok {
|
||||
return Constants.RetCode.UnknownError, nil
|
||||
}
|
||||
return 0, nil
|
||||
}
|
14
battle_srv/models/pumpkin.go
Normal file
14
battle_srv/models/pumpkin.go
Normal file
@@ -0,0 +1,14 @@
|
||||
package models
|
||||
|
||||
import "github.com/ByteArena/box2d"
|
||||
|
||||
type Pumpkin struct {
|
||||
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
|
||||
LinearSpeed float64 `json:"linearSpeed,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
Dir *Direction `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
RemovedAtFrameId int32 `json:"-"`
|
||||
}
|
73
battle_srv/models/ringbuf.go
Normal file
73
battle_srv/models/ringbuf.go
Normal file
@@ -0,0 +1,73 @@
|
||||
package models
|
||||
|
||||
type RingBuffer struct {
|
||||
Ed int32 // write index
|
||||
St int32 // read index
|
||||
EdFrameId int32
|
||||
StFrameId int32
|
||||
N int32
|
||||
Cnt int32 // the count of valid elements in the buffer, used mainly to distinguish what "st == ed" means for "Pop" and "Get" methods
|
||||
Eles []interface{}
|
||||
}
|
||||
|
||||
func NewRingBuffer(n int32) *RingBuffer {
|
||||
return &RingBuffer{
|
||||
Ed: 0,
|
||||
St: 0,
|
||||
N: n,
|
||||
Cnt: 0,
|
||||
Eles: make([]interface{}, n),
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) Put(pItem interface{}) {
|
||||
rb.Eles[rb.Ed] = pItem
|
||||
rb.EdFrameId++
|
||||
rb.Cnt++
|
||||
rb.Ed++
|
||||
if rb.Ed >= rb.N {
|
||||
rb.Ed -= rb.N // Deliberately not using "%" operator for performance concern
|
||||
}
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) Pop() interface{} {
|
||||
if 0 == rb.Cnt {
|
||||
return nil
|
||||
}
|
||||
pItem := rb.Eles[rb.St]
|
||||
rb.StFrameId++
|
||||
rb.Cnt--
|
||||
rb.St++
|
||||
if rb.St >= rb.N {
|
||||
rb.St -= rb.N
|
||||
}
|
||||
return pItem
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) GetByOffset(offsetFromSt int32) interface{} {
|
||||
if 0 == rb.Cnt {
|
||||
return nil
|
||||
}
|
||||
arrIdx := rb.St + offsetFromSt
|
||||
if rb.St < rb.Ed {
|
||||
// case#1: 0...st...ed...N-1
|
||||
if rb.St <= arrIdx && arrIdx < rb.Ed {
|
||||
return rb.Eles[arrIdx]
|
||||
}
|
||||
} else {
|
||||
// if rb.St >= rb.Ed
|
||||
// case#2: 0...ed...st...N-1
|
||||
if arrIdx >= rb.N {
|
||||
arrIdx -= rb.N
|
||||
}
|
||||
if arrIdx >= rb.St || arrIdx < rb.Ed {
|
||||
return rb.Eles[arrIdx]
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (rb *RingBuffer) GetByFrameId(frameId int32) interface{} {
|
||||
return rb.GetByOffset(frameId - rb.StFrameId)
|
||||
}
|
1450
battle_srv/models/room.go
Normal file
1450
battle_srv/models/room.go
Normal file
File diff suppressed because it is too large
Load Diff
138
battle_srv/models/room_heap_manager.go
Normal file
138
battle_srv/models/room_heap_manager.go
Normal file
@@ -0,0 +1,138 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"github.com/gorilla/websocket"
|
||||
"go.uber.org/zap"
|
||||
. "server/common"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// Reference https://github.com/genxium/GoStructPrac.
|
||||
type RoomHeap []*Room
|
||||
type RoomMap map[int32]*Room
|
||||
|
||||
var (
|
||||
// NOTE: For the package exported instances of non-primitive types to be accessed as singletons, they must be of pointer types.
|
||||
RoomHeapMux *sync.Mutex
|
||||
RoomHeapManagerIns *RoomHeap
|
||||
RoomMapManagerIns *RoomMap
|
||||
)
|
||||
|
||||
func (pPq *RoomHeap) PrintInOrder() {
|
||||
pq := *pPq
|
||||
fmt.Printf("The RoomHeap instance now contains:\n")
|
||||
for i := 0; i < len(pq); i++ {
|
||||
fmt.Printf("{index: %d, roomID: %d, score: %.2f} ", i, pq[i].Id, pq[i].Score)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func (pq RoomHeap) Len() int { return len(pq) }
|
||||
|
||||
func (pq RoomHeap) Less(i, j int) bool {
|
||||
return pq[i].Score > pq[j].Score
|
||||
}
|
||||
|
||||
func (pq *RoomHeap) Swap(i, j int) {
|
||||
(*pq)[i], (*pq)[j] = (*pq)[j], (*pq)[i]
|
||||
(*pq)[i].Index = i
|
||||
(*pq)[j].Index = j
|
||||
}
|
||||
|
||||
func (pq *RoomHeap) Push(pItem interface{}) {
|
||||
// NOTE: Must take input param type `*Room` here.
|
||||
n := len(*pq)
|
||||
pItem.(*Room).Index = n
|
||||
*pq = append(*pq, pItem.(*Room))
|
||||
}
|
||||
|
||||
func (pq *RoomHeap) Pop() interface{} {
|
||||
old := *pq
|
||||
n := len(old)
|
||||
if n == 0 {
|
||||
return nil
|
||||
}
|
||||
pItem := old[n-1]
|
||||
if pItem.Score <= float32(0.0) {
|
||||
return nil
|
||||
}
|
||||
pItem.Index = -1 // for safety
|
||||
*pq = old[0 : n-1]
|
||||
// NOTE: Must return instance which is directly castable to type `*Room` here.
|
||||
return pItem
|
||||
}
|
||||
|
||||
func (pq *RoomHeap) update(pItem *Room, Score float32) {
|
||||
// NOTE: Must use type `*Room` here.
|
||||
heap.Fix(pq, pItem.Index)
|
||||
}
|
||||
|
||||
func (pq *RoomHeap) Update(pItem *Room, Score float32) {
|
||||
pq.update(pItem, Score)
|
||||
}
|
||||
|
||||
func PrintRoomMap() {
|
||||
fmt.Printf("The RoomMap instance now contains:\n")
|
||||
for _, pR := range *RoomMapManagerIns {
|
||||
fmt.Printf("{roomID: %d, score: %.2f} ", pR.Id, pR.Score)
|
||||
}
|
||||
fmt.Printf("\n")
|
||||
}
|
||||
|
||||
func InitRoomHeapManager() {
|
||||
RoomHeapMux = new(sync.Mutex)
|
||||
// Init "pseudo class constants".
|
||||
InitRoomBattleStateIns()
|
||||
InitPlayerBattleStateIns()
|
||||
initialCountOfRooms := 32
|
||||
pq := make(RoomHeap, initialCountOfRooms)
|
||||
roomMap := make(RoomMap, initialCountOfRooms)
|
||||
|
||||
for i := 0; i < initialCountOfRooms; i++ {
|
||||
roomCapacity := 2
|
||||
joinIndexBooleanArr := make([]bool, roomCapacity)
|
||||
for index, _ := range joinIndexBooleanArr {
|
||||
joinIndexBooleanArr[index] = false
|
||||
}
|
||||
currentRoomBattleState := RoomBattleStateIns.IDLE
|
||||
pq[i] = &Room{
|
||||
Id: int32(i + 1),
|
||||
Players: make(map[int32]*Player),
|
||||
PlayerDownsyncSessionDict: make(map[int32]*websocket.Conn),
|
||||
PlayerSignalToCloseDict: make(map[int32]SignalToCloseConnCbType),
|
||||
Capacity: roomCapacity,
|
||||
Score: calRoomScore(0, roomCapacity, currentRoomBattleState),
|
||||
State: currentRoomBattleState,
|
||||
Index: i,
|
||||
Tick: 0,
|
||||
EffectivePlayerCount: 0,
|
||||
//BattleDurationNanos: int64(5 * 1000 * 1000 * 1000),
|
||||
BattleDurationNanos: int64(30 * 1000 * 1000 * 1000),
|
||||
ServerFPS: 60,
|
||||
Treasures: make(map[int32]*Treasure),
|
||||
Traps: make(map[int32]*Trap),
|
||||
GuardTowers: make(map[int32]*GuardTower),
|
||||
Bullets: make(map[int32]*Bullet),
|
||||
SpeedShoes: make(map[int32]*SpeedShoe),
|
||||
Barriers: make(map[int32]*Barrier),
|
||||
Pumpkins: make(map[int32]*Pumpkin),
|
||||
AccumulatedLocalIdForBullets: 0,
|
||||
AllPlayerInputsBuffer: NewRingBuffer(1024),
|
||||
LastAllConfirmedInputFrameId: -1,
|
||||
LastAllConfirmedInputFrameIdWithChange: -1,
|
||||
LastAllConfirmedInputList: make([]uint64, roomCapacity),
|
||||
InputDelayFrames: 4,
|
||||
InputScaleFrames: 2,
|
||||
JoinIndexBooleanArr: joinIndexBooleanArr,
|
||||
}
|
||||
roomMap[pq[i].Id] = pq[i]
|
||||
pq[i].ChooseStage()
|
||||
}
|
||||
heap.Init(&pq)
|
||||
RoomHeapManagerIns = &pq
|
||||
RoomMapManagerIns = &roomMap
|
||||
Logger.Info("The RoomHeapManagerIns has been initialized:", zap.Any("addr", fmt.Sprintf("%p", RoomHeapManagerIns)), zap.Any("size", len(*RoomHeapManagerIns)))
|
||||
Logger.Info("The RoomMapManagerIns has been initialized:", zap.Any("size", len(*RoomMapManagerIns)))
|
||||
}
|
17
battle_srv/models/speed_shoe.go
Normal file
17
battle_srv/models/speed_shoe.go
Normal file
@@ -0,0 +1,17 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ByteArena/box2d"
|
||||
)
|
||||
|
||||
type SpeedShoe struct {
|
||||
Id int32 `json:"id,omitempty"`
|
||||
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
Type int32 `json:"type,omitempty"`
|
||||
PickupBoundary *Polygon2D `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
RemovedAtFrameId int32 `json:"-"`
|
||||
}
|
537
battle_srv/models/tiled_map.go
Normal file
537
battle_srv/models/tiled_map.go
Normal file
@@ -0,0 +1,537 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/zlib"
|
||||
"encoding/base64"
|
||||
"encoding/xml"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/ByteArena/box2d"
|
||||
"go.uber.org/zap"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
. "server/common"
|
||||
"strconv"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
LOW_SCORE_TREASURE_TYPE = 1
|
||||
HIGH_SCORE_TREASURE_TYPE = 2
|
||||
|
||||
SPEED_SHOES_TYPE = 3
|
||||
|
||||
LOW_SCORE_TREASURE_SCORE = 100
|
||||
HIGH_SCORE_TREASURE_SCORE = 200
|
||||
|
||||
FLIPPED_HORIZONTALLY_FLAG uint32 = 0x80000000
|
||||
FLIPPED_VERTICALLY_FLAG uint32 = 0x40000000
|
||||
FLIPPED_DIAGONALLY_FLAG uint32 = 0x20000000
|
||||
)
|
||||
|
||||
// For either a "*.tmx" or "*.tsx" file. [begins]
|
||||
type TmxOrTsxProperty struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Value string `xml:"value,attr"`
|
||||
}
|
||||
|
||||
type TmxOrTsxProperties struct {
|
||||
Property []*TmxOrTsxProperty `xml:"property"`
|
||||
}
|
||||
|
||||
type TmxOrTsxPolyline struct {
|
||||
Points string `xml:"points,attr"`
|
||||
}
|
||||
|
||||
type TmxOrTsxObject struct {
|
||||
Id int `xml:"id,attr"`
|
||||
Gid *int `xml:"gid,attr"`
|
||||
X float64 `xml:"x,attr"`
|
||||
Y float64 `xml:"y,attr"`
|
||||
Properties *TmxOrTsxProperties `xml:"properties"`
|
||||
Polyline *TmxOrTsxPolyline `xml:"polyline"`
|
||||
Width *float64 `xml:"width,attr"`
|
||||
Height *float64 `xml:"height,attr"`
|
||||
}
|
||||
|
||||
type TmxOrTsxObjectGroup struct {
|
||||
Draworder string `xml:"draworder,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
Objects []*TmxOrTsxObject `xml:"object"`
|
||||
}
|
||||
|
||||
type TmxOrTsxImage struct {
|
||||
Source string `xml:"source,attr"`
|
||||
Width int `xml:"width,attr"`
|
||||
Height int `xml:"height,attr"`
|
||||
}
|
||||
|
||||
// For either a "*.tmx" or "*.tsx" file. [ends]
|
||||
|
||||
// Within a "*.tsx" file. [begins]
|
||||
type Tsx struct {
|
||||
Name string `xml:"name,attr"`
|
||||
TileWidth int `xml:"tilewidth,attr"`
|
||||
TileHeight int `xml:"tileheight,attr"`
|
||||
TileCount int `xml:"tilecount,attr"`
|
||||
Columns int `xml:"columns,attr"`
|
||||
Image []*TmxOrTsxImage `xml:"image"`
|
||||
Tiles []*TsxTile `xml:"tile"`
|
||||
}
|
||||
|
||||
type TsxTile struct {
|
||||
Id int `xml:"id,attr"`
|
||||
ObjectGroup *TmxOrTsxObjectGroup `xml:"objectgroup"`
|
||||
Properties *TmxOrTsxProperties `xml:"properties"`
|
||||
}
|
||||
|
||||
// Within a "*.tsx" file. [ends]
|
||||
|
||||
// Within a "*.tmx" file. [begins]
|
||||
type TmxLayerDecodedTileData struct {
|
||||
Id uint32
|
||||
Tileset *TmxTileset
|
||||
FlipHorizontal bool
|
||||
FlipVertical bool
|
||||
FlipDiagonal bool
|
||||
}
|
||||
|
||||
type TmxLayerEncodedData struct {
|
||||
Encoding string `xml:"encoding,attr"`
|
||||
Compression string `xml:"compression,attr"`
|
||||
Value string `xml:",chardata"`
|
||||
}
|
||||
|
||||
type TmxLayer struct {
|
||||
Name string `xml:"name,attr"`
|
||||
Width int `xml:"width,attr"`
|
||||
Height int `xml:"height,attr"`
|
||||
Data *TmxLayerEncodedData `xml:"data"`
|
||||
Tile []*TmxLayerDecodedTileData
|
||||
}
|
||||
|
||||
type TmxTileset struct {
|
||||
FirstGid uint32 `xml:"firstgid,attr"`
|
||||
Name string `xml:"name,attr"`
|
||||
TileWidth int `xml:"tilewidth,attr"`
|
||||
TileHeight int `xml:"tileheight,attr"`
|
||||
Images []*TmxOrTsxImage `xml:"image"`
|
||||
Source string `xml:"source,attr"`
|
||||
}
|
||||
|
||||
type TmxMap struct {
|
||||
Version string `xml:"version,attr"`
|
||||
Orientation string `xml:"orientation,attr"`
|
||||
Width int `xml:"width,attr"`
|
||||
Height int `xml:"height,attr"`
|
||||
TileWidth int `xml:"tilewidth,attr"`
|
||||
TileHeight int `xml:"tileheight,attr"`
|
||||
Properties []*TmxOrTsxProperties `xml:"properties"`
|
||||
Tilesets []*TmxTileset `xml:"tileset"`
|
||||
Layers []*TmxLayer `xml:"layer"`
|
||||
ObjectGroups []*TmxOrTsxObjectGroup `xml:"objectgroup"`
|
||||
}
|
||||
|
||||
// Within a "*.tmx" file. [ends]
|
||||
|
||||
func (d *TmxLayerEncodedData) decodeBase64() ([]byte, error) {
|
||||
r := bytes.NewReader([]byte(strings.TrimSpace(d.Value)))
|
||||
decr := base64.NewDecoder(base64.StdEncoding, r)
|
||||
if d.Compression == "zlib" {
|
||||
rclose, err := zlib.NewReader(decr)
|
||||
if err != nil {
|
||||
Logger.Error("tmx data decode zlib error: ", zap.Any("encoding", d.Encoding), zap.Any("compression", d.Compression), zap.Any("value", d.Value))
|
||||
return nil, err
|
||||
}
|
||||
return ioutil.ReadAll(rclose)
|
||||
}
|
||||
Logger.Error("tmx data decode invalid compression: ", zap.Any("encoding", d.Encoding), zap.Any("compression", d.Compression), zap.Any("value", d.Value))
|
||||
return nil, errors.New("Invalid compression.")
|
||||
}
|
||||
|
||||
func (l *TmxLayer) decodeBase64() ([]uint32, error) {
|
||||
databytes, err := l.Data.decodeBase64()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if l.Width == 0 || l.Height == 0 {
|
||||
return nil, errors.New("Zero width or height.")
|
||||
}
|
||||
if len(databytes) != l.Height*l.Width*4 {
|
||||
Logger.Error("TmxLayer decodeBase64 invalid data bytes:", zap.Any("width", l.Width), zap.Any("height", l.Height), zap.Any("data lenght", len(databytes)))
|
||||
return nil, errors.New("Data length error.")
|
||||
}
|
||||
dindex := 0
|
||||
gids := make([]uint32, l.Height*l.Width)
|
||||
for h := 0; h < l.Height; h++ {
|
||||
for w := 0; w < l.Width; w++ {
|
||||
gid := uint32(databytes[dindex]) |
|
||||
uint32(databytes[dindex+1])<<8 |
|
||||
uint32(databytes[dindex+2])<<16 |
|
||||
uint32(databytes[dindex+3])<<24
|
||||
dindex += 4
|
||||
gids[h*l.Width+w] = gid
|
||||
}
|
||||
}
|
||||
return gids, nil
|
||||
}
|
||||
|
||||
type Vec2DList []*Vec2D
|
||||
type Polygon2DList []*Polygon2D
|
||||
type StrToVec2DListMap map[string]*Vec2DList // Note that it's deliberately NOT using "map[string]Vec2DList", for the easy of passing return value to "models/room.go".
|
||||
type StrToPolygon2DListMap map[string]*Polygon2DList // Note that it's deliberately NOT using "map[string]Polygon2DList", for the easy of passing return value to "models/room.go".
|
||||
|
||||
func TmxPolylineToPolygon2DInB2World(pTmxMapIns *TmxMap, singleObjInTmxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline) (*Polygon2D, error) {
|
||||
if nil == targetPolyline {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
singleValueArray := strings.Split(targetPolyline.Points, " ")
|
||||
pointsCount := len(singleValueArray)
|
||||
|
||||
if pointsCount >= box2d.B2_maxPolygonVertices {
|
||||
return nil, errors.New(fmt.Sprintf("During `TmxPolylineToPolygon2DInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v, of polyines [%v]", pointsCount, box2d.B2_maxPolygonVertices, singleValueArray))
|
||||
}
|
||||
|
||||
theUntransformedAnchor := &Vec2D{
|
||||
X: singleObjInTmxFile.X,
|
||||
Y: singleObjInTmxFile.Y,
|
||||
}
|
||||
theTransformedAnchor := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedAnchor)
|
||||
thePolygon2DFromPolyline := &Polygon2D{
|
||||
Anchor: &theTransformedAnchor,
|
||||
Points: make([]*Vec2D, len(singleValueArray)),
|
||||
}
|
||||
|
||||
for k, value := range singleValueArray {
|
||||
thePolygon2DFromPolyline.Points[k] = &Vec2D{}
|
||||
for kk, v := range strings.Split(value, ",") {
|
||||
coordinateValue, err := strconv.ParseFloat(v, 64)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
if 0 == (kk % 2) {
|
||||
thePolygon2DFromPolyline.Points[k].X = (coordinateValue)
|
||||
} else {
|
||||
thePolygon2DFromPolyline.Points[k].Y = (coordinateValue)
|
||||
}
|
||||
}
|
||||
|
||||
// Transform to B2World space coordinate.
|
||||
tmp := &Vec2D{
|
||||
X: thePolygon2DFromPolyline.Points[k].X,
|
||||
Y: thePolygon2DFromPolyline.Points[k].Y,
|
||||
}
|
||||
transformedTmp := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(tmp)
|
||||
thePolygon2DFromPolyline.Points[k].X = transformedTmp.X
|
||||
thePolygon2DFromPolyline.Points[k].Y = transformedTmp.Y
|
||||
}
|
||||
|
||||
return thePolygon2DFromPolyline, nil
|
||||
}
|
||||
|
||||
func TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns *TmxMap, singleObjInTsxFile *TmxOrTsxObject, targetPolyline *TmxOrTsxPolyline, pTsxIns *Tsx) (*Polygon2D, error) {
|
||||
if nil == targetPolyline {
|
||||
return nil, nil
|
||||
}
|
||||
var factorHalf float64 = 0.5
|
||||
offsetFromTopLeftInTileLocalCoordX := singleObjInTsxFile.X
|
||||
offsetFromTopLeftInTileLocalCoordY := singleObjInTsxFile.Y
|
||||
|
||||
singleValueArray := strings.Split(targetPolyline.Points, " ")
|
||||
pointsCount := len(singleValueArray)
|
||||
|
||||
if pointsCount >= box2d.B2_maxPolygonVertices {
|
||||
return nil, errors.New(fmt.Sprintf("During `TsxPolylineToOffsetsWrtTileCenterInB2World`, you have a polygon with pointsCount == %v, exceeding or equal to box2d.B2_maxPolygonVertices == %v", pointsCount, box2d.B2_maxPolygonVertices))
|
||||
}
|
||||
|
||||
thePolygon2DFromPolyline := &Polygon2D{
|
||||
Anchor: nil,
|
||||
Points: make([]*Vec2D, pointsCount),
|
||||
TileWidth: pTsxIns.TileWidth,
|
||||
TileHeight: pTsxIns.TileHeight,
|
||||
}
|
||||
|
||||
/*
|
||||
[WARNING] In this case, the "Treasure"s and "GuardTower"s are put into Tmx file as "ImageObject"s, of each the "ProportionalAnchor" is (0.5, 0). Therefore we calculate that "thePolygon2DFromPolyline.Points" are "offsets(in B2World) w.r.t. the BottomCenter". See https://shimo.im/docs/SmLJJhXm2C8XMzZT for details.
|
||||
*/
|
||||
|
||||
for k, value := range singleValueArray {
|
||||
thePolygon2DFromPolyline.Points[k] = &Vec2D{}
|
||||
for kk, v := range strings.Split(value, ",") {
|
||||
coordinateValue, err := strconv.ParseFloat(v, 64)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
if 0 == (kk % 2) {
|
||||
// W.r.t. center.
|
||||
thePolygon2DFromPolyline.Points[k].X = (coordinateValue + offsetFromTopLeftInTileLocalCoordX) - factorHalf*float64(pTsxIns.TileWidth)
|
||||
} else {
|
||||
// W.r.t. bottom.
|
||||
thePolygon2DFromPolyline.Points[k].Y = float64(pTsxIns.TileHeight) - (coordinateValue + offsetFromTopLeftInTileLocalCoordY)
|
||||
}
|
||||
}
|
||||
|
||||
// No need to transform for B2World space coordinate because the marks in a Tsx file is already rectilinear.
|
||||
}
|
||||
|
||||
return thePolygon2DFromPolyline, nil
|
||||
}
|
||||
|
||||
func DeserializeTsxToColliderDict(pTmxMapIns *TmxMap, byteArrOfTsxFile []byte, firstGid int, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) error {
|
||||
pTsxIns := &Tsx{}
|
||||
err := xml.Unmarshal(byteArrOfTsxFile, pTsxIns)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
/*
|
||||
// For debug-printing only. -- YFLu, 2019-09-04.
|
||||
|
||||
reserializedTmxMap, err := pTmxMapIns.ToXML()
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
*/
|
||||
|
||||
for _, tile := range pTsxIns.Tiles {
|
||||
globalGid := (firstGid + int(tile.Id))
|
||||
/**
|
||||
Per tile xml str could be
|
||||
|
||||
```
|
||||
<tile id="13">
|
||||
<objectgroup draworder="index">
|
||||
<object id="1" x="-154" y="-159">
|
||||
<properties>
|
||||
<property name="boundary_type" value="guardTower"/>
|
||||
</properties>
|
||||
<polyline points="0,0 -95,179 18,407 361,434 458,168 333,-7"/>
|
||||
</object>
|
||||
</objectgroup>
|
||||
</tile>
|
||||
```
|
||||
, we currently REQUIRE that "`an object of a tile` with ONE OR MORE polylines must come with a single corresponding '<property name=`type` value=`...` />', and viceversa".
|
||||
|
||||
Refer to https://shimo.im/docs/SmLJJhXm2C8XMzZT for how we theoretically fit a "Polyline in Tsx" into a "Polygon2D" and then into the corresponding "B2BodyDef & B2Body in the `world of colliding bodies`".
|
||||
*/
|
||||
|
||||
theObjGroup := tile.ObjectGroup
|
||||
if nil == theObjGroup {
|
||||
continue
|
||||
}
|
||||
for _, singleObj := range theObjGroup.Objects {
|
||||
if nil == singleObj.Polyline {
|
||||
// Temporarily omit those non-polyline-containing objects.
|
||||
continue
|
||||
}
|
||||
if nil == singleObj.Properties.Property || "boundary_type" != singleObj.Properties.Property[0].Name {
|
||||
continue
|
||||
}
|
||||
|
||||
key := singleObj.Properties.Property[0].Value
|
||||
|
||||
var theStrToPolygon2DListMap StrToPolygon2DListMap
|
||||
if existingStrToPolygon2DListMap, ok := gidBoundariesMapInB2World[globalGid]; ok {
|
||||
theStrToPolygon2DListMap = existingStrToPolygon2DListMap
|
||||
} else {
|
||||
gidBoundariesMapInB2World[globalGid] = make(StrToPolygon2DListMap, 0)
|
||||
theStrToPolygon2DListMap = gidBoundariesMapInB2World[globalGid]
|
||||
}
|
||||
|
||||
var pThePolygon2DList *Polygon2DList
|
||||
if _, ok := theStrToPolygon2DListMap[key]; ok {
|
||||
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
||||
} else {
|
||||
thePolygon2DList := make(Polygon2DList, 0)
|
||||
theStrToPolygon2DListMap[key] = &thePolygon2DList
|
||||
pThePolygon2DList = theStrToPolygon2DListMap[key]
|
||||
}
|
||||
|
||||
thePolygon2DFromPolyline, err := TsxPolylineToOffsetsWrtTileCenterInB2World(pTmxMapIns, singleObj, singleObj.Polyline, pTsxIns)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
*pThePolygon2DList = append(*pThePolygon2DList, thePolygon2DFromPolyline)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func ParseTmxLayersAndGroups(pTmxMapIns *TmxMap, gidBoundariesMapInB2World map[int]StrToPolygon2DListMap) (int32, int32, int32, int32, StrToVec2DListMap, StrToPolygon2DListMap, error) {
|
||||
toRetStrToVec2DListMap := make(StrToVec2DListMap, 0)
|
||||
toRetStrToPolygon2DListMap := make(StrToPolygon2DListMap, 0)
|
||||
/*
|
||||
Note that both
|
||||
- "Vec2D"s of "toRetStrToVec2DListMap", and
|
||||
- "Polygon2D"s of "toRetStrToPolygon2DListMap"
|
||||
|
||||
are already transformed into the "coordinate of B2World".
|
||||
|
||||
-- YFLu
|
||||
*/
|
||||
|
||||
for _, objGroup := range pTmxMapIns.ObjectGroups {
|
||||
switch objGroup.Name {
|
||||
case "PlayerStartingPos":
|
||||
var pTheVec2DListToCache *Vec2DList
|
||||
_, ok := toRetStrToVec2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
theVec2DListToCache := make(Vec2DList, 0)
|
||||
toRetStrToVec2DListMap[objGroup.Name] = &theVec2DListToCache
|
||||
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
|
||||
} else {
|
||||
pTheVec2DListToCache = toRetStrToVec2DListMap[objGroup.Name]
|
||||
}
|
||||
for _, singleObjInTmxFile := range objGroup.Objects {
|
||||
theUntransformedPos := &Vec2D{
|
||||
X: singleObjInTmxFile.X,
|
||||
Y: singleObjInTmxFile.Y,
|
||||
}
|
||||
thePosInWorld := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedPos)
|
||||
*pTheVec2DListToCache = append(*pTheVec2DListToCache, &thePosInWorld)
|
||||
}
|
||||
case "Pumpkin", "SpeedShoe":
|
||||
case "Barrier":
|
||||
/*
|
||||
Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" is located exactly in an overlapping with "Polygon2D.Points[0]" w.r.t. B2World.
|
||||
|
||||
-- YFLu
|
||||
*/
|
||||
var pThePolygon2DListToCache *Polygon2DList
|
||||
_, ok := toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
thePolygon2DListToCache := make(Polygon2DList, 0)
|
||||
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
|
||||
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
} else {
|
||||
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
}
|
||||
|
||||
for _, singleObjInTmxFile := range objGroup.Objects {
|
||||
if nil == singleObjInTmxFile.Polyline {
|
||||
continue
|
||||
}
|
||||
if nil == singleObjInTmxFile.Properties.Property || "boundary_type" != singleObjInTmxFile.Properties.Property[0].Name || "barrier" != singleObjInTmxFile.Properties.Property[0].Value {
|
||||
continue
|
||||
}
|
||||
|
||||
thePolygon2DInWorld, err := TmxPolylineToPolygon2DInB2World(pTmxMapIns, singleObjInTmxFile, singleObjInTmxFile.Polyline)
|
||||
if nil != err {
|
||||
panic(err)
|
||||
}
|
||||
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
|
||||
}
|
||||
case "LowScoreTreasure", "GuardTower", "HighScoreTreasure":
|
||||
/*
|
||||
Note that in this case, the "Polygon2D.Anchor" of each "TmxOrTsxObject" ISN'T located exactly in an overlapping with "Polygon2D.Points[0]" w.r.t. B2World, refer to "https://shimo.im/docs/SmLJJhXm2C8XMzZT" for details.
|
||||
|
||||
-- YFLu
|
||||
*/
|
||||
for _, singleObjInTmxFile := range objGroup.Objects {
|
||||
if nil == singleObjInTmxFile.Gid {
|
||||
continue
|
||||
}
|
||||
theGlobalGid := singleObjInTmxFile.Gid
|
||||
theStrToPolygon2DListMap, ok := gidBoundariesMapInB2World[*theGlobalGid]
|
||||
if false == ok {
|
||||
continue
|
||||
}
|
||||
|
||||
pThePolygon2DList, ok := theStrToPolygon2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
continue
|
||||
}
|
||||
|
||||
var pThePolygon2DListToCache *Polygon2DList
|
||||
_, ok = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
if false == ok {
|
||||
thePolygon2DListToCache := make(Polygon2DList, 0)
|
||||
toRetStrToPolygon2DListMap[objGroup.Name] = &thePolygon2DListToCache
|
||||
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
} else {
|
||||
pThePolygon2DListToCache = toRetStrToPolygon2DListMap[objGroup.Name]
|
||||
}
|
||||
|
||||
for _, thePolygon2D := range *pThePolygon2DList {
|
||||
theUntransformedBottomCenterAsAnchor := &Vec2D{
|
||||
X: singleObjInTmxFile.X,
|
||||
Y: singleObjInTmxFile.Y,
|
||||
}
|
||||
|
||||
theTransformedBottomCenterAsAnchor := pTmxMapIns.continuousObjLayerOffsetToContinuousMapNodePos(theUntransformedBottomCenterAsAnchor)
|
||||
|
||||
thePolygon2DInWorld := &Polygon2D{
|
||||
Anchor: &theTransformedBottomCenterAsAnchor,
|
||||
Points: make([]*Vec2D, len(thePolygon2D.Points)),
|
||||
TileWidth: thePolygon2D.TileWidth,
|
||||
TileHeight: thePolygon2D.TileHeight,
|
||||
}
|
||||
if nil != singleObjInTmxFile.Width && nil != singleObjInTmxFile.Height {
|
||||
thePolygon2DInWorld.TmxObjectWidth = *singleObjInTmxFile.Width
|
||||
thePolygon2DInWorld.TmxObjectHeight = *singleObjInTmxFile.Height
|
||||
}
|
||||
for kk, p := range thePolygon2D.Points {
|
||||
// [WARNING] It's intentionally recreating a copy of "Vec2D" here.
|
||||
thePolygon2DInWorld.Points[kk] = &Vec2D{
|
||||
X: p.X,
|
||||
Y: p.Y,
|
||||
}
|
||||
}
|
||||
*pThePolygon2DListToCache = append(*pThePolygon2DListToCache, thePolygon2DInWorld)
|
||||
}
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return int32(pTmxMapIns.Width), int32(pTmxMapIns.Height), int32(pTmxMapIns.TileWidth), int32(pTmxMapIns.TileHeight), toRetStrToVec2DListMap, toRetStrToPolygon2DListMap, nil
|
||||
}
|
||||
|
||||
func (pTmxMap *TmxMap) ToXML() (string, error) {
|
||||
ret, err := xml.Marshal(pTmxMap)
|
||||
return string(ret[:]), err
|
||||
}
|
||||
|
||||
type TileRectilinearSize struct {
|
||||
Width float64
|
||||
Height float64
|
||||
}
|
||||
|
||||
func (pTmxMapIns *TmxMap) continuousObjLayerVecToContinuousMapNodeVec(continuousObjLayerVec *Vec2D) Vec2D {
|
||||
var tileRectilinearSize TileRectilinearSize
|
||||
tileRectilinearSize.Width = float64(pTmxMapIns.TileWidth)
|
||||
tileRectilinearSize.Height = float64(pTmxMapIns.TileHeight)
|
||||
tileSizeUnifiedLength := math.Sqrt(tileRectilinearSize.Width*tileRectilinearSize.Width*0.25 + tileRectilinearSize.Height*tileRectilinearSize.Height*0.25)
|
||||
isometricObjectLayerPointOffsetScaleFactor := (tileSizeUnifiedLength / tileRectilinearSize.Height)
|
||||
cosineThetaRadian := (tileRectilinearSize.Width * 0.5) / tileSizeUnifiedLength
|
||||
sineThetaRadian := (tileRectilinearSize.Height * 0.5) / tileSizeUnifiedLength
|
||||
|
||||
transMat := [...][2]float64{
|
||||
{isometricObjectLayerPointOffsetScaleFactor * cosineThetaRadian, -isometricObjectLayerPointOffsetScaleFactor * cosineThetaRadian},
|
||||
{-isometricObjectLayerPointOffsetScaleFactor * sineThetaRadian, -isometricObjectLayerPointOffsetScaleFactor * sineThetaRadian},
|
||||
}
|
||||
convertedVecX := transMat[0][0]*continuousObjLayerVec.X + transMat[0][1]*continuousObjLayerVec.Y
|
||||
convertedVecY := transMat[1][0]*continuousObjLayerVec.X + transMat[1][1]*continuousObjLayerVec.Y
|
||||
converted := Vec2D{
|
||||
X: convertedVecX,
|
||||
Y: convertedVecY,
|
||||
}
|
||||
return converted
|
||||
}
|
||||
|
||||
func (pTmxMapIns *TmxMap) continuousObjLayerOffsetToContinuousMapNodePos(continuousObjLayerOffset *Vec2D) Vec2D {
|
||||
layerOffset := Vec2D{
|
||||
X: 0,
|
||||
Y: float64(pTmxMapIns.Height*pTmxMapIns.TileHeight) * 0.5,
|
||||
}
|
||||
|
||||
calibratedVec := continuousObjLayerOffset
|
||||
convertedVec := pTmxMapIns.continuousObjLayerVecToContinuousMapNodeVec(calibratedVec)
|
||||
|
||||
toRet := Vec2D{
|
||||
X: layerOffset.X + convertedVec.X,
|
||||
Y: layerOffset.Y + convertedVec.Y,
|
||||
}
|
||||
|
||||
return toRet
|
||||
}
|
39
battle_srv/models/trap.go
Normal file
39
battle_srv/models/trap.go
Normal file
@@ -0,0 +1,39 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ByteArena/box2d"
|
||||
)
|
||||
|
||||
type Trap struct {
|
||||
Id int32 `json:"id,omitempty"`
|
||||
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
|
||||
Type int32 `json:"type,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
PickupBoundary *Polygon2D `json:"-"`
|
||||
TrapBullets []*Bullet `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
RemovedAtFrameId int32 `json:"-"`
|
||||
}
|
||||
|
||||
type GuardTower struct {
|
||||
Id int32 `json:"id,omitempty"`
|
||||
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
|
||||
Type int32 `json:"type,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
PickupBoundary *Polygon2D `json:"-"`
|
||||
TrapBullets []*Bullet `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
RemovedAtFrameId int32 `json:"-"`
|
||||
|
||||
InRangePlayers *InRangePlayerCollection `json:"-"`
|
||||
LastAttackTick int64 `json:"-"`
|
||||
|
||||
TileWidth float64 `json:"-"`
|
||||
TileHeight float64 `json:"-"`
|
||||
WidthInB2World float64 `json:"-"`
|
||||
HeightInB2World float64 `json:"-"`
|
||||
}
|
18
battle_srv/models/treasure.go
Normal file
18
battle_srv/models/treasure.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"github.com/ByteArena/box2d"
|
||||
)
|
||||
|
||||
type Treasure struct {
|
||||
Id int32 `json:"id,omitempty"`
|
||||
LocalIdInBattle int32 `json:"localIdInBattle,omitempty"`
|
||||
Score int32 `json:"score,omitempty"`
|
||||
X float64 `json:"x,omitempty"`
|
||||
Y float64 `json:"y,omitempty"`
|
||||
Removed bool `json:"removed,omitempty"`
|
||||
Type int32 `json:"type,omitempty"`
|
||||
|
||||
PickupBoundary *Polygon2D `json:"-"`
|
||||
CollidableBody *box2d.B2Body `json:"-"`
|
||||
}
|
74
battle_srv/models/type.go
Normal file
74
battle_srv/models/type.go
Normal file
@@ -0,0 +1,74 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
)
|
||||
|
||||
type NullInt64 struct {
|
||||
sql.NullInt64
|
||||
}
|
||||
|
||||
func NewNullInt64(s int64) NullInt64 {
|
||||
ns := NullInt64{}
|
||||
ns.Int64 = s
|
||||
ns.Valid = true
|
||||
return ns
|
||||
}
|
||||
|
||||
func (v NullInt64) MarshalJSON() ([]byte, error) {
|
||||
if v.Valid {
|
||||
return json.Marshal(v.Int64)
|
||||
} else {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *NullInt64) UnmarshalJSON(data []byte) error {
|
||||
var s *int64
|
||||
//Logger.Debugf("%s\n", data)
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s != nil {
|
||||
v.Valid = true
|
||||
v.Int64 = *s
|
||||
} else {
|
||||
v.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
type NullString struct {
|
||||
sql.NullString
|
||||
}
|
||||
|
||||
func NewNullString(s string) NullString {
|
||||
ns := NullString{}
|
||||
ns.String = s
|
||||
ns.Valid = true
|
||||
return ns
|
||||
}
|
||||
|
||||
func (v NullString) MarshalJSON() ([]byte, error) {
|
||||
if v.Valid {
|
||||
return json.Marshal(v.String)
|
||||
} else {
|
||||
return json.Marshal(nil)
|
||||
}
|
||||
}
|
||||
|
||||
func (v *NullString) UnmarshalJSON(data []byte) error {
|
||||
var s *string
|
||||
//Logger.Debugf("%s\n", data)
|
||||
if err := json.Unmarshal(data, &s); err != nil {
|
||||
return err
|
||||
}
|
||||
if s != nil {
|
||||
v.Valid = true
|
||||
v.String = *s
|
||||
} else {
|
||||
v.Valid = false
|
||||
}
|
||||
return nil
|
||||
}
|
2283
battle_srv/pb_output/room_downsync_frame.pb.go
Normal file
2283
battle_srv/pb_output/room_downsync_frame.pb.go
Normal file
File diff suppressed because it is too large
Load Diff
16
battle_srv/scheduler/player.go
Normal file
16
battle_srv/scheduler/player.go
Normal file
@@ -0,0 +1,16 @@
|
||||
package scheduler
|
||||
|
||||
import (
|
||||
. "server/common"
|
||||
"server/models"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
func HandleExpiredPlayerLoginToken() {
|
||||
Logger.Debug("HandleExpiredPlayerLoginToken start")
|
||||
err := models.CleanExpiredPlayerLoginToken()
|
||||
if err != nil {
|
||||
Logger.Debug("HandleExpiredPlayerLoginToken", zap.Error(err))
|
||||
}
|
||||
}
|
21
battle_srv/start_daemon.sh
Normal file
21
battle_srv/start_daemon.sh
Normal file
@@ -0,0 +1,21 @@
|
||||
#!/bin/bash
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo "Usage: $0 [TEST|PROD]"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
basedir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
|
||||
OS_USER=$USER
|
||||
ServerEnv=$1
|
||||
LOG_PATH="/var/log/treasure-hunter.log"
|
||||
|
||||
# Make sure that the following "PidFile" is "git ignored".
|
||||
PID_FILE="$basedir/treasure-hunter.pid"
|
||||
|
||||
sudo su - root -c "touch $LOG_PATH"
|
||||
sudo su - root -c "chown $OS_USER:$OS_USER $LOG_PATH"
|
||||
|
||||
ServerEnv=$ServerEnv $basedir/server >$LOG_PATH 2>&1 &
|
||||
echo $! > $PID_FILE
|
18
battle_srv/stop_daemon.sh
Normal file
18
battle_srv/stop_daemon.sh
Normal file
@@ -0,0 +1,18 @@
|
||||
#!/bin/bash
|
||||
|
||||
basedir=$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )
|
||||
|
||||
PID_FILE="$basedir/treasure-hunter.pid"
|
||||
if [ -f $PID_FILE ]; then
|
||||
pid=$( cat "$PID_FILE" )
|
||||
if [ -z $pid ]; then
|
||||
echo "There's no pid stored in $PID_FILE."
|
||||
else
|
||||
echo "Killing process of id $pid."
|
||||
kill $pid
|
||||
echo "Removing PidFile $PID_FILE."
|
||||
rm $PID_FILE
|
||||
fi
|
||||
else
|
||||
echo "There's no PidFile $PID_FILE."
|
||||
fi
|
6
battle_srv/storage/init.go
Normal file
6
battle_srv/storage/init.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package storage
|
||||
|
||||
func Init() {
|
||||
initMySQL()
|
||||
initRedis()
|
||||
}
|
22
battle_srv/storage/mysql_manager.go
Normal file
22
battle_srv/storage/mysql_manager.go
Normal file
@@ -0,0 +1,22 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
. "server/common"
|
||||
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"github.com/jmoiron/sqlx"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
MySQLManagerIns *sqlx.DB
|
||||
)
|
||||
|
||||
func initMySQL() {
|
||||
var err error
|
||||
MySQLManagerIns, err = sqlx.Connect("mysql", Conf.MySQL.DSN+"?charset=utf8mb4")
|
||||
ErrFatal(err)
|
||||
err = MySQLManagerIns.Ping()
|
||||
ErrFatal(err)
|
||||
Logger.Info("MySQLManagerIns", zap.Any("mysql", MySQLManagerIns))
|
||||
}
|
25
battle_srv/storage/redis_manager.go
Normal file
25
battle_srv/storage/redis_manager.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
. "server/common"
|
||||
|
||||
"github.com/go-redis/redis"
|
||||
_ "github.com/go-sql-driver/mysql"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
var (
|
||||
RedisManagerIns *redis.Client
|
||||
)
|
||||
|
||||
func initRedis() {
|
||||
RedisManagerIns = redis.NewClient(&redis.Options{
|
||||
Addr: fmt.Sprintf("%s:%d", Conf.Redis.Host, Conf.Redis.Port),
|
||||
Password: Conf.Redis.Password, // no password set
|
||||
DB: Conf.Redis.Dbname, // use default DB
|
||||
})
|
||||
pong, err := RedisManagerIns.Ping().Result()
|
||||
ErrFatal(err)
|
||||
Logger.Info("Redis", zap.String("ping", pong))
|
||||
}
|
9
battle_srv/test_cases/Makefile
Normal file
9
battle_srv/test_cases/Makefile
Normal file
@@ -0,0 +1,9 @@
|
||||
PROJECTNAME=tests
|
||||
ROOT_DIR=$(shell pwd)
|
||||
all: help
|
||||
|
||||
run-test: build
|
||||
GOPATH=$(GOPATH):$(ROOT_DIR)/.. ServerEnv=TEST ./$(PROJECTNAME)
|
||||
|
||||
build:
|
||||
go build -o $(ROOT_DIR)/$(PROJECTNAME) ./tests.go
|
79
battle_srv/test_cases/tests.go
Normal file
79
battle_srv/test_cases/tests.go
Normal file
@@ -0,0 +1,79 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
. "server/common"
|
||||
"server/models"
|
||||
)
|
||||
|
||||
var relativePath string
|
||||
|
||||
func loadTMX(fp string, pTmxMapIns *models.TmxMap) {
|
||||
if !filepath.IsAbs(fp) {
|
||||
panic("Tmx filepath must be absolute!")
|
||||
}
|
||||
|
||||
byteArr, err := ioutil.ReadFile(fp)
|
||||
ErrFatal(err)
|
||||
models.DeserializeToTmxMapIns(byteArr, pTmxMapIns)
|
||||
for _, info := range pTmxMapIns.TreasuresInfo {
|
||||
fmt.Printf("treasuresInfo: %v\n", info)
|
||||
}
|
||||
for _, info := range pTmxMapIns.HighTreasuresInfo {
|
||||
fmt.Printf("treasuresInfo: %v\n", info)
|
||||
}
|
||||
}
|
||||
|
||||
func loadTSX(fp string, pTsxIns *models.Tsx) {
|
||||
if !filepath.IsAbs(fp) {
|
||||
panic("Tmx filepath must be absolute!")
|
||||
}
|
||||
|
||||
byteArr, err := ioutil.ReadFile(fp)
|
||||
ErrFatal(err)
|
||||
models.DeserializeToTsxIns(byteArr, pTsxIns)
|
||||
for _, Pos := range pTsxIns.TrapPolyLineList {
|
||||
fmt.Printf("%v\n", Pos)
|
||||
}
|
||||
}
|
||||
|
||||
func getTMXInfo() {
|
||||
relativePath = "../frontend/assets/resources/map/treasurehunter.tmx"
|
||||
execPath, err := os.Executable()
|
||||
ErrFatal(err)
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
ErrFatal(err)
|
||||
|
||||
fmt.Printf("execPath = %v, pwd = %s, returning...\n", execPath, pwd)
|
||||
|
||||
tmxMapIns := models.TmxMap{}
|
||||
pTmxMapIns := &tmxMapIns
|
||||
fp := filepath.Join(pwd, relativePath)
|
||||
fmt.Printf("fp == %v\n", fp)
|
||||
loadTMX(fp, pTmxMapIns)
|
||||
}
|
||||
|
||||
func getTSXInfo() {
|
||||
|
||||
relativePath = "../frontend/assets/resources/map/tile_1.tsx"
|
||||
execPath, err := os.Executable()
|
||||
ErrFatal(err)
|
||||
|
||||
pwd, err := os.Getwd()
|
||||
ErrFatal(err)
|
||||
|
||||
fmt.Printf("execPath = %v, pwd = %s, returning...\n", execPath, pwd)
|
||||
tsxIns := models.Tsx{}
|
||||
pTsxIns := &tsxIns
|
||||
fp := filepath.Join(pwd, relativePath)
|
||||
fmt.Printf("fp == %v\n", fp)
|
||||
loadTSX(fp, pTsxIns)
|
||||
}
|
||||
|
||||
func main() {
|
||||
getTSXInfo()
|
||||
}
|
378
battle_srv/ws/serve.go
Normal file
378
battle_srv/ws/serve.go
Normal file
@@ -0,0 +1,378 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"container/heap"
|
||||
"fmt"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/golang/protobuf/proto"
|
||||
"github.com/gorilla/websocket"
|
||||
"go.uber.org/zap"
|
||||
"net/http"
|
||||
. "server/common"
|
||||
"server/models"
|
||||
pb "server/pb_output"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
READ_BUF_SIZE = 8 * 1024
|
||||
WRITE_BUF_SIZE = 8 * 1024
|
||||
)
|
||||
|
||||
var upgrader = websocket.Upgrader{
|
||||
ReadBufferSize: READ_BUF_SIZE,
|
||||
WriteBufferSize: WRITE_BUF_SIZE,
|
||||
CheckOrigin: func(r *http.Request) bool {
|
||||
Logger.Debug("origin", zap.Any("origin", r.Header.Get("Origin")))
|
||||
return true
|
||||
},
|
||||
}
|
||||
|
||||
func startOrFeedHeartbeatWatchdog(conn *websocket.Conn) bool {
|
||||
if nil == conn {
|
||||
return false
|
||||
}
|
||||
conn.SetReadDeadline(time.Now().Add(time.Millisecond * (ConstVals.Ws.WillKickIfInactiveFor)))
|
||||
return true
|
||||
}
|
||||
|
||||
func Serve(c *gin.Context) {
|
||||
token, ok := c.GetQuery("intAuthToken")
|
||||
if !ok {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
Logger.Info("Finding PlayerLogin record for ws authentication:", zap.Any("intAuthToken", token))
|
||||
boundRoomId := 0
|
||||
expectRoomId := 0
|
||||
var err error
|
||||
if boundRoomIdStr, hasBoundRoomId := c.GetQuery("boundRoomId"); hasBoundRoomId {
|
||||
boundRoomId, err = strconv.Atoi(boundRoomIdStr)
|
||||
if err != nil {
|
||||
// TODO: Abort with specific message.
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
Logger.Info("Finding PlayerLogin record for ws authentication:", zap.Any("intAuthToken", token), zap.Any("boundRoomId", boundRoomId))
|
||||
}
|
||||
if expectRoomIdStr, hasExpectRoomId := c.GetQuery("expectedRoomId"); hasExpectRoomId {
|
||||
expectRoomId, err = strconv.Atoi(expectRoomIdStr)
|
||||
if err != nil {
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
Logger.Info("Finding PlayerLogin record for ws authentication:", zap.Any("intAuthToken", token), zap.Any("expectedRoomId", expectRoomId))
|
||||
}
|
||||
|
||||
// TODO: Wrap the following 2 stmts by sql transaction!
|
||||
playerId, err := models.GetPlayerIdByToken(token)
|
||||
if err != nil || playerId == 0 {
|
||||
// TODO: Abort with specific message.
|
||||
Logger.Info("PlayerLogin record not found for ws authentication:", zap.Any("intAuthToken", token))
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
Logger.Info("PlayerLogin record has been found for ws authentication:", zap.Any("playerId", playerId))
|
||||
|
||||
conn, err := upgrader.Upgrade(c.Writer, c.Request, nil)
|
||||
if err != nil {
|
||||
Logger.Error("upgrade:", zap.Error(err), zap.Any("playerId", playerId))
|
||||
c.AbortWithStatus(http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
Logger.Debug("ConstVals.Ws.WillKickIfInactiveFor", zap.Duration("v", ConstVals.Ws.WillKickIfInactiveFor))
|
||||
/**
|
||||
* WARNING: After successfully upgraded to use the "persistent connection" of http1.1/websocket protocol, you CANNOT overwrite the http1.0 resp status by `c.AbortWithStatus(...)` any more!
|
||||
*/
|
||||
|
||||
connHasBeenSignaledToClose := int32(0)
|
||||
pConnHasBeenSignaledToClose := &connHasBeenSignaledToClose
|
||||
|
||||
var pRoom *models.Room = nil
|
||||
signalToCloseConnOfThisPlayer := func(customRetCode int, customRetMsg string) {
|
||||
if swapped := atomic.CompareAndSwapInt32(pConnHasBeenSignaledToClose, 0, 1); !swapped {
|
||||
return
|
||||
}
|
||||
Logger.Warn("signalToCloseConnOfThisPlayer:", zap.Any("playerId", playerId), zap.Any("customRetCode", customRetCode), zap.Any("customRetMsg", customRetMsg))
|
||||
if nil != pRoom {
|
||||
pRoom.OnPlayerDisconnected(int32(playerId))
|
||||
}
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Warn("Recovered from: ", zap.Any("panic", r))
|
||||
}
|
||||
}()
|
||||
/**
|
||||
* References
|
||||
* - https://tools.ietf.org/html/rfc6455
|
||||
* - https://godoc.org/github.com/gorilla/websocket#hdr-Control_Messages
|
||||
* - https://godoc.org/github.com/gorilla/websocket#FormatCloseMessage
|
||||
* - https://godoc.org/github.com/gorilla/websocket#Conn.WriteControl
|
||||
* - https://godoc.org/github.com/gorilla/websocket#hdr-Concurrency
|
||||
* - "The Close and WriteControl methods can be called concurrently with all other methods."
|
||||
*/
|
||||
|
||||
/**
|
||||
* References for the "WebsocketStdCloseCode"s. Note that we're using some "CustomCloseCode"s here as well.
|
||||
*
|
||||
* - https://tools.ietf.org/html/rfc6455#section-7.4
|
||||
* - https://godoc.org/github.com/gorilla/websocket#pkg-constants.
|
||||
*/
|
||||
closeMessage := websocket.FormatCloseMessage(customRetCode, customRetMsg)
|
||||
err := conn.WriteControl(websocket.CloseMessage, closeMessage, time.Now().Add(time.Millisecond*(ConstVals.Ws.WillKickIfInactiveFor)))
|
||||
if err != nil {
|
||||
Logger.Error("Unable to send the CloseFrame control message to player(client-side):", zap.Any("playerId", playerId), zap.Error(err))
|
||||
}
|
||||
|
||||
time.AfterFunc(3*time.Second, func() {
|
||||
// To actually terminates the underlying TCP connection which might be in `CLOSE_WAIT` state if inspected by `netstat`.
|
||||
conn.Close()
|
||||
})
|
||||
}
|
||||
|
||||
onReceivedCloseMessageFromClient := func(code int, text string) error {
|
||||
Logger.Warn("Triggered `onReceivedCloseMessageFromClient`:", zap.Any("code", code), zap.Any("playerId", playerId), zap.Any("message", text))
|
||||
signalToCloseConnOfThisPlayer(code, text)
|
||||
return nil
|
||||
}
|
||||
|
||||
/**
|
||||
* - "SetCloseHandler sets the handler for close messages received from the peer."
|
||||
*
|
||||
* - "The default close handler sends a close message back to the peer."
|
||||
*
|
||||
* - "The connection read methods return a CloseError when a close message is received. Most applications should handle close messages as part of their normal error handling. Applications should only set a close handler when the application must perform some action before sending a close message back to the peer."
|
||||
*
|
||||
* from reference https://godoc.org/github.com/gorilla/websocket#Conn.SetCloseHandler.
|
||||
*/
|
||||
conn.SetCloseHandler(onReceivedCloseMessageFromClient)
|
||||
|
||||
pPlayer, err := models.GetPlayerById(playerId)
|
||||
|
||||
if nil != err || nil == pPlayer {
|
||||
// TODO: Abort with specific message.
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.PlayerNotFound, "")
|
||||
}
|
||||
|
||||
Logger.Info("Player has logged in and its profile is found from persistent storage:", zap.Any("playerId", playerId), zap.Any("play", pPlayer))
|
||||
|
||||
// Find a room to join.
|
||||
Logger.Info("About to acquire RoomHeapMux for player:", zap.Any("playerId", playerId))
|
||||
(*(models.RoomHeapMux)).Lock()
|
||||
defer func() {
|
||||
(*(models.RoomHeapMux)).Unlock()
|
||||
Logger.Info("Released RoomHeapMux for player:", zap.Any("playerId", playerId))
|
||||
}()
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Error("Recovered from: ", zap.Any("panic", r))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
|
||||
}
|
||||
}()
|
||||
Logger.Info("Acquired RoomHeapMux for player:", zap.Any("playerId", playerId))
|
||||
// Logger.Info("The RoomHeapManagerIns has:", zap.Any("addr", fmt.Sprintf("%p", models.RoomHeapManagerIns)), zap.Any("size", len(*(models.RoomHeapManagerIns))))
|
||||
playerSuccessfullyAddedToRoom := false
|
||||
if 0 < boundRoomId {
|
||||
if tmpPRoom, existent := (*models.RoomMapManagerIns)[int32(boundRoomId)]; existent {
|
||||
pRoom = tmpPRoom
|
||||
Logger.Info("Successfully got:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forBoundRoomId", boundRoomId))
|
||||
res := pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer)
|
||||
if !res {
|
||||
Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forBoundRoomId", boundRoomId))
|
||||
} else {
|
||||
playerSuccessfullyAddedToRoom = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if 0 < expectRoomId {
|
||||
if tmpRoom, existent := (*models.RoomMapManagerIns)[int32(expectRoomId)]; existent {
|
||||
pRoom = tmpRoom
|
||||
Logger.Info("Successfully got:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forExpectedRoomId", expectRoomId))
|
||||
|
||||
if pRoom.ReAddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) {
|
||||
playerSuccessfullyAddedToRoom = true
|
||||
} else if pRoom.AddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer) {
|
||||
playerSuccessfullyAddedToRoom = true
|
||||
} else {
|
||||
Logger.Warn("Failed to get:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("forExpectedRoomId", expectRoomId))
|
||||
playerSuccessfullyAddedToRoom = false
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
if false == playerSuccessfullyAddedToRoom {
|
||||
defer func() {
|
||||
if pRoom != nil {
|
||||
heap.Push(models.RoomHeapManagerIns, pRoom)
|
||||
(models.RoomHeapManagerIns).Update(pRoom, pRoom.Score)
|
||||
}
|
||||
(models.RoomHeapManagerIns).PrintInOrder()
|
||||
}()
|
||||
tmpRoom, ok := heap.Pop(models.RoomHeapManagerIns).(*models.Room)
|
||||
if !ok {
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.LocallyNoAvailableRoom, fmt.Sprintf("Cannot pop a (*Room) for playerId == %v!", playerId))
|
||||
} else {
|
||||
pRoom = tmpRoom
|
||||
Logger.Info("Successfully popped:\n", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId))
|
||||
res := pRoom.AddPlayerIfPossible(pPlayer, conn, signalToCloseConnOfThisPlayer)
|
||||
if !res {
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.PlayerNotAddableToRoom, fmt.Sprintf("AddPlayerIfPossible returns false for roomId == %v, playerId == %v!", pRoom.Id, playerId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if swapped := atomic.CompareAndSwapInt32(pConnHasBeenSignaledToClose, 1, 1); swapped {
|
||||
return
|
||||
}
|
||||
|
||||
if pThePlayer, ok := pRoom.Players[int32(playerId)]; ok && (models.PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK == pThePlayer.BattleState || models.PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK == pThePlayer.BattleState) {
|
||||
defer func() {
|
||||
timeoutSeconds := time.Duration(5) * time.Second
|
||||
time.AfterFunc(timeoutSeconds, func() {
|
||||
if models.PlayerBattleStateIns.ADDED_PENDING_BATTLE_COLLIDER_ACK == pThePlayer.BattleState || models.PlayerBattleStateIns.READDED_PENDING_BATTLE_COLLIDER_ACK == pThePlayer.BattleState {
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, fmt.Sprintf("The expected Ack for BattleColliderInfo is not received in %s, for playerId == %v!", timeoutSeconds, playerId))
|
||||
}
|
||||
})
|
||||
}()
|
||||
|
||||
playerBattleColliderInfo := models.ToPbStrToBattleColliderInfo(int32(Constants.Ws.IntervalToPing), int32(Constants.Ws.WillKickIfInactiveFor), pRoom.Id, pRoom.StageName, pRoom.RawBattleStrToVec2DListMap, pRoom.RawBattleStrToPolygon2DListMap, pRoom.StageDiscreteW, pRoom.StageDiscreteH, pRoom.StageTileW, pRoom.StageTileH)
|
||||
|
||||
resp := &pb.WsResp{
|
||||
Ret: int32(Constants.RetCode.Ok),
|
||||
EchoedMsgId: int32(0),
|
||||
Act: models.DOWNSYNC_MSG_ACT_HB_REQ,
|
||||
BciFrame: playerBattleColliderInfo,
|
||||
}
|
||||
|
||||
// Logger.Info("Sending downsync HeartbeatRequirements:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("resp", resp))
|
||||
|
||||
theBytes, marshalErr := proto.Marshal(resp)
|
||||
if nil != marshalErr {
|
||||
Logger.Error("Error marshalling HeartbeatRequirements:", zap.Any("the error", marshalErr), zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, fmt.Sprintf("Error marshalling HeartbeatRequirements, playerId == %v and roomId == %v!", playerId, pRoom.Id))
|
||||
}
|
||||
|
||||
if err := conn.WriteMessage(websocket.BinaryMessage, theBytes); nil != err {
|
||||
Logger.Error("HeartbeatRequirements resp not written:", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Error(err))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, fmt.Sprintf("HeartbeatRequirements resp not written to roomId=%v, playerId == %v!", pRoom.Id, playerId))
|
||||
}
|
||||
}
|
||||
|
||||
/*
|
||||
TODO
|
||||
|
||||
Is there a way to EXPLICITLY make this "receivingLoopAgainstPlayer/conn.ReadXXX(...)" edge-triggered or yield/park otherwise? For example a C-style equivalent would be as follows.
|
||||
|
||||
```
|
||||
receivingLoopAgainstPlayer := func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Warn("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
|
||||
}
|
||||
Logger.Info("Goroutine `receivingLoopAgainstPlayer` is stopped for:", zap.Any("playerId", playerId), zap.Any("roomId", pRoom.Id))
|
||||
}()
|
||||
|
||||
// Set O_NONBLOCK on "fdOfThisConn".
|
||||
int flags = fcntl(fdOfThisConn, F_GETFL, 0);
|
||||
fcntl(fdOfThisConn, F_SETFL, flags | O_NONBLOCK);
|
||||
|
||||
int ep_fd = epoll_create1(0);
|
||||
epoll_event ev;
|
||||
ev.data.fd = fdOfThisConn;
|
||||
ev.events = (EPOLLIN | EPOLLET | CUSTOM_SIGNAL_TO_CLOSE); // Is this possible?
|
||||
epoll_ctl(ep_fd, EPOLL_CTL_ADD, fdOfThisConn, &ev);
|
||||
epoll_event *evs = (epoll_event*)calloc(MAXEVENTS, sizeof(epoll_event));
|
||||
|
||||
bool localAwarenessOfSignaledToClose = false;
|
||||
|
||||
while(true) {
|
||||
if (true == localAwarenessOfSignaledToClose) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Would yield the current KernelThread and park it to a "queue" for later being unparked from the same "queue", thus resumed running. See http://web.stanford.edu/~hhli/CS110Notes/CS110NotesCollection/Topic%204%20Networking%20(5).html for more information. However, multiple "goroutine"s might share a same KernelThread and could be an issue for yielding.
|
||||
int n = epoll_wait(ep_fd, evs, MAXEVENTS, -1);
|
||||
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (evs[i].data.fd == fdOfThisConn) {
|
||||
if (
|
||||
(evs[i].events & EPOLLERR) ||
|
||||
(evs[i].events & EPOLLHUP) ||
|
||||
(evs[i].events & CUSTOM_SIGNAL_TO_CLOSE)
|
||||
) {
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
|
||||
localAwarenessOfSignaledToClose = true;
|
||||
break;
|
||||
}
|
||||
int nbytes = 0;
|
||||
while(nbytes = recv(fdOfThisConn, buff, sizeof(buff)) && 0 < nbytes) {
|
||||
...
|
||||
}
|
||||
// Now that "0 == nbytes" or "EWOULDBLOCK == nbytes" or other errors came up.
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
```
|
||||
-- YFLu, 2020-07-03
|
||||
*/
|
||||
|
||||
// Starts the receiving loop against the client-side
|
||||
receivingLoopAgainstPlayer := func() error {
|
||||
defer func() {
|
||||
if r := recover(); r != nil {
|
||||
Logger.Warn("Goroutine `receivingLoopAgainstPlayer`, recovery spot#1, recovered from: ", zap.Any("panic", r))
|
||||
}
|
||||
Logger.Info("Goroutine `receivingLoopAgainstPlayer` is stopped for:", zap.Any("playerId", playerId), zap.Any("roomId", pRoom.Id))
|
||||
}()
|
||||
for {
|
||||
if swapped := atomic.CompareAndSwapInt32(pConnHasBeenSignaledToClose, 1, 1); swapped {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Tries to receive from client-side in a non-blocking manner.
|
||||
_, bytes, err := conn.ReadMessage()
|
||||
if nil != err {
|
||||
Logger.Error("About to `signalToCloseConnOfThisPlayer`", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Error(err))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
|
||||
return nil
|
||||
}
|
||||
|
||||
pReq := new(pb.WsReq)
|
||||
unmarshalErr := proto.Unmarshal(bytes, pReq)
|
||||
if nil != unmarshalErr {
|
||||
Logger.Error("About to `signalToCloseConnOfThisPlayer`", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Error(unmarshalErr))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
|
||||
}
|
||||
|
||||
// Logger.Info("Received request message from client", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Any("pReq", pReq))
|
||||
|
||||
switch pReq.Act {
|
||||
case models.UPSYNC_MSG_ACT_HB_PING:
|
||||
startOrFeedHeartbeatWatchdog(conn)
|
||||
case models.UPSYNC_MSG_ACT_PLAYER_CMD:
|
||||
startOrFeedHeartbeatWatchdog(conn)
|
||||
pRoom.OnBattleCmdReceived(pReq)
|
||||
case models.UPSYNC_MSG_ACT_PLAYER_COLLIDER_ACK:
|
||||
res := pRoom.OnPlayerBattleColliderAcked(int32(playerId))
|
||||
if false == res {
|
||||
Logger.Error("About to `signalToCloseConnOfThisPlayer`", zap.Any("roomId", pRoom.Id), zap.Any("playerId", playerId), zap.Error(err))
|
||||
signalToCloseConnOfThisPlayer(Constants.RetCode.UnknownError, "")
|
||||
return nil
|
||||
}
|
||||
default:
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
startOrFeedHeartbeatWatchdog(conn)
|
||||
go receivingLoopAgainstPlayer()
|
||||
}
|
Reference in New Issue
Block a user