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 管理面板

+

服务器状态

+

注册用户

ID用户名昵称头像简介注册时间操作
+
+` 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