From 148f27158615e223c3be410ce30482bec9937f0a Mon Sep 17 00:00:00 2001 From: Jiang-Red <79574799+Jiang-Red@users.noreply.github.com> Date: Wed, 4 Oct 2023 20:55:08 +0800 Subject: [PATCH] aifalse add nightstyle & chess use resvg (#782) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * aifalse add nightstyle & chess use resvg * make lint happy * make lint happy * chore(lint): 改进代码样式 (#25) Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> --- go.mod | 2 + go.sum | 4 + plugin/aifalse/main.go | 171 ++++++++++--- plugin/chess/chess.go | 106 ++++---- plugin/chess/core.go | 534 ++++++++++++++++++----------------------- 5 files changed, 428 insertions(+), 389 deletions(-) diff --git a/go.mod b/go.mod index 605efbd8..3df26a6a 100644 --- a/go.mod +++ b/go.mod @@ -29,6 +29,7 @@ require ( github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0 github.com/jinzhu/gorm v1.9.16 github.com/jozsefsallai/gophersauce v1.0.1 + github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 github.com/lithammer/fuzzysearch v1.1.5 github.com/mroth/weightedrand v1.0.0 github.com/notnil/chess v1.9.0 @@ -76,6 +77,7 @@ require ( github.com/quic-go/qpack v0.4.0 // indirect github.com/quic-go/qtls-go1-20 v0.3.3 // indirect github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect + github.com/tetratelabs/wazero v1.5.0 // indirect github.com/tidwall/match v1.1.1 // indirect github.com/tidwall/pretty v1.2.0 // indirect github.com/tklauser/go-sysconf v0.3.11 // indirect diff --git a/go.sum b/go.sum index ddf9df06..507c7007 100644 --- a/go.sum +++ b/go.sum @@ -129,6 +129,8 @@ github.com/jinzhu/now v1.0.1 h1:HjfetcXq097iXP0uoPCdnM4Efp5/9MsM0/M+XOTeR3M= github.com/jinzhu/now v1.0.1/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= github.com/jozsefsallai/gophersauce v1.0.1 h1:BA3ovtQRrAb1qYU9JoRLbDHpxnDunlNcEkEfhCvDDCM= github.com/jozsefsallai/gophersauce v1.0.1/go.mod h1:YVEI7djliMTmZ1Vh01YPF8bUHi+oKhe3yXgKf1T49vg= +github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5 h1:BXnB1Gz4y/zwQh+ZFNy7rgd+ZfMOrwRr4uZSHEI+ieY= +github.com/kanrichan/resvg-go v0.0.2-0.20231001163256-63db194ca9f5/go.mod h1:c9+VS9GaommgIOzNWb5ze4lYwfT8BZ2UDyGiuQTT7yc= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= @@ -192,6 +194,8 @@ github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/tetratelabs/wazero v1.5.0 h1:Yz3fZHivfDiZFUXnWMPUoiW7s8tC1sjdBtlJn08qYa0= +github.com/tetratelabs/wazero v1.5.0/go.mod h1:0U0G41+ochRKoPKCJlh0jMg1CHkyfK8kDqiirMmKY8A= github.com/tidwall/gjson v1.14.4 h1:uo0p8EbA09J7RQaflQ1aBRffTR7xedD2bcIVSYxLnkM= github.com/tidwall/gjson v1.14.4/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk= github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA= diff --git a/plugin/aifalse/main.go b/plugin/aifalse/main.go index 0ed7ba5e..a8addb24 100644 --- a/plugin/aifalse/main.go +++ b/plugin/aifalse/main.go @@ -5,6 +5,7 @@ import ( "bytes" "errors" "image" + "image/color" "math" "runtime" "strconv" @@ -45,9 +46,12 @@ const ( ) var ( - boottime = time.Now() - bgdata *[]byte - bgcount uintptr + boottime = time.Now() + bgdata *[]byte + bgcount uintptr + isday bool + lightcolor = [3][4]uint8{{255, 70, 0, 255}, {255, 165, 0, 255}, {145, 240, 145, 255}} + darkcolor = [3][4]uint8{{215, 50, 0, 255}, {205, 135, 0, 255}, {115, 200, 115, 255}} ) func init() { // 插件主体 @@ -70,7 +74,26 @@ func init() { // 插件主体 } engine.OnFullMatchGroup([]string{"检查身体", "自检", "启动自检", "系统状态"}, zero.AdminPermission).SetBlock(true). Handle(func(ctx *zero.Ctx) { - img, err := drawstatus(ctx.State["manager"].(*ctrl.Control[*zero.Ctx]), ctx.Event.SelfID, zero.BotConfig.NickName[0]) + now := time.Now().Hour() + isday = now > 7 && now < 19 + + botrunstatus := ctx.CallAction("get_status", zero.Params{}).Data + botverisoninfo := ctx.GetVersionInfo() + sb := &strings.Builder{} + sb.WriteString("在线(") + sb.WriteString(botverisoninfo.Get("app_name").String()) + sb.WriteString("-") + sb.WriteString(botverisoninfo.Get("app_version").String()) + sb.WriteString(") | 收") + sb.WriteString(botrunstatus.Get("stat").Get("message_received").String()) + sb.WriteString(" | 发") + sb.WriteString(botrunstatus.Get("stat").Get("message_sent").String()) + sb.WriteString(" | 群") + sb.WriteString(strconv.Itoa(len(ctx.GetGroupList().Array()))) + sb.WriteString(" | 好友") + sb.WriteString(strconv.Itoa(len(ctx.GetFriendList().Array()))) + + img, err := drawstatus(ctx.State["manager"].(*ctrl.Control[*zero.Ctx]), ctx.Event.SelfID, zero.BotConfig.NickName[0], sb.String()) if err != nil { ctx.SendChain(message.Text("ERROR: ", err)) return @@ -122,7 +145,7 @@ func init() { // 插件主体 }) } -func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg image.Image, err error) { +func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string, botrunstatus string) (sendimg image.Image, err error) { diskstate, err := diskstate() if err != nil { return @@ -194,6 +217,13 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg defer bwg.Done() blurback = imaging.Blur(canvas.Image(), 8) }() + + if !isday { + canvas.SetRGBA255(0, 0, 0, 50) + canvas.DrawRectangle(0, 0, cw, ch) + canvas.Fill() + } + wg := &sync.WaitGroup{} wg.Add(5) @@ -211,9 +241,9 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg titlecard.DrawRoundedRectangle(1, 1, float64(titlecard.W()-1*2), float64(titlecardh-1*2), 16) titlecard.SetLineWidth(3) - titlecard.SetRGBA255(255, 255, 255, 100) + titlecard.SetColor(colorswitch(100)) titlecard.StrokePreserve() - titlecard.SetRGBA255(255, 255, 255, 140) + titlecard.SetColor(colorswitch(140)) titlecard.Fill() titlecard.DrawImage(avatarf.Circle(0).Image(), (titlecardh-avatarf.H())/2, (titlecardh-avatarf.H())/2) @@ -224,7 +254,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg } fw, _ := titlecard.MeasureString(botname) - titlecard.SetRGBA255(30, 30, 30, 255) + titlecard.SetColor(fontcolorswitch()) titlecard.DrawStringAnchored(botname, float64(titlecardh)+fw/2, float64(titlecardh)*0.5/2, 0.5, 0.5) @@ -232,20 +262,24 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg if err != nil { return } - titlecard.SetRGBA255(30, 30, 30, 180) + titlecard.SetColor(fontcolorswitch()) titlecard.NewSubPath() titlecard.MoveTo(float64(titlecardh), float64(titlecardh)/2) titlecard.LineTo(float64(titlecard.W()-titlecardh), float64(titlecardh)/2) titlecard.Stroke() + fw, _ = titlecard.MeasureString(botrunstatus) + + titlecard.DrawStringAnchored(botrunstatus, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.25/2), 0.5, 0.5) + brt, err := botruntime() if err != nil { return } fw, _ = titlecard.MeasureString(brt) - titlecard.DrawStringAnchored(brt, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.25/2), 0.5, 0.5) + titlecard.DrawStringAnchored(brt, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.5/2), 0.5, 0.5) bs, err := botstatus() if err != nil { @@ -253,7 +287,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg } fw, _ = titlecard.MeasureString(bs) - titlecard.DrawStringAnchored(bs, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.5/2), 0.5, 0.5) + titlecard.DrawStringAnchored(bs, float64(titlecardh)+fw/2, float64(titlecardh)*(0.5+0.75/2), 0.5, 0.5) titleimg = rendercard.Fillet(titlecard.Image(), 16) }() go func() { @@ -264,26 +298,34 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg basiccard.DrawRoundedRectangle(1, 1, float64(basiccard.W()-1*2), float64(basiccardh-1*2), 16) basiccard.SetLineWidth(3) - basiccard.SetRGBA255(255, 255, 255, 100) + basiccard.SetColor(colorswitch(100)) basiccard.StrokePreserve() - basiccard.SetRGBA255(255, 255, 255, 140) + basiccard.SetColor(colorswitch(140)) basiccard.Fill() bslen := len(basicstate) for i, v := range basicstate { offset := float64(i) * ((float64(basiccard.W())-200*float64(bslen))/float64(bslen+1) + 200) - basiccard.SetRGBA255(235, 235, 235, 255) + basiccard.SetRGBA255(57, 57, 57, 255) + if isday { + basiccard.SetRGBA255(235, 235, 235, 255) + } basiccard.DrawCircle((float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200/2, 100) basiccard.Fill() + colors := darkcolor + if isday { + colors = lightcolor + } + switch { case v.precent > 90: - basiccard.SetRGBA255(255, 70, 0, 255) + basiccard.SetColor(slice2color(colors[0])) case v.precent > 70: - basiccard.SetRGBA255(255, 165, 0, 255) + basiccard.SetColor(slice2color(colors[1])) default: - basiccard.SetRGBA255(145, 240, 145, 255) + basiccard.SetColor(slice2color(colors[2])) } basiccard.NewSubPath() @@ -291,7 +333,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg basiccard.DrawEllipticalArc((float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200/2, 100, 100, -0.5*math.Pi, -0.5*math.Pi+2*v.precent*0.01*math.Pi) basiccard.Fill() - basiccard.SetRGBA255(255, 255, 255, 255) + basiccard.SetColor(colorswitch(255)) basiccard.DrawCircle((float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200/2, 80) basiccard.Fill() @@ -303,7 +345,8 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg basiccard.SetRGBA255(213, 213, 213, 255) basiccard.DrawStringAnchored(strconv.FormatFloat(v.precent, 'f', 0, 64)+"%", (float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200/2, 0.5, 0.5) - basiccard.SetRGBA255(30, 30, 30, 255) + basiccard.SetColor(fontcolorswitch()) + _, fw := basiccard.MeasureString(v.name) basiccard.DrawStringAnchored(v.name, (float64(basiccard.W())-200*float64(bslen))/float64(bslen+1)+200/2+offset, 20+200+15+basiccard.FontHeight()/2, 0.5, 0.5) @@ -311,7 +354,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg if err != nil { return } - basiccard.SetRGBA255(30, 30, 30, 180) + basiccard.SetColor(fontcolorswitch()) textoffsety := basiccard.FontHeight() + 10 for k, s := range v.text { @@ -328,9 +371,9 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg diskcard.DrawRoundedRectangle(1, 1, float64(diskcard.W()-1*2), float64(diskcardh-1*2), 16) diskcard.SetLineWidth(3) - diskcard.SetRGBA255(255, 255, 255, 100) + diskcard.SetColor(colorswitch(100)) diskcard.StrokePreserve() - diskcard.SetRGBA255(255, 255, 255, 140) + diskcard.SetColor(colorswitch(140)) diskcard.Fill() err = diskcard.ParseFontFace(fontbyte, 32) @@ -340,24 +383,33 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg dslen := len(diskstate) if dslen == 1 { - diskcard.SetRGBA255(192, 192, 192, 255) + diskcard.SetRGBA255(57, 57, 57, 255) + if isday { + diskcard.SetRGBA255(192, 192, 192, 255) + } diskcard.DrawRoundedRectangle(40, 40, float64(diskcard.W())-40-100, 50, 12) diskcard.ClipPreserve() diskcard.Fill() + colors := darkcolor + if isday { + colors = lightcolor + } + switch { case diskstate[0].precent > 90: - diskcard.SetRGBA255(255, 70, 0, 255) + diskcard.SetColor(slice2color(colors[0])) case diskstate[0].precent > 70: - diskcard.SetRGBA255(255, 165, 0, 255) + diskcard.SetColor(slice2color(colors[1])) default: - diskcard.SetRGBA255(145, 240, 145, 255) + diskcard.SetColor(slice2color(colors[2])) } diskcard.DrawRoundedRectangle(40, 40, (float64(diskcard.W())-40-100)*diskstate[0].precent*0.01, 50, 12) diskcard.Fill() diskcard.ResetClip() - diskcard.SetRGBA255(30, 30, 30, 255) + + diskcard.SetColor(fontcolorswitch()) fw, _ := diskcard.MeasureString(diskstate[0].name) fw1, _ := diskcard.MeasureString(diskstate[0].text[0]) @@ -369,23 +421,32 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg for i, v := range diskstate { offset := float64(i)*(50+20) - 20 - diskcard.SetRGBA255(192, 192, 192, 255) + diskcard.SetRGBA255(57, 57, 57, 255) + if isday { + diskcard.SetRGBA255(192, 192, 192, 255) + } + diskcard.DrawRoundedRectangle(40, 40+(float64(diskcardh-40*2)-50*float64(dslen))/float64(dslen-1)+offset, float64(diskcard.W())-40-100, 50, 12) diskcard.Fill() + colors := darkcolor + if isday { + colors = lightcolor + } + switch { case v.precent > 90: - diskcard.SetRGBA255(255, 70, 0, 255) + diskcard.SetColor(slice2color(colors[0])) case v.precent > 70: - diskcard.SetRGBA255(255, 165, 0, 255) + diskcard.SetColor(slice2color(colors[1])) default: - diskcard.SetRGBA255(145, 240, 145, 255) + diskcard.SetColor(slice2color(colors[2])) } diskcard.DrawRoundedRectangle(40, 40+(float64(diskcardh-40*2)-50*float64(dslen))/float64(dslen-1)+offset, (float64(diskcard.W())-40-100)*v.precent*0.01, 50, 12) diskcard.Fill() - diskcard.SetRGBA255(30, 30, 30, 255) + diskcard.SetColor(fontcolorswitch()) fw, _ := diskcard.MeasureString(v.name) fw1, _ := diskcard.MeasureString(v.text[0]) @@ -405,9 +466,9 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg moreinfocard.DrawRoundedRectangle(1, 1, float64(moreinfocard.W()-1*2), float64(moreinfocard.H()-1*2), 16) moreinfocard.SetLineWidth(3) - moreinfocard.SetRGBA255(255, 255, 255, 100) + moreinfocard.SetColor(colorswitch(100)) moreinfocard.StrokePreserve() - moreinfocard.SetRGBA255(255, 255, 255, 140) + moreinfocard.SetColor(colorswitch(140)) moreinfocard.Fill() err = moreinfocard.ParseFontFace(fontbyte, 32) @@ -419,7 +480,7 @@ func drawstatus(m *ctrl.Control[*zero.Ctx], uid int64, botname string) (sendimg for i, v := range moreinfo { offset := float64(i)*(20+moreinfocard.FontHeight()) - 20 - moreinfocard.SetRGBA255(30, 30, 30, 255) + moreinfocard.SetColor(fontcolorswitch()) fw, _ := moreinfocard.MeasureString(v.name) fw1, _ := moreinfocard.MeasureString(v.text[0]) @@ -497,6 +558,8 @@ func botstatus() (string, error) { t.WriteString(runtime.Version()) t.WriteString(" | ") t.WriteString(cases.Title(language.English).String(hostinfo.OS)) + t.WriteString(" ") + t.WriteString(runtime.GOARCH) return t.String(), nil } @@ -507,7 +570,7 @@ type status struct { } func basicstate() (stateinfo [3]*status, err error) { - percent, err := cpu.Percent(time.Second, false) + percent, err := cpu.Percent(time.Second, true) if err != nil { return } @@ -515,7 +578,15 @@ func basicstate() (stateinfo [3]*status, err error) { if err != nil { return } - cores := strconv.Itoa(int(cpuinfo[0].Cores)) + " Core" + cpucore, err := cpu.Counts(false) + if err != nil { + return + } + cputhread, err := cpu.Counts(true) + if err != nil { + return + } + cores := strconv.Itoa(cpucore) + "C" + strconv.Itoa(cputhread) + "T" times := "最大 " + strconv.FormatFloat(cpuinfo[0].Mhz/1000, 'f', 1, 64) + "Ghz" stateinfo[0] = &status{ @@ -595,6 +666,10 @@ func diskstate() (stateinfo []*status, err error) { } func moreinfo(m *ctrl.Control[*zero.Ctx]) (stateinfo []*status, err error) { + var mems runtime.MemStats + runtime.ReadMemStats(&mems) + fmtmem := storagefmt(float64(mems.Sys)) + hostinfo, err := host.Info() if err != nil { return @@ -603,12 +678,32 @@ func moreinfo(m *ctrl.Control[*zero.Ctx]) (stateinfo []*status, err error) { if err != nil { return } + count := len(m.Manager.M) stateinfo = []*status{ {name: "OS", text: []string{hostinfo.Platform}}, - {name: "CPU", text: []string{cpuinfo[0].ModelName}}, + {name: "CPU", text: []string{strings.TrimSpace(cpuinfo[0].ModelName)}}, {name: "Version", text: []string{hostinfo.PlatformVersion}}, {name: "Plugin", text: []string{"共 " + strconv.Itoa(count) + " 个"}}, + {name: "Memory", text: []string{"已用 " + fmtmem}}, } return } + +func colorswitch(a uint8) color.Color { + if isday { + return color.NRGBA{255, 255, 255, a} + } + return color.NRGBA{0, 0, 0, a} +} + +func fontcolorswitch() color.Color { + if isday { + return color.NRGBA{30, 30, 30, 255} + } + return color.NRGBA{235, 235, 235, 255} +} + +func slice2color(c [4]uint8) color.Color { + return color.NRGBA{c[0], c[1], c[2], c[3]} +} diff --git a/plugin/chess/chess.go b/plugin/chess/chess.go index 9ae65c60..f32e6bf2 100644 --- a/plugin/chess/chess.go +++ b/plugin/chess/chess.go @@ -58,9 +58,7 @@ func init() { dbFilePath := engine.DataFolder() + "chess.db" initDatabase(dbFilePath) // 注册指令 - engine.OnFullMatchGroup([]string{"下棋", "chess"}, zero.OnlyGroup). - SetBlock(true). - Limit(limit.LimitByGroup). + engine.OnFullMatchGroup([]string{"下棋", "chess"}, zero.OnlyGroup).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { if ctx.Event.Sender == nil { return @@ -68,42 +66,50 @@ func init() { userUin := ctx.Event.UserID userName := ctx.Event.Sender.NickName groupCode := ctx.Event.GroupID - if replyMessage := game(groupCode, userUin, userName); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := game(groupCode, userUin, userName) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"认输", "resign"}, zero.OnlyGroup). - SetBlock(true). - Limit(limit.LimitByGroup). + + engine.OnFullMatchGroup([]string{"认输", "resign"}, zero.OnlyGroup).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { userUin := ctx.Event.UserID groupCode := ctx.Event.GroupID - if replyMessage := resign(groupCode, userUin); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := resign(groupCode, userUin) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"和棋", "draw"}, zero.OnlyGroup). - SetBlock(true). - Limit(limit.LimitByGroup). + + engine.OnFullMatchGroup([]string{"和棋", "draw"}, zero.OnlyGroup).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { userUin := ctx.Event.UserID groupCode := ctx.Event.GroupID - if replyMessage := draw(groupCode, userUin); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := draw(groupCode, userUin) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"中断", "abort"}, zero.OnlyGroup, zero.AdminPermission). - SetBlock(true). - Limit(limit.LimitByGroup). + + engine.OnFullMatchGroup([]string{"中断", "abort"}, zero.OnlyGroup, zero.AdminPermission).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { groupCode := ctx.Event.GroupID - if replyMessage := abort(groupCode); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := abort(groupCode) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"盲棋", "blind"}, zero.OnlyGroup). - SetBlock(true). - Limit(limit.LimitByGroup). + + engine.OnFullMatchGroup([]string{"盲棋", "blind"}, zero.OnlyGroup).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { if ctx.Event.Sender == nil { return @@ -111,45 +117,54 @@ func init() { userUin := ctx.Event.UserID userName := ctx.Event.Sender.NickName groupCode := ctx.Event.GroupID - if replyMessage := blindfold(groupCode, userUin, userName); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := blindfold(groupCode, userUin, userName) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnRegex("^[!|!]([0-8]|[R|N|B|Q|K|O|a-h|x]|[-|=|+])+$", zero.OnlyGroup). - SetBlock(true). - Limit(limit.LimitByGroup). + + engine.OnRegex("^[!|!]([0-8]|[R|N|B|Q|K|O|a-h|x]|[-|=|+])+$", zero.OnlyGroup).SetBlock(true).Limit(limit.LimitByGroup). Handle(func(ctx *zero.Ctx) { userUin := ctx.Event.UserID groupCode := ctx.Event.GroupID userMsgStr := ctx.State["regex_matched"].([]string)[0] moveStr := strings.TrimPrefix(strings.TrimPrefix(userMsgStr, "!"), "!") - if replyMessage := play(userUin, groupCode, moveStr); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := play(groupCode, userUin, moveStr) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"排行榜", "ranking"}). - SetBlock(true). - Limit(limit.LimitByUser). + + engine.OnFullMatchGroup([]string{"排行榜", "ranking"}).SetBlock(true).Limit(limit.LimitByUser). Handle(func(ctx *zero.Ctx) { - if replyMessage := ranking(); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := getRanking() + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnFullMatchGroup([]string{"等级分", "rate"}). - SetBlock(true). - Limit(limit.LimitByUser). + + engine.OnFullMatchGroup([]string{"等级分", "rate"}).SetBlock(true).Limit(limit.LimitByUser). Handle(func(ctx *zero.Ctx) { if ctx.Event.Sender == nil { return } userUin := ctx.Event.UserID userName := ctx.Event.Sender.NickName - if replyMessage := rate(userUin, userName); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := rate(userUin, userName) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) - engine.OnPrefixGroup([]string{"清空等级分", ".clean.rate"}, zero.SuperUserPermission). - SetBlock(true). + + engine.OnPrefixGroup([]string{"清空等级分", ".clean.rate"}, zero.SuperUserPermission).SetBlock(true). Limit(limit.LimitByUser). Handle(func(ctx *zero.Ctx) { args := ctx.State["args"].(string) @@ -158,8 +173,11 @@ func init() { ctx.Send(fmt.Sprintf("解析失败「%s」不是正确的 QQ 号。", args)) return } - if replyMessage := cleanUserRate(playerUin); len(replyMessage) >= 1 { - ctx.Send(replyMessage) + replyMessage, err := cleanUserRate(playerUin) + if err != nil { + ctx.SendChain(message.Text("ERROR: ", err)) + return } + ctx.Send(replyMessage) }) } diff --git a/plugin/chess/core.go b/plugin/chess/core.go index a2940d6a..aecbf0d0 100644 --- a/plugin/chess/core.go +++ b/plugin/chess/core.go @@ -2,32 +2,33 @@ package chess import ( "bytes" - "encoding/base64" + "context" + "errors" "fmt" "image/color" - "io" - "os" - "os/exec" - "path" + "strconv" "strings" "time" "github.com/FloatTech/floatbox/binary" "github.com/FloatTech/floatbox/file" - "github.com/FloatTech/gg" "github.com/FloatTech/zbputils/control" "github.com/FloatTech/zbputils/img/text" + "github.com/RomiChan/syncx" "github.com/jinzhu/gorm" + resvg "github.com/kanrichan/resvg-go" "github.com/notnil/chess" - "github.com/notnil/chess/image" - log "github.com/sirupsen/logrus" + cimage "github.com/notnil/chess/image" "github.com/wdvxdr1123/ZeroBot/message" ) const eloDefault = 500 -var chessRoomMap syncx.Map[int64, *chessRoom] +var ( + chessRoomMap syncx.Map[int64, *chessRoom] + errNotExist = errors.New("对局不存在, 发送「下棋」或「chess」可创建对局。") +) type chessRoom struct { chessGame *chess.Game @@ -43,92 +44,86 @@ type chessRoom struct { } // game 下棋 -func game(groupCode, senderUin int64, senderName string) message.Message { +func game(groupCode, senderUin int64, senderName string) (message.Message, error) { return createGame(false, groupCode, senderUin, senderName) } // blindfold 盲棋 -func blindfold(groupCode, senderUin int64, senderName string) message.Message { +func blindfold(groupCode, senderUin int64, senderName string) (message.Message, error) { return createGame(true, groupCode, senderUin, senderName) } // abort 中断对局 -func abort(groupCode int64) message.Message { +func abort(groupCode int64) (message.Message, error) { if room, ok := chessRoomMap.Load(groupCode); ok { - return abortGame(*room, groupCode, "对局已被管理员中断,游戏结束。") + return abortGame(*room, groupCode, "对局已被管理员中断, 游戏结束。") } - return simpleText("对局不存在,发送「下棋」或「chess」可创建对局。") + return nil, errNotExist } // draw 和棋 -func draw(groupCode, senderUin int64) message.Message { +func draw(groupCode, senderUin int64) (msg message.Message, err error) { + msg = message.Message{message.At(senderUin)} // 检查对局是否存在 room, ok := chessRoomMap.Load(groupCode) if !ok { - return simpleText("对局不存在,发送「下棋」或「chess」可创建对局。") + return nil, errNotExist } // 检查消息发送者是否为对局中的玩家 if senderUin != room.whitePlayer && senderUin != room.blackPlayer { - return textWithAt(senderUin, "您不是对局中的玩家,无法请求和棋。") + return } // 处理和棋逻辑 room.lastMoveTime = time.Now().Unix() if room.drawPlayer == 0 { room.drawPlayer = senderUin chessRoomMap.Store(groupCode, room) - return textWithAt(senderUin, "请求和棋,发送「和棋」或「draw」接受和棋。走棋视为拒绝和棋。") + msg = append(msg, message.Text("请求和棋, 发送「和棋」或「draw」接受和棋。走棋视为拒绝和棋。")) + return } if room.drawPlayer == senderUin { - return textWithAt(senderUin, "已发起和棋请求,请勿重复发送。") + return } - err := room.chessGame.Draw(chess.DrawOffer) + err = room.chessGame.Draw(chess.DrawOffer) if err != nil { - log.Debugln("[chess]", "Fail to draw a game.", err) - return textWithAt(senderUin, fmt.Sprintln("程序发生了错误,和棋失败,请反馈开发者修复 bug。\nERROR:", err)) + return } chessString := getChessString(*room) eloString := "" if len(room.chessGame.Moves()) > 4 { - // 若走子次数超过 4 认为是有效对局,存入数据库 + // 若走子次数超过 4 认为是有效对局, 存入数据库 dbService := newDBService() - if err := dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { - log.Debugln("[chess]", "Fail to create PGN.", err) - return message.Message{message.Text("ERROR: ", err)} + if err = dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { + return } whiteScore, blackScore := 0.5, 0.5 - elo, err := getELOString(*room, whiteScore, blackScore) + eloString, err = getELOString(*room, whiteScore, blackScore) if err != nil { - log.Debugln("[chess]", "Fail to get eloString.", eloString, err) - return message.Message{message.Text("ERROR: ", err)} - } - eloString = elo - } - replyMsg := textWithAt(senderUin, "接受和棋,游戏结束。\n"+eloString+chessString) - if inkscapeExists() { - if err := cleanTempFiles(groupCode); err != nil { - log.Debugln("[chess]", "Fail to clean temp files", err) - return message.Message{message.Text("ERROR: ", err)} + return } } + msg = append(msg, message.Text("接受和棋, 游戏结束。\n", eloString, chessString)) chessRoomMap.Delete(groupCode) - return replyMsg + return } // resign 认输 -func resign(groupCode, senderUin int64) message.Message { +func resign(groupCode, senderUin int64) (msg message.Message, err error) { + msg = message.Message{message.At(senderUin)} // 检查对局是否存在 room, ok := chessRoomMap.Load(groupCode) if !ok { - return simpleText("对局不存在,发送「下棋」或「chess」可创建对局。") + return nil, errNotExist } // 检查是否是当前游戏玩家 if senderUin != room.whitePlayer && senderUin != room.blackPlayer { - return textWithAt(senderUin, "不是对局中的玩家,无法认输。") + return } - // 如果对局未建立,中断对局 + // 如果对局未建立, 中断对局 if room.whitePlayer == 0 || room.blackPlayer == 0 { chessRoomMap.Delete(groupCode) - return simpleText("对局已释放。") + msg = append(msg, message.Text("对局结束")) + return } // 计算认输方 var resignColor chess.Color @@ -148,11 +143,10 @@ func resign(groupCode, senderUin int64) message.Message { chessString := getChessString(*room) eloString := "" if len(room.chessGame.Moves()) > 4 { - // 若走子次数超过 4 认为是有效对局,存入数据库 + // 若走子次数超过 4 认为是有效对局, 存入数据库 dbService := newDBService() - if err := dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { - log.Debugln("[chess]", "Fail to create PGN.", err) - return message.Message{message.Text("ERROR: ", err)} + if err = dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { + return } whiteScore, blackScore := 1.0, 1.0 if resignColor == chess.White { @@ -160,63 +154,58 @@ func resign(groupCode, senderUin int64) message.Message { } else { blackScore = 0.0 } - elo, err := getELOString(*room, whiteScore, blackScore) + eloString, err = getELOString(*room, whiteScore, blackScore) if err != nil { - log.Debugln("[chess]", "Fail to get eloString.", eloString, err) - return message.Message{message.Text("ERROR: ", err)} + return } - eloString = elo } - replyMsg := textWithAt(senderUin, "认输,游戏结束。\n"+eloString+chessString) + msg = append(msg, message.Text("认输, 游戏结束。\n", eloString, chessString)) if isAprilFoolsDay() { - replyMsg = textWithAt(senderUin, "对手认输,游戏结束,你胜利了。\n"+eloString+chessString) - } - // 删除临时文件 - if inkscapeExists() { - if err := cleanTempFiles(groupCode); err != nil { - log.Debugln("[chess]", "Fail to clean temp files", err) - return message.Message{message.Text("ERROR: ", err)} - } + msg = append(msg, message.Text("对手认输, 游戏结束, 你胜利了。\n", eloString, chessString)) } chessRoomMap.Delete(groupCode) - return replyMsg + return } // play 走棋 -func play(senderUin int64, groupCode int64, moveStr string) message.Message { +func play(senderUin int64, groupCode int64, moveStr string) (msg message.Message, err error) { + msg = message.Message{message.At(senderUin)} // 检查对局是否存在 room, ok := chessRoomMap.Load(groupCode) if !ok { - return nil + return nil, errNotExist } - // 不是对局中的玩家,忽略消息 + // 不是对局中的玩家, 忽略消息 if (senderUin != room.whitePlayer) && (senderUin != room.blackPlayer) && !isAprilFoolsDay() { - return nil + return } // 对局未建立 if (room.whitePlayer == 0) || (room.blackPlayer == 0) { - return textWithAt(senderUin, "请等候其他玩家加入游戏。") + msg = append(msg, message.Text("请等候其他玩家加入游戏。")) + return } // 需要对手走棋 if ((senderUin == room.whitePlayer) && (room.chessGame.Position().Turn() != chess.White)) || ((senderUin == room.blackPlayer) && (room.chessGame.Position().Turn() != chess.Black)) { - return textWithAt(senderUin, "请等待对手走棋。") + msg = append(msg, message.Text("请等待对手走棋。")) + return } room.lastMoveTime = time.Now().Unix() // 走棋 - if err := room.chessGame.MoveStr(moveStr); err != nil { + if err = room.chessGame.MoveStr(moveStr); err != nil { // 指令错误时检查 if !room.isBlindfold { - // 未开启盲棋,提示指令错误 - return simpleText(fmt.Sprintf("移动「%s」违规,请检查,格式请参考「代数记谱法」(Algebraic notation)。", moveStr)) + // 未开启盲棋, 提示指令错误 + msg = append(msg, message.Text("移动「", moveStr, "」违规, 请检查, 格式请参考「代数记谱法」(Algebraic notation)。")) + return } - // 开启盲棋,判断违例情况 + // 开启盲棋, 判断违例情况 var currentPlayerColor chess.Color if senderUin == room.whitePlayer { currentPlayerColor = chess.White } else { currentPlayerColor = chess.Black } - // 第一次违例,提示 + // 第一次违例, 提示 _flag := false if (currentPlayerColor == chess.White) && !room.whiteErr { room.whiteErr = true @@ -229,23 +218,18 @@ func play(senderUin int64, groupCode int64, moveStr string) message.Message { _flag = true } if _flag { - return simpleText(fmt.Sprintf("移动「%s」违例,再次违例会立即判负。", moveStr)) + msg = append(msg, message.Text("移动「", moveStr, "」违规, 再次违规会立即判负。")) + return } - // 出现多次违例,判负 + // 出现多次违例, 判负 room.chessGame.Resign(currentPlayerColor) chessString := getChessString(*room) - replyMsg := textWithAt(senderUin, "违例两次,游戏结束。\n"+chessString) - // 删除临时文件 - if inkscapeExists() { - if err := cleanTempFiles(groupCode); err != nil { - log.Debugln("[chess]", "Fail to clean temp files", err) - return message.Message{message.Text("ERROR: ", err)} - } - } + msg = append(msg, message.Text("违规两次,游戏结束。\n", chessString)) + chessRoomMap.Delete(groupCode) - return replyMsg + return } - // 走子之后,视为拒绝和棋 + // 走子之后, 视为拒绝和棋 if room.drawPlayer != 0 { room.drawPlayer = 0 chessRoomMap.Store(groupCode, room) @@ -253,26 +237,25 @@ func play(senderUin int64, groupCode int64, moveStr string) message.Message { // 生成棋盘图片 var boardImgEle message.MessageSegment if !room.isBlindfold { - boardMsg, ok, errMsg := getBoardElement(groupCode) - boardImgEle = boardMsg - if !ok { - return errorText(errMsg) + boardImgEle, err = getBoardElement(groupCode) + if err != nil { + return } } // 检查游戏是否结束 if room.chessGame.Method() != chess.NoMethod { whiteScore, blackScore := 0.5, 0.5 var msgBuilder strings.Builder - msgBuilder.WriteString("游戏结束,") + msgBuilder.WriteString("游戏结束, ") switch room.chessGame.Method() { case chess.FivefoldRepetition: - msgBuilder.WriteString("和棋,因为五次重复走子。\n") + msgBuilder.WriteString("和棋, 因为五次重复走子。\n") case chess.SeventyFiveMoveRule: - msgBuilder.WriteString("和棋,因为七十五步规则。\n") + msgBuilder.WriteString("和棋, 因为七十五步规则。\n") case chess.InsufficientMaterial: - msgBuilder.WriteString("和棋,因为不可能将死。\n") + msgBuilder.WriteString("和棋, 因为不可能将死。\n") case chess.Stalemate: - msgBuilder.WriteString("和棋,因为逼和(无子可动和棋)。\n") + msgBuilder.WriteString("和棋, 因为逼和(无子可动和棋)。\n") case chess.Checkmate: var winner string if room.chessGame.Position().Turn() == chess.White { @@ -285,7 +268,7 @@ func play(senderUin int64, groupCode int64, moveStr string) message.Message { winner = "白方" } msgBuilder.WriteString(winner) - msgBuilder.WriteString("胜利,因为将杀。\n") + msgBuilder.WriteString("胜利, 因为将杀。\n") case chess.NoMethod: case chess.Resignation: case chess.DrawOffer: @@ -296,34 +279,26 @@ func play(senderUin int64, groupCode int64, moveStr string) message.Message { chessString := getChessString(*room) eloString := "" if len(room.chessGame.Moves()) > 4 { - // 若走子次数超过 4 认为是有效对局,存入数据库 + // 若走子次数超过 4 认为是有效对局, 存入数据库 dbService := newDBService() - if err := dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { - log.Debugln("[chess]", "Fail to create PGN.", err) - return message.Message{message.Text("ERROR: ", err)} + if err = dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { + return } // 仅有效对局才会计算等级分 - elo, err := getELOString(*room, whiteScore, blackScore) + eloString, err = getELOString(*room, whiteScore, blackScore) if err != nil { - log.Debugln("[chess]", "Fail to get eloString.", eloString, err) - return message.Message{message.Text("ERROR: ", err)} + return } - eloString = elo } msgBuilder.WriteString(eloString) msgBuilder.WriteString(chessString) - replyMsg := simpleText(msgBuilder.String()) + msg = append(msg, message.Text(msgBuilder.String())) if !room.isBlindfold { - replyMsg = append(replyMsg, boardImgEle) - } - if inkscapeExists() { - if err := cleanTempFiles(groupCode); err != nil { - log.Debugln("[chess]", "Fail to clean temp files", err) - return message.Message{message.Text("ERROR: ", err)} - } + msg = append(msg, boardImgEle) } + chessRoomMap.Delete(groupCode) - return replyMsg + return } // 提示玩家继续游戏 var currentPlayer int64 @@ -332,49 +307,43 @@ func play(senderUin int64, groupCode int64, moveStr string) message.Message { } else { currentPlayer = room.blackPlayer } - return append(textWithAt(currentPlayer, "对手已走子,游戏继续。"), boardImgEle) -} - -// ranking 排行榜 -func ranking() message.Message { - ranking, err := getRankingString() - if err != nil { - log.Debugln("[chess]", "Fail to get player ranking.", err) - return simpleText(fmt.Sprintln("服务器错误,无法获取排行榜信息。请联系开发者修 bug。", err)) - } - return simpleText(ranking) + msg = message.Message{message.At(currentPlayer), message.Text("对手已走子, 游戏继续。"), boardImgEle} + return } // rate 获取等级分 -func rate(senderUin int64, senderName string) message.Message { +func rate(senderUin int64, senderName string) (msg message.Message, err error) { + rate := 0 dbService := newDBService() - rate, err := dbService.getELORateByUin(senderUin) - if err == gorm.ErrRecordNotFound { - return simpleText("没有查找到等级分信息。请至少进行一局对局。") - } + rate, err = dbService.getELORateByUin(senderUin) if err != nil { - log.Debugln("[chess]", "Fail to get player rank.", err) - return simpleText(fmt.Sprintln("服务器错误,无法获取等级分信息。请联系开发者修 bug。", err)) + if err != gorm.ErrRecordNotFound { + err = errors.New("无法获取等级分信息。") + return + } + err = errors.New("没有查找到等级分信息, 请至少进行一局对局。") } - return simpleText(fmt.Sprintf("玩家「%s」目前的等级分:%d", senderName, rate)) + msg = append(msg, message.Text("玩家「", senderName, "」目前的等级分: ", rate)) + return } // cleanUserRate 清空用户等级分 -func cleanUserRate(senderUin int64) message.Message { +func cleanUserRate(senderUin int64) (msg message.Message, err error) { dbService := newDBService() - err := dbService.cleanELOByUin(senderUin) - if err == gorm.ErrRecordNotFound { - return simpleText("没有查找到等级分信息。请检查用户 uid 是否正确。") - } + err = dbService.cleanELOByUin(senderUin) if err != nil { - log.Debugln("[chess]", "Fail to clean player rank.", err) - return simpleText(fmt.Sprintln("服务器错误,无法清空等级分。请联系开发者修 bug。", err)) + if err != gorm.ErrRecordNotFound { + err = errors.New("无法清空等级分。") + return + } + err = errors.New("没有查找到等级分信息, 请检查用户 uid 是否正确。") } - return simpleText(fmt.Sprintf("已清空用户「%d」的等级分。", senderUin)) + msg = append(msg, message.Text("已清空用户「", senderUin, "」的等级分。")) + return } // createGame 创建游戏 -func createGame(isBlindfold bool, groupCode int64, senderUin int64, senderName string) message.Message { +func createGame(isBlindfold bool, groupCode int64, senderUin int64, senderName string) (msg message.Message, err error) { room, ok := chessRoomMap.Load(groupCode) if !ok { chessRoomMap.Store(groupCode, &chessRoom{ @@ -389,79 +358,79 @@ func createGame(isBlindfold bool, groupCode int64, senderUin int64, senderName s whiteErr: false, blackErr: false, }) + text := "已创建新的对局, 发送「下棋」或「chess」可加入对局。" if isBlindfold { - return simpleText("已创建新的盲棋对局,发送「盲棋」或「blind」可加入对局。") + text = "已创建新的盲棋对局, 发送「盲棋」或「blind」可加入对局。" } - return simpleText("已创建新的对局,发送「下棋」或「chess」可加入对局。") + msg = append(msg, message.Text(text)) + return } + msg = message.Message{message.At(senderUin)} if room.blackPlayer != 0 { // 检测对局是否已存在超过 6 小时 if (time.Now().Unix() - room.lastMoveTime) > 21600 { - autoAbortMsg := abortGame(*room, groupCode, "对局已存在超过 6 小时,游戏结束。") - autoAbortMsg = append(autoAbortMsg, message.Text("\n\n已有对局已被中断,如需创建新对局请重新发送指令。")) - autoAbortMsg = append(autoAbortMsg, message.At(senderUin)) - return autoAbortMsg + msg, err = abortGame(*room, groupCode, "对局已存在超过 6 小时, 游戏结束。") + msg = append(msg, message.Text("\n\n已有对局已被中断, 如需创建新对局请重新发送指令。")) + msg = append(msg, message.At(senderUin)) + return } // 对局在进行 - msg := textWithAt(senderUin, "对局已在进行中,无法创建或加入对局,当前对局玩家为:") + msg = append(msg, message.Text("对局已在进行中, 无法创建或加入对局, 当前对局玩家为: ")) if room.whitePlayer != 0 { msg = append(msg, message.At(room.whitePlayer)) } if room.blackPlayer != 0 { msg = append(msg, message.At(room.blackPlayer)) } - msg = append(msg, message.Text(",群主或管理员发送「中断」或「abort」可中断对局(自动判和)。")) - return msg + msg = append(msg, message.Text(", 群主或管理员发送「中断」或「abort」可中断对局(自动判和)。")) + return } if senderUin == room.whitePlayer { - return textWithAt(senderUin, "请等候其他玩家加入游戏。") + msg = append(msg, message.Text("请等候其他玩家加入游戏。")) + return } if room.isBlindfold && !isBlindfold { - return simpleText("已创建盲棋对局,请加入或等待盲棋对局结束之后创建普通对局。") + msg = append(msg, message.Text("已创建盲棋对局, 请加入或等待盲棋对局结束之后创建普通对局。")) + return } if !room.isBlindfold && isBlindfold { - return simpleText("已创建普通对局,请加入或等待普通对局结束之后创建盲棋对局。") + msg = append(msg, message.Text("已创建普通对局, 请加入或等待普通对局结束之后创建盲棋对局。")) + return } room.blackPlayer = senderUin room.blackName = senderName chessRoomMap.Store(groupCode, room) var boardImgEle message.MessageSegment if !room.isBlindfold { - boardMsg, ok, errMsg := getBoardElement(groupCode) - if !ok { - return errorText(errMsg) + boardImgEle, err = getBoardElement(groupCode) + if err != nil { + return } - boardImgEle = boardMsg } - if isBlindfold { - return append(simpleText("黑棋已加入对局,请白方下棋。"), message.At(room.whitePlayer)) + msg = append(msg, message.Text("黑棋已加入对局, 请白方下棋。"), message.At(room.whitePlayer)) + if !isBlindfold { + msg = append(msg, boardImgEle) } - return append(simpleText("黑棋已加入对局,请白方下棋。"), message.At(room.whitePlayer), boardImgEle) + return } // abortGame 中断游戏 -func abortGame(room chessRoom, groupCode int64, hint string) message.Message { +func abortGame(room chessRoom, groupCode int64, hint string) (message.Message, error) { + var msg message.Message err := room.chessGame.Draw(chess.DrawOffer) if err != nil { - log.Debugln("[chess]", "Fail to draw a game.", err) - return simpleText(fmt.Sprintln("程序发生了错误,和棋失败,请反馈开发者修复 bug。", err)) + return nil, err } chessString := getChessString(room) if len(room.chessGame.Moves()) > 4 { dbService := newDBService() if err := dbService.createPGN(chessString, room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName); err != nil { - log.Debugln("[chess]", "Fail to create PGN.", err) - return message.Message{message.Text("ERROR: ", err)} - } - } - if inkscapeExists() { - if err := cleanTempFiles(groupCode); err != nil { - log.Debugln("[chess]", "Fail to clean temp files", err) - return message.Message{message.Text("ERROR: ", err)} + return nil, err } } + chessRoomMap.Delete(groupCode) - msg := simpleText(hint) + msg = append(msg, message.Text(hint)) if room.whitePlayer != 0 { msg = append(msg, message.At(room.whitePlayer)) } @@ -469,26 +438,18 @@ func abortGame(room chessRoom, groupCode int64, hint string) message.Message { msg = append(msg, message.At(room.blackPlayer)) } msg = append(msg, message.Text("\n\n"+chessString)) - return msg + return msg, nil } // getBoardElement 获取棋盘图片的消息内容 -func getBoardElement(groupCode int64) (message.MessageSegment, bool, string) { +func getBoardElement(groupCode int64) (imgMsg message.MessageSegment, err error) { + fontdata, err := file.GetLazyData(text.GNUUnifontFontFile, control.Md5File, true) + if err != nil { + return + } room, ok := chessRoomMap.Load(groupCode) if !ok { - log.Debugln(fmt.Sprintf("No room for groupCode %d.", groupCode)) - return message.MessageSegment{}, false, "对局不存在" - } - // 未安装 inkscape 直接返回对局字符串 - // TODO: 使用原生 go 库渲染 svg - if !inkscapeExists() { - boardString := room.chessGame.Position().Board().Draw() - boardImageB64, err := generateCharBoardImage(boardString) - if err != nil { - return message.MessageSegment{}, false, "生成棋盘图片时发生错误" - } - replyMsg := message.Image("base64://" + boardImageB64) - return replyMsg, true, "" + return imgMsg, errNotExist } // 获取高亮方块 highlightSquare := make([]chess.Square, 0, 2) @@ -499,27 +460,72 @@ func getBoardElement(groupCode int64) (message.MessageSegment, bool, string) { highlightSquare = append(highlightSquare, lastMove.S2()) } // 生成棋盘 svg 文件 - svgFilePath := path.Join(tempFileDir, fmt.Sprintf("%d.svg", groupCode)) + buf := bytes.NewBuffer([]byte{}) fenStr := room.chessGame.FEN() gameTurn := room.chessGame.Position().Turn() - if err := generateBoardSVG(svgFilePath, fenStr, gameTurn, highlightSquare...); err != nil { - log.Debugln("[chess]", "Unable to generate svg file.", err) - return message.MessageSegment{}, false, "无法生成 svg 图片,请检查后台日志。" + pos := &chess.Position{} + if err = pos.UnmarshalText(binary.StringToBytes(fenStr)); err != nil { + return } - // 调用 inkscape 将 svg 图片转化为 png 图片 - pngFilePath := path.Join(tempFileDir, fmt.Sprintf("%d.png", groupCode)) - if err := exec.Command("inkscape", "-w", "720", "-h", "720", svgFilePath, "-o", pngFilePath).Run(); err != nil { - log.Debugln("[chess]", "Unable to convert to png.", err) - return message.MessageSegment{}, false, "无法生成 png 图片,请检查 inkscape 安装情况及其依赖 libfuse。" - } - // 尝试读取 png 图片 - imgData, err := os.ReadFile(pngFilePath) + yellow := color.RGBA{255, 255, 0, 1} + mark := cimage.MarkSquares(yellow, highlightSquare...) + board := pos.Board() + fromBlack := cimage.Perspective(gameTurn) + err = cimage.SVG(buf, board, fromBlack, mark) if err != nil { - log.Debugln("[chess]", fmt.Sprintf("Unable to read image file in %s.", pngFilePath), err) - return message.MessageSegment{}, false, "无法读取 png 图片" + return } - imgMsg := message.Image("base64://" + base64.StdEncoding.EncodeToString(imgData)) - return imgMsg, true, "" + + worker, err := resvg.NewDefaultWorker(context.Background()) + if err != nil { + return + } + defer worker.Close() + + tree, err := worker.NewTreeFromData(buf.Bytes(), &resvg.Options{ + Dpi: 96, + FontFamily: "Unifont", + FontSize: 24.0, + }) + if err != nil { + return + } + defer tree.Close() + + fontdb, err := worker.NewFontDBDefault() + if err != nil { + return + } + defer fontdb.Close() + + err = fontdb.LoadFontData(fontdata) + if err != nil { + return + } + + err = tree.ConvertText(fontdb) + if err != nil { + return + } + + pixmap, err := worker.NewPixmap(720, 720) + if err != nil { + return + } + defer pixmap.Close() + + err = tree.Render(resvg.TransformFromScale(2, 2), pixmap) + if err != nil { + return + } + + out, err := pixmap.EncodePNG() + if err != nil { + return + } + + imgMsg = message.ImageBytes(out) + return imgMsg, nil } // getELOString 获得玩家等级分的文本内容 @@ -528,55 +534,46 @@ func getELOString(room chessRoom, whiteScore, blackScore float64) (string, error return "", nil } var msgBuilder strings.Builder - msgBuilder.WriteString("玩家等级分:\n") + msgBuilder.WriteString("玩家等级分: \n") dbService := newDBService() if err := updateELORate(room.whitePlayer, room.blackPlayer, room.whiteName, room.blackName, whiteScore, blackScore, dbService); err != nil { - msgBuilder.WriteString("发生错误,无法更新等级分。") - msgBuilder.WriteString(err.Error()) - return msgBuilder.String(), err + return "", err } whiteRate, blackRate, err := getELORate(room.whitePlayer, room.blackPlayer, dbService) if err != nil { - msgBuilder.WriteString("发生错误,无法获取等级分。") - msgBuilder.WriteString(err.Error()) - return msgBuilder.String(), err + return "", err } - msgBuilder.WriteString(fmt.Sprintf("%s:%d\n%s:%d\n\n", room.whiteName, whiteRate, room.blackName, blackRate)) + msgBuilder.WriteString(room.whiteName) + msgBuilder.WriteString(": ") + msgBuilder.WriteString(strconv.Itoa(whiteRate)) + msgBuilder.WriteString("\n") + msgBuilder.WriteString(room.blackName) + msgBuilder.WriteString(": ") + msgBuilder.WriteString(strconv.Itoa(blackRate)) + msgBuilder.WriteString("\n\n") return msgBuilder.String(), nil } // getRankingString 获取等级分排行榜的文本内容 -func getRankingString() (string, error) { +func getRanking() (message.Message, error) { dbService := newDBService() eloList, err := dbService.getHighestRateList() if err != nil { - return "", err + return nil, err } var msgBuilder strings.Builder - msgBuilder.WriteString("当前等级分排行榜:\n\n") + msgBuilder.WriteString("当前等级分排行榜: \n\n") for _, elo := range eloList { - msgBuilder.WriteString(fmt.Sprintf("%s: %d\n", elo.Name, elo.Rate)) + msgBuilder.WriteString(elo.Name) + msgBuilder.WriteString(": ") + msgBuilder.WriteString(strconv.Itoa(elo.Rate)) + msgBuilder.WriteString("\n") } - return msgBuilder.String(), nil -} - -func simpleText(msg string) message.Message { - return []message.MessageSegment{message.Text(msg)} -} - -func textWithAt(target int64, msg string) message.Message { - if target == 0 { - return simpleText("@全体成员 " + msg) - } - return []message.MessageSegment{message.At(target), message.Text(msg)} -} - -func errorText(errMsg string) message.Message { - return simpleText("发生错误,请联系开发者修 bug。\n错误信息:" + errMsg) + return message.Message{message.Text(msgBuilder.String())}, nil } // updateELORate 更新 elo 等级分 -// 当数据库中没有玩家的等级分信息时,自动新建一条记录 +// 当数据库中没有玩家的等级分信息时, 自动新建一条记录 func updateELORate(whiteUin, blackUin int64, whiteName, blackName string, whiteScore, blackScore float64, dbService *chessDBService) error { whiteRate, err := dbService.getELORateByUin(whiteUin) if err != nil { @@ -609,77 +606,6 @@ func updateELORate(whiteUin, blackUin int64, whiteName, blackName string, whiteS return dbService.updateELOByUin(blackUin, blackName, blackRate) } -// cleanTempFiles 清理临时文件 -func cleanTempFiles(groupCode int64) error { - svgFilePath := path.Join(tempFileDir, fmt.Sprintf("%d.svg", groupCode)) - if err := os.Remove(svgFilePath); err != nil { - return err - } - pngFilePath := path.Join(tempFileDir, fmt.Sprintf("%d.png", groupCode)) - return os.Remove(pngFilePath) -} - -// generateCharBoardImage 生成文字版的棋盘 -func generateCharBoardImage(boardString string) (string, error) { - boardString = strings.Trim(boardString, "\n") - const FontSize = 72 - h := FontSize*8 + 36 - w := FontSize*9 + 24 - dc := gg.NewContext(h, w) - dc.SetRGB(1, 1, 1) - dc.Clear() - dc.SetRGB(0, 0, 0) - fontdata, err := file.GetLazyData(text.GNUUnifontFontFile, control.Md5File, true) - if err != nil { - // TODO: err solve - panic(err) - } - if err := dc.ParseFontFace(fontdata, FontSize); err != nil { - return "", err - } - lines := strings.Split(boardString, "\n") - if len(lines) != 9 { - lines = make([]string, 9) - lines[0] = "ERROR [500]" - lines[1] = "程序内部错误" - lines[2] = "棋盘字符串不合法" - lines[3] = "请反馈开发者修复" - } - for i := 0; i < 9; i++ { - dc.DrawString(lines[i], 18, float64(FontSize*(i+1))) - } - imgBuffer := bytes.NewBuffer([]byte{}) - if err := dc.EncodePNG(imgBuffer); err != nil { - return "", err - } - imgData, err := io.ReadAll(imgBuffer) - if err != nil { - return "", err - } - imgB64 := base64.StdEncoding.EncodeToString(imgData) - return imgB64, nil -} - -// generateBoardSVG 生成棋盘 SVG 图片 -func generateBoardSVG(svgFilePath, fenStr string, gameTurn chess.Color, sqs ...chess.Square) error { - os.Remove(svgFilePath) - f, err := os.Create(svgFilePath) - if err != nil { - return err - } - defer f.Close() - - pos := &chess.Position{} - if err := pos.UnmarshalText(binary.StringToBytes(fenStr)); err != nil { - return err - } - yellow := color.RGBA{255, 255, 0, 1} - mark := image.MarkSquares(yellow, sqs...) - board := pos.Board() - fromBlack := image.Perspective(gameTurn) - return image.SVG(f, board, fromBlack, mark) -} - // getChessString 获取 PGN 字符串 func getChessString(room chessRoom) string { game := room.chessGame @@ -704,12 +630,6 @@ func getELORate(whiteUin, blackUin int64, dbService *chessDBService) (whiteRate return } -// inkscapeExists 判断 inkscape 是否存在 -func inkscapeExists() bool { - _, err := exec.LookPath("inkscape") - return err == nil -} - // isAprilFoolsDay 判断当前时间是否为愚人节期间 func isAprilFoolsDay() bool { now := time.Now()