diff --git a/.gitignore b/.gitignore
index 28f9fce..d7596c5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -22,4 +22,6 @@ log/*
cmake-build-debug
+server.exe
+
diff --git a/admin/admin.go b/admin/admin.go
new file mode 100644
index 0000000..77fd4d8
--- /dev/null
+++ b/admin/admin.go
@@ -0,0 +1,118 @@
+package admin
+
+import (
+ "Embalming_Girl_Server/db"
+ "encoding/json"
+ "fmt"
+ "net/http"
+)
+
+const AdminPassword = "admin123"
+
+func Register(mux *http.ServeMux, getStats func() map[string]interface{}) {
+ mux.HandleFunc("/admin", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "text/html; charset=utf-8")
+ fmt.Fprint(w, adminHTML)
+ })
+ mux.HandleFunc("/api/admin/login", func(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ Password string `json:"password"`
+ }
+ json.NewDecoder(r.Body).Decode(&req)
+ if req.Password != AdminPassword {
+ json.NewEncoder(w).Encode(map[string]interface{}{"ok": false})
+ return
+ }
+ json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
+ })
+ mux.HandleFunc("/api/admin/stats", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(getStats())
+ })
+ mux.HandleFunc("/api/admin/users", func(w http.ResponseWriter, r *http.Request) {
+ rows, err := db.DB.Query("SELECT id, username, nickname, bio, avatar, created_at, last_login FROM users ORDER BY id DESC")
+ if err != nil {
+ json.NewEncoder(w).Encode([]int{})
+ return
+ }
+ defer rows.Close()
+ var users []map[string]interface{}
+ for rows.Next() {
+ var id int64
+ var username, nickname, bio, avatar string
+ var created, login interface{}
+ rows.Scan(&id, &username, &nickname, &bio, &avatar, &created, &login)
+ users = append(users, map[string]interface{}{
+ "id": id, "username": username, "nickname": nickname,
+ "bio": bio, "avatar": avatar, "created_at": created, "last_login": login,
+ })
+ }
+ w.Header().Set("Content-Type", "application/json")
+ json.NewEncoder(w).Encode(users)
+ })
+ mux.HandleFunc("/api/admin/delete_user", func(w http.ResponseWriter, r *http.Request) {
+ var req struct {
+ ID int64 `json:"id"`
+ }
+ json.NewDecoder(r.Body).Decode(&req)
+ db.DB.Exec("DELETE FROM sessions WHERE user_id=?", req.ID)
+ db.DB.Exec("DELETE FROM users WHERE id=?", req.ID)
+ json.NewEncoder(w).Encode(map[string]interface{}{"ok": true})
+ })
+}
+
+const adminHTML = `
管理面板
+
+管理面板
+
+Embalming Girl 管理面板
+
+
+
+`
diff --git a/db/database.go b/db/database.go
new file mode 100644
index 0000000..a960f8e
--- /dev/null
+++ b/db/database.go
@@ -0,0 +1,143 @@
+package db
+
+import (
+ "crypto/rand"
+ "crypto/sha256"
+ "database/sql"
+ "encoding/hex"
+ "fmt"
+ "os"
+ "path/filepath"
+ "time"
+
+ _ "modernc.org/sqlite"
+)
+
+var DB *sql.DB
+
+func Init() error {
+ dir, _ := os.Executable()
+ dbPath := filepath.Join(filepath.Dir(dir), "data", "game.db")
+ os.MkdirAll(filepath.Dir(dbPath), 0755)
+
+ var err error
+ DB, err = sql.Open("sqlite", dbPath)
+ if err != nil {
+ return err
+ }
+ _, err = DB.Exec(`
+ CREATE TABLE IF NOT EXISTS users (
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
+ username TEXT UNIQUE NOT NULL,
+ password_hash TEXT NOT NULL,
+ salt TEXT NOT NULL,
+ nickname TEXT NOT NULL,
+ bio TEXT DEFAULT '',
+ avatar TEXT DEFAULT '',
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ last_login TIMESTAMP
+ );
+ CREATE TABLE IF NOT EXISTS sessions (
+ token TEXT PRIMARY KEY,
+ user_id INTEGER NOT NULL,
+ username TEXT NOT NULL,
+ nickname TEXT NOT NULL,
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
+ FOREIGN KEY (user_id) REFERENCES users(id)
+ );
+ `)
+ return err
+}
+
+type User struct {
+ ID int64
+ Username string
+ Nickname string
+ Bio string
+ Avatar string
+}
+
+func hashPassword(password, salt string) string {
+ h := sha256.Sum256([]byte(password + salt))
+ return hex.EncodeToString(h[:])
+}
+
+func generateSalt() string {
+ b := make([]byte, 16)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func generateToken() string {
+ b := make([]byte, 32)
+ rand.Read(b)
+ return hex.EncodeToString(b)
+}
+
+func Register(username, password, nickname string) (*User, error) {
+ if len(username) < 2 || len(password) < 4 {
+ return nil, fmt.Errorf("用户名至少2位,密码至少4位")
+ }
+ if nickname == "" {
+ nickname = username
+ }
+ salt := generateSalt()
+ hash := hashPassword(password, salt)
+ res, err := DB.Exec("INSERT INTO users (username, password_hash, salt, nickname) VALUES (?, ?, ?, ?)",
+ username, hash, salt, nickname)
+ if err != nil {
+ return nil, fmt.Errorf("用户名已存在")
+ }
+ id, _ := res.LastInsertId()
+ return &User{ID: id, Username: username, Nickname: nickname}, nil
+}
+
+func Login(username, password string) (string, *User, error) {
+ var id int64
+ var hash, salt, nickname, bio, avatar string
+ err := DB.QueryRow("SELECT id, password_hash, salt, nickname, bio, avatar FROM users WHERE username = ?",
+ username).Scan(&id, &hash, &salt, &nickname, &bio, &avatar)
+ if err != nil {
+ return "", nil, fmt.Errorf("用户不存在")
+ }
+ if hashPassword(password, salt) != hash {
+ return "", nil, fmt.Errorf("密码错误")
+ }
+ DB.Exec("UPDATE users SET last_login = ? WHERE id = ?", time.Now(), id)
+
+ token := generateToken()
+ DB.Exec("INSERT OR REPLACE INTO sessions (token, user_id, username, nickname) VALUES (?, ?, ?, ?)",
+ token, id, username, nickname)
+
+ return token, &User{ID: id, Username: username, Nickname: nickname, Bio: bio, Avatar: avatar}, nil
+}
+
+func ValidateToken(token string) (*User, error) {
+ var id int64
+ var username, nickname string
+ err := DB.QueryRow("SELECT user_id, username, nickname FROM sessions WHERE token = ?", token).
+ Scan(&id, &username, &nickname)
+ if err != nil {
+ return nil, fmt.Errorf("无效的令牌")
+ }
+ return &User{ID: id, Username: username, Nickname: nickname}, nil
+}
+
+func Logout(token string) {
+ DB.Exec("DELETE FROM sessions WHERE token = ?", token)
+}
+
+func UpdateProfile(userID int64, nickname, bio, avatar string) error {
+ _, err := DB.Exec("UPDATE users SET nickname=?, bio=?, avatar=? WHERE id=?", nickname, bio, avatar, userID)
+ return err
+}
+
+func GetProfile(username string) (*User, error) {
+ var u User
+ err := DB.QueryRow("SELECT id, username, nickname, bio, avatar FROM users WHERE username=?", username).
+ Scan(&u.ID, &u.Username, &u.Nickname, &u.Bio, &u.Avatar)
+ if err != nil {
+ return nil, fmt.Errorf("用户不存在")
+ }
+ return &u, nil
+}
diff --git a/game/engine.go b/game/engine.go
index 477631f..0de4c17 100644
--- a/game/engine.go
+++ b/game/engine.go
@@ -29,9 +29,14 @@ type Engine struct {
SelectedCard *Card
SelectedBy string
Effect *EffectState
+ ChaosMode bool
+ ActionLogs []string
rng *rand.Rand
}
+func (e *Engine) log(msg string) { e.ActionLogs = append(e.ActionLogs, msg) }
+func (e *Engine) FlushLogs() []string { l := e.ActionLogs; e.ActionLogs = nil; return l }
+
func NewEngine(playerIDs []string, nicknames []string, botFlags []bool, playerCount int) *Engine {
e := &Engine{
Phase: "dealing",
@@ -152,6 +157,7 @@ func (e *Engine) doSkill(p *PlayerState) error {
card := e.SelectedCard
e.removeFromHand(p, card.UID)
p.SkillZone = append(p.SkillZone, card)
+ e.log(p.Nickname + " 使用了特技「" + card.Name + "」")
e.SelectedCard = nil
e.SelectedBy = ""
@@ -167,6 +173,7 @@ func (e *Engine) doHarmony(p *PlayerState) error {
card := e.SelectedCard
e.removeFromHand(p, card.UID)
e.HarmonyZone = append(e.HarmonyZone, card)
+ e.log(p.Nickname + " 将一张牌放入了调和区")
e.SelectedCard = nil
e.SelectedBy = ""
e.finishTurn(p)
@@ -184,6 +191,7 @@ func (e *Engine) doChallenge(p *PlayerState, targetID string) error {
card := e.SelectedCard
e.removeFromHand(p, card.UID)
target.ChallengeZone = append(target.ChallengeZone, card)
+ e.log(p.Nickname + " 质疑了 " + target.Nickname)
e.SelectedCard = nil
e.SelectedBy = ""
e.finishTurn(p)
@@ -193,6 +201,7 @@ func (e *Engine) doChallenge(p *PlayerState, targetID string) error {
func (e *Engine) finishTurn(p *PlayerState) {
if len(p.Hand) <= 1 {
p.IsExited = true
+ e.log(p.Nickname + " 暂时退出了游戏")
}
if e.allExited() {
e.Phase = "all_exited"
@@ -237,6 +246,16 @@ func (e *Engine) IsEffectResolving() bool {
return e.Effect != nil && e.Effect.Active
}
+func (e *Engine) IsHandTargetable(p *PlayerState) bool {
+ if len(p.Hand) == 0 {
+ return false
+ }
+ if e.ChaosMode {
+ return true
+ }
+ return !p.IsExited
+}
+
func (e *Engine) EffectNeedsResponseFrom(playerID string) bool {
if !e.IsEffectResolving() {
return false
diff --git a/go.mod b/go.mod
index 0618892..efe22bf 100644
--- a/go.mod
+++ b/go.mod
@@ -2,4 +2,19 @@ module Embalming_Girl_Server
go 1.22
-require github.com/gorilla/websocket v1.5.3
+require (
+ github.com/gorilla/websocket v1.5.3
+ modernc.org/sqlite v1.34.5
+)
+
+require (
+ github.com/dustin/go-humanize v1.0.1 // indirect
+ github.com/google/uuid v1.6.0 // indirect
+ github.com/mattn/go-isatty v0.0.20 // indirect
+ github.com/ncruces/go-strftime v0.1.9 // indirect
+ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
+ golang.org/x/sys v0.22.0 // indirect
+ modernc.org/libc v1.55.3 // indirect
+ modernc.org/mathutil v1.6.0 // indirect
+ modernc.org/memory v1.8.0 // indirect
+)
diff --git a/go.sum b/go.sum
index 25a9fc4..5282d57 100644
--- a/go.sum
+++ b/go.sum
@@ -1,2 +1,45 @@
+github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
+github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo=
+github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
+github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
+github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
+github.com/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
+github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
+github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
+golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
+golang.org/x/mod v0.16.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
+golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/tools v0.19.0 h1:tfGCXNR1OsFG+sVdLAitlpjAvD/I6dHDKnYrpEZUHkw=
+golang.org/x/tools v0.19.0/go.mod h1:qoJWxmGSIBmAeriMx19ogtrEPrGtDbPK634QFIcLAhc=
+modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ=
+modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ=
+modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y=
+modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s=
+modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE=
+modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ=
+modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw=
+modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU=
+modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U=
+modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w=
+modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4=
+modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo=
+modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E=
+modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU=
+modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4=
+modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0=
+modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc=
+modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss=
+modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g=
+modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE=
+modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
+modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
+modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
+modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
diff --git a/handler/ws.go b/handler/ws.go
index 65655ee..3ee9d59 100644
--- a/handler/ws.go
+++ b/handler/ws.go
@@ -1,6 +1,7 @@
package handler
import (
+ "Embalming_Girl_Server/db"
"Embalming_Girl_Server/game"
"Embalming_Girl_Server/room"
"encoding/json"
@@ -18,11 +19,15 @@ var upgrader = websocket.Upgrader{
}
type Client struct {
- ID string
- RoomID string
- Conn *websocket.Conn
- Send chan []byte
- mu sync.Mutex
+ ID string
+ RoomID string
+ UserID int64
+ Username string
+ Nickname string
+ Token string
+ Conn *websocket.Conn
+ Send chan []byte
+ mu sync.Mutex
}
type Hub struct {
@@ -40,6 +45,12 @@ func NewHub() *Hub {
}
}
+func (h *Hub) OnlineCount() int {
+ h.mu.RLock()
+ defer h.mu.RUnlock()
+ return len(h.clients)
+}
+
func (h *Hub) InitTestRooms() {
for _, n := range []int{3, 4, 5, 6} {
rid := "TEST" + string(rune('0'+n))
@@ -113,6 +124,14 @@ func (h *Hub) handleMessage(c *Client, msg map[string]interface{}) {
t, _ := msg["type"].(string)
p, _ := msg["payload"].(map[string]interface{})
switch t {
+ case "register":
+ h.handleRegister(c, p)
+ case "login":
+ h.handleLogin(c, p)
+ case "update_profile":
+ h.handleUpdateProfile(c, p)
+ case "get_profile":
+ h.handleGetProfile(c, p)
case "create_room":
h.handleCreateRoom(c, p)
case "join_room":
@@ -133,6 +152,8 @@ func (h *Hub) handleMessage(c *Client, msg map[string]interface{}) {
h.handleEffectResponse(c, p)
case "send_chat":
h.handleChat(c, p)
+ case "set_chaos":
+ h.handleSetChaos(c, p)
case "heartbeat":
h.sendTo(c, "heartbeat_ack", nil)
}
@@ -144,6 +165,80 @@ func (h *Hub) handleListRooms(c *Client) {
h.sendTo(c, "room_list", map[string]interface{}{"rooms": h.RoomMgr.ListRooms()})
}
+func (h *Hub) handleRegister(c *Client, p map[string]interface{}) {
+ username, _ := p["username"].(string)
+ password, _ := p["password"].(string)
+ nickname, _ := p["nickname"].(string)
+ user, err := db.Register(username, password, nickname)
+ if err != nil {
+ h.sendTo(c, "register_result", map[string]interface{}{"success": false, "message": err.Error()})
+ return
+ }
+ h.sendTo(c, "register_result", map[string]interface{}{
+ "success": true, "message": "注册成功", "username": user.Username, "nickname": user.Nickname,
+ })
+}
+
+func (h *Hub) handleLogin(c *Client, p map[string]interface{}) {
+ username, _ := p["username"].(string)
+ password, _ := p["password"].(string)
+ token, _ := p["token"].(string)
+
+ var user *db.User
+ var err error
+ if token != "" {
+ user, err = db.ValidateToken(token)
+ if err == nil {
+ token = token
+ }
+ } else {
+ token, user, err = db.Login(username, password)
+ }
+ if err != nil {
+ h.sendTo(c, "login_result", map[string]interface{}{"success": false, "message": err.Error()})
+ return
+ }
+
+ c.UserID = user.ID
+ c.Username = user.Username
+ c.Nickname = user.Nickname
+ c.Token = token
+ log.Printf("User logged in: %s (%s)", user.Username, c.ID)
+
+ h.sendTo(c, "login_result", map[string]interface{}{
+ "success": true, "token": token,
+ "username": user.Username, "nickname": user.Nickname, "avatar": user.Avatar, "bio": user.Bio,
+ })
+}
+
+func (h *Hub) handleUpdateProfile(c *Client, p map[string]interface{}) {
+ if c.UserID == 0 {
+ h.sendTo(c, "error", map[string]interface{}{"message": "未登录"})
+ return
+ }
+ nick, _ := p["nickname"].(string)
+ bio, _ := p["bio"].(string)
+ avatar, _ := p["avatar"].(string)
+ if nick == "" {
+ nick = c.Nickname
+ }
+ db.UpdateProfile(c.UserID, nick, bio, avatar)
+ c.Nickname = nick
+ h.sendTo(c, "profile_updated", map[string]interface{}{"success": true, "nickname": nick, "bio": bio, "avatar": avatar})
+}
+
+func (h *Hub) handleGetProfile(c *Client, p map[string]interface{}) {
+ username, _ := p["username"].(string)
+ user, err := db.GetProfile(username)
+ if err != nil {
+ h.sendTo(c, "profile_info", map[string]interface{}{"success": false})
+ return
+ }
+ h.sendTo(c, "profile_info", map[string]interface{}{
+ "success": true, "username": user.Username, "nickname": user.Nickname, "bio": user.Bio, "avatar": user.Avatar,
+ })
+}
+
func (h *Hub) handleCreateRoom(c *Client, p map[string]interface{}) {
nick, _ := p["nickname"].(string)
mp := 4
@@ -190,6 +285,19 @@ func (h *Hub) handleLeaveRoom(c *Client) {
}
}
+func (h *Hub) handleSetChaos(c *Client, p map[string]interface{}) {
+ if c.RoomID == "" {
+ return
+ }
+ rm := h.RoomMgr.GetRoom(c.RoomID)
+ if rm == nil {
+ return
+ }
+ enabled, _ := p["enabled"].(bool)
+ rm.ChaosMode = enabled
+ h.broadcastRoomState(rm)
+}
+
func (h *Hub) handleChat(c *Client, p map[string]interface{}) {
if c.RoomID == "" {
return
@@ -255,6 +363,7 @@ func (h *Hub) handleStartGame(c *Client) {
}
eng := game.NewEngine(ids, nicks, bots, len(ids))
+ eng.ChaosMode = rm.ChaosMode
eng.Deal()
h.mu.Lock()
@@ -318,6 +427,7 @@ func (h *Hub) handleConfirmAction(c *Client, p map[string]interface{}) {
}
h.broadcastSnapshots(c.RoomID)
+ h.flushGameLogs(c.RoomID)
if eng.IsEffectResolving() {
h.processBotEffectResponses(c.RoomID)
@@ -431,6 +541,7 @@ func (h *Hub) processBotTurns(roomID string) {
eng.ConfirmAction(cp.ID, action, target)
h.broadcastSnapshots(roomID)
+ h.flushGameLogs(roomID)
if eng.IsEffectResolving() {
h.processBotEffectResponses(roomID)
@@ -515,6 +626,28 @@ func (h *Hub) handleAllExited(roomID string) {
// ---- Audio relay ----
+func (h *Hub) flushGameLogs(roomID string) {
+ h.mu.RLock()
+ eng := h.engines[roomID]
+ h.mu.RUnlock()
+ if eng == nil {
+ return
+ }
+ rm := h.RoomMgr.GetRoom(roomID)
+ if rm == nil {
+ return
+ }
+ for _, msg := range eng.FlushLogs() {
+ for _, pid := range rm.PlayerOrder {
+ p := rm.Players[pid]
+ if p.IsBot || p.Send == nil {
+ continue
+ }
+ h.sendToChannel(p.Send, "game_log", map[string]interface{}{"text": msg})
+ }
+ }
+}
+
func (h *Hub) relayAudio(sender *Client, pcm []byte) {
if sender.RoomID == "" {
return
diff --git a/main.go b/main.go
index a5be766..a6eb2d4 100644
--- a/main.go
+++ b/main.go
@@ -1,25 +1,44 @@
package main
import (
+ "Embalming_Girl_Server/admin"
+ "Embalming_Girl_Server/db"
"Embalming_Girl_Server/handler"
"log"
"net/http"
)
func main() {
+ if err := db.Init(); err != nil {
+ log.Fatal("Database init failed:", err)
+ }
+ log.Println("Database initialized")
+
hub := handler.NewHub()
hub.InitTestRooms()
- http.HandleFunc("/ws", hub.ServeWS)
- http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
+ mux := http.NewServeMux()
+ mux.HandleFunc("/ws", hub.ServeWS)
+ mux.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
- w.Write([]byte("Embalming Girl Server v0.1.0"))
+ w.Write([]byte("Embalming Girl Server v0.2.0"))
+ })
+
+ admin.Register(mux, func() map[string]interface{} {
+ var totalUsers int
+ db.DB.QueryRow("SELECT COUNT(*) FROM users").Scan(&totalUsers)
+ return map[string]interface{}{
+ "total_users": totalUsers,
+ "active_rooms": len(hub.RoomMgr.ListRooms()),
+ "online_clients": hub.OnlineCount(),
+ }
})
addr := ":8080"
log.Printf("Server started on %s", addr)
+ log.Printf("Admin panel: http://localhost%s/admin", addr)
log.Printf("Test rooms: TEST3, TEST4, TEST5, TEST6")
- if err := http.ListenAndServe(addr, nil); err != nil {
+ if err := http.ListenAndServe(addr, mux); err != nil {
log.Fatal("ListenAndServe:", err)
}
}
diff --git a/server.exe b/server.exe
index 935fbac..a475a35 100644
Binary files a/server.exe and b/server.exe differ