party.gno
6.24 Kb · 318 lines
1// Package party is the Shoal crew book. Members share a fight and do not
2// bite each other. This realm holds no ugnot and no FISH. The fish realm pays a kill.
3// Deployed on onyx-1 at gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/shoal/party.
4// The older gno.land/r/g1n4pl5uc4yt5r96m9w6fmdznx3x0jyg8l6arhmt/party package stays as it was.
5package party
6
7import (
8 "chain"
9 "chain/runtime/unsafe"
10 "strconv"
11)
12
13const (
14 partyCap = 8
15 nameMax = 16
16 capFloor = 2
17 capCeil = 32
18)
19
20type crew struct {
21 id int
22 name string
23 leader address
24 members []address
25 closed bool
26}
27
28var (
29 crews []*crew
30 seat map[address]int
31 invited map[address]int
32 nextPID int
33 crewCap = partyCap
34 admin address
35 adminSet bool
36)
37
38func Create(cur realm, name string) int {
39 caller := requireUser(cur)
40 takeExact(0)
41 if seatOf(caller) != 0 {
42 panic("party: seat")
43 }
44 clean := cleanName(name)
45 if clean == "" || taken(clean) {
46 panic("party: name")
47 }
48 ensure()
49 nextPID++
50 id := nextPID
51 body := &crew{
52 id: id,
53 name: clean,
54 leader: caller,
55 members: []address{caller},
56 }
57 for len(crews) <= id {
58 crews = append(crews, nil)
59 }
60 crews[id] = body
61 seat[caller] = id
62 chain.Emit("Create", "id", strconv.Itoa(id), "leader", caller.String(), "name", clean)
63 return id
64}
65
66func Invite(cur realm, who address) {
67 caller := requireUser(cur)
68 takeExact(0)
69 if !who.IsValid() {
70 panic("party: addr")
71 }
72 if who == caller {
73 panic("party: self")
74 }
75 id := seatOf(caller)
76 body := openCrew(id)
77 if seatOf(who) != 0 {
78 panic("party: seat")
79 }
80 if len(body.members) >= crewCap {
81 panic("party: full")
82 }
83 ensure()
84 invited[who] = id
85 chain.Emit("Invite", "id", strconv.Itoa(id), "who", who.String())
86}
87
88func Accept(cur realm) int {
89 caller := requireUser(cur)
90 takeExact(0)
91 if seatOf(caller) != 0 {
92 panic("party: seat")
93 }
94 id := 0
95 if invited != nil {
96 id = invited[caller]
97 }
98 body := openCrew(id)
99 if len(body.members) >= crewCap {
100 panic("party: full")
101 }
102 delete(invited, caller)
103 body.members = append(body.members, caller)
104 seat[caller] = id
105 chain.Emit("Join", "id", strconv.Itoa(id), "who", caller.String())
106 return id
107}
108
109func Leave(cur realm) {
110 caller := requireUser(cur)
111 takeExact(0)
112 id := seatOf(caller)
113 body := openCrew(id)
114 dropMember(body, caller)
115 chain.Emit("Leave", "id", strconv.Itoa(id), "who", caller.String())
116}
117
118func Kick(cur realm, who address) {
119 caller := requireUser(cur)
120 takeExact(0)
121 id := seatOf(caller)
122 body := openCrew(id)
123 if body.leader != caller {
124 panic("party: leader")
125 }
126 if who == caller || seatOf(who) != id {
127 panic("party: member")
128 }
129 dropMember(body, who)
130 chain.Emit("Kick", "id", strconv.Itoa(id), "who", who.String())
131}
132
133func Disband(cur realm) {
134 caller := requireUser(cur)
135 takeExact(0)
136 id := seatOf(caller)
137 body := openCrew(id)
138 if body.leader != caller {
139 panic("party: leader")
140 }
141 for _, member := range body.members {
142 delete(seat, member)
143 }
144 body.members = nil
145 body.closed = true
146 body.name = ""
147 dropInvites(id)
148 chain.Emit("Disband", "id", strconv.Itoa(id))
149}
150
151// Cap is the seat limit for a new invite. The default is 8.
152func Cap() int { return crewCap }
153
154// Version names this local package shape. The live party realm is unchanged.
155func Version() string { return "party-1" }
156
157// Admin is the address that claimed the cap hook. It is empty until ClaimAdmin.
158func Admin() address { return admin }
159
160// ClaimAdmin records the first caller. A later caller cannot replace them here.
161func ClaimAdmin(cur realm) {
162 caller := requireUser(cur)
163 takeExact(0)
164 if adminSet {
165 panic("party: admin")
166 }
167 admin = caller
168 adminSet = true
169 chain.Emit("ClaimAdmin", "who", caller.String())
170}
171
172// SetCap changes how many seats a crew may fill. Crews that are already larger stay as they are.
173func SetCap(cur realm, n int) {
174 caller := requireUser(cur)
175 takeExact(0)
176 if !adminSet || caller != admin {
177 panic("party: admin")
178 }
179 if n < capFloor || n > capCeil {
180 panic("party: cap")
181 }
182 crewCap = n
183 chain.Emit("SetCap", "n", strconv.Itoa(n))
184}
185
186func PartyOf(cur realm, who address) int {
187 takeExact(0)
188 return seatOf(who)
189}
190
191func SameParty(cur realm, a, b address) bool {
192 takeExact(0)
193 id := seatOf(a)
194 return id != 0 && id == seatOf(b)
195}
196
197func NameOf(cur realm, id int) string {
198 takeExact(0)
199 return openCrew(id).name
200}
201
202func LeaderOf(cur realm, id int) address {
203 takeExact(0)
204 return openCrew(id).leader
205}
206
207func MemberCount(cur realm, id int) int {
208 takeExact(0)
209 return len(openCrew(id).members)
210}
211
212func MemberAt(cur realm, id, index int) address {
213 takeExact(0)
214 body := openCrew(id)
215 if index < 0 || index >= len(body.members) {
216 panic("party: index")
217 }
218 return body.members[index]
219}
220
221func requireUser(cur realm) address {
222 if !cur.Previous().IsUserCall() {
223 panic("party: eoa")
224 }
225 return cur.Previous().Address()
226}
227
228func takeExact(n int64) {
229 sent := unsafe.OriginSend()
230 if n == 0 {
231 if len(sent) != 0 {
232 panic("party: payment")
233 }
234 return
235 }
236 panic("party: payment")
237}
238
239func ensure() {
240 if seat == nil {
241 seat = make(map[address]int)
242 }
243 if invited == nil {
244 invited = make(map[address]int)
245 }
246}
247
248func seatOf(who address) int {
249 if seat == nil || !who.IsValid() {
250 return 0
251 }
252 return seat[who]
253}
254
255func openCrew(id int) *crew {
256 if id < 1 || id >= len(crews) || crews[id] == nil || crews[id].closed {
257 panic("party: none")
258 }
259 return crews[id]
260}
261
262func taken(name string) bool {
263 for _, body := range crews {
264 if body != nil && !body.closed && body.name == name {
265 return true
266 }
267 }
268 return false
269}
270
271func cleanName(name string) string {
272 if len(name) < 1 || len(name) > nameMax {
273 return ""
274 }
275 for _, c := range name {
276 if (c < 'a' || c > 'z') && (c < 'A' || c > 'Z') && (c < '0' || c > '9') && c != ' ' {
277 return ""
278 }
279 }
280 return name
281}
282
283func dropMember(body *crew, who address) {
284 next := make([]address, 0, len(body.members))
285 for _, member := range body.members {
286 if member != who {
287 next = append(next, member)
288 }
289 }
290 delete(seat, who)
291 if invited != nil {
292 delete(invited, who)
293 }
294 if len(next) == 0 {
295 body.members = nil
296 body.closed = true
297 body.name = ""
298 dropInvites(body.id)
299 return
300 }
301 body.members = next
302 body.leader = next[0]
303}
304
305func dropInvites(id int) {
306 if invited == nil {
307 return
308 }
309 var drop []address
310 for who, pid := range invited {
311 if pid == id {
312 drop = append(drop, who)
313 }
314 }
315 for _, who := range drop {
316 delete(invited, who)
317 }
318}