Search Apps Documentation Source Content File Folder Download Copy Actions Download State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

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}