Initial commit.

This commit is contained in:
genxium 2022-09-20 23:50:01 +08:00
commit e90a335c56
432 changed files with 101884 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
* text=auto
* core.ignorecase=false

134
.gitignore vendored Normal file
View File

@ -0,0 +1,134 @@
battle_srv/test_cases/test_cases
battle_srv/test_cases/tests
*.pid
local
local/*
temp
temp/*
*.rdb
*.rdb.meta
configs
configs/*
battle_srv/server
battle_srv/main
*.sqlite-journal
frontend/assets/plugin_scripts/conf.js
frontend/assets/plugin_scripts/conf.js.meta
frontend/library
frontend/library/*
.skeema
*.exe
*.zip
# logrotate status file
*.status
# mongodb backup
*.mongobak
# vim
#
*.sw*
# OSX
#
.DS_Store
*/.DS_Store
__MACOSX
# Xcode
#
build/
*.pbxuser
!default.pbxuser
*.mode1v3
!default.mode1v3
*.mode2v3
!default.mode2v3
*.perspectivev3
!default.perspectivev3
xcuserdata
*.xccheckout
*.moved-aside
DerivedData
*.hmap
*.ipa
*.xcuserstate
project.xcworkspace
# Android/IJ
#
*.iml
.idea
.idea/*
android/local.properties
android/build
android/build/*
android/.gradle
android/.gradle/*
# iOS
#
ios/build
ios/build/*
# node
#
node_modules/
./frontend/node_modules/
# BUCK
buck-out/
\.buckd/
android/app/libs
android/keystores/debug.keystore
# local logs
*.log
*.log.*
# crash log
core
# webpack
frontend/bin
# Docs
docs
docs/*
# database data
database/all_data*.sql
# General asymmetric key files
*.pem
# Skeema
.skeema
latest_diff_between_live_and_expected_schema.sql
# nohup.out
*.out
*.pcap
out
out/*
*.iml
.gradle
*/.gradle
*/.gradle/*
*/gradle/*
*/build
*/build/*
*/target
*/target/*
*.swp
gradle
gradle/*
*.classpath
*.project
*~

53
README.md Normal file
View File

@ -0,0 +1,53 @@
# 0. Preface
If you'd like to play with the backend code seriously, please read the detailed explanation of its important "lifecycle events" in [this note](https://app.yinxiang.com/fx/5c575124-01db-419b-9c02-ec81f78c6ddc).
There could be some left over wechat-game related code pieces, but they're neither meant to work nor supported anymore.
# 1. Database Server
The database product to be used for this project is MySQL 5.7.
We use [skeema](https://github.com/skeema/skeema) for schematic synchronization under `<proj-root>/database/skeema-repo-root/` which intentionally doesn't contain a `.skeema` file. Please read [this tutorial](https://shimo.im/doc/wQ0LvB0rlZcbHF5V) for more information.
You can use [this node module (still under development)](https://github.com/genxium/node-mysqldiff-bridge) instead under `Windows10`, other versions of Windows are not yet tested for compatibility.
The following command(s)
```
### Optional.
user@proj-root/database/skeema-repo-root> cp .skeema.template .skeema
###
user@proj-root/database/skeema-repo-root> skeema diff
```
is recommended to be used for checking difference from your "live MySQL server" to the latest expected schema tracked in git.
# 2. Building & running
## 2.1 Golang1.11
See https://github.com/genxium/Go111ModulePrac for details.
## 2.2 MySQL
On a product machine, you can install and manage `MySQL` server by [these scripts](https://github.com/genxium/Ubuntu14InitScripts/tree/master/database/mysql).
## 2.3 Required Config Files
### 2.3.1 Backend
- It needs `<proj-root>/battle_srv/configs/*` which is generated by `cd <proj-root>/battle_srv && cp -r ./configs.template ./configs` and necessary customization.
### 2.3.2 Frontend
- It needs CocosCreator v2.2.1 to build.
- A required "CocosCreator plugin `i18n`" is already enclosed in the project, if you have a globally installed "CocosCreator plugin `i18n`"(often located at `$HOME/.CocosCreator/packages/`) they should be OK to co-exist.
- It needs `<proj-root>/frontend/assets/plugin_scripts/conf.js` which is generated by `cd <proj-root>/frontend/assets/plugin_scripts && cp conf.js.template conf.js`.
## 2.4 Troubleshooting
### 2.4.1 Redis snapshot writing failure
```
ErrFatal {"err": "MISCONF Redis is configured to save RDB snapshots, but is currently not able to persist on disk. Commands that may modify the data set are disabled. Please check Redis logs for details about the error."}
```
Just restart your `redis-server` process.
# 3. Git configs cautions
Please make sure that you've set `ignorecase = false` in your `[core] section of <proj-root>/.git/config`.

32
battle_srv/Makefile Normal file
View 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

View 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
View 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
View 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: &params,
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
}

View 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
}

View 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
View 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
}

View 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
}
}

View 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))
}

View 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"`
}

View File

@ -0,0 +1,7 @@
{
"PLAYER": {
"SMS_EXPIRED_SECONDS":30,
"SMS_VALID_RESEND_PERIOD_SECONDS":8,
"INT_AUTH_TOKEN_TTL_SECONDS":1800
}
}

View 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)
}

View 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)
}

View 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()
}

View 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&timestamp=%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
View 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
}

View File

@ -0,0 +1,7 @@
{
"secondsBeforeSummoning": 10,
"protocol": "http",
"host": "localhost",
"port": 15351,
"symmetricKey": ""
}

View File

@ -0,0 +1,7 @@
{
"host": "localhost",
"port": 3306,
"dbname": "tsrht",
"username": "root",
"password": "root"
}

Binary file not shown.

View File

@ -0,0 +1,6 @@
{
"host": "localhost",
"port": 6379,
"dbname": 0,
"password": ""
}

View File

@ -0,0 +1,3 @@
{
"hostAndPort": "0.0.0.0:9992"
}

Binary file not shown.

View 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",
}

View 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
}

View 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
View 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
View 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=

View 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
View 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()
}

View 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
}

View 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:"-"`
}

View 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
}

View 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
View 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)
}

View 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
View 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
}

View 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
}

View 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
}

View 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
}

View 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:"-"`
}

View 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

File diff suppressed because it is too large Load Diff

View 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)))
}

View 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:"-"`
}

View 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
View 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:"-"`
}

View 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
View 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
}

File diff suppressed because it is too large Load Diff

View 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))
}
}

View 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
View 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

View File

@ -0,0 +1,6 @@
package storage
func Init() {
initMySQL()
initRedis()
}

View 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))
}

View 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))
}

View 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

View 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
View 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()
}

View File

@ -0,0 +1,15 @@
[production]
socket=/tmp/mysql.sock # Depending on the live MySQL server version and host OS, this "socket path" might vary. For example on "Ubuntu14.04/16.04", it's possible to find it via grepping "socket" of "/etc/my.cnf" or "/etc/mysql/".
user=root
host=localhost
schema=tsrht
[development]
socket=/tmp/mysql.sock # Alternative for "Ubuntu16.04 + MySQL5.7" could be "/var/run/mysqld/mysqld.sock".
user=root
host=localhost
allow-unsafe
skip-alter-wrapper
skip-alter-algorithm
skip-alter-lock
schema=tsrht_test

View File

@ -0,0 +1,32 @@
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `player` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`name` varchar(32) NOT NULL,
`display_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`created_at` bigint(20) unsigned NOT NULL,
`updated_at` bigint(20) unsigned NOT NULL,
`deleted_at` bigint(20) unsigned DEFAULT NULL,
`tutorial_stage` smallint(5) unsigned NOT NULL DEFAULT '0',
`avatar` varchar(256) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

View File

@ -0,0 +1,30 @@
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `player_auth_binding` (
`channel` int(11) unsigned NOT NULL,
`player_id` int(10) unsigned NOT NULL,
`ext_auth_id` varchar(64) NOT NULL,
`created_at` bigint(20) unsigned NOT NULL,
`updated_at` bigint(20) unsigned NOT NULL,
`deleted_at` bigint(20) unsigned DEFAULT NULL,
PRIMARY KEY (`player_id`,`channel`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

View File

@ -0,0 +1,33 @@
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `player_login` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`int_auth_token` varchar(64) NOT NULL,
`player_id` int(11) unsigned NOT NULL,
`display_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`from_public_ip` varchar(32) DEFAULT NULL,
`created_at` bigint(20) unsigned NOT NULL,
`updated_at` bigint(20) unsigned NOT NULL,
`deleted_at` bigint(20) unsigned DEFAULT NULL,
`avatar` varchar(256) DEFAULT '',
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=206 DEFAULT CHARSET=utf8;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

View File

@ -0,0 +1,29 @@
/*!40101 SET @OLD_CHARACTER_SET_CLIENT=@@CHARACTER_SET_CLIENT */;
/*!40101 SET @OLD_CHARACTER_SET_RESULTS=@@CHARACTER_SET_RESULTS */;
/*!40101 SET @OLD_COLLATION_CONNECTION=@@COLLATION_CONNECTION */;
/*!40101 SET NAMES utf8 */;
/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
/*!40103 SET TIME_ZONE='+00:00' */;
/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='' */;
/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
/*!40101 SET @saved_cs_client = @@character_set_client */;
/*!40101 SET character_set_client = utf8 */;
CREATE TABLE `player_wallet` (
`id` int(11) unsigned NOT NULL AUTO_INCREMENT,
`gem` int(11) unsigned NOT NULL DEFAULT '0',
`created_at` bigint(20) unsigned NOT NULL,
`updated_at` bigint(20) unsigned NOT NULL,
`deleted_at` bigint(20) unsigned DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=latin1;
/*!40101 SET character_set_client = @saved_cs_client */;
/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
/*!40101 SET COLLATION_CONNECTION=@OLD_COLLATION_CONNECTION */;
/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;

View File

@ -0,0 +1,5 @@
{
"ver": "1.0.1",
"uuid": "a351f972-c36f-48ae-b978-30512b7e8b6b",
"subMetas": {}
}

View File

@ -0,0 +1,66 @@
function NetworkDoctor(serverFps, clientUpsyncFps) {
this.serverFps = serverFps;
this.clientUpsyncFps = clientUpsyncFps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
this.setupFps = function(fps) {
this.serverFps = this.clientUpsyncFps = fps;
this.millisPerServerFrame = parseInt(1000 / this.serverFps);
this._tooLongSinceLastFrameDiffReceivedThreshold = (this.millisPerServerFrame << 6);
}
this._lastFrameDiffRecvTime = null;
this._tooLongSinceLastFrameDiffReceived = function() {
if (undefined === this._lastFrameDiffRecvTime || null === this._lastFrameDiffRecvTime) return false;
return (this._tooLongSinceLastFrameDiffReceivedThreshold <= (Date.now() - this._lastFrameDiffRecvTime));
};
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold = 120;
this.onNewFrameDiffReceived = function(frameDiff) {
var now = Date.now();
if (undefined !== this._lastFrameDiffRecvTime && null !== this._lastFrameDiffRecvTime) {
var intervalFromLastFrameDiff = (now - this._lastFrameDiffRecvTime);
if ((this.millisPerServerFrame << 5) < intervalFromLastFrameDiff) {
++this._consecutiveALittleLongFrameDiffReceivedIntervalCount;
console.log('Medium delay, intervalFromLastFrameDiff is', intervalFromLastFrameDiff);
} else {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
}
}
this._lastFrameDiffRecvTime = now;
};
this._networkComplaintPrefix = "\nNetwork is not good >_<\n";
this.generateNetworkComplaint = function(excludeTypeConstantALittleLongFrameDiffReceivedInterval, excludeTypeTooLongSinceLastFrameDiffReceived) {
if (this.hasBattleStopped) return null;
var shouldComplain = false;
var ret = this._networkComplaintPrefix;
if (true != excludeTypeConstantALittleLongFrameDiffReceivedInterval && this._consecutiveALittleLongFrameDiffReceivedIntervalCountThreshold <= this._consecutiveALittleLongFrameDiffReceivedIntervalCount) {
this._consecutiveALittleLongFrameDiffReceivedIntervalCount = 0;
ret += "\nConstantly having a little long recv interval.\n";
shouldComplain = true;
}
if (true != excludeTypeTooLongSinceLastFrameDiffReceived && this._tooLongSinceLastFrameDiffReceived()) {
ret += "\nToo long since last received frameDiff.\n";
shouldComplain = true;
}
return (shouldComplain ? ret : null);
};
this.hasBattleStopped = false;
this.onBattleStopped = function() {
this.hasBattleStopped = true;
};
this.isClientSessionConnected = function() {
if (!window.game) return false;
if (!window.game.clientSession) return false;
return window.game.clientSession.connected;
};
}
window.NetworkDoctor = NetworkDoctor;

View File

@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "477c07c3-0d50-4d55-96f0-6eaf9f25e2da",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -0,0 +1,585 @@
'use strict';
function _typeof6(obj) {
if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") {
_typeof6 = function _typeof6(obj) {
return typeof obj;
};
} else {
_typeof6 = function _typeof6(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
}
return _typeof6(obj);
}
function _typeof5(obj) {
if (typeof Symbol === "function" && _typeof6(Symbol.iterator) === "symbol") {
_typeof5 = function _typeof5(obj) {
return _typeof6(obj);
};
} else {
_typeof5 = function _typeof5(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof6(obj);
};
}
return _typeof5(obj);
}
function _typeof4(obj) {
if (typeof Symbol === "function" && _typeof5(Symbol.iterator) === "symbol") {
_typeof4 = function _typeof4(obj) {
return _typeof5(obj);
};
} else {
_typeof4 = function _typeof4(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof5(obj);
};
}
return _typeof4(obj);
}
function _typeof3(obj) {
if (typeof Symbol === "function" && _typeof4(Symbol.iterator) === "symbol") {
_typeof3 = function _typeof3(obj) {
return _typeof4(obj);
};
} else {
_typeof3 = function _typeof3(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof4(obj);
};
}
return _typeof3(obj);
}
function _typeof2(obj) {
if (typeof Symbol === "function" && _typeof3(Symbol.iterator) === "symbol") {
_typeof2 = function _typeof2(obj) {
return _typeof3(obj);
};
} else {
_typeof2 = function _typeof2(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof3(obj);
};
}
return _typeof2(obj);
}
function _typeof(obj) {
if (typeof Symbol === "function" && _typeof2(Symbol.iterator) === "symbol") {
_typeof = function _typeof(obj) {
return _typeof2(obj);
};
} else {
_typeof = function _typeof(obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : _typeof2(obj);
};
}
return _typeof(obj);
}
var NetworkUtils = NetworkUtils || {};
window.NetworkUtils = NetworkUtils;
NetworkUtils.ArrayProto = Array.prototype;
NetworkUtils.ObjProto = Object.prototype;
NetworkUtils.hasOwn = NetworkUtils.ObjProto.hasOwnProperty;
NetworkUtils.toString = NetworkUtils.ObjProto.toString;
NetworkUtils.nativeForEach = NetworkUtils.ArrayProto.forEach;
NetworkUtils.slice = NetworkUtils.ArrayProto.slice;
NetworkUtils.nativeKeys = Object.keys;
NetworkUtils.nativeIsArray = Array.isArray;
NetworkUtils.isFunction = function(o) {
return typeof o == "function" || false;
};
NetworkUtils.isObject = function(o) {
var type = typeof o === 'undefined' ? 'undefined' : _typeof(o);
return type === 'function' || type === 'object' && !!o;
};
NetworkUtils.isArray = NetworkUtils.nativeIsArray || function(obj) {
return NetworkUtils.toString.call(obj) === '[object Array]';
};
NetworkUtils.isString = function(o) {
return typeof o === 'string';
};
NetworkUtils.isNotEmptyString = function(s) {
return NetworkUtils.isString(s) && s !== '';
};
NetworkUtils.each = function(o, fn, ctx) {
if (o == null) return;
if (NetworkUtils.nativeForEach && o.forEach === NetworkUtils.nativeForEach) {
o.forEach(fn, ctx);
} else if (o.length === +o.length) {
for (var i = 0, l = o.length; i < l; i++) {
if (i in o && fn.call(ctx, o[i], i, o) === {}) return;
}
} else {
for (var key in o) {
if (NetworkUtils.hasOwn.call(o, key)) {
if (fn.call(ctx, o[key], key, o) === {}) return;
}
}
}
};
NetworkUtils.numFormat = function(num) {
if (num > 9999) {
return Math.floor(num / 1000).toString() + 'K';
} else {
return num.toString();
}
}; //1000=>1,000
NetworkUtils.numberFmt = function(num) {
if (!/^(\+|-)?(\d+)(\.\d+)?$/.test(num)) {
return num;
}
var a = RegExp.$1,
b = RegExp.$2,
c = RegExp.$3,
re = new RegExp();
re.compile("(\\d)(\\d{3})(,|$)");
while (re.test(b)) {
b = b.replace(re, "$1,$2$3");
}
return a + "" + b + "" + c;
}; //1,000=>1000
NetworkUtils.fmtNumber = function(str) {
if (!NetworkUtils.isNotEmptyString(str)) return 0;
return parseInt(str.replace(/,/g, ''), 10);
};
NetworkUtils.defaults = function(obj) {
NetworkUtils.each(NetworkUtils.slice.call(arguments, 1), function(o) {
for (var k in o) {
if (obj[k] == null)
obj[k] = o[k];
}
});
return obj;
};
NetworkUtils.keys = function(obj) {
if (!NetworkUtils.isObject(obj)) return [];
if (NetworkUtils.nativeKeys) return NetworkUtils.nativeKeys(obj);
var keys = [];
for (var key in obj) {
if (NetworkUtils.hasOwn.call(obj, key)) keys.push(key);
}
return keys;
};
NetworkUtils.values = function(obj) {
var keys = NetworkUtils.keys(obj);
var length = keys.length;
var values = Array(length);
for (var i = 0; i < length; i++) {
values[i] = obj[keys[i]];
}
return values;
};
NetworkUtils.noop = function() {};
NetworkUtils.cutstr = function(str, len) {
var temp,
icount = 0,
patrn = /[^\x00-\xff]/,
strre = "";
for (var i = 0; i < str.length; i++) {
if (icount < len - 1) {
temp = str.substr(i, 1);
if (patrn.exec(temp) == null) {
icount = icount + 1;
} else {
icount = icount + 2;
}
strre += temp;
} else {
break;
}
}
if (str == strre) {
return strre;
} else {
return strre + "...";
}
};
NetworkUtils.clamp = function(n, min, max) {
if (n < min) return min;
if (n > max) return max;
return n;
};
NetworkUtils.Progress = {};
NetworkUtils.Progress.settings = {
minimum: 0.1,
trickle: true,
trickleRate: 0.3,
trickleSpeed: 100
};
NetworkUtils.Progress.status = null;
NetworkUtils.Progress.set = function(n) {
var progress = NetworkUtils.Progress;
n = NetworkUtils.clamp(n, progress.settings.minimum, 1);
progress.status = n;
progress.cb(progress.status);
return this;
};
NetworkUtils.Progress.inc = function(amount) {
var progress = NetworkUtils.Progress,
n = progress.status;
if (!n) {
return progress.start();
} else {
amount = (1 - n) * NetworkUtils.clamp(Math.random() * n, 0.1, 0.95);
n = NetworkUtils.clamp(n + amount, 0, 0.994);
return progress.set(n);
}
};
NetworkUtils.Progress.trickle = function() {
var progress = NetworkUtils.Progress;
return progress.inc(Math.random() * progress.settings.trickleRate);
};
NetworkUtils.Progress.start = function(cb) {
var progress = NetworkUtils.Progress;
progress.cb = cb || NetworkUtils.noop;
if (!progress.status) progress.set(0);
var _timer = function timer() {
if (progress.status === 1) {
clearTimeout(_timer);
_timer = null;
return;
}
progress.trickle();
work();
};
var work = function work() {
setTimeout(_timer, progress.settings.trickleSpeed);
};
if (progress.settings.trickle) work();
return this;
};
NetworkUtils.Progress.done = function() {
var progress = NetworkUtils.Progress;
return progress.inc(0.3 + 0.5 * Math.random()).set(1);
};
NetworkUtils.decode = decodeURIComponent;
NetworkUtils.encode = encodeURIComponent;
NetworkUtils.formData = function(o) {
var kvps = [],
regEx = /%20/g;
for (var k in o) {
if (!o[k]) continue;
kvps.push(NetworkUtils.encode(k).replace(regEx, "+") + "=" + NetworkUtils.encode(o[k].toString()).replace(regEx, "+"));
}
return kvps.join('&');
};
NetworkUtils.ajax = function(o) {
var xhr = cc.loader.getXMLHttpRequest();
o = Object.assign({
type: "GET",
data: null,
dataType: 'json',
progress: null,
contentType: "application/x-www-form-urlencoded"
}, o);
if (o.progress) NetworkUtils.Progress.start(o.progress);
xhr.onreadystatechange = function() {
if (xhr.readyState == 4) {
if (xhr.status < 300) {
var res;
if (o.dataType == 'json') {
if (xhr.responseText) {
res = window.JSON ? window.JSON.parse(xhr.responseText) : eval(xhr.responseText);
}
} else {
res = xhr.responseText;
}
if (!!res) o.success(res);
if (o.progress) NetworkUtils.Progress.done();
} else {
if (o.error) o.error(xhr, xhr.status, xhr.statusText);
}
}
}; //if("withCredentials" in xhr) xhr.withCredentials = true;
var url = o.url,
data = null;
var isPost = o.type === "POST" || o.type === "PUT";
if (o.data) {
if (!isPost) {
url += "?" + NetworkUtils.formData(o.data);
data = null;
} else if (isPost && _typeof(o.data) === 'object') {
data = NetworkUtils.formData(o.data);
} else {
data = o.data;
}
}
xhr.open(o.type, url, true);
if (isPost) {
xhr.setRequestHeader("Content-Type", o.contentType);
}
xhr.timeout = 3000;
xhr.ontimeout = function() {
// XMLHttpRequest 超时
if ('function' === typeof o.timeout) {
o.timeout();
}
};
xhr.onerror = function() {
if ('function' === typeof o.error) {
o.error();
}
}
xhr.send(data);
return xhr;
};
NetworkUtils.get = function(url, data, success, error) {
if (NetworkUtils.isFunction(data)) {
error = success;
success = data;
data = {};
}
NetworkUtils.ajax({
url: url,
type: "GET",
data: data,
success: success,
error: error || NetworkUtils.noop
});
};
NetworkUtils.post = function(url, data, success, error, timeout) {
if (NetworkUtils.isFunction(data)) {
error = success;
success = data;
data = {};
}
NetworkUtils.ajax({
url: url,
type: "POST",
data: data,
success: success,
error: error || NetworkUtils.noop,
timeout: timeout
});
};
NetworkUtils.now = Date.now || function() {
return new Date().getTime();
};
NetworkUtils.same = function(s) {
return s;
};
NetworkUtils.parseCookieString = function(text) {
var cookies = {};
if (NetworkUtils.isString(text) && text.length > 0) {
var cookieParts = text.split(/;\s/g);
var cookieName;
var cookieValue;
var cookieNameValue;
for (var i = 0, len = cookieParts.length; i < len; i++) {
// Check for normally-formatted cookie (name-value)
cookieNameValue = cookieParts[i].match(/([^=]+)=/i);
if (cookieNameValue instanceof Array) {
try {
cookieName = NetworkUtils.decode(cookieNameValue[1]);
cookieValue = cookieParts[i].substring(cookieNameValue[1].length + 1);
} catch (ex) { // Intentionally ignore the cookie -
// the encoding is wrong
}
} else {
// Means the cookie does not have an =", so treat it as
// a boolean flag
cookieName = NetworkUtils.decode(cookieParts[i]);
cookieValue = '';
}
if (cookieName) {
cookies[cookieName] = cookieValue;
}
}
}
return cookies;
};
NetworkUtils.getCookie = function(name) {
if (!NetworkUtils.isNotEmptyString(name)) {
throw new TypeError('Cookie name must be a non-empty string');
}
var cookies = NetworkUtils.parseCookieString(document.cookie);
return cookies[name];
};
NetworkUtils.setCookie = function(name, value, options) {
if (!NetworkUtils.isNotEmptyString(name)) {
throw new TypeError('Cookie name must be a non-empty string');
}
options = options || {};
var expires = options['expires'];
var domain = options['domain'];
var path = options['path'];
if (!options['raw']) {
value = NetworkUtils.encode(String(value));
}
var text = name + '=' + value; // expires
var date = expires;
if (typeof date === 'number') {
date = new Date();
date.setDate(date.getDate() + expires);
}
if (date instanceof Date) {
text += '; expires=' + date.toUTCString();
} // domain
if (NetworkUtils.isNotEmptyString(domain)) {
text += '; domain=' + domain;
} // path
if (NetworkUtils.isNotEmptyString(path)) {
text += '; path=' + path;
} // secure
if (options['secure']) {
text += '; secure';
}
document.cookie = text;
return text;
};
NetworkUtils.removeCookie = function(name, options) {
options = options || {};
options['expires'] = new Date(0);
return NetworkUtils.setCookie(name, '', options);
};
NetworkUtils.dragNode = function(node) {
var isMoving = false,
size = cc.director.getVisibleSize(),
touchLoc = void 0,
oldPos = void 0,
moveToPos = void 0;
node.on(cc.Node.EventType.TOUCH_START, function(event) {
var touches = event.getTouches();
touchLoc = touches[0].getLocation();
oldPos = node.position;
});
node.on(cc.Node.EventType.TOUCH_MOVE, function(event) {
var touches = event.getTouches();
moveToPos = touches[0].getLocation();
isMoving = true;
});
node.on(cc.Node.EventType.TOUCH_END, function(event) {
isMoving = false;
});
return function() {
if (!isMoving) return;
var x = oldPos.x + moveToPos.x - touchLoc.x;
var xEdge = node.width * node.anchorX / 2;
if (Math.abs(x) < xEdge) {
node.x = x;
} else {
node.x = x > 0 ? xEdge : -xEdge;
isMoving = false;
}
if (node.height > size.height) {
var y = oldPos.y + moveToPos.y - touchLoc.y;
var yEdge = (node.height - size.height) / 2;
if (Math.abs(y) < yEdge) {
node.y = y;
} else {
node.y = y > 0 ? yEdge : -yEdge;
isMoving = false;
}
}
};
};
NetworkUtils.getQueryVariable = function(key) {
var query = cc.sys.platform == cc.sys.WECHAT_GAME ? '' : window.location.search.substring(1),
vars = query.split('&');
for (var i = 0, l = vars.length; i < l; i++) {
var pair = vars[i].split('=');
if (decodeURIComponent(pair[0]) === key) {
return decodeURIComponent(pair[1]);
}
}
};

View File

@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "c116dddb-470e-47de-af9b-560adb7c78fb",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -0,0 +1,200 @@
"use strict";
window.getQueryParamDict = function() {
// Kindly note that only the first occurrence of duplicated keys will be picked up.
var query = cc.sys.platform == cc.sys.WECHAT_GAME ? '' : window.location.search.substring(1);
var kvPairs = query.split('&');
var toRet = {};
for (var i = 0; i < kvPairs.length; ++i) {
var kAndV = kvPairs[i].split('=');
if (undefined === kAndV || null === kAndV || 2 != kAndV.length) return;
var k = kAndV[0];
var v = decodeURIComponent(kAndV[1]);
toRet[k] = v;
}
return toRet;
}
let IS_USING_WKWECHAT_KERNEL = null;
window.isUsingWebkitWechatKernel = function() {
if (null == IS_USING_WKWECHAT_KERNEL) {
// The extraction of `browserType` might take a considerable amount of time in mobile browser kernels.
IS_USING_WKWECHAT_KERNEL = (cc.sys.BROWSER_TYPE_WECHAT == cc.sys.browserType);
}
return IS_USING_WKWECHAT_KERNEL;
};
let IS_USING_X5_BLINK_KERNEL = null;
window.isUsingX5BlinkKernel = function() {
if (null == IS_USING_X5_BLINK_KERNEL) {
// The extraction of `browserType` might take a considerable amount of time in mobile browser kernels.
IS_USING_X5_BLINK_KERNEL = (cc.sys.BROWSER_TYPE_MOBILE_QQ == cc.sys.browserType);
}
return IS_USING_X5_BLINK_KERNEL;
};
let IS_USING_X5_BLINK_KERNEL_OR_WKWECHAT_KERNEL = null;
window.isUsingX5BlinkKernelOrWebkitWeChatKernel = function() {
if (null == IS_USING_X5_BLINK_KERNEL_OR_WKWECHAT_KERNEL) {
// The extraction of `browserType` might take a considerable amount of time in mobile browser kernels.
IS_USING_X5_BLINK_KERNEL_OR_WKWECHAT_KERNEL = (cc.sys.BROWSER_TYPE_MOBILE_QQ == cc.sys.browserType || cc.sys.BROWSER_TYPE_WECHAT == cc.sys.browserType);
}
return IS_USING_X5_BLINK_KERNEL_OR_WKWECHAT_KERNEL;
};
window.getRandomInt = function(min, max) {
min = Math.ceil(min);
max = Math.floor(max);
return Math.floor(Math.random() * (max - min)) + min;
}
window.safelyAssignParent = function(proposedChild, proposedParent) {
if (proposedChild.parent == proposedParent) return false;
proposedChild.parent = proposedParent;
return true;
};
window.get2dRotation = function(aCCNode) {
// return aCCNode.rotation; // For cc2.0+
return aCCNode.angle; // For cc2.1+
};
window.set2dRotation = function(aCCNode, clockwiseAngle) {
// aCCNode.rotation = angle; // For cc2.0+
aCCNode.angle = -clockwiseAngle; // For cc2.1+
};
window.setLocalZOrder = function(aCCNode, zIndex) {
aCCNode.zIndex = zIndex; // For cc2.0+
};
window.getLocalZOrder = function(aCCNode) {
return aCCNode.zIndex; // For cc2.0+
};
window.safelyAddChild = function(proposedParent, proposedChild) {
if (proposedChild.parent == proposedParent) return false;
setLocalZOrder(proposedChild, getLocalZOrder(proposedParent) + 1);
proposedParent.addChild(proposedChild);
return true;
};
window.setVisible = function(aCCNode) {
aCCNode.opacity = 255;
};
window.setInvisible = function(aCCNode) {
aCCNode.opacity = 0;
};
window.randomProperty = function (obj) {
var keys = Object.keys(obj)
return obj[keys[ keys.length * Math.random() << 0]];
};
window.gidSpriteFrameMap = {};
window.getOrCreateSpriteFrameForGid = function(gid, tiledMapInfo, tilesElListUnderTilesets) {
if (null != gidSpriteFrameMap[gid]) return gidSpriteFrameMap[gid];
if (false == gidSpriteFrameMap[gid]) return null;
var tilesets = tiledMapInfo.getTilesets();
var targetTileset = null;
for (var i = 0; i < tilesets.length; ++i) {
// TODO: Optimize by binary search.
if (gid < tilesets[i].firstGid) continue;
if (i < tilesets.length - 1) {
if (gid >= tilesets[i + 1].firstGid) continue;
}
targetTileset = tilesets[i];
break;
}
if (!targetTileset) return null;
var tileIdWithinTileset = (gid - targetTileset.firstGid);
var tilesElListUnderCurrentTileset = tilesElListUnderTilesets[targetTileset.name + ".tsx"];
var targetTileEl = null;
for (var tileIdx = 0; tileIdx < tilesElListUnderCurrentTileset.length; ++tileIdx) {
var tmpTileEl = tilesElListUnderCurrentTileset[tileIdx];
if (tileIdWithinTileset != parseInt(tmpTileEl.id)) continue;
targetTileEl = tmpTileEl;
break;
}
var tileId = tileIdWithinTileset;
var tilesPerRow = (targetTileset.sourceImage.width / targetTileset._tileSize.width);
var row = parseInt(tileId / tilesPerRow);
var col = (tileId % tilesPerRow);
var offset = cc.v2(targetTileset._tileSize.width * col, targetTileset._tileSize.height * row);
var origSize = targetTileset._tileSize;
var rect = cc.rect(offset.x, offset.y, origSize.width, origSize.height);
var sf = new cc.SpriteFrame(targetTileset.sourceImage, rect, false /* rotated */ , cc.v2() /* DON'T use `offset` here or you will have an offsetted image from the `cc.Sprite.node.anchor`. */, origSize);
const data = {
origSize: targetTileset._tileSize,
spriteFrame: sf,
}
window.gidSpriteFrameMap[gid] = data;
return data;
}
window.gidAnimationClipMap = {};
window.getOrCreateAnimationClipForGid = function(gid, tiledMapInfo, tilesElListUnderTilesets) {
if (null != gidAnimationClipMap[gid]) return gidAnimationClipMap[gid];
if (false == gidAnimationClipMap[gid]) return null;
var tilesets = tiledMapInfo.getTilesets();
var targetTileset = null;
for (var i = 0; i < tilesets.length; ++i) {
// TODO: Optimize by binary search.
if (gid < tilesets[i].firstGid) continue;
if (i < tilesets.length - 1) {
if (gid >= tilesets[i + 1].firstGid) continue;
}
targetTileset = tilesets[i];
break;
}
if (!targetTileset) return null;
var tileIdWithinTileset = (gid - targetTileset.firstGid);
var tilesElListUnderCurrentTileset = tilesElListUnderTilesets[targetTileset.name + ".tsx"];
var targetTileEl = null;
for (var tileIdx = 0; tileIdx < tilesElListUnderCurrentTileset.length; ++tileIdx) {
var tmpTileEl = tilesElListUnderCurrentTileset[tileIdx];
if (tileIdWithinTileset != parseInt(tmpTileEl.id)) continue;
targetTileEl = tmpTileEl;
break;
}
if (!targetTileEl) return null;
var animElList = targetTileEl.getElementsByTagName("animation");
if (!animElList || 0 >= animElList.length) return null;
var animEl = animElList[0];
var uniformDurationSecondsPerFrame = null;
var totDurationSeconds = 0;
var sfList = [];
var frameElListUnderAnim = animEl.getElementsByTagName("frame");
var tilesPerRow = (targetTileset.sourceImage.width/targetTileset._tileSize.width);
for (var k = 0; k < frameElListUnderAnim.length; ++k) {
var frameEl = frameElListUnderAnim[k];
var tileId = parseInt(frameEl.attributes.tileid.value);
var durationSeconds = frameEl.attributes.duration.value/1000;
if (null == uniformDurationSecondsPerFrame) uniformDurationSecondsPerFrame = durationSeconds;
totDurationSeconds += durationSeconds;
var row = parseInt(tileId / tilesPerRow);
var col = (tileId % tilesPerRow);
var offset = cc.v2(targetTileset._tileSize.width*col, targetTileset._tileSize.height*row);
var origSize = targetTileset._tileSize;
var rect = cc.rect(offset.x, offset.y, origSize.width, origSize.height);
var sf = new cc.SpriteFrame(targetTileset.sourceImage, rect, false /* rotated */, cc.v2(), origSize);
sfList.push(sf);
}
var sampleRate = 1/uniformDurationSecondsPerFrame; // A.k.a. fps.
var animClip = cc.AnimationClip.createWithSpriteFrames(sfList, sampleRate);
// http://docs.cocos.com/creator/api/en/enums/WrapMode.html.
animClip.wrapMode = cc.WrapMode.Loop;
return {
origSize: targetTileset._tileSize,
animationClip: animClip,
};
};

View File

@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "c119f5bb-720a-4ac8-960f-6f5eeda0c360",
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -0,0 +1,37 @@
"use strict";
if (CC_DEBUG) {
var backendAddress = {
PROTOCOL: 'http',
HOST: 'localhost',
PORT: "9992",
WS_PATH_PREFIX: "/tsrht",
};
var wechatAddress = {
PROTOCOL: "http",
HOST: "119.29.236.44",
PORT: "8089",
PROXY: "",
APPID_LITERAL: "appid=wx5432dc1d6164d4e",
};
} else {
var backendAddress = {
PROTOCOL: 'https',
HOST: 'tsrht.lokcol.com',
PORT: "443",
WS_PATH_PREFIX: "/tsrht",
};
var wechatAddress = {
PROTOCOL: "https",
HOST: "open.weixin.qq.com",
PORT: "",
PROXY: "",
APPID_LITERAL: "appid=wxe7063ab415266544",
};
}
window.language = "zh";
window.backendAddress = backendAddress;
window.wechatAddress = wechatAddress;

View File

@ -0,0 +1,5 @@
{
"ver": "1.0.1",
"uuid": "1712c9ec-6940-4356-a87d-37887608f224",
"subMetas": {}
}

View File

@ -0,0 +1,170 @@
"use strict";
var _ROUTE_PATH;
function _defineProperty(obj, key, value) {
if (key in obj) {
Object.defineProperty(obj, key, {
value: value,
enumerable: true,
configurable: true,
writable: true
});
} else {
obj[key] = value;
}
return obj;
}
var constants = {
BGM: {
DIR_PATH: "resources/musicEffect/",
FILE_NAME: {
TREASURE_PICKEDUP: "TreasurePicked",
CRASHED_BY_TRAP_BULLET: "CrashedByTrapBullet",
HIGH_SCORE_TREASURE_PICKED:"HighScoreTreasurePicked",
COUNT_DOWN_10SEC_TO_END:"countDown10SecToEnd",
BGM: "BGM"
}
},
PLAYER_NAME: {
1: "Merdan",
2: "Monroe",
},
SOCKET_EVENT: {
CONTROL: "control",
SYNC: "sync",
LOGIN: "login",
CREATE: "create"
},
WECHAT: {
AUTHORIZE_PATH: "/connect/oauth2/authorize",
REDIRECT_RUI_KEY: "redirect_uri=",
RESPONSE_TYPE: "response_type=code",
SCOPE: "scope=snsapi_userinfo",
FIN: "#wechat_redirect"
},
ROUTE_PATH: (_ROUTE_PATH = {
PLAYER: "/player",
JSCONFIG: "/jsconfig",
API: "/api",
VERSION: "/v1",
SMS_CAPTCHA: "/SmsCaptcha",
INT_AUTH_TOKEN: "/IntAuthToken",
LOGIN: "/login",
LOGOUT: "/logout",
GET: "/get",
TUTORIAL: "/tutorial",
REPORT: "/report",
LIST: "/list",
READ: "/read",
PROFILE: "/profile",
WECHAT: "/wechat",
WECHATGAME: "/wechatGame",
FETCH: "/fetch",
}, _defineProperty(_ROUTE_PATH, "LOGIN", "/login"), _defineProperty(_ROUTE_PATH, "RET_CODE", "/retCode"), _defineProperty(_ROUTE_PATH, "REGEX", "/regex"), _defineProperty(_ROUTE_PATH, "SMS_CAPTCHA", "/SmsCaptcha"), _defineProperty(_ROUTE_PATH, "GET", "/get"), _ROUTE_PATH),
REQUEST_QUERY: {
ROOM_ID: "roomId",
TOKEN: "/token"
},
GAME_SYNC: {
SERVER_UPSYNC: 30,
CLIENT_UPSYNC: 30
},
RET_CODE: {
/**
* NOTE: The "RET_CODE"s from 1000-1015 are reserved for the websocket "WebsocketStdCloseCode"s.
*
* References
* - https://tools.ietf.org/html/rfc6455#section-7.4
* - https://godoc.org/github.com/gorilla/websocket#pkg-constants.
*/
"__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,
"__comment__": "SMS",
"SMS_CAPTCHA_REQUESTED_TOO_FREQUENTLY": 5001,
"SMS_CAPTCHA_NOT_MATCH": 5002,
"INVALID_TOKEN": 2001,
"DUPLICATED": 2002,
"INCORRECT_HANDLE": 2004,
"NONEXISTENT_HANDLE": 2005,
"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,
"PASSWORD_RESET_CODE_GENERATION_PER_EMAIL_TOO_FREQUENTLY": 4000,
"TRADE_CREATION_TOO_FREQUENTLY": 4002,
"MAP_NOT_UNLOCKED": 4003,
"NOT_IMPLEMENTED_YET": 65535
},
ALERT: {
TIP_NODE: 'captchaTips',
TIP_LABEL: {
INCORRECT_PHONE_COUNTRY_CODE: '国家号不正确',
CAPTCHA_ERR: '验证码不正确',
PHONE_ERR: '手机号格式不正确',
TOKEN_EXPIRED: 'token已过期!',
SMS_CAPTCHA_FREEQUENT_REQUIRE: '请求过于频繁',
SMS_CAPTCHA_NOT_MATCH: '验证码不正确',
TEST_USER: '该账号为测试账号',
INCORRECT_PHONE_NUMBER: '手机号不正确',
LOG_OUT: '您已在其他地方登陆',
GAME_OVER: '游戏结束,您的得分是',
WECHAT_LOGIN_FAILS: "微信登录失败",
},
CONFIRM_BUTTON_LABEL: {
RESTART: '重新开始'
}
},
PLAYER: '玩家',
ONLINE: '在线',
NOT_ONLINE: '',
SPEED: {
NORMAL: 100,
PAUSE: 0
},
COUNTDOWN_LABEL: {
BASE: '倒计时 ',
MINUTE: '00',
SECOND: '30'
},
SCORE_LABEL: {
BASE: '分数 ',
PLUS_SCORE: 5,
MINUS_SECOND: 5,
INIT_SCORE: 0
},
TUTORIAL_STAGE: {
NOT_YET_STARTED: 0,
ENDED: 1,
},
};
window.constants = constants;

View File

@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "31ca9500-50c9-44f9-8d33-f9a969e53195",
"isPlugin": false,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,9 @@
{
"ver": "1.0.5",
"uuid": "bd514df4-095e-4088-9060-d99397a29a4f",
"isPlugin": true,
"loadPluginInWeb": true,
"loadPluginInNative": true,
"loadPluginInEditor": false,
"subMetas": {}
}

View File

@ -0,0 +1,5 @@
{
"ver": "1.0.1",
"uuid": "0bcc8702-42bd-4b5f-b576-6a95eabfe996",
"subMetas": {}
}

View File

@ -0,0 +1,5 @@
{
"ver": "1.0.1",
"uuid": "3b59087c-771e-40f1-b126-2f5ec4bcde3d",
"subMetas": {}
}

View File

@ -0,0 +1,7 @@
{
"ver": "1.0.1",
"uuid": "0e243c83-a137-4880-9bfe-9e1b57adc453",
"isSubpackage": false,
"subpackageName": "",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "Bottom",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "783f1240-d608-40be-8108-3013ab53cfe6"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "393e649b-addb-4f91-b687-438433026c8d"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "115ea7bb-d47f-4d3c-a52a-f46584346c3f",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "BottomLeft",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "748c55f0-e761-40f6-b13b-e416b3d8a55c"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "6164bac7-9882-43ce-b3d0-9d062d6d0b49"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "a1bf7c7c-b9f7-4b65-86e3-f86a9e798fb6",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "BottomRight",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "34cf9fbb-8def-4faf-a56e-123b4c45706c"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "b6709dd6-6ba7-4222-af38-de79ac80ce8b"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "d5af527a-9f0c-4398-b2dd-84426be7bd32",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "Left",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "02cd24e3-1c4a-46d7-85af-9034c9445ba7"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "1f121837-a493-4a41-90e5-74ea560930ad"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "b60618d7-569d-4f13-bdeb-f20341fbadb6",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "Right",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "a6ec6a1c-dde5-459d-84f9-7b2b8a163e7b"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "b5d11244-f30a-4b0d-b67b-23648d253d44"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "0b3fb38e-9110-4191-9b72-6b64a224d049",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "Top",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "8967d249-e9cf-4e44-85e8-6b9377129d9e"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "492a57fb-6a5c-423a-bcfe-0695a7828881"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "1bc6de53-800b-4da3-ab8e-4a45e3aa4230",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "TopLeft",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "96187689-85df-46e8-b4db-410eae03c135"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "6b002583-7688-43d3-b3fa-102ae0046628"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "ee0d670c-893e-4e4d-96dd-5571db18ee97",
"subMetas": {}
}

View File

@ -0,0 +1,31 @@
{
"__type__": "cc.AnimationClip",
"_name": "TopRight",
"_objFlags": 0,
"_native": "",
"_duration": 0.25,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "ba89046f-8b70-4edb-9f61-534dff476325"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "bc1ef316-d6ad-4538-b223-a0fed8094609"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "596df84a-2e4e-4f1d-967c-a82649f564a8",
"subMetas": {}
}

View File

@ -0,0 +1,43 @@
{
"__type__": "cc.AnimationClip",
"_name": "attackedLeft",
"_objFlags": 0,
"_native": "",
"_duration": 0.3333333333333333,
"sample": 12,
"speed": 1,
"wrapMode": "2",
"curveData": {
"comps": {
"cc.Sprite": {
"spriteFrame": [
{
"frame": 0,
"value": {
"__uuid__": "02cd24e3-1c4a-46d7-85af-9034c9445ba7"
}
},
{
"frame": 0.08333333333333333,
"value": {
"__uuid__": "8c522ad3-ee82-41a7-892e-05c05442e2e3"
}
},
{
"frame": 0.16666666666666666,
"value": {
"__uuid__": "1f121837-a493-4a41-90e5-74ea560930ad"
}
},
{
"frame": 0.25,
"value": {
"__uuid__": "1c0ec5ec-51cb-467d-b597-8e69ce580cfd"
}
}
]
}
}
},
"events": []
}

View File

@ -0,0 +1,5 @@
{
"ver": "2.1.0",
"uuid": "8acc4e9f-3c47-4b66-9a9d-d012709680f6",
"subMetas": {}
}

Some files were not shown because too many files have changed in this diff Show More