This commit is contained in:
Dawid Wysokiński 2023-12-15 08:16:51 +01:00
commit d787e2f431
Signed by: Kichiyaki
GPG Key ID: B5445E357FB8B892
30 changed files with 4298 additions and 0 deletions

2
.commitlintrc.yml Normal file
View File

@ -0,0 +1,2 @@
extends:
- "@commitlint/config-conventional"

19
.dockerignore Normal file
View File

@ -0,0 +1,19 @@
.git
.env*
README.md
LICENSE
renovate.json
.gitignore
.dockerignore
bin
.golangci.yml
.pre-commit-config.yaml
build
k8s
.commitlintrc.yml
additional_envs.sh
skaffold.yml
.editorconfig
.woodpecker
.kpt-pipeline
tmp

5
.envrc Normal file
View File

@ -0,0 +1,5 @@
PATH_add bin
export GOBIN=$PWD/bin
if [ -f "additional_envs.sh" ]; then
source <(cat additional_envs.sh)
fi

5
.gitignore vendored Normal file
View File

@ -0,0 +1,5 @@
.idea
bin/*
additional_envs.sh
.kpt-pipeline
tmp

476
.golangci.yml Normal file
View File

@ -0,0 +1,476 @@
run:
tests: true
timeout: 5m
linters:
disable-all: true
enable:
- gocyclo
- asasalint
- asciicheck
- bodyclose
- bidichk
- exportloopref
- errcheck
- gocritic
- gosec
- gofmt
- goimports
- gosimple
- govet
- ineffassign
- misspell
- nakedret
- prealloc
- staticcheck
- typecheck
- unconvert
- unused
- lll
- nestif
- thelper
- nonamedreturns
- tenv
- testpackage
- noctx
- tparallel
- usestdlibvars
- unconvert
- makezero
- grouper
- errname
- exhaustive
- tagliatelle
- contextcheck
- gocheckcompilerdirectives
- errname
- forcetypeassert
- durationcheck
- predeclared
- promlinter
- wastedassign
- testifylint
- inamedparam
- sloglint
- revive
linters-settings:
gocyclo:
min-complexity: 10
tagliatelle:
case:
rules:
json: camel
bun: snake
lll:
line-length: 120
govet:
enable:
- asmdecl
- assign
- atomic
- atomicalign
- bools
- buildtag
- cgocall
- composites
- copylocks
- deepequalerrors
- errorsas
- findcall
- framepointer
- httpresponse
- ifaceassert
- loopclosure
- lostcancel
- nilfunc
- nilness
- printf
- reflectvaluecompare
- shadow
- shift
- sigchanyzer
- sortslice
- stdmethods
- stringintconv
- structtag
- testinggoroutine
- tests
- unmarshal
- unreachable
- unsafeptr
- unusedresult
- unusedwrite
testifylint:
enable-all: true
sloglint:
attr-only: true
revive:
rules:
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#add-constant
- name: add-constant
severity: warning
disabled: true
arguments:
- maxLitCount: "3"
allowStrs: "\"\""
ignoreFuncs: os.Exit,wg.Add,make
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#argument-limit
- name: argument-limit
severity: warning
disabled: true
arguments: [4]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#atomic
- name: atomic
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#banned-characters
- name: banned-characters
severity: warning
disabled: true
arguments: []
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bare-return
- name: bare-return
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#blank-imports
- name: blank-imports
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr
- name: bool-literal-in-expr
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#call-to-gc
- name: call-to-gc
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cognitive-complexity
- name: cognitive-complexity
severity: warning
disabled: true
arguments: [7]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#comment-spacings
- name: comment-spacings
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-naming
- name: confusing-naming
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#confusing-results
- name: confusing-results
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#constant-logical-expr
- name: constant-logical-expr
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-as-argument
- name: context-as-argument
severity: error
disabled: false
arguments:
- allowTypesBefore: "*testing.T,testing.TB"
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#context-keys-type
- name: context-keys-type
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#cyclomatic
- name: cyclomatic
severity: warning
disabled: true
arguments: [10]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#datarace
- name: datarace
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#deep-exit
- name: deep-exit
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#defer
- name: defer
severity: error
disabled: false
arguments:
- [call-chain, loop]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#dot-imports
- name: dot-imports
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#duplicated-imports
- name: duplicated-imports
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return
- name: early-return
severity: error
disabled: false
arguments:
- preserveScope
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-block
- name: empty-block
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines
- name: empty-lines
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#enforce-map-style
- name: enforce-map-style
severity: error
disabled: false
arguments:
- make
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-naming
- name: error-naming
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-return
- name: error-return
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#error-strings
- name: error-strings
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#errorf
- name: errorf
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#exported
- name: exported
severity: warning
disabled: true
arguments:
- preserveScope
- checkPrivateReceivers
- sayRepetitiveInsteadOfStutters
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#file-header
- name: file-header
severity: warning
disabled: true
arguments:
- This is the text that must appear at the top of source files.
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#flag-parameter
- name: flag-parameter
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-result-limit
- name: function-result-limit
severity: warning
disabled: false
arguments: [4]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#function-length
- name: function-length
severity: warning
disabled: true
arguments: [10, 0]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#get-return
- name: get-return
severity: warning
disabled: true
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#identical-branches
- name: identical-branches
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#if-return
- name: if-return
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#increment-decrement
- name: increment-decrement
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#indent-error-flow
- name: indent-error-flow
severity: warning
disabled: false
arguments:
- preserveScope
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-alias-naming
- name: import-alias-naming
severity: warning
disabled: false
arguments:
- ^[a-z][a-z0-9]{0,}$
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#imports-blacklist
- name: imports-blacklist
severity: error
disabled: false
arguments:
- reflect
- github.com/pkg/errors
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#import-shadowing
- name: import-shadowing
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#line-length-limit
- name: line-length-limit
severity: warning
# lll is enabled
disabled: true
arguments: [80]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#max-public-structs
- name: max-public-structs
severity: warning
disabled: true
arguments: [3]
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-parameter
- name: modifies-parameter
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#modifies-value-receiver
- name: modifies-value-receiver
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#nested-structs
- name: nested-structs
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#optimize-operands-order
- name: optimize-operands-order
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#package-comments
- name: package-comments
severity: warning
disabled: true
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range
- name: range
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-in-closure
- name: range-val-in-closure
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#range-val-address
- name: range-val-address
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#receiver-naming
- name: receiver-naming
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redundant-import-alias
- name: redundant-import-alias
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#redefines-builtin-id
- name: redefines-builtin-id
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-of-int
- name: string-of-int
severity: warning
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format
- name: string-format
severity: warning
disabled: true
arguments:
- - core.WriteError[1].Message
- /^([^A-Z]|$)/
- must not start with a capital letter
- - fmt.Errorf[0]
- /(^|[^\.!?])$/
- must not end in punctuation
- - panic
- /^[^\n]*$/
- must not contain line breaks
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag
- name: struct-tag
arguments:
- json,inline
- bson,outline,gnu
severity: warning
# tagliatelle is enabled
disabled: true
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#superfluous-else
- name: superfluous-else
severity: error
disabled: false
arguments:
- preserveScope
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-equal
- name: time-equal
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#time-naming
- name: time-naming
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-naming
- name: var-naming
severity: error
disabled: false
arguments:
- [] # AllowList
- [] # DenyList
- - upperCaseConst: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#var-declaration
- name: var-declaration
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unconditional-recursion
- name: unconditional-recursion
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming
- name: unexported-naming
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-return
- name: unexported-return
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error
- name: unhandled-error
severity: warning
disabled: false
arguments:
- fmt.Printf
- fmt.Println
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unnecessary-stmt
- name: unnecessary-stmt
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unreachable-code
- name: unreachable-code
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter
- name: unused-parameter
severity: error
disabled: false
arguments:
- allowRegex: ^_
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver
- name: unused-receiver
severity: error
disabled: true
arguments:
- allowRegex: ^_
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break
- name: useless-break
severity: error
disabled: false
# https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#waitgroup-by-value
- name: waitgroup-by-value
severity: warning
disabled: false
issues:
exclude-rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- gosec
- gocyclo
- path: _test\.go
text: add-constant
- linters:
- lll
source: "^//go:generate "

2
.hadolint.yaml Normal file
View File

@ -0,0 +1,2 @@
failure-threshold: error
format: tty

20
.pre-commit-config.yaml Normal file
View File

@ -0,0 +1,20 @@
repos:
- repo: https://github.com/alessandrojcm/commitlint-pre-commit-hook
rev: v9.10.0
hooks:
- id: commitlint
stages: [commit-msg]
additional_dependencies: ["@commitlint/config-conventional"]
- repo: https://github.com/golangci/golangci-lint
rev: v1.55.2
hooks:
- id: golangci-lint
- repo: https://github.com/hadolint/hadolint
rev: v2.12.0
hooks:
- id: hadolint
- repo: https://github.com/adrienverge/yamllint.git
rev: v1.33.0
hooks:
- id: yamllint
args: [--strict, -c=./.yamllint.yml]

View File

@ -0,0 +1,41 @@
when:
- event: [cron]
cron: govulncheck
- event: [pull_request]
- event: push
branch:
- ${CI_REPO_DEFAULT_BRANCH}
variables:
- &go_image 'golang:1.21'
steps:
govulncheck:
image: *go_image
pull: true
commands:
- make generate
- go install golang.org/x/vuln/cmd/govulncheck@latest
- govulncheck ./...
notify:
image: deblan/woodpecker-email
settings:
from:
from_secret: email_from
from.name: Woodpecker
host:
from_secret: email_host
username:
from_secret: email_username
password:
from_secret: email_password
recipients:
- notifications@dwysokinski.me
recipients_only: true
subject:
"[govulncheck - {{ build.status }}] {{ repo.owner }}/{{ repo.name }}
({{ build.branch }} - {{ truncate build.commit 8 }})"
when:
status: [success, failure]
event: cron

71
.woodpecker/test.yml Normal file
View File

@ -0,0 +1,71 @@
when:
- event: [pull_request]
- event: push
branch:
- ${CI_REPO_DEFAULT_BRANCH}
services:
database:
image: postgres:14
pull: true
environment:
POSTGRES_DB: twhelp
POSTGRES_PASSWORD: twhelp
variables:
- &go_image 'golang:1.21'
steps:
generate:
image: *go_image
pull: true
commands:
- go mod download
- make generate
test:
image: *go_image
group: test
pull: true
environment:
TESTS_POSTGRES_CONNECTION_STRING:
postgres://postgres:twhelp@database:5432/twhelp?sslmode=disable
commands:
- go test -race -coverprofile=coverage.txt -covermode=atomic ./...
lint:
image: golangci/golangci-lint:v1.55
pull: true
group: test
commands:
- golangci-lint run
check-go-mod:
image: *go_image
group: test
pull: true
commands:
- go mod tidy
- git diff --exit-code go.mod
hadolint:
group: test
image: hadolint/hadolint:2.12.0-debian
commands:
- hadolint build/docker/twhelp/prod/Dockerfile build/docker/twhelp/dev/Dockerfile
when:
- path:
- build/docker/twhelp/prod/Dockerfile
- build/docker/twhelp/dev/Dockerfile
yamllint:
group: test
image: cytopia/yamllint:1
pull: true
commands:
- yamllint --strict .
when:
- path:
include:
- "**/*.yaml"
- "**/*.yml"

