增加了一些功能
This commit is contained in:
@@ -22,4 +22,6 @@ log/*
|
||||
|
||||
cmake-build-debug
|
||||
|
||||
server.exe
|
||||
|
||||
|
||||
|
||||
+118
@@ -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 = `<!DOCTYPE html><html><head><meta charset="utf-8"><title>管理面板</title>
|
||||
<style>
|
||||
*{margin:0;padding:0;box-sizing:border-box}
|
||||
body{background:#1a1f2e;color:#e0e0e0;font-family:system-ui;padding:20px}
|
||||
h1{color:#5eb3e6;margin-bottom:20px;text-align:center}
|
||||
.card{background:#252b3d;border-radius:12px;padding:20px;margin-bottom:16px}
|
||||
.card h2{color:#d4c5a3;margin-bottom:12px;font-size:18px}
|
||||
.stats{display:flex;gap:16px;flex-wrap:wrap}
|
||||
.stat{background:#1a1f2e;border-radius:8px;padding:16px;text-align:center;min-width:120px}
|
||||
.stat .num{font-size:28px;color:#5eb3e6;font-weight:bold}
|
||||
.stat .label{font-size:12px;color:#8a8f9d;margin-top:4px}
|
||||
table{width:100%;border-collapse:collapse;margin-top:8px}
|
||||
th,td{padding:8px 12px;text-align:left;border-bottom:1px solid #2a3040}
|
||||
th{color:#8a8f9d;font-size:12px}
|
||||
.btn{padding:4px 12px;border:none;border-radius:4px;cursor:pointer;font-size:12px}
|
||||
.btn-danger{background:#d94f5c;color:white}
|
||||
.btn-danger:hover{background:#c0392b}
|
||||
#loginBox{max-width:300px;margin:100px auto;text-align:center}
|
||||
#loginBox input{width:100%;padding:10px;margin:8px 0;border-radius:6px;border:1px solid #3a3f4e;background:#1a1f2e;color:white}
|
||||
#loginBox button{width:100%;padding:10px;background:#5eb3e6;border:none;border-radius:6px;color:white;cursor:pointer;font-size:14px}
|
||||
#main{display:none}
|
||||
</style></head><body>
|
||||
<div id="loginBox"><h1>管理面板</h1><input id="pw" type="password" placeholder="管理密码">
|
||||
<button onclick="doLogin()">登录</button></div>
|
||||
<div id="main"><h1>Embalming Girl 管理面板</h1>
|
||||
<div class="card"><h2>服务器状态</h2><div class="stats" id="stats"></div></div>
|
||||
<div class="card"><h2>注册用户</h2><table><thead><tr><th>ID</th><th>用户名</th><th>昵称</th><th>头像</th><th>简介</th><th>注册时间</th><th>操作</th></tr></thead><tbody id="users"></tbody></table></div>
|
||||
</div>
|
||||
<script>
|
||||
async function doLogin(){
|
||||
let r=await fetch('/api/admin/login',{method:'POST',body:JSON.stringify({password:document.getElementById('pw').value})});
|
||||
let d=await r.json();
|
||||
if(d.ok){document.getElementById('loginBox').style.display='none';document.getElementById('main').style.display='block';loadData();}
|
||||
else alert('密码错误');
|
||||
}
|
||||
async function loadData(){
|
||||
let s=await(await fetch('/api/admin/stats')).json();
|
||||
document.getElementById('stats').innerHTML=
|
||||
'<div class="stat"><div class="num">'+s.total_users+'</div><div class="label">注册用户</div></div>'+
|
||||
'<div class="stat"><div class="num">'+s.active_rooms+'</div><div class="label">活跃房间</div></div>'+
|
||||
'<div class="stat"><div class="num">'+s.online_clients+'</div><div class="label">在线连接</div></div>';
|
||||
let u=await(await fetch('/api/admin/users')).json();
|
||||
let h='';(u||[]).forEach(x=>{
|
||||
h+='<tr><td>'+x.id+'</td><td>'+x.username+'</td><td>'+x.nickname+'</td><td>'+
|
||||
(x.avatar||'-')+'</td><td>'+(x.bio||'-')+'</td><td>'+(x.created_at||'-')+'</td>'+
|
||||
'<td><button class="btn btn-danger" onclick="delUser('+x.id+')">删除</button></td></tr>';
|
||||
});
|
||||
document.getElementById('users').innerHTML=h||'<tr><td colspan="7">暂无用户</td></tr>';
|
||||
}
|
||||
async function delUser(id){
|
||||
if(!confirm('确定删除此用户?'))return;
|
||||
await fetch('/api/admin/delete_user',{method:'POST',body:JSON.stringify({id})});
|
||||
loadData();
|
||||
}
|
||||
</script></body></html>`
|
||||
+143
@@ -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
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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=
|
||||
|
||||
+133
@@ -1,6 +1,7 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"Embalming_Girl_Server/db"
|
||||
"Embalming_Girl_Server/game"
|
||||
"Embalming_Girl_Server/room"
|
||||
"encoding/json"
|
||||
@@ -20,6 +21,10 @@ var upgrader = websocket.Upgrader{
|
||||
type Client struct {
|
||||
ID string
|
||||
RoomID string
|
||||
UserID int64
|
||||
Username string
|
||||
Nickname string
|
||||
Token string
|
||||
Conn *websocket.Conn
|
||||
Send chan []byte
|
||||
mu sync.Mutex
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
BIN
Binary file not shown.
Reference in New Issue
Block a user