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

import_v1.gno

6.92 Kb · 206 lines
  1package golf
  2
  3import (
  4	"chain"
  5	"strconv"
  6	"strings"
  7
  8	v1 "gno.land/r/nym-alexiscolin000/gnogolf/golf"
  9)
 10
 11// The migration (onyx only: stage.sh leaves this file out of the mainnet
 12// deploy): v1's course holes and records, read from v1 on chain in the same
 13// call that writes them, so no one types a value in. Not replayed: v1 already
 14// replayed each with this same physics. A hole keeps v1's id, so its weather
 15// and its records fit it.
 16
 17// EventImported is a v1 record copied in; EventNotImported one left on v1, a
 18// shot of it finer than v2 takes (validShot): its ghost would not replay here.
 19const (
 20	EventImported    = "Imported"
 21	EventNotImported = "NotImported"
 22)
 23
 24// ImportHole copies v1's current version of a course slot, by its id
 25// ("garden/7/v3"): its data, provenance (by, height, note), plays and wear.
 26// Only the owner can, into a slot v2 has nothing in yet.
 27func ImportHole(cur realm, id string) string {
 28	begin()
 29	r := importHoleNow(cur, id)
 30	flush(cur) // (only once it went through: a refusal writes nothing)
 31	return r
 32}
 33
 34func importHoleNow(cur realm, id string) string {
 35	if !isOwner(cur.Previous().Address()) {
 36		panic("golf: only the owner can import a course hole")
 37	}
 38	cut := strings.LastIndex(id, "/v")
 39	if cut < 0 || v1.Current(id[:cut]) != id {
 40		panic("golf: " + id + " is not a course slot's current version on v1")
 41	}
 42	slot := id[:cut]
 43	key, ok := slotKeyOf(slot)
 44	if !ok {
 45		panic("golf: " + slot + " is not a course slot")
 46	}
 47	return importHole(id, key, v1.HoleData(id), jobject(v1.Versions(slot), id), jobject(v1.Holes(), id), v1.HoleState(id))
 48}
 49
 50// importHole keeps the version id of slot key as v1 lists it: its data (hex),
 51// its row of Versions (ver), of Holes (row) and its HoleState (state).
 52func importHole(id, key, hexData, ver, row, state string) string {
 53	cut := strings.LastIndex(id, "/v")
 54	slot := id[:cut]
 55	if slots.Has(key) {
 56		panic("golf: " + slot + " already has a version here")
 57	}
 58	h, data, sum := decodeData(hexData)
 59	if own := h.World + "/" + strconv.Itoa(int(h.Order)); own != slot {
 60		panic("golf: v1's data of " + id + " is " + own) // v1 checked it at publish: cannot happen
 61	}
 62	e := dataEntry(slot, atoi(id[cut+2:]), h, address(jget(ver, "by")), sum, jget(ver, "note"))
 63	e.height = atoi64(jget(ver, "height"))
 64	e.plays = atoi(jget(row, "plays"))
 65	e.official = true
 66	saveEntry(e)
 67	holeData.Set(e.id, data)
 68	slots.Set(key, e.id)
 69	for i, c := range strings.Split(jget(state, "cells"), ",") {
 70		if c != "0" && c != "" && i < wearCells {
 71			e.wear().Set(pad(i, 3), itoa(atoi(c)))
 72		}
 73	}
 74	chain.Emit(EventHolePublished, "hole", e.id, "slot", slot, "sha", sum, "by", e.by.String(), "official", "true")
 75	return e.id
 76}
 77
 78// ImportRecord copies a player's v1 best on an imported hole in a mode
 79// (v1's Ghost: strokes, height, period, shots) and seats it as a finish
 80// would, onto the hole imported from v1 (the same data: its sha is v1's). Only
 81// the owner can, as the migration's script runs it: v1 plays on, with no
 82// near-copy rule, so a round played there later is not one to rank here.
 83// Idempotent: a record already as good stays; a better v1 one takes its
 84// place. A record with a shot finer than 0.01 (never one the game sends)
 85// stays on v1. It answers whether it copied anything.
 86func ImportRecord(cur realm, hole, mode string, player address) bool {
 87	begin()
 88	r := importRecordNow(cur, hole, mode, player)
 89	flush(cur) // (only once it went through: a refusal writes nothing)
 90	return r
 91}
 92
 93func importRecordNow(cur realm, hole, mode string, player address) bool {
 94	if !isOwner(cur.Previous().Address()) {
 95		panic("golf: only the owner can import a record")
 96	}
 97	e, m := mustHole(hole), modeOf(mode)
 98	if !e.official || e.next != "" {
 99		panic("golf: " + hole + " is not a current course hole here")
100	}
101	// (Versions lists a slot's newest 100: a version past them would be refused,
102	// never wrongly let in; v1's slots have a few each)
103	if jget(jobject(v1.Versions(e.slot), e.id), "sha") != e.sha {
104		panic("golf: " + hole + " here is not v1's " + hole)
105	}
106	// v1's standing of the player, for its tie-break: the page of Players
107	// that starts just before their key
108	p, at := player.String(), int64(0)
109	if row := v1.Players(mode, p[:len(p)-1], 1); jget(row, "player") == p {
110		at = atoi64(jget(row, "height"))
111	}
112	return importRecord(e, m, p, v1.Ghost(hole, mode, player), at)
113}
114
115// importRecord copies p's best on e in mode m as v1's Ghost (g) has it; at is
116// v1's standing's height (the course ranking's tie-break: the finish that last
117// improved it, perhaps on a version since archived), 0 for none.
118func importRecord(e *entry, m int, p, g string, at int64) bool {
119	if g == "null" {
120		return false
121	}
122	strokes, shots := atoi(jget(g, "strokes")), jget(g, "shots")
123	if strokes < 1 || jget(g, "hole") != e.id {
124		panic("golf: v1's record of " + e.id + " does not read") // v1 writes them: cannot happen
125	}
126	for _, s := range strings.Split(shots, ";") {
127		if a, pw, _ := parseShot(s); !cents(a) || !cents(pw) {
128			chain.Emit(EventNotImported, "hole", e.id, "player", p, "mode", modeNames[m], "shot", s)
129			return false
130		}
131	}
132	best, _ := e.bests(m).Get(p)
133	was, wasCopy := bestStrokes(best), e.copies(m).Has(p)
134	if was > 0 && was <= strokes && !wasCopy { // (their own, from v1, replaces a copy played here)
135		return false
136	}
137	if wasCopy { // a copy played here: never counted, never on the board
138		e.copies(m).Remove(p)
139		was = 0
140	} else if was > 0 {
141		e.board(m).Remove(boardKey(best, p))
142	}
143	best = itoa(strokes) + " " + i64(atoi64(jget(g, "height"))) + " " + i64(atoi64(jget(g, "period"))) + " " + shots
144	e.bests(m).Set(p, best)
145	r := &row{player: p}
146	if v, ok := totals[m].Get(p); ok {
147		r = decodeRow(v)
148	}
149	if at == 0 { // (no standing on v1: its finishes, not this call)
150		at = atoi64(jget(g, "height"))
151	}
152	if at < r.height { // (a finish here since: the later one)
153		at = r.height
154	}
155	if was == 0 {
156		setRow(m, r, r.holes+1, r.strokes+strokes, r.par+e.par, at)
157	} else {
158		setRow(m, r, r.holes, r.strokes+strokes-was, r.par, at)
159	}
160	if named(p) {
161		seat(e, m, p, best)
162	}
163	saveEntry(e)
164	chain.Emit(EventImported, "hole", e.id, "player", p, "mode", modeNames[m], "strokes", itoa(strokes))
165	return true
166}
167
168// jobject is a v1 list from the object whose "id" is id on ("" if none):
169// jget then reads that object's fields, which every object of the list has.
170func jobject(j, id string) string {
171	i := strings.Index(j, `{"id":`+jstr(id)+`,`)
172	if i < 0 {
173		return ""
174	}
175	return j[i:]
176}
177
178// jget is a field of v1's JSON, as v1 writes it (jstr): a string's value,
179// unescaped, or a number's or an array's inside, "" if absent.
180func jget(j, k string) string {
181	i := strings.Index(j, `"`+k+`":`)
182	if i < 0 {
183		return ""
184	}
185	v := j[i+len(k)+3:]
186	switch {
187	case v == "":
188		return ""
189	case v[0] == '"':
190		end := 1
191		for end < len(v) && v[end] != '"' {
192			if v[end] == '\\' {
193				end++
194			}
195			end++
196		}
197		s, err := strconv.Unquote(v[:end+1])
198		if err != nil {
199			panic("golf: v1's " + k + " does not read")
200		}
201		return s
202	case v[0] == '[':
203		return v[1:strings.Index(v, "]")]
204	}
205	return v[:strings.IndexAny(v, ",}]")]
206}