15
.yamllint.yml Normal file
View File

@ -0,0 +1,15 @@
extends: default
rules:
line-length:
max: 120
level: error
document-start: disable
truthy:
level: error
comments:
min-spaces-from-content: 1
quoted-strings:
level: warning
required: only-when-needed
quote-type: double

30
Makefile Normal file
View File

@ -0,0 +1,30 @@
GOOS=$(shell go env GOOS)
GOARCH=$(shell go env GOARCH)
GOBIN=$(shell go env GOBIN)
ifeq ($(GOBIN),)
GOBIN := $(shell go env GOPATH)/bin
endif
OSARCH=$(shell uname -m)
GOLANGCI_LINT_PATH=$(GOBIN)/golangci-lint
.PHONY: install-git-hooks
install-git-hooks:
@echo "Installing git hooks..."
pre-commit install --hook-type pre-commit
pre-commit install --hook-type commit-msg
.PHONY: install-golangci-lint
install-golangci-lint:
@echo "Installing github.com/golangci/golangci-lint..."
@(test -f $(GOLANGCI_LINT_PATH) && echo "github.com/golangci/golangci-lint is already installed. Skipping...") || curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b $(GOBIN) v1.55.2
.PHONY: install-tools
install-tools: install-golangci-lint
.PHONY: install
install: install-tools install-git-hooks
.PHONY: generate
generate:
@echo "Running go generate..."
go generate ./...

View File

@ -0,0 +1,17 @@
FROM golang:1.21 as builder
WORKDIR /app
COPY go.mod go.sum ./
RUN go mod download
COPY . .
# `skaffold debug` sets SKAFFOLD_GO_GCFLAGS to disable compiler optimizations
ARG SKAFFOLD_GO_GCFLAGS
RUN go build -gcflags="${SKAFFOLD_GO_GCFLAGS}" -o twhelp ./cmd/twhelp
FROM ubuntu:22.04
# Define GOTRACEBACK to mark this container as using the Go language runtime
# for `skaffold debug` (https://skaffold.dev/docs/workflows/debug/).
WORKDIR /root
ENV GOTRACEBACK=single
RUN apt update && apt install -y ca-certificates tzdata
COPY --from=builder /app/twhelp .
ENTRYPOINT ["./twhelp"]

View File

@ -0,0 +1,22 @@
FROM golang:1.21.5-alpine3.19 AS builder
WORKDIR /twhelp
COPY ../../../../go.mod go.sum ./
RUN go mod download && apk --no-cache add make
COPY ../../../.. .
RUN make generate
ARG CI_COMMIT_TAG="development"
RUN CGO_ENABLED=0 go build -ldflags "-X main.version=${CI_COMMIT_TAG##v}" -trimpath -o twhelp ./cmd/twhelp
######## Start a new stage from scratch #######
FROM alpine:3.19
RUN apk --no-cache add ca-certificates tzdata
COPY --from=builder /twhelp/twhelp /usr/bin/
EXPOSE 9234/tcp
ENTRYPOINT ["twhelp"]

14
go.mod Normal file
View File

@ -0,0 +1,14 @@
module gitea.dwysokinski.me/twhelp/corev3
go 1.21
require (
github.com/elliotchance/phpserialize v1.3.3
github.com/stretchr/testify v1.8.4
)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

12
go.sum Normal file
View File

@ -0,0 +1,12 @@
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/elliotchance/phpserialize v1.3.3 h1:hV4QVmGdCiYgoBbw+ADt6fNgyZ2mYX0OgpnON1adTCM=
github.com/elliotchance/phpserialize v1.3.3/go.mod h1:gt7XX9+ETUcLXbtTKEuyrqW3lcLUAeS/AnGZ2e49TZs=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@ -0,0 +1,42 @@
package tw
import (
"encoding/xml"
)
type Building struct {
MaxLevel int `xml:"max_level"`
MinLevel int `xml:"min_level"`
Wood int `xml:"wood"`
Stone int `xml:"stone"`
Iron int `xml:"iron"`
Pop int `xml:"pop"`
WoodFactor float64 `xml:"wood_factor"`
StoneFactor float64 `xml:"stone_factor"`
IronFactor float64 `xml:"iron_factor"`
PopFactor float64 `xml:"pop_factor"`
BuildTime float64 `xml:"build_time"`
BuildTimeFactor float64 `xml:"build_time_factor"`
}
type BuildingInfo struct {
XMLName xml.Name `xml:"config"`
Text string `xml:",chardata"`
Main Building `xml:"main"`
Barracks Building `xml:"barracks"`
Stable Building `xml:"stable"`
Garage Building `xml:"garage"`
Watchtower Building `xml:"watchtower"`
Snob Building `xml:"snob"`
Smith Building `xml:"smith"`
Place Building `xml:"place"`
Statue Building `xml:"statue"`
Market Building `xml:"market"`
Wood Building `xml:"wood"`
Stone Building `xml:"stone"`
Iron Building `xml:"iron"`
Farm Building `xml:"farm"`
Storage Building `xml:"storage"`
Hide Building `xml:"hide"`
Wall Building `xml:"wall"`
}

