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}