feat: server map generator #59
|
@ -505,6 +505,9 @@ issues:
|
|||
linters:
|
||||
- gosec
|
||||
- gocyclo
|
||||
- path: server_map.go
|
||||
linters:
|
||||
- mnd
|
||||
- path: _test\.go
|
||||
text: add-constant
|
||||
- path: bun/migrations
|
||||
|
|
|
@ -1,33 +1,27 @@
|
|||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"go/ast"
|
||||
"go/constant"
|
||||
"go/importer"
|
||||
"go/parser"
|
||||
"go/token"
|
||||
"go/types"
|
||||
"io/fs"
|
||||
"log"
|
||||
"os"
|
||||
"slices"
|
||||
"strings"
|
||||
|
||||
"golang.org/x/tools/go/packages"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func main() {
|
||||
if _, err := os.Stat("./go.mod"); errors.Is(err, os.ErrNotExist) {
|
||||
log.Fatalln("go.mod not found")
|
||||
}
|
||||
|
||||
if err := generateYAML("./internal/domain"); err != nil {
|
||||
if err := generateYAML("gitea.dwysokinski.me/twhelp/core/internal/domain"); err != nil {
|
||||
log.Fatalln(err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateYAML(root string) error {
|
||||
m, err := getErrorCodesFromFolder(root)
|
||||
func generateYAML(pkgPattern string) error {
|
||||
m, err := getErrorCodesFromPackage(pkgPattern)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
@ -48,6 +42,8 @@ func generateYAML(root string) error {
|
|||
enum = append(enum, c)
|
||||
}
|
||||
|
||||
slices.Sort(enum)
|
||||
|
||||
return yaml.NewEncoder(os.Stdout).Encode(struct {
|
||||
Type string `yaml:"type"`
|
||||
Description string `yaml:"description"`
|
||||
|
@ -60,27 +56,25 @@ func generateYAML(root string) error {
|
|||
}
|
||||
|
||||
//nolint:gocyclo
|
||||
func getErrorCodesFromFolder(root string) (map[string]string, error) {
|
||||
fset := token.NewFileSet()
|
||||
|
||||
astDomainPkg, err := parseDomainPackage(fset, root)
|
||||
func getErrorCodesFromPackage(pkgPattern string) (map[string]string, error) {
|
||||
pkg, err := packages.Load(&packages.Config{
|
||||
Tests: false,
|
||||
Mode: packages.NeedName |
|
||||
packages.NeedFiles |
|
||||
packages.NeedCompiledGoFiles |
|
||||
packages.NeedTypes |
|
||||
packages.NeedTypesInfo |
|
||||
packages.NeedSyntax,
|
||||
}, pkgPattern)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
conf := types.Config{Importer: importer.Default()}
|
||||
|
||||
files := make([]*ast.File, 0, len(astDomainPkg.Files))
|
||||
for _, f := range astDomainPkg.Files {
|
||||
files = append(files, f)
|
||||
if len(pkg) == 0 {
|
||||
return nil, fmt.Errorf("'%s': pkg not found", pkgPattern)
|
||||
}
|
||||
|
||||
domainPkg, err := conf.Check(astDomainPkg.Name, fset, files, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
scope := domainPkg.Scope()
|
||||
|
||||
domainPkg := pkg[0]
|
||||
scope := domainPkg.Types.Scope()
|
||||
m := make(map[string]string)
|
||||
|
||||
for _, n := range scope.Names() {
|
||||
|
@ -95,14 +89,21 @@ func getErrorCodesFromFolder(root string) (map[string]string, error) {
|
|||
continue
|
||||
}
|
||||
|
||||
f := astDomainPkg.Files[fset.File(c.Pos()).Name()]
|
||||
position := fset.Position(c.Pos())
|
||||
commentMap := ast.NewCommentMap(fset, f, f.Comments)
|
||||
filename := domainPkg.Fset.File(c.Pos()).Name()
|
||||
fileIdx := slices.IndexFunc(domainPkg.GoFiles, func(f string) bool {
|
||||
return f == filename
|
||||
})
|
||||
if fileIdx < 0 {
|
||||
return nil, fmt.Errorf("'%s': file not found", filename)
|
||||
}
|
||||
f := domainPkg.Syntax[fileIdx]
|
||||
position := domainPkg.Fset.Position(c.Pos())
|
||||
commentMap := ast.NewCommentMap(domainPkg.Fset, f, f.Comments)
|
||||
|
||||
var desc string
|
||||
|
||||
for _, decl := range f.Decls {
|
||||
declPosition := fset.Position(decl.Pos())
|
||||
declPosition := domainPkg.Fset.Position(decl.Pos())
|
||||
if declPosition.Filename != position.Filename || declPosition.Line != position.Line {
|
||||
continue
|
||||
}
|
||||
|
@ -123,27 +124,3 @@ func getErrorCodesFromFolder(root string) (map[string]string, error) {
|
|||
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func parseDomainPackage(fset *token.FileSet, root string) (*ast.Package, error) {
|
||||
pkgs, err := parser.ParseDir(fset, root, func(info fs.FileInfo) bool {
|
||||
if info.IsDir() && info.Name() != root {
|
||||
return false
|
||||
}
|
||||
|
||||
if info.IsDir() || strings.HasSuffix(info.Name(), "_test.go") {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}, parser.AllErrors|parser.ParseComments)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
astDomainPkg, ok := pkgs["domain"]
|
||||
if !ok {
|
||||
return nil, errors.New("domain pkg not found")
|
||||
}
|
||||
|
||||
return astDomainPkg, nil
|
||||
}
|
||||
|
|
5
go.mod
5
go.mod
|
@ -28,7 +28,9 @@ require (
|
|||
github.com/uptrace/bun/extra/bundebug v1.2.1
|
||||
github.com/urfave/cli/v2 v2.27.2
|
||||
go.uber.org/automaxprocs v1.5.3
|
||||
golang.org/x/image v0.17.0
|
||||
golang.org/x/sync v0.7.0
|
||||
golang.org/x/tools v0.19.0
|
||||
gopkg.in/yaml.v3 v3.0.1
|
||||
)
|
||||
|
||||
|
@ -43,7 +45,7 @@ require (
|
|||
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/docker/cli v20.10.17+incompatible // indirect
|
||||
github.com/docker/docker v20.10.7+incompatible // indirect
|
||||
github.com/docker/docker v20.10.11+incompatible // indirect
|
||||
github.com/docker/go-connections v0.4.0 // indirect
|
||||
github.com/docker/go-units v0.4.0 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
|
@ -90,7 +92,6 @@ require (
|
|||
golang.org/x/crypto v0.21.0 // indirect
|
||||
golang.org/x/mod v0.16.0 // indirect
|
||||
golang.org/x/sys v0.18.0 // indirect
|
||||
golang.org/x/tools v0.19.0 // indirect
|
||||
gopkg.in/yaml.v2 v2.3.0 // indirect
|
||||
mellium.im/sasl v0.3.1 // indirect
|
||||
modernc.org/gc/v3 v3.0.0-20240304020402-f0dba7c97c2b // indirect
|
||||
|
|
6
go.sum
6
go.sum
|
@ -40,8 +40,8 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
|
|||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/docker/cli v20.10.17+incompatible h1:eO2KS7ZFeov5UJeaDmIs1NFEDRf32PaqRpvoEkKBy5M=
|
||||
github.com/docker/cli v20.10.17+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8=
|
||||
github.com/docker/docker v20.10.7+incompatible h1:Z6O9Nhsjv+ayUEeI1IojKbYcsGdgYSNqxe1s2MYzUhQ=
|
||||
github.com/docker/docker v20.10.7+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/docker v20.10.11+incompatible h1:OqzI/g/W54LczvhnccGqniFoQghHx3pklbLuhfXpqGo=
|
||||
github.com/docker/docker v20.10.11+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk=
|
||||
github.com/docker/go-connections v0.4.0 h1:El9xVISelRB7BuFusrZozjnkIM5YnzCViNKohAFqRJQ=
|
||||
github.com/docker/go-connections v0.4.0/go.mod h1:Gbd7IOopHjR8Iph03tsViu4nIes5XhDvyHbTtUxmeec=
|
||||
github.com/docker/go-units v0.4.0 h1:3uh0PgVws3nIA0Q+MwDC8yjEPf9zjRfZZWXZYDct3Tw=
|
||||
|
@ -233,6 +233,8 @@ golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8U
|
|||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||
golang.org/x/image v0.17.0 h1:nTRVVdajgB8zCMZVsViyzhnMKPwYeroEERRC64JuLco=
|
||||
golang.org/x/image v0.17.0/go.mod h1:4yyo5vMFQjVjUcVk4jEQcU9MGy/rulF5WvUILseCM2E=
|
||||
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||
golang.org/x/mod v0.16.0 h1:QX4fJ0Rr5cPQCF7O9lh9Se4pmwfwskqZfq5moyldzic=
|
||||
|
|
|
@ -20,7 +20,9 @@ func NewServerConfig(tb TestingTB) domain.ServerConfig {
|
|||
domain.ServerConfigBuildings{},
|
||||
domain.ServerConfigSnob{},
|
||||
domain.ServerConfigAlly{Limit: gofakeit.IntRange(1, 20)},
|
||||
domain.ServerConfigCoord{},
|
||||
domain.ServerConfigCoord{
|
||||
MapSize: gofakeit.IntRange(1000, 1000),
|
||||
},
|
||||
domain.ServerConfigSitter{},
|
||||
domain.ServerConfigSleep{},
|
||||
domain.ServerConfigNight{},
|
||||
|
|
|
@ -37,6 +37,8 @@ func NewVillageCursor(tb TestingTB, opts ...func(cfg *VillageCursorConfig)) doma
|
|||
type VillageConfig struct {
|
||||
ID int
|
||||
ServerKey string
|
||||
X int
|
||||
Y int
|
||||
PlayerID int
|
||||
}
|
||||
|
||||
|
@ -46,6 +48,8 @@ func NewVillage(tb TestingTB, opts ...func(cfg *VillageConfig)) domain.Village {
|
|||
cfg := &VillageConfig{
|
||||
ID: RandID(),
|
||||
ServerKey: RandServerKey(),
|
||||
X: gofakeit.IntRange(1, 999),
|
||||
Y: gofakeit.IntRange(1, 999),
|
||||
PlayerID: gofakeit.IntRange(0, 10000),
|
||||
}
|
||||
|
||||
|
@ -58,8 +62,8 @@ func NewVillage(tb TestingTB, opts ...func(cfg *VillageConfig)) domain.Village {
|
|||
cfg.ServerKey,
|
||||
gofakeit.LetterN(50),
|
||||
gofakeit.IntRange(1, 10000),
|
||||
gofakeit.IntRange(1, 999),
|
||||
gofakeit.IntRange(1, 999),
|
||||
cfg.X,
|
||||
cfg.Y,
|
||||
gofakeit.LetterN(3),
|
||||
0,
|
||||
cfg.PlayerID,
|
||||
|
|
|
@ -1,23 +1,109 @@
|
|||
package domain
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"io"
|
||||
"strconv"
|
||||
|
||||
"golang.org/x/image/draw"
|
||||
"golang.org/x/image/font"
|
||||
"golang.org/x/image/font/basicfont"
|
||||
"golang.org/x/image/math/fixed"
|
||||
)
|
||||
|
||||
type ServerMapMarker struct {
|
||||
village VillageMeta
|
||||
color color.RGBA
|
||||
}
|
||||
|
||||
func NewServerMapMarker(village VillageMeta) ServerMapMarker {
|
||||
return ServerMapMarker{
|
||||
village: village,
|
||||
color: color.RGBA{R: 255, G: 99, B: 71, A: 255},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServerMapMarker) Village() VillageMeta {
|
||||
return m.village
|
||||
}
|
||||
|
||||
func (m *ServerMapMarker) SetColor(c color.RGBA) error {
|
||||
m.color = c
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMapMarker) Color() color.RGBA {
|
||||
return m.color
|
||||
}
|
||||
|
||||
type ServerMapLegendEntry struct {
|
||||
color color.Color
|
||||
names []string
|
||||
}
|
||||
|
||||
func NewServerMapLegendEntry(c color.Color, names ...string) ServerMapLegendEntry {
|
||||
return ServerMapLegendEntry{color: c, names: names}
|
||||
}
|
||||
|
||||
func (e ServerMapLegendEntry) Color() color.Color {
|
||||
return e.color
|
||||
}
|
||||
|
||||
func (e ServerMapLegendEntry) Names() []string {
|
||||
return e.names
|
||||
}
|
||||
|
||||
type ServerMap struct {
|
||||
server Server
|
||||
villagesCh <-chan VillageMetaWithRelations
|
||||
size int
|
||||
markersCh <-chan ServerMapMarker
|
||||
backgroundColor color.RGBA
|
||||
scale float32
|
||||
centerX int
|
||||
centerY int
|
||||
continentGrid bool
|
||||
gridLineColor color.RGBA
|
||||
continentNumbers bool
|
||||
continentNumberColor color.RGBA
|
||||
legendEntries []ServerMapLegendEntry
|
||||
}
|
||||
|
||||
const serverMapModelName = "ServerMap"
|
||||
|
||||
func NewServerMap(server Server, villagesCh <-chan VillageMetaWithRelations) ServerMap {
|
||||
func NewServerMap(server Server, markersCh <-chan ServerMapMarker) ServerMap {
|
||||
size := server.Config().Coord().MapSize
|
||||
|
||||
return ServerMap{
|
||||
server: server,
|
||||
villagesCh: villagesCh,
|
||||
size: size,
|
||||
markersCh: markersCh,
|
||||
backgroundColor: color.RGBA{
|
||||
A: 255,
|
||||
},
|
||||
scale: 1,
|
||||
centerX: size / 2,
|
||||
centerY: size / 2,
|
||||
continentGrid: true,
|
||||
gridLineColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
continentNumbers: true,
|
||||
continentNumberColor: color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServerMap) Scale() float32 {
|
||||
return m.scale
|
||||
}
|
||||
|
||||
const (
|
||||
serverMapMinScale = 1
|
||||
serverMapMaxScale = 5
|
||||
)
|
||||
|
||||
func (m *ServerMap) SetScale(scale float32) error {
|
||||
if err := validateInRange(scale, 0, 1); err != nil {
|
||||
if err := validateInRange(scale, serverMapMinScale, serverMapMaxScale); err != nil {
|
||||
return ValidationError{
|
||||
Model: serverMapModelName,
|
||||
Field: "scale",
|
||||
|
@ -29,3 +115,271 @@ func (m *ServerMap) SetScale(scale float32) error {
|
|||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) BackgroundColor() color.RGBA {
|
||||
return m.backgroundColor
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetBackgroundColor(backgroundColor color.RGBA) error {
|
||||
m.backgroundColor = backgroundColor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) CenterX() int {
|
||||
return m.centerX
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetCenterX(centerX int) error {
|
||||
m.centerX = centerX
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) CenterY() int {
|
||||
return m.centerY
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetCenterY(centerY int) error {
|
||||
m.centerY = centerY
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) ContinentGrid() bool {
|
||||
return m.continentGrid
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetContinentGrid(continentGrid bool) error {
|
||||
m.continentGrid = continentGrid
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) GridLineColor() color.RGBA {
|
||||
return m.gridLineColor
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetGridLineColor(gridLineColor color.RGBA) error {
|
||||
m.gridLineColor = gridLineColor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) ContinentNumbers() bool {
|
||||
return m.continentNumbers
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetContinentNumbers(continentNumbers bool) error {
|
||||
m.continentNumbers = continentNumbers
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) ContinentNumberColor() color.RGBA {
|
||||
return m.continentNumberColor
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetContinentNumberColor(continentNumberColor color.RGBA) error {
|
||||
m.continentNumberColor = continentNumberColor
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) LegendEntries() []ServerMapLegendEntry {
|
||||
return m.legendEntries
|
||||
}
|
||||
|
||||
func (m *ServerMap) SetLegendEntries(legendEntries []ServerMapLegendEntry) error {
|
||||
m.legendEntries = legendEntries
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *ServerMap) Generate(w io.Writer) error {
|
||||
r := image.Rectangle{Max: image.Point{X: m.size, Y: m.size}}
|
||||
imgHalfWidth := r.Dx() / 2
|
||||
imgHalfHeight := r.Dy() / 2
|
||||
|
||||
img := image.NewRGBA(r)
|
||||
|
||||
draw.Draw(img, img.Bounds(), image.NewUniform(m.backgroundColor), image.Point{}, draw.Src)
|
||||
m.drawContinentGrid(img)
|
||||
m.drawMarkers(img)
|
||||
m.drawContinentNumbers(img)
|
||||
legend, err := m.generateLegend()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
scaledImg := m.scaleImg(img)
|
||||
|
||||
final := image.NewRGBA(image.Rect(0, 0, r.Dx()+legend.Rect.Dx(), r.Dy()))
|
||||
draw.Draw(final, scaledImg.Rect, scaledImg, image.Point{
|
||||
X: int(float32(m.centerX)*m.scale) - imgHalfWidth,
|
||||
Y: int(float32(m.centerY)*m.scale) - imgHalfHeight,
|
||||
}, draw.Src)
|
||||
draw.Draw(final, image.Rect(r.Dx(), 0, final.Rect.Dx(), final.Rect.Dy()), legend, image.Point{}, draw.Src)
|
||||
|
||||
if err = png.Encode(w, final); err != nil {
|
||||
return fmt.Errorf("couldn't encode image: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
const serverMapContinentDx = 100
|
||||
|
||||
func (m *ServerMap) drawContinentGrid(img *image.RGBA) {
|
||||
if !m.continentGrid {
|
||||
return
|
||||
}
|
||||
|
||||
for y := serverMapContinentDx; y < m.size; y += serverMapContinentDx {
|
||||
for x := range m.size {
|
||||
img.Set(x, y, m.gridLineColor)
|
||||
img.Set(y, x, m.gridLineColor)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const serverMapMarkerRectSideLength = 1
|
||||
|
||||
func (m *ServerMap) drawMarkers(img *image.RGBA) {
|
||||
for marker := range m.markersCh {
|
||||
village := marker.Village()
|
||||
x := village.X()
|
||||
y := village.Y()
|
||||
rect := image.Rect(x, y, x+serverMapMarkerRectSideLength, y+serverMapMarkerRectSideLength)
|
||||
draw.Draw(img, rect, &image.Uniform{C: marker.color}, image.Point{}, draw.Over)
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServerMap) drawContinentNumbers(img *image.RGBA) {
|
||||
if !m.continentNumbers {
|
||||
return
|
||||
}
|
||||
|
||||
continent := 0
|
||||
|
||||
d := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.NewUniform(m.continentNumberColor),
|
||||
Face: basicfont.Face7x13,
|
||||
}
|
||||
|
||||
for y := serverMapContinentDx; y <= m.size; y += serverMapContinentDx {
|
||||
for x := serverMapContinentDx; x <= m.size; x += serverMapContinentDx {
|
||||
continentStr := strconv.Itoa(continent)
|
||||
|
||||
d.Dot = fixed.Point26_6{
|
||||
X: fixed.I(x) - d.MeasureString(continentStr),
|
||||
Y: fixed.I(y - 1),
|
||||
}
|
||||
|
||||
d.DrawString(continentStr)
|
||||
|
||||
continent++
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (m *ServerMap) scaleImg(img *image.RGBA) *image.RGBA {
|
||||
if m.scale == 1 {
|
||||
return img
|
||||
}
|
||||
|
||||
scaledSize := int(float32(m.size) * m.scale)
|
||||
scaledImg := image.NewRGBA(image.Rect(0, 0, scaledSize, scaledSize))
|
||||
draw.NearestNeighbor.Scale(scaledImg, scaledImg.Rect, img, img.Rect, draw.Over, nil)
|
||||
|
||||
return scaledImg
|
||||
}
|
||||
|
||||
const (
|
||||
serverMapLegendWidth = 400
|
||||
serverMapLegendPadding = 10
|
||||
serverMapColorWidth = 40
|
||||
serverMapSpaceColorText = 10
|
||||
serverMapLegendStringSeparator = " + "
|
||||
serverMapLegendStringOverflow = "..."
|
||||
serverMapLegendMaxStringLength = 45
|
||||
)
|
||||
|
||||
var ErrTooManyLegendEntries error = simpleError{
|
||||
msg: "too many legend entries",
|
||||
typ: ErrorTypeIncorrectInput,
|
||||
code: "too-many-legend-entries",
|
||||
}
|
||||
|
||||
func (m *ServerMap) generateLegend() (*image.RGBA, error) {
|
||||
if len(m.legendEntries) == 0 {
|
||||
return image.NewRGBA(image.Rectangle{}), nil
|
||||
}
|
||||
|
||||
img := image.NewRGBA(image.Rectangle{Max: image.Point{X: serverMapLegendWidth, Y: m.size}})
|
||||
d := &font.Drawer{
|
||||
Dst: img,
|
||||
Src: image.NewUniform(m.continentNumberColor),
|
||||
Face: basicfont.Face7x13,
|
||||
}
|
||||
lineHeight := basicfont.Face7x13.Metrics().Ascent.Ceil() + basicfont.Face7x13.Metrics().Descent.Ceil()
|
||||
lineMaxWidth := img.Rect.Dx() - serverMapLegendPadding*2 - serverMapColorWidth - serverMapSpaceColorText
|
||||
offset := serverMapLegendPadding
|
||||
|
||||
for _, e := range m.legendEntries {
|
||||
namesLen := len(e.Names())
|
||||
|
||||
currentWidth := 0
|
||||
line := ""
|
||||
var lines []string
|
||||
|
||||
for i, name := range e.Names() {
|
||||
if len(name) > serverMapLegendMaxStringLength {
|
||||
name = name[:serverMapLegendMaxStringLength-len(serverMapLegendStringOverflow)] + "..."
|
||||
}
|
||||
|
||||
if i != namesLen-1 {
|
||||
name += serverMapLegendStringSeparator
|
||||
}
|
||||
|
||||
strWidth := d.MeasureString(name).Ceil()
|
||||
|
||||
if currentWidth+strWidth > lineMaxWidth {
|
||||
lines = append(lines, line)
|
||||
currentWidth = 0
|
||||
line = ""
|
||||
}
|
||||
|
||||
line += name
|
||||
currentWidth += strWidth
|
||||
}
|
||||
|
||||
if line != "" {
|
||||
lines = append(lines, line)
|
||||
}
|
||||
|
||||
yMin := offset + (len(lines) * lineHeight / 2) - lineHeight/2
|
||||
|
||||
draw.Draw(
|
||||
img,
|
||||
image.Rect(
|
||||
serverMapLegendPadding,
|
||||
yMin,
|
||||
serverMapColorWidth+serverMapLegendPadding,
|
||||
yMin+lineHeight,
|
||||
),
|
||||
image.NewUniform(e.Color()),
|
||||
image.Point{},
|
||||
draw.Src,
|
||||
)
|
||||
|
||||
for i, l := range lines {
|
||||
d.Dot = fixed.Point26_6{
|
||||
X: fixed.I(serverMapColorWidth + serverMapLegendPadding + serverMapSpaceColorText),
|
||||
Y: fixed.I(offset + (i+1)*lineHeight),
|
||||
}
|
||||
d.DrawString(l)
|
||||
}
|
||||
|
||||
offset += (len(lines) * lineHeight) + serverMapLegendPadding
|
||||
|
||||
if offset > img.Bounds().Dy()-serverMapLegendPadding {
|
||||
return nil, ErrTooManyLegendEntries
|
||||
}
|
||||
}
|
||||
|
||||
return img, nil
|
||||
}
|
||||
|
|
155
internal/domain/server_map_test.go
Normal file
155
internal/domain/server_map_test.go
Normal file
|
@ -0,0 +1,155 @@
|
|||
package domain_test
|
||||
|
||||
import (
|
||||
"image/color"
|
||||
"io"
|
||||
"testing"
|
||||
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/domain"
|
||||
"gitea.dwysokinski.me/twhelp/core/internal/domain/domaintest"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestServerMap_SetScale(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
type args struct {
|
||||
scale float32
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
expectedErr error
|
||||
}{
|
||||
{
|
||||
name: "OK",
|
||||
args: args{
|
||||
scale: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: scale < 1",
|
||||
args: args{
|
||||
scale: 0.99,
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ServerMap",
|
||||
Field: "scale",
|
||||
Err: domain.MinGreaterEqualError[float32]{
|
||||
Min: 1,
|
||||
Current: 0.99,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "ERR: scale > 5",
|
||||
args: args{
|
||||
scale: 5.00001,
|
||||
},
|
||||
expectedErr: domain.ValidationError{
|
||||
Model: "ServerMap",
|
||||
Field: "scale",
|
||||
Err: domain.MaxLessEqualError[float32]{
|
||||
Max: 5,
|
||||
Current: 5.00001,
|
||||
},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
params := domain.NewServerMap(domaintest.NewServer(t), make(chan domain.ServerMapMarker))
|
||||
|
||||
require.ErrorIs(t, params.SetScale(tt.args.scale), tt.expectedErr)
|
||||
if tt.expectedErr != nil {
|
||||
return
|
||||
}
|
||||
assert.InDelta(t, tt.args.scale, params.Scale(), 0.001)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestServerMap_Generate(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
t.Run("OK: default", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch := make(chan domain.ServerMapMarker)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for i := range 500 {
|
||||
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||
cfg.X = i
|
||||
cfg.Y = i
|
||||
}).Meta())
|
||||
}
|
||||
}()
|
||||
|
||||
m := domain.NewServerMap(domaintest.NewServer(t), ch)
|
||||
require.NoError(t, m.Generate(io.Discard))
|
||||
})
|
||||
|
||||
t.Run("OK: scale=2 legend", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch := make(chan domain.ServerMapMarker)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for i := range 500 {
|
||||
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||
cfg.X = i
|
||||
cfg.Y = i
|
||||
}).Meta())
|
||||
}
|
||||
}()
|
||||
|
||||
m := domain.NewServerMap(domaintest.NewServer(t), ch)
|
||||
require.NoError(t, m.SetLegendEntries([]domain.ServerMapLegendEntry{
|
||||
domain.NewServerMapLegendEntry(
|
||||
color.RGBA{R: 255, G: 204, B: 229, A: 255},
|
||||
"test22222^",
|
||||
"test222",
|
||||
),
|
||||
domain.NewServerMapLegendEntry(
|
||||
color.RGBA{R: 255, G: 255, B: 255, A: 255},
|
||||
"test22222^",
|
||||
"test2223",
|
||||
"longstringlongstringlongstringlongstring",
|
||||
"longstringlongstringlongstringlongstringlongstringlongstringlongstringlongstring",
|
||||
"longstringlongstring2",
|
||||
),
|
||||
}))
|
||||
require.NoError(t, m.SetScale(2))
|
||||
require.NoError(t, m.Generate(io.Discard))
|
||||
})
|
||||
|
||||
t.Run("OK: scale=2 centerX=250 centerY=250", func(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
ch := make(chan domain.ServerMapMarker)
|
||||
|
||||
go func() {
|
||||
defer close(ch)
|
||||
for i := range 500 {
|
||||
ch <- domain.NewServerMapMarker(domaintest.NewVillage(t, func(cfg *domaintest.VillageConfig) {
|
||||
cfg.X = i
|
||||
cfg.Y = i
|
||||
}).Meta())
|
||||
}
|
||||
}()
|
||||
|
||||
m := domain.NewServerMap(domaintest.NewServer(t), ch)
|
||||
require.NoError(t, m.SetCenterX(250))
|
||||
require.NoError(t, m.SetCenterY(250))
|
||||
require.NoError(t, m.SetScale(2))
|
||||
require.NoError(t, m.Generate(io.Discard))
|
||||
})
|
||||
}
|
Loading…
Reference in New Issue
Block a user