804
internal/tw/client.go Normal file
View File

@ -0,0 +1,804 @@
package tw
import (
"cmp"
"context"
"encoding/csv"
"encoding/xml"
"errors"
"fmt"
"io"
"net/http"
"net/url"
"slices"
"strconv"
"time"
"github.com/elliotchance/phpserialize"
)
const (
endpointPlayers = "/map/player.txt"
endpointTribes = "/map/ally.txt"
endpointVillages = "/map/village.txt"
endpointPlayersODA = "/map/kill_att.txt"
endpointPlayersODD = "/map/kill_def.txt"
endpointPlayersODS = "/map/kill_sup.txt"
endpointPlayersOD = "/map/kill_all.txt"
endpointTribesODA = "/map/kill_att_tribe.txt"
endpointTribesODD = "/map/kill_def_tribe.txt"
endpointTribesOD = "/map/kill_all_tribe.txt"
endpointEnnoblements = "/map/conquer_extended.txt"
endpointInterface = "/interface.php"
endpointGetServers = "/backend/get_servers.php"
endpointGame = "/game.php"
)
const (
queryConfig = "func=get_config"
queryUnitInfo = "func=get_unit_info"
queryBuildingInfo = "func=get_building_info"
queryInterfaceEnnoblements = "func=get_conquer_extended&since=%d"
queryPlayerProfile = "screen=info_player&id=%d"
queryTribeProfile = "screen=info_ally&id=%d"
queryVillageProfile = "screen=info_village&id=%d"
)
type Client struct {
client *http.Client
userAgent string
ennoblementsUseInterfaceFunc func(since time.Time) bool
}
func NewClient(opts ...ClientOption) *Client {
cfg := newClientConfig(opts...)
return &Client{
client: cfg.client,
userAgent: cfg.userAgent,
ennoblementsUseInterfaceFunc: cfg.ennoblementsUseInterfaceFunc,
}
}
var (
ErrInvalidServerKey = errors.New("invalid server key")
ErrInvalidServerURL = errors.New("invalid server URL")
)
func (c *Client) GetOpenServers(ctx context.Context, rawBaseURL string) ([]Server, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
}
u := buildURL(baseURL, endpointGetServers)
resp, err := c.get(ctx, u)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
b, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("%s: couldn't read response body: %w", u, err)
}
m, err := phpserialize.UnmarshalAssociativeArray(b)
if err != nil {
return nil, fmt.Errorf("%s: couldn't unmarshal response body: %w", u, err)
}
servers := make([]Server, 0, len(m))
for key, val := range m {
keyStr, ok := key.(string)
if !ok || keyStr == "" {
return nil, fmt.Errorf("%s: parsing '%v': %w", u, key, ErrInvalidServerKey)
}
urlStr, ok := val.(string)
if !ok || urlStr == "" {
return nil, fmt.Errorf("%s: parsing '%v': %w", u, val, ErrInvalidServerURL)
}
servers = append(servers, Server{
Key: keyStr,
URL: urlStr,
})
}
return servers, nil
}
func (c *Client) GetServerConfig(ctx context.Context, rawBaseURL string) (ServerConfig, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return ServerConfig{}, err
}
var cfg ServerConfig
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryConfig), &cfg); err != nil {
return ServerConfig{}, err
}
return cfg, nil
}
func (c *Client) GetBuildingInfo(ctx context.Context, rawBaseURL string) (BuildingInfo, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return BuildingInfo{}, err
}
var info BuildingInfo
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryBuildingInfo), &info); err != nil {
return BuildingInfo{}, err
}
return info, nil
}
func (c *Client) GetUnitInfo(ctx context.Context, rawBaseURL string) (UnitInfo, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return UnitInfo{}, err
}
var info UnitInfo
if err = c.getXML(ctx, buildURLWithQuery(baseURL, endpointInterface, queryUnitInfo), &info); err != nil {
return UnitInfo{}, err
}
return info, nil
}
func (c *Client) GetTribes(ctx context.Context, rawBaseURL string) ([]Tribe, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
}
od, err := c.getOD(ctx, baseURL, true)
if err != nil {
return nil, err
}
resp, err := c.get(ctx, buildURL(baseURL, endpointTribes))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
return tribeCSVParser{
r: resp.Body,
baseURL: baseURL,
od: od,
}.parse()
}
func (c *Client) GetPlayers(ctx context.Context, rawBaseURL string) ([]Player, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
}
od, err := c.getOD(ctx, baseURL, false)
if err != nil {
return nil, err
}
resp, err := c.get(ctx, buildURL(baseURL, endpointPlayers))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
return playerCSVParser{
r: resp.Body,
baseURL: baseURL,
od: od,
}.parse()
}
func (c *Client) getOD(ctx context.Context, baseURL *url.URL, tribe bool) (map[int]OpponentsDefeated, error) {
m := make(map[int]OpponentsDefeated)
urls := buildODURLs(baseURL, tribe)
for _, u := range urls {
if u == nil {
continue
}
records, err := c.getSingleODFile(ctx, u)
if err != nil {
return nil, err
}
for _, rec := range records {
od := m[rec.ID]
switch u {
case urls[0]:
od.RankTotal = rec.Rank
od.ScoreTotal = rec.Score
case urls[1]:
od.RankAtt = rec.Rank
od.ScoreAtt = rec.Score
case urls[2]:
od.RankDef = rec.Rank
od.ScoreDef = rec.Score
case urls[3]:
od.RankSup = rec.Rank
od.ScoreSup = rec.Score
}
m[rec.ID] = od
}
}
return m, nil
}
func (c *Client) getSingleODFile(ctx context.Context, u *url.URL) ([]odRecord, error) {
resp, err := c.get(ctx, u)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
return odCSVParser{
r: resp.Body,
}.parse()
}
func (c *Client) GetVillages(ctx context.Context, rawBaseURL string) ([]Village, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
}
resp, err := c.get(ctx, buildURL(baseURL, endpointVillages))
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
return villageCSVParser{
r: resp.Body,
baseURL: baseURL,
}.parse()
}
func (c *Client) GetEnnoblements(ctx context.Context, rawBaseURL string, since time.Time) ([]Ennoblement, error) {
baseURL, err := url.ParseRequestURI(rawBaseURL)
if err != nil {
return nil, err
}
u := buildURL(baseURL, endpointEnnoblements)
if c.ennoblementsUseInterfaceFunc(since) {
u = buildURLWithQuery(baseURL, endpointInterface, fmt.Sprintf(queryInterfaceEnnoblements, since.Unix()))
}
resp, err := c.get(ctx, u)
if err != nil {
return nil, err
}
defer func() {
_ = resp.Body.Close()
}()
return ennoblementCSVParser{
r: resp.Body,
since: since,
}.parse()
}
func (c *Client) getXML(ctx context.Context, u *url.URL, v any) error {
resp, err := c.get(ctx, u)
if err != nil {
return err
}
defer func() {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
}()
if err = xml.NewDecoder(resp.Body).Decode(v); err != nil {
return fmt.Errorf("%s: %w", u, err)
}
return nil
}
func (c *Client) get(ctx context.Context, u *url.URL) (*http.Response, error) {
urlStr := u.String()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, urlStr, nil)
if err != nil {
return nil, fmt.Errorf("%s: %w", urlStr, err)
}
// headers
req.Header.Set("User-Agent", c.userAgent)
resp, err := c.client.Do(req)
if err != nil {
return nil, fmt.Errorf("%s: %w", urlStr, err)
}
if resp.StatusCode != http.StatusOK {
_, _ = io.Copy(io.Discard, resp.Body)
_ = resp.Body.Close()
return nil, fmt.Errorf("%s: got non-ok HTTP status: %d", urlStr, resp.StatusCode)
}
return resp, nil
}
type tribeCSVParser struct {
r io.Reader
baseURL *url.URL
od map[int]OpponentsDefeated
}
const fieldsPerRecordTribe = 8
func (t tribeCSVParser) parse() ([]Tribe, error) {
csvR := newCSVReader(t.r, fieldsPerRecordTribe)
var tribes []Tribe
for {
rec, err := csvR.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
tribe, err := t.parseRecord(rec)
if err != nil {
return nil, err
}
tribes = append(tribes, tribe)
}
slices.SortFunc(tribes, func(a, b Tribe) int {
return cmp.Compare(a.ID, b.ID)
})
return tribes, nil
}
func (t tribeCSVParser) parseRecord(record []string) (Tribe, error) {
var err error
var tribe Tribe
// $id, $name, $tag, $members, $villages, $points, $all_points, $rank
tribe.ID, err = strconv.Atoi(record[0])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[0], Field: "Tribe.ID"}
}
tribe.Name, err = url.QueryUnescape(record[1])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[1], Field: "Tribe.Name"}
}
tribe.Tag, err = url.QueryUnescape(record[2])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[2], Field: "Tribe.Tag"}
}
tribe.NumMembers, err = strconv.Atoi(record[3])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[3], Field: "Tribe.NumMembers"}
}
tribe.NumVillages, err = strconv.Atoi(record[4])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[4], Field: "Tribe.NumVillages"}
}
tribe.Points, err = strconv.Atoi(record[5])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[5], Field: "Tribe.Points"}
}
tribe.AllPoints, err = strconv.Atoi(record[6])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[6], Field: "Tribe.AllPoints"}
}
tribe.Rank, err = strconv.Atoi(record[7])
if err != nil {
return Tribe{}, ParseError{Err: err, Str: record[7], Field: "Tribe.Rank"}
}
tribe.OpponentsDefeated = t.od[tribe.ID]
tribe.ProfileURL = buildURLWithQuery(t.baseURL, endpointGame, fmt.Sprintf(queryTribeProfile, tribe.ID)).String()
return tribe, nil
}
type playerCSVParser struct {
r io.Reader
od map[int]OpponentsDefeated
baseURL *url.URL
}
const fieldsPerRecordPlayer = 6
func (p playerCSVParser) parse() ([]Player, error) {
csvR := newCSVReader(p.r, fieldsPerRecordPlayer)
var players []Player
for {
rec, err := csvR.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
player, err := p.parseRecord(rec)
if err != nil {
return nil, err
}
players = append(players, player)
}
slices.SortFunc(players, func(a, b Player) int {
return cmp.Compare(a.ID, b.ID)
})
return players, nil
}
func (p playerCSVParser) parseRecord(record []string) (Player, error) {
var err error
var player Player
// $id, $name, $ally, $villages, $points, $rank
player.ID, err = strconv.Atoi(record[0])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[0], Field: "Player.ID"}
}
player.Name, err = url.QueryUnescape(record[1])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[1], Field: "Player.Name"}
}
player.TribeID, err = strconv.Atoi(record[2])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[2], Field: "Player.TribeID"}
}
player.NumVillages, err = strconv.Atoi(record[3])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[3], Field: "Player.NumVillages"}
}
player.Points, err = strconv.Atoi(record[4])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[4], Field: "Player.Points"}
}
player.Rank, err = strconv.Atoi(record[5])
if err != nil {
return Player{}, ParseError{Err: err, Str: record[5], Field: "Player.Rank"}
}
player.OpponentsDefeated = p.od[player.ID]
player.ProfileURL = buildURLWithQuery(p.baseURL, endpointGame, fmt.Sprintf(queryPlayerProfile, player.ID)).String()
return player, nil
}
type villageCSVParser struct {
r io.Reader
baseURL *url.URL
}
const fieldsPerRecordVillage = 7
func (v villageCSVParser) parse() ([]Village, error) {
csvR := newCSVReader(v.r, fieldsPerRecordVillage)
var villages []Village
for {
rec, err := csvR.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
village, err := v.parseRecord(rec)
if err != nil {
return nil, err
}
villages = append(villages, village)
}
slices.SortFunc(villages, func(a, b Village) int {
return cmp.Compare(a.ID, b.ID)
})
return villages, nil
}
func (v villageCSVParser) parseRecord(record []string) (Village, error) {
var err error
var village Village
// $id, $name, $x, $y, $player, $points, $bonus_id
village.ID, err = strconv.Atoi(record[0])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[0], Field: "Village.ID"}
}
village.Name, err = url.QueryUnescape(record[1])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[1], Field: "Village.Name"}
}
village.X, err = strconv.Atoi(record[2])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[2], Field: "Village.X"}
}
village.Y, err = strconv.Atoi(record[3])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[3], Field: "Village.Y"}
}
village.PlayerID, err = strconv.Atoi(record[4])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[4], Field: "Village.PlayerID"}
}
village.Points, err = strconv.Atoi(record[5])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[5], Field: "Village.Points"}
}
village.Bonus, err = strconv.Atoi(record[6])
if err != nil {
return Village{}, ParseError{Err: err, Str: record[6], Field: "Village.Bonus"}
}
village.Continent = v.buildContinent(record, village.X, village.Y)
village.ProfileURL = buildURLWithQuery(v.baseURL, endpointGame, fmt.Sprintf(queryVillageProfile, village.ID)).String()
return village, nil
}
func (v villageCSVParser) buildContinent(record []string, x, y int) string {
continent := "K"
switch {
case x < 100 && y < 100:
continent += "0"
case x > 100 && y < 100:
continent += string(record[2][0])
case x < 100 && y > 100:
continent += string(record[3][0]) + "0"
default:
continent += string(record[3][0]) + string(record[2][0])
}
return continent
}
type odCSVParser struct {
r io.Reader
}
type odRecord struct {
ID int
Rank int
Score int
}
const fieldsPerRecordOD = 3
func (o odCSVParser) parse() ([]odRecord, error) {
csvR := newCSVReader(o.r, fieldsPerRecordOD)
var odRecords []odRecord
for {
rec, err := csvR.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
od, err := o.parseRecord(rec)
if err != nil {
return nil, err
}
odRecords = append(odRecords, od)
}
return odRecords, nil
}
func (o odCSVParser) parseRecord(record []string) (odRecord, error) {
var err error
var rec odRecord
// $rank, $id, $score
rec.Rank, err = strconv.Atoi(record[0])
if err != nil {
return odRecord{}, ParseError{Err: err, Str: record[0], Field: "odRecord.Rank"}
}
rec.ID, err = strconv.Atoi(record[1])
if err != nil {
return odRecord{}, ParseError{Err: err, Str: record[1], Field: "odRecord.ID"}
}
rec.Score, err = strconv.Atoi(record[2])
if err != nil {
return odRecord{}, ParseError{Err: err, Str: record[2], Field: "odRecord.Score"}
}
return rec, nil
}
type ennoblementCSVParser struct {
r io.Reader
since time.Time
}
const fieldsPerRecordEnnoblement = 7
func (e ennoblementCSVParser) parse() ([]Ennoblement, error) {
csvR := newCSVReader(e.r, fieldsPerRecordEnnoblement)
var ennoblements []Ennoblement
for {
rec, err := csvR.Read()
if errors.Is(err, io.EOF) {
break
}
if err != nil {
return nil, err
}
ennoblement, err := e.parseRecord(rec)
if err != nil {
return nil, err
}
if ennoblement.CreatedAt.Before(e.since) {
continue
}
ennoblements = append(ennoblements, ennoblement)
}
slices.SortFunc(ennoblements, func(a, b Ennoblement) int {
return a.CreatedAt.Compare(b.CreatedAt)
})
return ennoblements, nil
}
func (e ennoblementCSVParser) parseRecord(record []string) (Ennoblement, error) {
var err error
var ennoblement Ennoblement
// $village_id, $unix_timestamp, $new_owner, $old_owner, $old_tribe_id, $new_tribe_id, $points
ennoblement.VillageID, err = strconv.Atoi(record[0])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[0], Field: "Ennoblement.VillageID"}
}
ennoblement.CreatedAt, err = e.parseTimestamp(record[1])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[1], Field: "Ennoblement.CreatedAt"}
}
ennoblement.NewOwnerID, err = strconv.Atoi(record[2])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[2], Field: "Ennoblement.NewOwnerID"}
}
ennoblement.OldOwnerID, err = strconv.Atoi(record[3])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[3], Field: "Ennoblement.OldOwnerID"}
}
ennoblement.OldTribeID, err = strconv.Atoi(record[4])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[4], Field: "Ennoblement.OldTribeID"}
}
ennoblement.NewTribeID, err = strconv.Atoi(record[5])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[5], Field: "Ennoblement.NewTribeID"}
}
ennoblement.Points, err = strconv.Atoi(record[6])
if err != nil {
return Ennoblement{}, ParseError{Err: err, Str: record[6], Field: "Ennoblement.Points"}
}
return ennoblement, nil
}
func (e ennoblementCSVParser) parseTimestamp(s string) (time.Time, error) {
timestamp, err := strconv.ParseInt(s, 10, 64)
if err != nil {
return time.Time{}, err
}
return time.Unix(timestamp, 0), nil
}
func newCSVReader(r io.Reader, fieldsPerRecord int) *csv.Reader {
csvR := csv.NewReader(r)
csvR.Comma = ','
csvR.FieldsPerRecord = fieldsPerRecord
csvR.ReuseRecord = true
return csvR
}
//nolint:revive
func buildODURLs(base *url.URL, tribe bool) [4]*url.URL {
// there is no endpoint to get opponents defeated as supporter for tribes :(
if tribe {
return [4]*url.URL{
buildURL(base, endpointTribesOD),
buildURL(base, endpointTribesODA),
buildURL(base, endpointTribesODD),
nil,
}
}
return [4]*url.URL{
buildURL(base, endpointPlayersOD),
buildURL(base, endpointPlayersODA),
buildURL(base, endpointPlayersODD),
buildURL(base, endpointPlayersODS),
}
}
func buildURL(base *url.URL, path string) *url.URL {
return buildURLWithQuery(base, path, "")
}
func buildURLWithQuery(base *url.URL, path, query string) *url.URL {
return &url.URL{
Scheme: base.Scheme,
Host: base.Host,
Path: path,
RawQuery: query,
}
}

