mirror of
https://github.com/tiennm99/gomoku.git
synced 2026-05-24 02:24:49 +00:00
5ccd4e7ce2
Rewrite README with usage guide, deployment instructions, and protocol docs. Update CLAUDE.md to reflect gomoku-only architecture. Add English doc comments to all key server Go files, replacing Chinese comments. Create docs/system-architecture.md (state machine, protocol, database schema) and docs/deployment-guide.md (local dev, Docker, production nginx, resource requirements). Update Dockerfile to Go 1.22 with repo-root build context to include web client. Update docker-compose to match.
425 lines
9.5 KiB
Go
425 lines
9.5 KiB
Go
package database
|
|
|
|
import (
|
|
"sort"
|
|
stringx "strings"
|
|
"sync/atomic"
|
|
"time"
|
|
|
|
"github.com/awesome-cap/hashmap"
|
|
"github.com/ratel-online/core/log"
|
|
modelx "github.com/ratel-online/core/model"
|
|
"github.com/ratel-online/core/network"
|
|
"github.com/ratel-online/core/util/async"
|
|
"github.com/ratel-online/core/util/json"
|
|
"github.com/ratel-online/core/util/strings"
|
|
"github.com/ratel-online/server/consts"
|
|
)
|
|
|
|
// In-memory data store. All state is volatile — server restart clears everything.
|
|
var roomIds int64 = 0
|
|
var players = hashmap.New() // all players ever connected (by ID)
|
|
var connPlayers = hashmap.New() // currently connected players
|
|
var rooms = hashmap.New() // active rooms (by room ID)
|
|
var roomPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
|
|
var roomSpectators = hashmap.New() // map[roomID] -> map[playerID]int (join order)
|
|
var roomKickedPlayers = hashmap.New() // map[roomID] -> map[playerID]bool
|
|
var roomPropsSetter = map[string]func(r *Room, v string){
|
|
consts.RoomPropsPassword: func(r *Room, v string) {
|
|
if v == "off" {
|
|
r.Password = ""
|
|
} else {
|
|
r.Password = v
|
|
}
|
|
},
|
|
consts.RoomPropsShowIP: func(r *Room, v string) {
|
|
r.EnableShowIP = v == "on"
|
|
},
|
|
}
|
|
|
|
func init() {
|
|
async.Async(func() {
|
|
loopCount := 0
|
|
for {
|
|
loopCount++
|
|
if loopCount%60 == 0 {
|
|
log.Infof("[database.init] Room cleanup loop count: %d (running for %d hours)\n", loopCount, loopCount/60)
|
|
}
|
|
time.Sleep(1 * time.Minute)
|
|
rooms.Foreach(func(e *hashmap.Entry) {
|
|
roomCancel(e.Value().(*Room))
|
|
})
|
|
}
|
|
})
|
|
}
|
|
|
|
func Connected(conn *network.Conn, info *modelx.AuthInfo) *Player {
|
|
player := &Player{
|
|
ID: conn.ID(),
|
|
IP: conn.IP(),
|
|
Name: strings.Desensitize(info.Name),
|
|
Amount: 2000,
|
|
}
|
|
player.Conn(conn)
|
|
players.Set(conn.ID(), player)
|
|
connPlayers.Set(conn.ID(), player)
|
|
return player
|
|
}
|
|
|
|
func CreateRoom(creator int64, t int) *Room {
|
|
room := &Room{
|
|
ID: atomic.AddInt64(&roomIds, 1),
|
|
Type: t,
|
|
State: consts.RoomStateWaiting,
|
|
Creator: creator,
|
|
ActiveTime: time.Now(),
|
|
MaxPlayers: 2,
|
|
EnableChat: true,
|
|
}
|
|
roomPlayers.Set(room.ID, map[int64]bool{})
|
|
roomSpectators.Set(room.ID, map[int64]int{})
|
|
rooms.Set(room.ID, room)
|
|
return room
|
|
}
|
|
|
|
func deleteRoom(room *Room) {
|
|
if room != nil {
|
|
rooms.Del(room.ID)
|
|
roomPlayers.Del(room.ID)
|
|
roomSpectators.Del(room.ID)
|
|
if room.Game != nil {
|
|
room.Game.Clean()
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetRooms() []*Room {
|
|
list := make([]*Room, 0)
|
|
rooms.Foreach(func(e *hashmap.Entry) {
|
|
list = append(list, e.Value().(*Room))
|
|
})
|
|
sort.Slice(list, func(i, j int) bool {
|
|
return list[i].ID < list[j].ID
|
|
})
|
|
return list
|
|
}
|
|
|
|
func GetRoom(roomId int64) *Room {
|
|
return getRoom(roomId)
|
|
}
|
|
|
|
func getRoom(roomId int64) *Room {
|
|
if v, ok := rooms.Get(roomId); ok {
|
|
return v.(*Room)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getPlayer(playerId int64) *Player {
|
|
if v, ok := players.Get(playerId); ok {
|
|
return v.(*Player)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func SetRoomProps(room *Room, k, v string) {
|
|
// Only allow properties appropriate for the game type
|
|
allowedProps := getAllowedPropsByGameType(room.Type)
|
|
|
|
if !allowedProps[k] {
|
|
return
|
|
}
|
|
|
|
if setter, ok := roomPropsSetter[k]; ok {
|
|
setter(room, v)
|
|
}
|
|
}
|
|
|
|
// getAllowedPropsByGameType returns allowed room properties for each game type.
|
|
func getAllowedPropsByGameType(gameType int) map[string]bool {
|
|
return map[string]bool{
|
|
consts.RoomPropsShowIP: true,
|
|
consts.RoomPropsPassword: true,
|
|
}
|
|
}
|
|
|
|
func getRoomPlayers(roomId int64) map[int64]bool {
|
|
if v, ok := roomPlayers.Get(roomId); ok {
|
|
return v.(map[int64]bool)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func getRoomSpectators(roomId int64) map[int64]int {
|
|
if v, ok := roomSpectators.Get(roomId); ok {
|
|
return v.(map[int64]int)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func IsValidPlayer(roomId, playerId int64) bool {
|
|
room := getRoom(roomId)
|
|
if room != nil {
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
playersIds := getRoomPlayers(roomId)
|
|
if playersIds != nil {
|
|
_, exists := playersIds[playerId]
|
|
if exists {
|
|
return true
|
|
}
|
|
}
|
|
spectatorsIds := getRoomSpectators(roomId)
|
|
if spectatorsIds != nil {
|
|
_, exists := spectatorsIds[playerId]
|
|
if exists {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
return false
|
|
}
|
|
|
|
// JoinRoom adds a player to a room. If the room is full or running, the player
|
|
// becomes a spectator instead. Returns an error if the player was previously kicked.
|
|
func JoinRoom(roomId, playerId int64) error {
|
|
player := getPlayer(playerId)
|
|
if player == nil {
|
|
return consts.ErrorsExist
|
|
}
|
|
room := getRoom(roomId)
|
|
if room == nil {
|
|
return consts.ErrorsRoomInvalid
|
|
}
|
|
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
|
|
if hasKicked(roomId, playerId) {
|
|
return consts.ErrorsJoinFailForKicked
|
|
}
|
|
|
|
room.ActiveTime = time.Now()
|
|
|
|
if room.Players >= room.MaxPlayers || room.State == consts.RoomStateRunning {
|
|
spectatorsIds := getRoomSpectators(roomId)
|
|
spectatorsIds[playerId] = len(spectatorsIds)
|
|
player.RoomID = roomId
|
|
player.Role = RoleSpectator
|
|
} else {
|
|
playersIds := getRoomPlayers(roomId)
|
|
playersIds[playerId] = true
|
|
room.Players++
|
|
player.RoomID = roomId
|
|
player.Role = RolePlayer
|
|
if room.Creator == playerId {
|
|
player.Role = RoleOwner
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func LeaveRoom(roomId, playerId int64) {
|
|
room := getRoom(roomId)
|
|
if room != nil {
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
leaveRoom(room, getPlayer(playerId))
|
|
}
|
|
}
|
|
|
|
func Backfill(roomId int64) *Player {
|
|
room := getRoom(roomId)
|
|
if room != nil {
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
return backfill(room)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func backfill(room *Room) *Player {
|
|
if room.Players >= room.MaxPlayers {
|
|
return nil
|
|
}
|
|
spectatorsIds := getRoomSpectators(room.ID)
|
|
if len(spectatorsIds) == 0 {
|
|
return nil
|
|
}
|
|
spectators := make([]struct {
|
|
id int64
|
|
index int
|
|
}, 0)
|
|
|
|
for id, index := range spectatorsIds {
|
|
spectators = append(spectators, struct {
|
|
id int64
|
|
index int
|
|
}{id: id, index: index})
|
|
}
|
|
sort.Slice(spectators, func(i, j int) bool {
|
|
return spectators[i].index < spectators[j].index
|
|
})
|
|
|
|
playerId := spectators[0].id
|
|
|
|
delete(spectatorsIds, playerId)
|
|
playersIds := getRoomPlayers(room.ID)
|
|
playersIds[playerId] = true
|
|
room.Players++
|
|
player := getPlayer(playerId)
|
|
if player != nil {
|
|
player.Role = RolePlayer
|
|
}
|
|
return player
|
|
}
|
|
|
|
func Kicking(roomId, playerId int64) {
|
|
room := getRoom(roomId)
|
|
if room != nil {
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
leaveRoom(room, getPlayer(playerId))
|
|
|
|
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
|
|
if !ok {
|
|
kickedPlayers = map[int64]bool{}
|
|
roomKickedPlayers.Set(roomId, kickedPlayers)
|
|
}
|
|
kickedPlayers.(map[int64]bool)[playerId] = true
|
|
}
|
|
}
|
|
|
|
func hasKicked(roomId, playerId int64) bool {
|
|
kickedPlayers, ok := roomKickedPlayers.Get(roomId)
|
|
if !ok {
|
|
return false
|
|
}
|
|
_, exists := kickedPlayers.(map[int64]bool)[playerId]
|
|
return exists
|
|
}
|
|
|
|
func leaveRoom(room *Room, player *Player) {
|
|
if room == nil || player == nil {
|
|
return
|
|
}
|
|
room.ActiveTime = time.Now()
|
|
playersIds := getRoomPlayers(room.ID)
|
|
if _, ok := playersIds[player.ID]; ok {
|
|
room.Players--
|
|
player.RoomID = 0
|
|
player.Role = ""
|
|
delete(playersIds, player.ID)
|
|
if len(playersIds) > 0 && room.Creator == player.ID {
|
|
for k := range playersIds {
|
|
room.Creator = k
|
|
if p := getPlayer(k); p != nil {
|
|
p.Role = RoleOwner
|
|
}
|
|
break
|
|
}
|
|
}
|
|
}
|
|
spectatorsIds := getRoomSpectators(room.ID)
|
|
if _, ok := spectatorsIds[player.ID]; ok {
|
|
player.RoomID = 0
|
|
player.Role = ""
|
|
delete(spectatorsIds, player.ID)
|
|
}
|
|
if len(playersIds) == 0 && len(spectatorsIds) == 0 {
|
|
deleteRoom(room)
|
|
}
|
|
}
|
|
|
|
func roomCancel(room *Room) {
|
|
if room.ActiveTime.Add(24 * time.Hour).Before(time.Now()) {
|
|
log.Infof("room %d is timeout 24 hours, removed.\n", room.ID)
|
|
deleteRoom(room)
|
|
return
|
|
}
|
|
living := false
|
|
playerIds := getRoomPlayers(room.ID)
|
|
for id := range playerIds {
|
|
if getPlayer(id).online {
|
|
living = true
|
|
break
|
|
}
|
|
}
|
|
spectatorIds := getRoomSpectators(room.ID)
|
|
for id := range spectatorIds {
|
|
if getPlayer(id).online {
|
|
living = true
|
|
break
|
|
}
|
|
}
|
|
if !living {
|
|
log.Infof("room %d is not living, removed.\n", room.ID)
|
|
deleteRoom(room)
|
|
}
|
|
}
|
|
|
|
func RoomPlayers(roomId int64) map[int64]bool {
|
|
return getRoomPlayers(roomId)
|
|
}
|
|
|
|
func RoomSpectators(roomId int64) map[int64]int {
|
|
return getRoomSpectators(roomId)
|
|
}
|
|
|
|
func broadcast(room *Room, msg string, exclude ...int64) {
|
|
room.ActiveTime = time.Now()
|
|
excludeSet := map[int64]bool{}
|
|
for _, exc := range exclude {
|
|
excludeSet[exc] = true
|
|
}
|
|
for playerId := range getRoomPlayers(room.ID) {
|
|
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
|
|
_ = player.WriteString(">> " + msg)
|
|
}
|
|
}
|
|
for playerId := range getRoomSpectators(room.ID) {
|
|
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
|
|
_ = player.WriteString(">> " + msg)
|
|
}
|
|
}
|
|
}
|
|
|
|
func Broadcast(roomId int64, msg string, exclude ...int64) {
|
|
room := getRoom(roomId)
|
|
if room == nil {
|
|
return
|
|
}
|
|
broadcast(room, msg, exclude...)
|
|
}
|
|
|
|
func BroadcastChat(player *Player, msg string, exclude ...int64) {
|
|
log.Infof("chat msg, player %s[%d] %s say: %s\n", player.Name, player.ID, player.IP, stringx.TrimSpace(msg))
|
|
Broadcast(player.RoomID, strings.Desensitize(msg), exclude...)
|
|
}
|
|
|
|
func BroadcastObject(roomId int64, object interface{}, exclude ...int64) {
|
|
room := getRoom(roomId)
|
|
if room == nil {
|
|
return
|
|
}
|
|
room.Lock()
|
|
defer room.Unlock()
|
|
excludeSet := map[int64]bool{}
|
|
for _, exc := range exclude {
|
|
excludeSet[exc] = true
|
|
}
|
|
msg := json.Marshal(object)
|
|
playerIds := getRoomPlayers(roomId)
|
|
for playerId := range playerIds {
|
|
if player := getPlayer(playerId); player != nil && !excludeSet[playerId] {
|
|
_ = player.WriteString(string(msg))
|
|
}
|
|
}
|
|
}
|
|
|
|
func GetPlayer(playerId int64) *Player {
|
|
return getPlayer(playerId)
|
|
}
|