moth

Monarch Of The Hill game server
git clone https://git.woozle.org/neale/moth.git

moth / pkg / award
Neale Pickett  ·  2021-10-14

award.go

  1// Package award defines a MOTH award, and provides tools to use them.
  2package award
  3
  4import (
  5	"bytes"
  6	"encoding/json"
  7	"fmt"
  8	"net/url"
  9	"reflect"
 10	"strconv"
 11	"strings"
 12)
 13
 14// T represents a single award event.
 15type T struct {
 16	// Unix epoch time of this event
 17	When     int64
 18	TeamID   string
 19	Category string
 20	Points   int
 21}
 22
 23// List is a collection of award events.
 24type List []T
 25
 26// Len returns the length of the awards list.
 27func (awards List) Len() int {
 28	return len(awards)
 29}
 30
 31// Less returns true if i was awarded before j.
 32func (awards List) Less(i, j int) bool {
 33	return awards[i].When < awards[j].When
 34}
 35
 36// Swap exchanges the awards in positions i and j.
 37func (awards List) Swap(i, j int) {
 38	tmp := awards[i]
 39	awards[i] = awards[j]
 40	awards[j] = tmp
 41}
 42
 43// Parse parses a string log entry into an award.T.
 44func Parse(s string) (T, error) {
 45	ret := T{}
 46
 47	s = strings.TrimSpace(s)
 48
 49	n, err := fmt.Sscanf(s, "%d %s %s %d", &ret.When, &ret.TeamID, &ret.Category, &ret.Points)
 50	if err != nil {
 51		return ret, err
 52	} else if n != 4 {
 53		return ret, fmt.Errorf("malformed award string: only parsed %d fields", n)
 54	}
 55
 56	return ret, nil
 57}
 58
 59// String returns a log entry string for an award.T.
 60func (a T) String() string {
 61	return fmt.Sprintf("%d %s %s %d", a.When, a.TeamID, a.Category, a.Points)
 62}
 63
 64// Filename returns a string version of an award suitable for a filesystem
 65func (a T) Filename() string {
 66	return fmt.Sprintf(
 67		"%d-%s-%s-%d.award",
 68		a.When,
 69		url.PathEscape(a.TeamID),
 70		url.PathEscape(a.Category),
 71		a.Points,
 72	)
 73}
 74
 75// MarshalJSON returns the award event, encoded as a list.
 76func (a T) MarshalJSON() ([]byte, error) {
 77	ao := []interface{}{
 78		a.When,
 79		a.TeamID,
 80		a.Category,
 81		a.Points,
 82	}
 83
 84	return json.Marshal(ao)
 85}
 86
 87// UnmarshalJSON decodes the JSON string b.
 88func (a T) UnmarshalJSON(b []byte) error {
 89	r := bytes.NewReader(b)
 90	dec := json.NewDecoder(r)
 91	dec.UseNumber() // Don't use floats
 92
 93	// All this to make sure we get `[`
 94	t, err := dec.Token()
 95	if err != nil {
 96		return err
 97	}
 98	switch token := t.(type) {
 99	case json.Delim:
100		if token.String() != "[" {
101			return &json.UnmarshalTypeError{
102				Value:  token.String(),
103				Type:   reflect.TypeOf(a),
104				Offset: 0,
105			}
106		}
107	default:
108		return &json.UnmarshalTypeError{
109			Value:  fmt.Sprintf("%v", t),
110			Type:   reflect.TypeOf(a),
111			Offset: 0,
112		}
113	}
114
115	var num json.Number
116	if err := dec.Decode(&num); err != nil {
117		return err
118	}
119	if a.When, err = strconv.ParseInt(string(num), 10, 64); err != nil {
120		return err
121	}
122	if err := dec.Decode(&a.Category); err != nil {
123		return err
124	}
125	if err := dec.Decode(&a.TeamID); err != nil {
126		return err
127	}
128	if err := dec.Decode(&num); err != nil {
129		return err
130	}
131	if a.Points, err = strconv.Atoi(string(num)); err != nil {
132		return err
133	}
134
135	// All this to make sure we get `]`
136	t, err = dec.Token()
137	if err != nil {
138		return err
139	}
140	switch token := t.(type) {
141	case json.Delim:
142		if token.String() != "]" {
143			return &json.UnmarshalTypeError{
144				Value:  token.String(),
145				Type:   reflect.TypeOf(a),
146				Offset: 0,
147			}
148		}
149	default:
150		return &json.UnmarshalTypeError{
151			Value:  fmt.Sprintf("%v", t),
152			Type:   reflect.TypeOf(a),
153			Offset: 0,
154		}
155	}
156
157	return nil
158}
159
160// Equal returns true if two award events represent the same award.
161// Timestamps are ignored in this comparison!
162func (a T) Equal(o T) bool {
163	switch {
164	case a.TeamID != o.TeamID:
165		return false
166	case a.Category != o.Category:
167		return false
168	case a.Points != o.Points:
169		return false
170	}
171	return true
172}