View File

@ -0,0 +1,61 @@
package tw
import (
"net/http"
"time"
)
const (
defaultUserAgent = "tribalwarshelp/development"
defaultTimeout = 10 * time.Second
)
type EnnoblementsUseInterfaceFunc func(since time.Time) bool
type clientConfig struct {
userAgent string
client *http.Client
ennoblementsUseInterfaceFunc EnnoblementsUseInterfaceFunc
}
type ClientOption func(c *clientConfig)
func newClientConfig(opts ...ClientOption) *clientConfig {
cfg := &clientConfig{
userAgent: defaultUserAgent,
client: &http.Client{
Timeout: defaultTimeout,
},
ennoblementsUseInterfaceFunc: func(since time.Time) bool {
return since.After(time.Now().Add(-23 * time.Hour))
},
}
for _, opt := range opts {
opt(cfg)
}
return cfg
}
func WithUserAgent(ua string) ClientOption {
return func(cfg *clientConfig) {
cfg.userAgent = ua
}
}
func WithHTTPClient(hc *http.Client) ClientOption {
return func(cfg *clientConfig) {
cfg.client = hc
}
}
// WithEnnoblementsUseInterfaceFunc takes a function that will be called on every Client.GetEnnoblements call
// to determine whether the client should use /interface.php?func=get_conquer_extended or /map/conquer_extended.txt
// The default function checks whether since is after now() - 23h,
// if so, it calls /map/conquer_extended.txt, otherwise /interface.php?func=get_conquer_extended.
func WithEnnoblementsUseInterfaceFunc(f EnnoblementsUseInterfaceFunc) ClientOption {
return func(cfg *clientConfig) {
cfg.ennoblementsUseInterfaceFunc = f
}
}

