kills.gno
3.07 Kb · 108 lines
1package game
2
3import (
4 "chain"
5 "errors"
6)
7
8const (
9 maxKillEntries = 32 // players per SubmitKills call
10 maxKillCount = 1000 // kills per player per call
11
12 // maxKillsLen bounds a kills string, so that an oversized input is
13 // refused before it is read. An address is 40 characters, a count 4 at
14 // most, plus the two separators: 64 a player leaves room to spare.
15 maxKillsLen = maxKillEntries * 64
16)
17
18var errKillsFormat = errors.New(`gnofly: kills must be 1 to 32 entries "address:count" separated by ";"`)
19
20// SubmitKills adds kills to players' on-chain counts. Referee only.
21//
22// kills is 1 to 32 entries "address:count" separated by ";", for example
23//
24// g1abc...:3;g1def...:1
25//
26// Each address must be valid and listed once, each count from 1 to 1000.
27// Anything else aborts the transaction and nothing is recorded. The counts
28// are added to what the players already have, and the kill leaderboard is
29// updated. Kills are player actions: Version does not move.
30func SubmitKills(cur realm, kills string) {
31 referee := assertReferee(0, cur)
32 addrs, counts, err := parseKills(kills)
33 if err != nil {
34 panic(err)
35 }
36
37 size := leaderboardSize()
38 added := int64(0)
39 for i, addr := range addrs {
40 total := recordedKills(addr) + counts[i]
41 pilots.Set(addr.String(), total)
42
43 if next, changed := killers.raise(addr, total-counts[i], total, size); changed {
44 killers = next
45 }
46 added += counts[i]
47 }
48 chain.Emit("KillsSubmitted",
49 "referee", referee.String(),
50 "players", itoa(int64(len(addrs))),
51 "kills", itoa(added),
52 )
53}
54
55// parseKills parses and validates a kills string in a single pass. It
56// returns the players and their counts, in the order of the string.
57func parseKills(s string) ([]address, []int64, error) {
58 if s == "" || len(s) > maxKillsLen {
59 return nil, nil, errKillsFormat
60 }
61 var addrs []address
62 var counts []int64
63 seen := make(map[address]bool) // lookups only, never iterated
64 i := 0
65 for {
66 index := len(addrs)
67 if index == maxKillEntries {
68 return nil, nil, errKillsFormat
69 }
70 colon := indexFrom(s, i, ':')
71 if colon == len(s) {
72 return nil, nil, errKillsFormat
73 }
74 end := indexFrom(s, colon+1, ';')
75
76 addr := address(s[i:colon])
77 if !validAddress(addr) {
78 return nil, nil, killsEntryError(index, "invalid address")
79 }
80 if seen[addr] {
81 return nil, nil, killsEntryError(index, "address listed twice")
82 }
83 // At most 4 digits: 1000 is the highest count, and a bounded length
84 // keeps the number from overflowing.
85 if end-colon-1 > 4 {
86 return nil, nil, killsEntryError(index, "count must be a number from 1 to 1000")
87 }
88 count, ok := digits(s[colon+1 : end])
89 if !ok || count < 1 || count > maxKillCount {
90 return nil, nil, killsEntryError(index, "count must be a number from 1 to 1000")
91 }
92 seen[addr] = true
93 addrs = append(addrs, addr)
94 counts = append(counts, count)
95
96 if end == len(s) {
97 return addrs, counts, nil
98 }
99 i = end + 1
100 if i == len(s) {
101 return nil, nil, errKillsFormat // a trailing ";"
102 }
103 }
104}
105
106func killsEntryError(index int, reason string) error {
107 return errors.New("gnofly: kills entry " + itoa(int64(index)) + ": " + reason)
108}