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