1719
internal/tw/client_test.go Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,15 @@
package tw
type ParseError struct {
Field string
Str string
Err error
}
func (e ParseError) Error() string {
return e.Field + ": parsing '" + e.Str + "': " + e.Err.Error()
}
func (e ParseError) Unwrap() error {
return e.Err
}

View File

@ -0,0 +1,186 @@
package tw
import (
"encoding/xml"
"fmt"
"strings"
)
type ServerConfigBuild struct {
Destroy int `xml:"destroy"`
}
type ServerConfigMisc struct {
KillRanking int `xml:"kill_ranking"`
Tutorial int `xml:"tutorial"`
TradeCancelTime int `xml:"trade_cancel_time"`
}
type ServerConfigCommands struct {
MillisArrival int `xml:"millis_arrival"`
CommandCancelTime int `xml:"command_cancel_time"`
}
type ServerConfigNewbie struct {
Days int `xml:"days"`
RatioDays int `xml:"ratio_days"`
Ratio int `xml:"ratio"`
RemoveNewbieVillages int `xml:"removeNewbieVillages"`
}
type KnightNewItems int
func (k KnightNewItems) Int() int {
return int(k)
}
func (k *KnightNewItems) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
var s string
if err := d.DecodeElement(&s, &start); err != nil {
return err
}
switch strings.ToUpper(s) {
case "ON", "1":
*k = 1
case "OFF", "0", "":
*k = 0
default:
return xml.UnmarshalError(fmt.Sprintf(`KnightNewItems: parsing "%s": invalid syntax`, s))
}
return nil
}
type ServerConfigGame struct {
BuildtimeFormula int `xml:"buildtime_formula"`
Knight int `xml:"knight"`
KnightNewItems KnightNewItems `xml:"knight_new_items"`
Archer int `xml:"archer"`
Tech int `xml:"tech"`
FarmLimit int `xml:"farm_limit"`
Church int `xml:"church"`
Watchtower int `xml:"watchtower"`
Stronghold int `xml:"stronghold"`
FakeLimit float64 `xml:"fake_limit"`
BarbarianRise float64 `xml:"barbarian_rise"`
BarbarianShrink int `xml:"barbarian_shrink"`
BarbarianMaxPoints int `xml:"barbarian_max_points"`
Scavenging int `xml:"scavenging"`
Hauls int `xml:"hauls"`
HaulsBase int `xml:"hauls_base"`
HaulsMax int `xml:"hauls_max"`
BaseProduction int `xml:"base_production"`
Event int `xml:"event"`
SuppressEvents int `xml:"suppress_events"`
}
type ServerConfigBuildings struct {
CustomMain int `xml:"custom_main"`
CustomFarm int `xml:"custom_farm"`
CustomStorage int `xml:"custom_storage"`
CustomPlace int `xml:"custom_place"`
CustomBarracks int `xml:"custom_barracks"`
CustomChurch int `xml:"custom_church"`
CustomSmith int `xml:"custom_smith"`
CustomWood int `xml:"custom_wood"`
CustomStone int `xml:"custom_stone"`
CustomIron int `xml:"custom_iron"`
CustomMarket int `xml:"custom_market"`
CustomStable int `xml:"custom_stable"`
CustomWall int `xml:"custom_wall"`
CustomGarage int `xml:"custom_garage"`
CustomHide int `xml:"custom_hide"`
CustomSnob int `xml:"custom_snob"`
CustomStatue int `xml:"custom_statue"`
CustomWatchtower int `xml:"custom_watchtower"`
}
type ServerConfigSnob struct {
Gold int `xml:"gold"`
CheapRebuild int `xml:"cheap_rebuild"`
Rise int `xml:"rise"`
MaxDist int `xml:"max_dist"`
Factor float64 `xml:"factor"`
CoinWood int `xml:"coin_wood"`
CoinStone int `xml:"coin_stone"`
CoinIron int `xml:"coin_iron"`
NoBarbConquer int `xml:"no_barb_conquer"`
}
type ServerConfigAlly struct {
NoHarm int `xml:"no_harm"`
NoOtherSupport int `xml:"no_other_support"`
NoOtherSupportType int `xml:"no_other_support_type"`
AllytimeSupport int `xml:"allytime_support"`
NoLeave int `xml:"no_leave"`
NoJoin int `xml:"no_join"`
Limit int `xml:"limit"`
FixedAllies int `xml:"fixed_allies"`
PointsMemberCount int `xml:"points_member_count"`
WarsMemberRequirement int `xml:"wars_member_requirement"`
WarsPointsRequirement int `xml:"wars_points_requirement"`
WarsAutoacceptDays int `xml:"wars_autoaccept_days"`
Levels int `xml:"levels"`
XpRequirements string `xml:"xp_requirements"`
}
type ServerConfigCoord struct {
MapSize int `xml:"map_size"`
Func int `xml:"func"`
EmptyVillages int `xml:"empty_villages"`
BonusVillages int `xml:"bonus_villages"`
BonusNew int `xml:"bonus_new"`
Inner int `xml:"inner"`
SelectStart int `xml:"select_start"`
VillageMoveWait int `xml:"village_move_wait"`
NobleRestart int `xml:"noble_restart"`
StartVillages int `xml:"start_villages"`
}
type ServerConfigSitter struct {
Allow int `xml:"allow"`
}
type ServerConfigSleep struct {
Active int `xml:"active"`
Delay int `xml:"delay"`
Min int `xml:"min"`
Max int `xml:"max"`
MinAwake int `xml:"min_awake"`
MaxAwake int `xml:"max_awake"`
WarnTime int `xml:"warn_time"`
}
type ServerConfigNight struct {
Active int `xml:"active"`
StartHour int `xml:"start_hour"`
EndHour int `xml:"end_hour"`
DefFactor float64 `xml:"def_factor"`
Duration int `xml:"duration"`
}
type ServerConfigWin struct {
Check int `xml:"check"`
}
type ServerConfig struct {
XMLName xml.Name `xml:"config"`
Speed float64 `xml:"speed"`
UnitSpeed float64 `xml:"unit_speed"`
Moral int `xml:"moral"`
Build ServerConfigBuild `xml:"build"`
Misc ServerConfigMisc `xml:"misc"`
Commands ServerConfigCommands `xml:"commands"`
Newbie ServerConfigNewbie `xml:"newbie"`
Game ServerConfigGame `xml:"game"`
Buildings ServerConfigBuildings `xml:"buildings"`
Snob ServerConfigSnob `xml:"snob"`
Ally ServerConfigAlly `xml:"ally"`
Coord ServerConfigCoord `xml:"coord"`
Sitter ServerConfigSitter `xml:"sitter"`
Sleep ServerConfigSleep `xml:"sleep"`
Night ServerConfigNight `xml:"night"`
Win ServerConfigWin `xml:"win"`
}

