rows.gno
5.37 Kb · 207 lines
1package planes
2
3import (
4 "strings"
5
6 "gno.land/p/nt/grc721/v0"
7)
8
9// plane is one row of the table: what a plane looks like. Nothing here is
10// stored per token, the table is the only copy.
11type plane struct {
12 id int64
13 name string
14 model string // a model the client draws, see models
15 pilot string // none or gnome
16 body string // colours, "#rrggbb"; on the super-gnome: cape, suit, emblem
17 wing string
18 accent string
19 tier string // common, rare, epic, legendary
20 laser string // the colour of its shots, "#rrggbb"
21}
22
23// The table is one string of 777 lines,
24// "id|name|model|pilot|body|wing|accent|tier|laser",
25// in id order (see table.gno). It is kept as a string and read in place
26// because storage is paid per byte at deploy: 777 structs would cost about
27// ten times as much as the text, and a plane is read far less often than it
28// is paid for.
29//
30// A row is found by bisection on the offsets of the string: from any offset,
31// the start of its line is at most a line's length back. Ten steps of a
32// hundred bytes each, where walking the lines from the top would be forty
33// thousand.
34
35// row returns the line of plane id, or "" when there is no such row.
36func row(id int64) string {
37 if id < 1 || id > supply {
38 return ""
39 }
40 lo, hi := 0, len(table)-1
41 for lo <= hi {
42 mid := (lo + hi) / 2
43 start, end := lineAt(mid)
44 n := idAt(start, end)
45 switch {
46 case n == id:
47 return table[start:end]
48 case n < id:
49 lo = end + 1
50 default:
51 hi = start - 1
52 }
53 }
54 return ""
55}
56
57// lineAt returns the bounds of the line containing offset i: start is its
58// first byte, end the offset of its newline, or len(table) for the last line.
59func lineAt(i int) (start, end int) {
60 start = i
61 for start > 0 && table[start-1] != '\n' {
62 start--
63 }
64 end = i
65 for end < len(table) && table[end] != '\n' {
66 end++
67 }
68 return start, end
69}
70
71// rowID reads the id of the line containing offset i, or 0 when the line
72// does not start with a number.
73func rowID(i int) int64 {
74 start, end := lineAt(i)
75 return idAt(start, end)
76}
77
78// idAt reads the id at the start of the line table[start:end].
79func idAt(start, end int) int64 {
80 n := int64(0)
81 for j := start; j < end; j++ {
82 c := table[j]
83 if c == '|' {
84 return n
85 }
86 if c < '0' || c > '9' || j-start > 3 {
87 return 0
88 }
89 n = n*10 + int64(c-'0')
90 }
91 return 0
92}
93
94// parsePlane turns a row into a plane. It checks the shape of the row, not
95// its values: those are the table's promise, checked by the tests.
96func parsePlane(line string) (plane, bool) {
97 f := strings.Split(line, "|")
98 if len(f) != 9 {
99 return plane{}, false
100 }
101 id := int64(0)
102 for i := 0; i < len(f[0]); i++ {
103 id = id*10 + int64(f[0][i]-'0')
104 }
105 return plane{id: id, name: f[1], model: f[2], pilot: f[3], body: f[4], wing: f[5], accent: f[6], tier: f[7], laser: f[8]}, true
106}
107
108// getPlane returns plane id from the table, or false when the id is out of
109// range.
110func getPlane(id int64) (plane, bool) {
111 line := row(id)
112 if line == "" {
113 return plane{}, false
114 }
115 p, ok := parsePlane(line)
116 if !ok {
117 panic(errCorruptRow)
118 }
119 return p, true
120}
121
122// mustPlane returns plane id from the table, or panics.
123func mustPlane(id int64) plane {
124 p, ok := getPlane(id)
125 if !ok {
126 panic(errUnknownPlane)
127 }
128 return p
129}
130
131// tokenID is the GRC721 id of plane id: its number, in decimal.
132func tokenID(id int64) grc721.TokenID {
133 return grc721.TokenID(itoa(id))
134}
135
136// valid reports whether p respects every rule of the table. The tests run it
137// on all 777 rows; the realm trusts the table after that, which is what
138// makes the hand-built JSON and markdown of this realm safe.
139func (p plane) valid() bool {
140 return p.id >= 1 && p.id <= supply &&
141 validName(p.name) &&
142 oneOf(p.model, models) &&
143 oneOf(p.pilot, pilots) &&
144 validColor(p.body) && validColor(p.wing) && validColor(p.accent) && validColor(p.laser) &&
145 oneOf(p.tier, tiers)
146}
147
148// validName reports whether s matches [A-Za-z0-9 -]{1,24}.
149func validName(s string) bool {
150 if len(s) == 0 || len(s) > 24 {
151 return false
152 }
153 for i := 0; i < len(s); i++ {
154 c := s[i]
155 if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') && c != ' ' && c != '-' {
156 return false
157 }
158 }
159 return true
160}
161
162// validColor reports whether s is "#rrggbb" with lowercase hex digits.
163func validColor(s string) bool {
164 if len(s) != 7 || s[0] != '#' {
165 return false
166 }
167 for i := 1; i < len(s); i++ {
168 c := s[i]
169 if !(c >= '0' && c <= '9') && !(c >= 'a' && c <= 'f') {
170 return false
171 }
172 }
173 return true
174}
175
176// validBase reports whether s can be an image base: an https URL ending in
177// "/", 12 to 200 characters of [A-Za-z0-9./_-:]. The charset keeps it safe
178// in JSON and markdown without escaping.
179func validBase(s string) bool {
180 if len(s) < 12 || len(s) > 200 || !strings.HasPrefix(s, "https://") || s[len(s)-1] != '/' {
181 return false
182 }
183 for i := 0; i < len(s); i++ {
184 c := s[i]
185 if !(c >= 'a' && c <= 'z') && !(c >= 'A' && c <= 'Z') && !(c >= '0' && c <= '9') &&
186 c != '.' && c != '/' && c != '_' && c != '-' && c != ':' {
187 return false
188 }
189 }
190 return true
191}
192
193// validAddress reports whether addr is a valid address in its canonical,
194// lowercase form, the only form the game realm and this one key on.
195func validAddress(addr address) bool {
196 return addr.IsValid() && addr[0] == 'g'
197}
198
199// oneOf reports whether s is one of the comma-separated words of list.
200func oneOf(s, list string) bool {
201 for _, w := range strings.Split(list, ",") {
202 if s == w {
203 return true
204 }
205 }
206 return false
207}