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

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

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}