Files
bd2/go/internal/server/eventactions/voting.go
T
Flechazo d8771caef2 feat(all): add hunting and events, commerce and rewards, binary calendars, GameData caching and login diagnostics
Implement hunting AP and progress, event tasks, exchanges, games, stories and actions; connect battle and reward handling.

Add CashShop plugin and installation, server purchases and entitlements, recharge rules and plugin version metadata.

Distribute binary calendars independently; cache GameData in memory and fix single-connection query deadlocks.

Improve automatic login and recovery diagnostics; document configuration and release flows.
2026-10-05 11:50:31 +08:00

322 lines
8.9 KiB
Go

package eventactions
import (
"bd2server/internal/server/events"
"bd2server/internal/server/gamedata"
"bd2server/internal/server/wire"
"errors"
"fmt"
"sort"
"time"
)
func (s *Service) voteEvent() (events.Schedule, gamedata.EventActionRow, bool) {
v, ok := s.find(23)
if !ok {
return v, gamedata.EventActionRow{}, false
}
r, ok := s.design.Row("VotingEventTable", 6, v.ID)
return v, r, ok
}
func (s *Service) rounds(v events.Schedule, r gamedata.EventActionRow) (uint64, []gamedata.EventActionRow) {
var rows []gamedata.EventActionRow
current := uint64(0)
for _, x := range s.design.Tables["VotingRoundTable"] {
if x.V(2) == r.V(14) {
rows = append(rows, x)
}
}
sort.Slice(rows, func(i, j int) bool { return rows[i].V(3) < rows[j].V(3) })
for _, x := range rows {
if s.now().UnixMilli() >= v.Start+int64(x.V(4))*86400000 {
current = x.V(3)
}
}
return current, rows
}
func (s *Service) candidates(r gamedata.EventActionRow) []uint64 {
var ids []uint64
for _, x := range s.design.Tables["VotingCandidateTable"] {
if x.V(1) == r.V(10) {
ids = append(ids, x.V(2))
}
}
sort.Slice(ids, func(i, j int) bool { return ids[i] < ids[j] })
return ids
}
func contains(ids []uint64, id uint64) bool {
for _, v := range ids {
if v == id {
return true
}
}
return false
}
func voteWire(v *vote) []byte {
b := wire.AppendVarint(nil, 1, v.Round)
b = wire.AppendVarint(b, 2, v.Candidate)
b = wire.AppendVarint(b, 3, v.Normal+v.Additional)
b = wire.AppendVarint(b, 4, v.Normal)
return wire.AppendVarint(b, 5, v.Additional)
}
func (s *Service) voteHandle(path string, b []byte, identity string) ([]byte, error) {
v, r, ok := s.voteEvent()
if !ok {
if path == "/CharVoteInfo" || path == "/CharVoteSeasonRanking" {
return nil, nil
}
if path == "/CharVoteTotalRanking" {
return s.totalRanking(val(b, 2)), nil
}
return nil, errors.New("eventactions: voting event inactive")
}
round, rounds := s.rounds(v, r)
ids := s.candidates(r)
ids = s.advancedCandidates(v, r, round, ids)
switch path {
case "/CharVoteInfo":
out := wire.AppendVarint(nil, 1, round)
for _, id := range ids {
out = wire.AppendVarint(out, 2, id)
}
for k, x := range s.state.Votes {
var uid, ro, c uint64
if _, e := fmt.Sscanf(k, "%d/%d/%d", &uid, &ro, &c); e == nil && uid == v.UID {
out = wire.AppendBytes(out, 3, voteWire(x))
}
}
for k := range s.state.VoteRewards {
var uid, id uint64
if _, e := fmt.Sscanf(k, "%d/%d", &uid, &id); e == nil && uid == v.UID {
x := wire.AppendVarint(nil, 1, v.ID)
x = wire.AppendVarint(x, 2, id)
out = wire.AppendBytes(out, 4, x)
}
}
for _, id := range ids {
if s.state.Favorites[id] {
out = wire.AppendVarint(out, 5, id)
}
if s.state.NormalVoted[key(v.UID, id)] {
out = wire.AppendVarint(out, 6, id)
}
}
out = wire.AppendVarint(out, 7, uint64(s.now().UTC().Truncate(24*time.Hour).Add(24*time.Hour).UnixMilli()))
for i, x := range rounds {
start := v.Start + int64(x.V(4))*86400000
end := v.End
if i+1 < len(rounds) {
end = v.Start + int64(rounds[i+1].V(4))*86400000
}
a := wire.AppendVarint(nil, 1, x.V(3))
a = wire.AppendVarint(a, 2, uint64(start))
a = wire.AppendVarint(a, 3, uint64(end))
out = wire.AppendBytes(out, 9, a)
}
return out, nil
case "/CharVoteFavoriteAdd", "/CharVoteFavoriteDelete":
id := val(b, 2)
if !contains(ids, id) {
return nil, errors.New("eventactions: unknown candidate")
}
if path == "/CharVoteFavoriteAdd" {
s.state.Favorites[id] = true
} else {
delete(s.state.Favorites, id)
}
return nil, nil
case "/CharVoteSave":
id, count, typ := val(b, 2), val(b, 4), val(b, 5)
if !contains(ids, id) || count == 0 || count > 2147483647 || typ > 1 || round == 0 {
return nil, errors.New("eventactions: invalid vote")
}
normalKey := key(v.UID, id)
if typ == 0 && (s.state.NormalVoted[normalKey] || count != 1) {
return nil, errors.New("eventactions: daily normal vote exhausted")
}
cost := gamedata.Reward{Type: r.V(9), ID: r.V(8), Count: r.V(7) * count}
if typ == 1 {
cost = gamedata.Reward{Type: r.V(3), ID: r.V(2), Count: r.V(1) * count}
}
// A first vote may use the additional ticket when the normal ticket
// is unavailable; the client still sends vote_type=0.
if typ == 0 {
var submittedID uint64
_ = wire.Walk(b, func(f wire.Field) error {
if f.Number == 3 && f.Type == 2 {
submittedID = val(f.Value, 2)
}
return nil
})
if submittedID == r.V(2) {
cost = gamedata.Reward{Type: r.V(3), ID: r.V(2), Count: r.V(1) * count}
}
}
if cost.Type == 0 || cost.Count == 0 {
return nil, errors.New("eventactions: missing vote cost")
}
vk := key(v.UID, round, id)
current := s.state.Votes[vk]
if current == nil {
current = &vote{Round: round, Candidate: id}
}
var total uint64
for k, x := range s.state.Votes {
var uid, ro, c uint64
if _, e := fmt.Sscanf(k, "%d/%d/%d", &uid, &ro, &c); e == nil && uid == v.UID {
total += x.Normal + x.Additional
}
}
var rewards []gamedata.Reward
var newIDs []uint64
for _, rr := range s.design.Tables["VotingCountRewardTable"] {
if rr.V(1) == r.V(11) && rr.V(6) <= total+count && !s.state.VoteRewards[key(v.UID, rr.V(2))] {
rewards = append(rewards, rr.Rewards...)
newIDs = append(newIDs, rr.V(2))
}
}
bundle, e := s.economy.Apply(identity, []gamedata.Reward{cost}, rewards)
if e != nil {
return nil, e
}
s.state.Votes[vk] = current
if typ == 0 {
current.Normal += count
s.state.NormalVoted[normalKey] = true
} else {
current.Additional += count
}
out := wire.AppendBytes(nil, 1, bundle)
for _, id := range newIDs {
s.state.VoteRewards[key(v.UID, id)] = true
x := wire.AppendVarint(nil, 1, v.ID)
x = wire.AppendVarint(x, 2, id)
out = wire.AppendBytes(out, 2, x)
}
out = wire.AppendBytes(out, 4, voteWire(current))
return out, nil
case "/CharVoteRanking":
wanted := val(b, 2)
if wanted == 0 {
wanted = round
}
out := wire.AppendVarint(nil, 1, v.ID)
out = wire.AppendVarint(out, 2, wanted)
for _, x := range s.rankRows(v.UID, wanted, ids) {
out = wire.AppendBytes(out, 3, x)
}
return out, nil
case "/CharVoteTotalRanking":
return s.totalRanking(val(b, 2)), nil
case "/CharVoteSeasonRanking":
var out []byte
for _, schedule := range s.registry.List() {
if schedule.Type != 23 {
continue
}
for _, id := range ids {
var total uint64
for k, x := range s.state.Votes {
var uid, ro, c uint64
if _, e := fmt.Sscanf(k, "%d/%d/%d", &uid, &ro, &c); e == nil && uid == schedule.UID && c == id {
total += x.Normal + x.Additional
}
}
if total == 0 {
continue
}
x := wire.AppendVarint(nil, 1, schedule.ID)
x = wire.AppendVarint(x, 2, id)
x = wire.AppendVarint(x, 3, total)
x = wire.AppendVarint(x, 4, total)
out = wire.AppendBytes(out, 1, x)
}
}
return out, nil
}
return nil, errors.New("eventactions: unknown voting operation")
}
func (s *Service) rankRows(uid, round uint64, ids []uint64) [][]byte {
type result struct{ id, count, last uint64 }
var rows []result
var totals map[uint64]uint64
if s.voteTotals != nil {
totals, _ = s.voteTotals(uid, round)
}
for _, id := range ids {
r := result{id: id}
for k, x := range s.state.Votes {
var u, ro, c uint64
if _, e := fmt.Sscanf(k, "%d/%d/%d", &u, &ro, &c); e == nil && u == uid && c == id && (round == 0 || round == ro) {
r.count += x.Normal + x.Additional
if ro > r.last {
r.last = ro
}
}
}
if totals != nil {
r.count = totals[id]
}
rows = append(rows, r)
}
sort.Slice(rows, func(i, j int) bool {
if rows[i].count == rows[j].count {
return rows[i].id < rows[j].id
}
return rows[i].count > rows[j].count
})
var out [][]byte
var lastCount, rank uint64
for i, r := range rows {
if i == 0 || r.count != lastCount {
rank = uint64(i + 1)
}
lastCount = r.count
x := wire.AppendVarint(nil, 1, r.id)
x = wire.AppendVarint(x, 2, r.count)
x = wire.AppendVarint(x, 3, rank)
x = wire.AppendVarint(x, 4, r.last)
out = append(out, x)
}
return out
}
func (s *Service) advancedCandidates(v events.Schedule, event gamedata.EventActionRow, round uint64, ids []uint64) []uint64 {
for previous := uint64(1); previous < round; previous++ {
var count uint64
for _, r := range s.design.Tables["VotingRoundTable"] {
if r.V(2) == event.V(14) && r.V(3) == previous {
count = r.V(1)
}
}
if count == 0 {
continue
}
ranked := s.rankRows(v.UID, previous, ids)
var next []uint64
for _, b := range ranked {
if val(b, 3) <= count {
next = append(next, val(b, 1))
}
}
ids = next
}
return ids
}
func (s *Service) totalRanking(event uint64) []byte {
out := wire.AppendVarint(nil, 1, event)
for _, v := range s.registry.List() {
if v.Type != 23 || v.ID != event {
continue
}
r, ok := s.design.Row("VotingEventTable", 6, event)
if !ok {
continue
}
for _, row := range s.rankRows(v.UID, 0, s.candidates(r)) {
out = wire.AppendBytes(out, 2, row)
}
}
return out
}