game.gno
21.05 Kb · 663 lines
1// GnoMoji — Main Game Realm
2// gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/game4
3//
4// On-chain game engine for GnoMoji on Gno.land (v1.5.0 / Onyx testnet API).
5// Tracks mined coins, casino rounds, PvP lobbies, tools, skills, achievements.
6//
7// Token minting is handled by gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/minter4.
8// Unified coin balances live at gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/coins.
9
10package game4
11
12import (
13 "strconv"
14 "strings"
15 "time"
16
17 "gno.land/p/nt/avl/v0"
18 "gno.land/p/nt/ufmt/v0"
19)
20
21// ── State ────────────────────────────────────────────────────────────────────
22
23var (
24 admin address
25 minterAddr address // authorized minter realm (minter3/minter4) — set via SetMinter
26 pendingMints avl.Tree // addr+":"+coinId → amount string
27 balances avl.Tree // addr+":"+coinId → amount string
28 veinSupply avl.Tree // coinId → "remaining:max"
29 casinoRounds avl.Tree // roundId → JSON string
30 pvpLobbies avl.Tree // lobbyId → JSON string
31 ownedTools avl.Tree // addr → comma-separated tool IDs
32 toolLife avl.Tree // addr+":"+toolId → remaining life string
33 lifetimeMined avl.Tree // addr+":"+coinId → total mined string
34 achievements avl.Tree // addr+":"+achId → "1"
35 dailyCheckins avl.Tree // addr → unix seconds string
36 skillRanks avl.Tree // addr+":"+skillId → rank string
37 miners avl.Tree // addr → total mined all-time string
38 lastClaim avl.Tree // addr → unix timestamp of last ClaimMine (velocity limit)
39 paused bool
40)
41
42func init(cur realm) {
43 admin = cur.Previous().Address()
44}
45
46// ── Admin ────────────────────────────────────────────────────────────────────
47
48func isAdmin(cur realm) bool {
49 caller := cur.Previous().Address()
50 return caller == admin || (minterAddr != address("") && caller == minterAddr)
51}
52
53// SetMinter registers the authorized minter realm address so it can call
54// ClearPendingMints. Called once after minter4 is deployed.
55func SetMinter(cur realm, newMinter string) {
56 if cur.Previous().Address() != admin {
57 panic("unauthorized: only admin can set minter")
58 }
59 minterAddr = address(newMinter)
60}
61
62func UpdateConfig(cur realm, newAdmin string) {
63 if !isAdmin(cur) {
64 panic("unauthorized")
65 }
66 admin = address(newAdmin)
67}
68
69func SetPaused(cur realm, p bool) {
70 if !isAdmin(cur) {
71 panic("unauthorized")
72 }
73 paused = p
74}
75
76func requireNotPaused() {
77 if paused {
78 panic("game is paused")
79 }
80}
81
82// ── Mining ───────────────────────────────────────────────────────────────────
83
84// ClaimMine records mined coins into pending mints for the caller.
85// coinsJson format: [["coinId","amount"],...]
86func ClaimMine(cur realm, coinsJson string) {
87 requireNotPaused()
88 caller := cur.Previous().Address().String()
89
90 // Velocity limit: minimum 5 seconds between ClaimMine calls per address
91 now := time.Now().Unix()
92 if lastClaim.Has(caller) {
93 last, _ := strconv.ParseInt(lastClaim.Get(caller).(string), 10, 64)
94 if now-last < 5 {
95 panic("claiming too fast — please wait 5 seconds between claims")
96 }
97 }
98 lastClaim.Set(caller, strconv.FormatInt(now, 10))
99
100 coinsJson = strings.TrimSpace(coinsJson)
101 coinsJson = strings.TrimPrefix(coinsJson, "[")
102 coinsJson = strings.TrimSuffix(coinsJson, "]")
103
104 for _, entry := range splitJsonArray(coinsJson) {
105 entry = strings.TrimSpace(entry)
106 entry = strings.TrimPrefix(entry, "[")
107 entry = strings.TrimSuffix(entry, "]")
108 parts := strings.SplitN(entry, ",", 2)
109 if len(parts) != 2 {
110 continue
111 }
112 coinId := strings.Trim(strings.TrimSpace(parts[0]), `"`)
113 amtStr := strings.Trim(strings.TrimSpace(parts[1]), `"`)
114 amt, err := strconv.ParseUint(amtStr, 10, 64)
115 if err != nil || amt == 0 {
116 continue
117 }
118
119 // Cap at vein supply if registered
120 if veinSupply.Has(coinId) {
121 vsStr := veinSupply.Get(coinId).(string)
122 p2 := strings.SplitN(vsStr, ":", 2)
123 if len(p2) == 2 {
124 rem, _ := strconv.ParseUint(p2[0], 10, 64)
125 if rem == 0 {
126 continue
127 }
128 if amt > rem {
129 amt = rem
130 }
131 maxV, _ := strconv.ParseUint(p2[1], 10, 64)
132 veinSupply.Set(coinId, ufmt.Sprintf("%d:%d", rem-amt, maxV))
133 }
134 }
135
136 key := caller + ":" + coinId
137
138 // Accumulate pending mints
139 ex := uint64(0)
140 if pendingMints.Has(key) {
141 ex, _ = strconv.ParseUint(pendingMints.Get(key).(string), 10, 64)
142 }
143 pendingMints.Set(key, strconv.FormatUint(ex+amt, 10))
144
145 // Track lifetime mined per coin
146 lifEx := uint64(0)
147 if lifetimeMined.Has(key) {
148 lifEx, _ = strconv.ParseUint(lifetimeMined.Get(key).(string), 10, 64)
149 }
150 lifetimeMined.Set(key, strconv.FormatUint(lifEx+amt, 10))
151
152 // Update leaderboard total (all coins summed)
153 mt := uint64(0)
154 if miners.Has(caller) {
155 mt, _ = strconv.ParseUint(miners.Get(caller).(string), 10, 64)
156 }
157 miners.Set(caller, strconv.FormatUint(mt+amt, 10))
158 }
159}
160
161// ClearPendingMints is called by the treasury server after successful GRC20 minting.
162// Removes pending balance and credits the player's spendable balances tree.
163func ClearPendingMints(cur realm, playerAddr string, coinsJson string) {
164 if !isAdmin(cur) {
165 panic("unauthorized: only admin/minter can clear pending mints")
166 }
167 coinsJson = strings.TrimSpace(coinsJson)
168 coinsJson = strings.TrimPrefix(coinsJson, "[")
169 coinsJson = strings.TrimSuffix(coinsJson, "]")
170 for _, entry := range splitJsonArray(coinsJson) {
171 coinId := strings.Trim(strings.TrimSpace(entry), `"`)
172 if coinId == "" {
173 continue
174 }
175 pendingKey := playerAddr + ":" + coinId
176
177 // Read minted amount before clearing, then credit to balances
178 if pendingMints.Has(pendingKey) {
179 amtStr := pendingMints.Get(pendingKey).(string)
180 amt, err := strconv.ParseUint(amtStr, 10, 64)
181 if err == nil && amt > 0 {
182 balKey := playerAddr + ":" + coinId
183 existing := uint64(0)
184 if balances.Has(balKey) {
185 existing, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
186 }
187 balances.Set(balKey, strconv.FormatUint(existing+amt, 10))
188 }
189 }
190
191 pendingMints.Remove(pendingKey)
192 }
193}
194
195// ── Casino ───────────────────────────────────────────────────────────────────
196
197func CommitCasinoSeed(cur realm, roundId string, seedHash string) {
198 if !isAdmin(cur) {
199 panic("unauthorized")
200 }
201 casinoRounds.Set(roundId, `{"roundId":"`+roundId+`","seedHash":"`+seedHash+`","status":"committed","bets":[]}`)
202}
203
204func PlaceCasinoBet(cur realm, roundId string, game string, coinId string, betAmount string, choice string, clientSeed string) {
205 requireNotPaused()
206 caller := cur.Previous().Address().String()
207 amt, err := strconv.ParseUint(betAmount, 10, 64)
208 if err != nil || amt == 0 {
209 panic("invalid bet amount")
210 }
211
212 balKey := caller + ":" + coinId
213 existing := uint64(0)
214 if balances.Has(balKey) {
215 existing, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
216 }
217 if existing < amt {
218 panic("insufficient balance")
219 }
220 balances.Set(balKey, strconv.FormatUint(existing-amt, 10))
221
222 if !casinoRounds.Has(roundId) {
223 panic("round not found")
224 }
225 bet := `{"addr":"` + caller + `","game":"` + game + `","coinId":"` + coinId + `","amount":` + betAmount + `,"choice":"` + choice + `","clientSeed":"` + clientSeed + `"}`
226 casinoRounds.Set(roundId+":bet:"+caller, bet)
227}
228
229func RevealCasinoSeed(cur realm, roundId string, serverSeed string) {
230 if !isAdmin(cur) {
231 panic("unauthorized")
232 }
233 casinoRounds.Set(roundId+":serverSeed", serverSeed)
234 casinoRounds.Set(roundId+":status", "revealed")
235}
236
237func RefundCasinoBet(cur realm, roundId string) {
238 requireNotPaused()
239 caller := cur.Previous().Address().String()
240 betKey := roundId + ":bet:" + caller
241 if !casinoRounds.Has(betKey) {
242 panic("no bet found for this round")
243 }
244
245 // Parse bet JSON to recover coinId and amount before removing the record.
246 // Bet JSON format: {"addr":"...","game":"...","coinId":"...","amount":N,...}
247 betJson := casinoRounds.Get(betKey).(string)
248
249 // Extract coinId (quoted string value)
250 coinId := ""
251 coinIdStart := strings.Index(betJson, `"coinId":"`) + len(`"coinId":"`)
252 if coinIdStart > len(`"coinId":"`) {
253 coinIdEnd := strings.Index(betJson[coinIdStart:], `"`) + coinIdStart
254 if coinIdEnd > coinIdStart {
255 coinId = betJson[coinIdStart:coinIdEnd]
256 }
257 }
258
259 // Extract amount (unquoted numeric value)
260 amountStr := ""
261 amountStart := strings.Index(betJson, `"amount":`) + len(`"amount":`)
262 if amountStart > len(`"amount":`) {
263 amountEnd := strings.IndexAny(betJson[amountStart:], `,}`) + amountStart
264 if amountEnd > amountStart {
265 amountStr = betJson[amountStart:amountEnd]
266 }
267 }
268
269 amount, _ := strconv.ParseUint(amountStr, 10, 64)
270 if amount > 0 && coinId != "" {
271 // Restore the deducted balance
272 balKey := caller + ":" + coinId
273 current := uint64(0)
274 if balances.Has(balKey) {
275 current, _ = strconv.ParseUint(balances.Get(balKey).(string), 10, 64)
276 }
277 balances.Set(balKey, strconv.FormatUint(current+amount, 10))
278 }
279
280 casinoRounds.Remove(betKey)
281}
282
283// ── PvP Lobbies ──────────────────────────────────────────────────────────────
284
285func CreatePvpLobby(cur realm, game string, coinId string, betAmount string) string {
286 requireNotPaused()
287 caller := cur.Previous().Address().String()
288 prefix := caller
289 if len(prefix) > 8 {
290 prefix = caller[:8]
291 }
292 lobbyId := ufmt.Sprintf("pvp-%s-%d", prefix, time.Now().Unix())
293 lobby := `{"lobbyId":"` + lobbyId + `","creator":"` + caller + `","game":"` + game + `","coinId":"` + coinId + `","betAmount":` + betAmount + `,"status":"open","round":1,"createdAt":` + strconv.FormatInt(time.Now().Unix(), 10) + `}`
294 pvpLobbies.Set(lobbyId, lobby)
295 return lobbyId
296}
297
298func JoinPvpLobby(cur realm, lobbyId string) {
299 requireNotPaused()
300 caller := cur.Previous().Address().String()
301 if !pvpLobbies.Has(lobbyId) {
302 panic("lobby not found")
303 }
304 pvpLobbies.Set(lobbyId+":challenger", caller)
305 pvpLobbies.Set(lobbyId+":status", "joined")
306}
307
308func CommitPvpMove(cur realm, lobbyId string, moveHash string) {
309 requireNotPaused()
310 caller := cur.Previous().Address().String()
311 pvpLobbies.Set(lobbyId+":commit:"+caller, moveHash)
312}
313
314// RevealPvpMove requires both players to have committed before either can reveal.
315func RevealPvpMove(cur realm, lobbyId string, choice string, salt string) {
316 requireNotPaused()
317 caller := cur.Previous().Address().String()
318
319 // Verify lobby exists and both players have committed
320 if !pvpLobbies.Has(lobbyId) {
321 panic("lobby not found")
322 }
323 lobbyJson := pvpLobbies.Get(lobbyId).(string)
324 // Extract creator address from JSON
325 creatorStart := strings.Index(lobbyJson, `"creator":"`) + len(`"creator":"`)
326 creatorEnd := strings.Index(lobbyJson[creatorStart:], `"`) + creatorStart
327 creator := lobbyJson[creatorStart:creatorEnd]
328
329 // Require challenger has joined
330 if !pvpLobbies.Has(lobbyId + ":challenger") {
331 panic("no opponent has joined yet")
332 }
333 challenger := pvpLobbies.Get(lobbyId + ":challenger").(string)
334
335 // Require both commits are present
336 if !pvpLobbies.Has(lobbyId+":commit:"+creator) || !pvpLobbies.Has(lobbyId+":commit:"+challenger) {
337 panic("both players must commit moves before revealing")
338 }
339
340 pvpLobbies.Set(lobbyId+":reveal:"+caller, choice+":"+salt)
341 pvpLobbies.Set(lobbyId+":status", "revealed")
342}
343
344// ClaimPvpTimeout allows a lobby participant to claim a timeout win.
345// Only the creator or challenger may call this.
346func ClaimPvpTimeout(cur realm, lobbyId string) {
347 requireNotPaused()
348 caller := cur.Previous().Address().String()
349
350 if !pvpLobbies.Has(lobbyId) {
351 panic("lobby not found")
352 }
353 lobbyJson := pvpLobbies.Get(lobbyId).(string)
354
355 // Extract creator address
356 creatorStart := strings.Index(lobbyJson, `"creator":"`) + len(`"creator":"`)
357 creatorEnd := strings.Index(lobbyJson[creatorStart:], `"`) + creatorStart
358 creator := lobbyJson[creatorStart:creatorEnd]
359
360 // Get challenger (may not exist yet)
361 challenger := ""
362 if pvpLobbies.Has(lobbyId + ":challenger") {
363 challenger = pvpLobbies.Get(lobbyId + ":challenger").(string)
364 }
365
366 // Verify caller is a participant
367 if caller != creator && caller != challenger {
368 panic("not a lobby participant")
369 }
370
371 // Require minimum 10 minutes since lobby creation
372 createdAtStart := strings.Index(lobbyJson, `"createdAt":`) + len(`"createdAt":`)
373 createdAtEnd := strings.IndexAny(lobbyJson[createdAtStart:], `,}`) + createdAtStart
374 createdAt, _ := strconv.ParseInt(lobbyJson[createdAtStart:createdAtEnd], 10, 64)
375 if time.Now().Unix()-createdAt < 600 {
376 panic("timeout not yet claimable — minimum 10 minutes must elapse")
377 }
378
379 pvpLobbies.Set(lobbyId+":winner", caller)
380 pvpLobbies.Set(lobbyId+":status", "finished")
381}
382
383func ExpireLobbies(cur realm) {
384 if !isAdmin(cur) {
385 panic("unauthorized")
386 }
387}
388
389// ── Tools ────────────────────────────────────────────────────────────────────
390
391func BuyTool(cur realm, toolId string) {
392 requireNotPaused()
393 caller := cur.Previous().Address().String()
394 existing := ""
395 if ownedTools.Has(caller) {
396 existing = ownedTools.Get(caller).(string)
397 }
398 if existing == "" {
399 ownedTools.Set(caller, toolId)
400 } else {
401 ownedTools.Set(caller, existing+","+toolId)
402 }
403}
404
405func RepairTool(cur realm, toolId string) {
406 requireNotPaused()
407 caller := cur.Previous().Address().String()
408 toolLife.Set(caller+":"+toolId, "10000")
409}
410
411// ── Progression ──────────────────────────────────────────────────────────────
412
413func TrainSkill(cur realm, skillId string) {
414 requireNotPaused()
415 caller := cur.Previous().Address().String()
416 key := caller + ":" + skillId
417 rank := uint64(0)
418 if skillRanks.Has(key) {
419 rank, _ = strconv.ParseUint(skillRanks.Get(key).(string), 10, 64)
420 }
421 skillRanks.Set(key, strconv.FormatUint(rank+1, 10))
422}
423
424func ClaimAchievement(cur realm, achievementId string) {
425 requireNotPaused()
426 caller := cur.Previous().Address().String()
427 key := caller + ":" + achievementId
428 if achievements.Has(key) {
429 panic("already claimed")
430 }
431
432 // Require that the caller has actually played (mined at least 1 coin lifetime)
433 if !miners.Has(caller) {
434 panic("no mining activity — play the game first")
435 }
436 total, _ := strconv.ParseUint(miners.Get(caller).(string), 10, 64)
437 if total == 0 {
438 panic("no mining activity — play the game first")
439 }
440
441 achievements.Set(key, "1")
442}
443
444func DailyCheckin(cur realm) {
445 requireNotPaused()
446 caller := cur.Previous().Address().String()
447 now := time.Now().Unix()
448 if dailyCheckins.Has(caller) {
449 last, _ := strconv.ParseInt(dailyCheckins.Get(caller).(string), 10, 64)
450 if now-last < 72000 {
451 panic("too soon for daily check-in")
452 }
453 }
454 dailyCheckins.Set(caller, strconv.FormatInt(now, 10))
455}
456
457// CreateTask registers a new task on-chain.
458func CreateTask(cur realm, taskId string, desc string) {
459 requireNotPaused()
460 caller := cur.Previous().Address().String()
461 // Store task metadata keyed by taskId; only the creator can create it
462 achievements.Set("task:"+taskId+":creator", caller)
463 achievements.Set("task:"+taskId+":desc", desc)
464}
465
466func CompleteTask(cur realm, taskId string) {
467 requireNotPaused()
468 caller := cur.Previous().Address().String()
469 achievements.Set(caller+":"+taskId, "task:1")
470}
471
472func RegisterVein(cur realm, coinId string, maxSupply string) {
473 if !isAdmin(cur) {
474 panic("unauthorized")
475 }
476 veinSupply.Set(coinId, maxSupply+":"+maxSupply)
477}
478
479// ── Queries (pure reads — no cur realm needed) ────────────────────────────────
480
481func PendingMints(playerAddr string) string {
482 var parts []string
483 pendingMints.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
484 coinId := strings.TrimPrefix(key, playerAddr+":")
485 parts = append(parts, `["`+coinId+`","`+val.(string)+`"]`)
486 return false
487 })
488 return ufmt.Sprintf(`{"pending":[%s]}`, strings.Join(parts, ","))
489}
490
491func AllBalances(playerAddr string) string {
492 var parts []string
493 balances.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
494 coinId := strings.TrimPrefix(key, playerAddr+":")
495 parts = append(parts, `"`+coinId+`":`+val.(string))
496 return false
497 })
498 return "{" + strings.Join(parts, ",") + "}"
499}
500
501func VeinSupply(coinId string) string {
502 if !veinSupply.Has(coinId) {
503 return `{"remaining":"0","max":"0"}`
504 }
505 p := strings.SplitN(veinSupply.Get(coinId).(string), ":", 2)
506 if len(p) != 2 {
507 return `{"remaining":"0","max":"0"}`
508 }
509 return `{"remaining":"` + p[0] + `","max":"` + p[1] + `"}`
510}
511
512func CasinoRound(roundId string) string {
513 if !casinoRounds.Has(roundId) {
514 return "null"
515 }
516 return casinoRounds.Get(roundId).(string)
517}
518
519func CasinoHistory(_ string) string { return "[]" }
520
521func PvpLobby(lobbyId string) string {
522 if !pvpLobbies.Has(lobbyId) {
523 return "null"
524 }
525 return pvpLobbies.Get(lobbyId).(string)
526}
527
528func OpenLobbies() string {
529 var parts []string
530 pvpLobbies.Iterate("pvp-", "pvp-\xFF", func(key string, val any) bool {
531 if !strings.Contains(key, ":") {
532 parts = append(parts, val.(string))
533 }
534 return false
535 })
536 return "[" + strings.Join(parts, ",") + "]"
537}
538
539func JackpotPool(coinId string) string {
540 return `{"pool":"0","coinId":"` + coinId + `"}`
541}
542
543func PlayerProfile(playerAddr string) string {
544 return `{"address":"` + playerAddr + `"}`
545}
546
547func OwnedTools(playerAddr string) string {
548 if !ownedTools.Has(playerAddr) {
549 return `["hands"]`
550 }
551 ids := strings.Split(ownedTools.Get(playerAddr).(string), ",")
552 var quoted []string
553 for _, id := range ids {
554 quoted = append(quoted, `"`+id+`"`)
555 }
556 return "[" + strings.Join(quoted, ",") + "]"
557}
558
559func LifetimeMined(playerAddr string) string {
560 var parts []string
561 lifetimeMined.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
562 coinId := strings.TrimPrefix(key, playerAddr+":")
563 parts = append(parts, `"`+coinId+`":`+val.(string))
564 return false
565 })
566 return "{" + strings.Join(parts, ",") + "}"
567}
568
569func AllMiners(limit int, offset int) string {
570 var parts []string
571 count, skipped := 0, 0
572 miners.Iterate("", "\xFF", func(key string, val any) bool {
573 if skipped < offset {
574 skipped++
575 return false
576 }
577 if count >= limit {
578 return true
579 }
580 display := key
581 if len(display) > 12 {
582 display = display[:8] + "..."
583 }
584 parts = append(parts, `{"address":"`+key+`","display":"`+display+`","total":`+val.(string)+`}`)
585 count++
586 return false
587 })
588 return "[" + strings.Join(parts, ",") + "]"
589}
590
591func GetMedals(playerAddr string) string {
592 var claimed []string
593 achievements.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
594 achId := strings.TrimPrefix(key, playerAddr+":")
595 claimed = append(claimed, `"`+achId+`"`)
596 return false
597 })
598 return ufmt.Sprintf(`{"medals":[%s]}`, strings.Join(claimed, ","))
599}
600
601func DailyCheckinStatus(playerAddr string) string {
602 now := time.Now().Unix()
603 last := int64(0)
604 if dailyCheckins.Has(playerAddr) {
605 last, _ = strconv.ParseInt(dailyCheckins.Get(playerAddr).(string), 10, 64)
606 }
607 done := now-last < 72000
608 return ufmt.Sprintf(`{"ok":true,"done":%t,"streak":1,"next":%d}`, done, last+86400)
609}
610
611func ListTasks(_ string) string { return `{"ok":true,"tasks":[]}` }
612
613func SkillRanks(playerAddr string) string {
614 var parts []string
615 skillRanks.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
616 skillId := strings.TrimPrefix(key, playerAddr+":")
617 parts = append(parts, `"`+skillId+`":`+val.(string))
618 return false
619 })
620 return "{" + strings.Join(parts, ",") + "}"
621}
622
623func ToolLife(playerAddr string) string {
624 var parts []string
625 toolLife.Iterate(playerAddr+":", playerAddr+":\xFF", func(key string, val any) bool {
626 toolId := strings.TrimPrefix(key, playerAddr+":")
627 parts = append(parts, `"`+toolId+`":`+val.(string))
628 return false
629 })
630 return "{" + strings.Join(parts, ",") + "}"
631}
632
633// ── Render ───────────────────────────────────────────────────────────────────
634
635func Render(_ string) string {
636 return ufmt.Sprintf("# GnoMoji Game Realm\n\nAdmin: `%s`\n\nPath: `gno.land/r/g138gdmwj6heduu3st796lw6qqdwylgaxf7ty5gz/game4`\n\nMine emoji coins on Gno.land!\n", admin.String())
637}
638
639// ── Helpers ──────────────────────────────────────────────────────────────────
640
641func splitJsonArray(s string) []string {
642 var result []string
643 depth, start := 0, 0
644 for i := 0; i < len(s); i++ {
645 switch s[i] {
646 case '[', '{':
647 depth++
648 case ']', '}':
649 depth--
650 case ',':
651 if depth == 0 {
652 if e := strings.TrimSpace(s[start:i]); e != "" {
653 result = append(result, e)
654 }
655 start = i + 1
656 }
657 }
658 }
659 if e := strings.TrimSpace(s[start:]); e != "" {
660 result = append(result, e)
661 }
662 return result
663}