View File

@ -0,0 +1,65 @@
package tw_test
import (
"encoding/xml"
"testing"
"gitea.dwysokinski.me/twhelp/corev3/internal/tw"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func TestKnightNewItems_UnmarshalXML(t *testing.T) {
t.Parallel()
tests := []struct {
data string
expected tw.KnightNewItems
expectedErr error
}{
{
data: "<knight_new_items/>",
expected: 0,
},
{
data: "<knight_new_items>Off</knight_new_items>",
expected: 0,
},
{
data: "<knight_new_items>0</knight_new_items>",
expected: 0,
},
{
data: "<knight_new_items>On</knight_new_items>",
expected: 1,
},
{
data: "<knight_new_items>ON</knight_new_items>",
expected: 1,
},
{
data: "<knight_new_items>on</knight_new_items>",
expected: 1,
},
{
data: "<knight_new_items>1</knight_new_items>",
expected: 1,
},
{
data: "<knight_new_items>invalid value</knight_new_items>",
expectedErr: xml.UnmarshalError(`KnightNewItems: parsing "invalid value": invalid syntax`),
},
}
for _, tt := range tests {
tt := tt
t.Run(tt.data, func(t *testing.T) {
t.Parallel()
var res tw.KnightNewItems
require.ErrorIs(t, tt.expectedErr, xml.Unmarshal([]byte(tt.data), &res))
assert.Equal(t, tt.expected, res)
})
}
}

241
internal/tw/testdata/building_info.xml vendored Normal file
View File

@ -0,0 +1,241 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<config>
<main>
<max_level>30</max_level>
<min_level>1</min_level>
<wood>90</wood>
<stone>80</stone>
<iron>70</iron>
<pop>5</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>900</build_time>
<build_time_factor>1.2</build_time_factor>
</main>
<barracks>
<max_level>25</max_level>
<min_level>0</min_level>
<wood>200</wood>
<stone>170</stone>
<iron>90</iron>
<pop>7</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.28</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>1800</build_time>
<build_time_factor>1.2</build_time_factor>
</barracks>
<stable>
<max_level>20</max_level>
<min_level>0</min_level>
<wood>270</wood>
<stone>240</stone>
<iron>260</iron>
<pop>8</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.28</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>6000</build_time>
<build_time_factor>1.2</build_time_factor>
</stable>
<garage>
<max_level>15</max_level>
<min_level>0</min_level>
<wood>300</wood>
<stone>240</stone>
<iron>260</iron>
<pop>8</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.28</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>6000</build_time>
<build_time_factor>1.2</build_time_factor>
</garage>
<watchtower>
<max_level>20</max_level>
<min_level>0</min_level>
<wood>12000</wood>
<stone>14000</stone>
<iron>10000</iron>
<pop>500</pop>
<wood_factor>1.17</wood_factor>
<stone_factor>1.17</stone_factor>
<iron_factor>1.18</iron_factor>
<pop_factor>1.18</pop_factor>
<build_time>13200</build_time>
<build_time_factor>1.2</build_time_factor>
</watchtower>
<snob>
<max_level>1</max_level>
<min_level>0</min_level>
<wood>15000</wood>
<stone>25000</stone>
<iron>10000</iron>
<pop>80</pop>
<wood_factor>2</wood_factor>
<stone_factor>2</stone_factor>
<iron_factor>2</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>586800</build_time>
<build_time_factor>1.2</build_time_factor>
</snob>
<smith>
<max_level>20</max_level>
<min_level>0</min_level>
<wood>220</wood>
<stone>180</stone>
<iron>240</iron>
<pop>20</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>6000</build_time>
<build_time_factor>1.2</build_time_factor>
</smith>
<place>
<max_level>1</max_level>
<min_level>0</min_level>
<wood>10</wood>
<stone>40</stone>
<iron>30</iron>
<pop>0</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>10860</build_time>
<build_time_factor>1.2</build_time_factor>
</place>
<statue>
<max_level>1</max_level>
<min_level>0</min_level>
<wood>220</wood>
<stone>220</stone>
<iron>220</iron>
<pop>10</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>1500</build_time>
<build_time_factor>1.2</build_time_factor>
</statue>
<market>
<max_level>25</max_level>
<min_level>0</min_level>
<wood>100</wood>
<stone>100</stone>
<iron>100</iron>
<pop>20</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>2700</build_time>
<build_time_factor>1.2</build_time_factor>
</market>
<wood>
<max_level>30</max_level>
<min_level>0</min_level>
<wood>50</wood>
<stone>60</stone>
<iron>40</iron>
<pop>5</pop>
<wood_factor>1.25</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.245</iron_factor>
<pop_factor>1.155</pop_factor>
<build_time>900</build_time>
<build_time_factor>1.2</build_time_factor>
</wood>
<stone>
<max_level>30</max_level>
<min_level>0</min_level>
<wood>65</wood>
<stone>50</stone>
<iron>40</iron>
<pop>10</pop>
<wood_factor>1.27</wood_factor>
<stone_factor>1.265</stone_factor>
<iron_factor>1.24</iron_factor>
<pop_factor>1.14</pop_factor>
<build_time>900</build_time>
<build_time_factor>1.2</build_time_factor>
</stone>
<iron>
<max_level>30</max_level>
<min_level>0</min_level>
<wood>75</wood>
<stone>65</stone>
<iron>70</iron>
<pop>10</pop>
<wood_factor>1.252</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.24</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>1080</build_time>
<build_time_factor>1.2</build_time_factor>
</iron>
<farm>
<max_level>30</max_level>
<min_level>1</min_level>
<wood>45</wood>
<stone>40</stone>
<iron>30</iron>
<pop>0</pop>
<wood_factor>1.3</wood_factor>
<stone_factor>1.32</stone_factor>
<iron_factor>1.29</iron_factor>
<pop_factor>1</pop_factor>
<build_time>1200</build_time>
<build_time_factor>1.2</build_time_factor>
</farm>
<storage>
<max_level>30</max_level>
<min_level>1</min_level>
<wood>60</wood>
<stone>50</stone>
<iron>40</iron>
<pop>0</pop>
<wood_factor>1.265</wood_factor>
<stone_factor>1.27</stone_factor>
<iron_factor>1.245</iron_factor>
<pop_factor>1.15</pop_factor>
<build_time>1020</build_time>
<build_time_factor>1.2</build_time_factor>
</storage>
<hide>
<max_level>10</max_level>
<min_level>0</min_level>
<wood>50</wood>
<stone>60</stone>
<iron>50</iron>
<pop>2</pop>
<wood_factor>1.25</wood_factor>
<stone_factor>1.25</stone_factor>
<iron_factor>1.25</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>1800</build_time>
<build_time_factor>1.2</build_time_factor>
</hide>
<wall>
<max_level>20</max_level>
<min_level>0</min_level>
<wood>50</wood>
<stone>100</stone>
<iron>20</iron>
<pop>5</pop>
<wood_factor>1.26</wood_factor>
<stone_factor>1.275</stone_factor>
<iron_factor>1.26</iron_factor>
<pop_factor>1.17</pop_factor>
<build_time>3600</build_time>
<build_time_factor>1.2</build_time_factor>
</wall>
</config>

View File

@ -0,0 +1 @@
a:16:{s:5:"pl159";s:25:"https://pl159.plemiona.pl";s:5:"pl161";s:25:"https://pl161.plemiona.pl";s:5:"pl164";s:25:"https://pl164.plemiona.pl";s:4:"plc1";s:24:"https://plc1.plemiona.pl";s:4:"pls1";s:24:"https://pls1.plemiona.pl";s:4:"plp7";s:24:"https://plp7.plemiona.pl";s:5:"pl165";s:25:"https://pl165.plemiona.pl";s:5:"pl167";s:25:"https://pl167.plemiona.pl";s:5:"pl168";s:25:"https://pl168.plemiona.pl";s:4:"plp8";s:24:"https://plp8.plemiona.pl";s:5:"pl169";s:25:"https://pl169.plemiona.pl";s:5:"pl170";s:25:"https://pl170.plemiona.pl";s:5:"pl171";s:25:"https://pl171.plemiona.pl";s:5:"pl172";s:25:"https://pl172.plemiona.pl";s:5:"pl173";s:25:"https://pl173.plemiona.pl";s:5:"pl174";s:25:"https://pl174.plemiona.pl";}

125
internal/tw/testdata/server_config.xml vendored Normal file
View File

@ -0,0 +1,125 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<config>
<speed>4.5</speed>
<unit_speed>0.5</unit_speed>
<moral>1</moral>
<build>
<destroy>1</destroy>
</build>
<misc>
<kill_ranking>2</kill_ranking>
<tutorial>0</tutorial>
<trade_cancel_time>300</trade_cancel_time>
</misc>
<commands>
<millis_arrival>0</millis_arrival>
<command_cancel_time>600</command_cancel_time>
</commands>
<newbie>
<days>3</days>
<ratio_days>15</ratio_days>
<ratio>10</ratio>
<removeNewbieVillages>1</removeNewbieVillages>
</newbie>
<game>
<buildtime_formula>2</buildtime_formula>
<knight>0</knight>
<knight_new_items>On</knight_new_items>
<archer>1</archer>
<tech>2</tech>
<farm_limit>0</farm_limit>
<church>0</church>
<watchtower>0</watchtower>
<stronghold>0</stronghold>
<fake_limit>0.5</fake_limit>
<barbarian_rise>0.001</barbarian_rise>
<barbarian_shrink>1</barbarian_shrink>
<barbarian_max_points>800</barbarian_max_points>
<scavenging>1</scavenging>
<hauls>2</hauls>
<hauls_base>5000</hauls_base>
<hauls_max>1000000</hauls_max>
<base_production>75</base_production>
<event>0</event>
<suppress_events>1</suppress_events>
</game>
<buildings>
<custom_main>-1</custom_main>
<custom_farm>-1</custom_farm>
<custom_storage>-1</custom_storage>
<custom_place>-1</custom_place>
<custom_barracks>-1</custom_barracks>
<custom_church>-1</custom_church>
<custom_smith>-1</custom_smith>
<custom_wood>-1</custom_wood>
<custom_stone>-1</custom_stone>
<custom_iron>-1</custom_iron>
<custom_market>-1</custom_market>
<custom_stable>-1</custom_stable>
<custom_wall>-1</custom_wall>
<custom_garage>-1</custom_garage>
<custom_hide>-1</custom_hide>
<custom_snob>-1</custom_snob>
<custom_statue>-1</custom_statue>
<custom_watchtower>-1</custom_watchtower>
</buildings>
<snob>
<gold>1</gold>
<cheap_rebuild>0</cheap_rebuild>
<rise>2</rise>
<max_dist>50</max_dist>
<factor>1</factor>
<coin_wood>28000</coin_wood>
<coin_stone>30000</coin_stone>
<coin_iron>25000</coin_iron>
<no_barb_conquer>0</no_barb_conquer>
</snob>
<ally>
<no_harm>0</no_harm>
<no_other_support>0</no_other_support>
<no_other_support_type>0</no_other_support_type>
<allytime_support>0</allytime_support>
<no_leave></no_leave>
<no_join></no_join>
<limit>15</limit>
<fixed_allies>0</fixed_allies>
<wars_member_requirement>5</wars_member_requirement>
<wars_points_requirement>15000</wars_points_requirement>
<wars_autoaccept_days>7</wars_autoaccept_days>
<levels>1</levels>
<xp_requirements>v1</xp_requirements>
</ally>
<coord>
<map_size>1000</map_size>
<func>4</func>
<empty_villages>11</empty_villages>
<bonus_villages>1</bonus_villages>
<inner>550</inner>
<select_start>0</select_start>
<village_move_wait>336</village_move_wait>
<noble_restart>1</noble_restart>
<start_villages>1</start_villages>
</coord>
<sitter>
<allow>1</allow>
</sitter>
<sleep>
<active>0</active>
<delay>60</delay>
<min>6</min>
<max>10</max>
<min_awake>12</min_awake>
<max_awake>36</max_awake>
<warn_time>10</warn_time>
</sleep>
<night>
<active>2</active>
<start_hour>23</start_hour>
<end_hour>7</end_hour>
<def_factor>3.5</def_factor>
<duration>14</duration>
</night>
<win>
<check>3</check>
</win>
</config>

123
internal/tw/testdata/unit_info.xml vendored Normal file
View File

@ -0,0 +1,123 @@
<?xml version="1.0" encoding="UTF-8" standalone="yes" ?>
<config>
<spear>
<build_time>226.66666666667</build_time>
<pop>1</pop>
<speed>8</speed>
<attack>10</attack>
<defense>15</defense>
<defense_cavalry>45</defense_cavalry>
<defense_archer>20</defense_archer>
<carry>25</carry>
</spear>
<sword>
<build_time>333.33333333333</build_time>
<pop>1</pop>
<speed>9.7777777777778</speed>
<attack>25</attack>
<defense>50</defense>
<defense_cavalry>15</defense_cavalry>
<defense_archer>40</defense_archer>
<carry>15</carry>
</sword>
<axe>
<build_time>293.33333333333</build_time>
<pop>1</pop>
<speed>8</speed>
<attack>40</attack>
<defense>10</defense>
<defense_cavalry>5</defense_cavalry>
<defense_archer>10</defense_archer>
<carry>10</carry>
</axe>
<archer>
<build_time>400</build_time>
<pop>1</pop>
<speed>8</speed>
<attack>15</attack>
<defense>50</defense>
<defense_cavalry>40</defense_cavalry>
<defense_archer>5</defense_archer>
<carry>10</carry>
</archer>
<spy>
<build_time>200</build_time>
<pop>2</pop>
<speed>4</speed>
<attack>0</attack>
<defense>2</defense>
<defense_cavalry>1</defense_cavalry>
<defense_archer>2</defense_archer>
<carry>0</carry>
</spy>
<light>
<build_time>400</build_time>
<pop>4</pop>
<speed>4.4444444444444</speed>
<attack>130</attack>
<defense>30</defense>
<defense_cavalry>40</defense_cavalry>
<defense_archer>30</defense_archer>
<carry>80</carry>
</light>
<marcher>
<build_time>600</build_time>
<pop>5</pop>
<speed>4.4444444444444</speed>
<attack>120</attack>
<defense>40</defense>
<defense_cavalry>30</defense_cavalry>
<defense_archer>50</defense_archer>
<carry>50</carry>
</marcher>
<heavy>
<build_time>800</build_time>
<pop>6</pop>
<speed>4.8888888888889</speed>
<attack>150</attack>
<defense>200</defense>
<defense_cavalry>80</defense_cavalry>
<defense_archer>180</defense_archer>
<carry>50</carry>
</heavy>
<ram>
<build_time>1066.6666666667</build_time>
<pop>5</pop>
<speed>13.333333333333</speed>
<attack>2</attack>
<defense>20</defense>
<defense_cavalry>50</defense_cavalry>
<defense_archer>20</defense_archer>
<carry>0</carry>
</ram>
<catapult>
<build_time>1600</build_time>
<pop>8</pop>
<speed>13.333333333333</speed>
<attack>100</attack>
<defense>100</defense>
<defense_cavalry>50</defense_cavalry>
<defense_archer>100</defense_archer>
<carry>0</carry>
</catapult>
<snob>
<build_time>4000</build_time>
<pop>100</pop>
<speed>15.555555555556</speed>
<attack>30</attack>
<defense>100</defense>
<defense_cavalry>50</defense_cavalry>
<defense_archer>100</defense_archer>
<carry>0</carry>
</snob>
<militia>
<build_time>1</build_time>
<pop>0</pop>
<speed>0.016666666666667</speed>
<attack>0</attack>
<defense>15</defense>
<defense_cavalry>45</defense_cavalry>
<defense_archer>25</defense_archer>
<carry>0</carry>
</militia>
</config>

69
internal/tw/tw.go Normal file
View File

@ -0,0 +1,69 @@
package tw
import (
"time"
)
type Server struct {
Key string
URL string
}
type OpponentsDefeated struct {
RankAtt int
ScoreAtt int
RankDef int
ScoreDef int
RankSup int
ScoreSup int
RankTotal int
ScoreTotal int
}
type Tribe struct {
OpponentsDefeated
ID int
Name string
Tag string
NumMembers int
NumVillages int
Points int
AllPoints int
Rank int
ProfileURL string
}
type Player struct {
OpponentsDefeated
ID int
Name string
NumVillages int
Points int
Rank int
TribeID int
ProfileURL string
}
type Village struct {
ID int
Name string
Points int
X int
Y int
Continent string
Bonus int
PlayerID int
ProfileURL string
}
type Ennoblement struct {
VillageID int
NewOwnerID int
NewTribeID int
OldOwnerID int
OldTribeID int
Points int
CreatedAt time.Time
}

34
internal/tw/unit_info.go Normal file
View File

@ -0,0 +1,34 @@
package tw
import (
"encoding/xml"
)
type Unit struct {
BuildTime float64 `xml:"build_time"`
Pop int `xml:"pop"`
Speed float64 `xml:"speed"`
Attack int `xml:"attack"`
Defense int `xml:"defense"`
DefenseCavalry int `xml:"defense_cavalry"`
DefenseArcher int `xml:"defense_archer"`
Carry int `xml:"carry"`
}
type UnitInfo struct {
XMLName xml.Name `xml:"config"`
Text string `xml:",chardata"`
Spear Unit `xml:"spear"`
Sword Unit `xml:"sword"`
Axe Unit `xml:"axe"`
Archer Unit `xml:"archer"`
Spy Unit `xml:"spy"`
Light Unit `xml:"light"`
Marcher Unit `xml:"marcher"`
Heavy Unit `xml:"heavy"`
Ram Unit `xml:"ram"` //nolint:revive
Catapult Unit `xml:"catapult"`
Knight Unit `xml:"knight"`
Snob Unit `xml:"snob"`
Militia Unit `xml:"militia"`
}

17
renovate.json Normal file
View File

@ -0,0 +1,17 @@
{
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
"labels": [
"dependencies"
],
"extends": [
"config:base",
":semanticCommits",
":semanticCommitTypeAll(chore)"
],
"postUpdateOptions": [
"gomodTidy"
],
"ignorePaths": [
".woodpecker/*"
]
}

45
skaffold.yml Normal file
View File

@ -0,0 +1,45 @@
apiVersion: skaffold/v3
kind: Config
profiles:
- name: dev
build:
tagPolicy:
customTemplate:
template: latest
artifacts:
- image: twhelp
hooks:
before:
- command: [ "sh", "-c", "make generate" ]
os: [ darwin, linux ]
context: .
docker:
dockerfile: ./build/docker/twhelp/dev/Dockerfile
manifests:
kustomize:
paths:
- k8s/overlays/dev
deploy:
kubectl: {}
- name: resources
deploy:
helm:
releases:
- name: twhelpdb
repo: https://charts.bitnami.com/bitnami
remoteChart: postgresql
version: 13.2.24
wait: true
setValues:
image.tag: 14.8.0-debian-11-r4
auth.username: twhelp
auth.password: twhelp
auth.database: twhelp
- name: twhelprmq
repo: https://charts.bitnami.com/bitnami
remoteChart: rabbitmq
version: 12.5.6
wait: true
setValues:
auth.username: twhelp
auth.password: twhelp