game_world.gno
5.13 Kb · 191 lines
1package game_world
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime"
7 "chain/runtime/unsafe"
8 "strconv"
9
10 "gno.land/p/akkadia/beta/accesscontrol"
11 "gno.land/r/akkadia/beta/admin"
12)
13
14const (
15 CreateGameWorldEvent = "CreateGameWorld"
16 RestrictGameWorldEvent = "RestrictGameWorld"
17)
18
19type CreationMode int
20
21const (
22 CreationModeInvalid CreationMode = iota
23 CreationModeOperatorOnly
24 CreationModeApprovedCreator
25 CreationModePublicCreator
26)
27
28type GameWorldStatus int
29
30const (
31 GameWorldStatusInvalid GameWorldStatus = iota
32 GameWorldStatusExists
33 GameWorldStatusRestricted
34)
35
36type GameWorld struct {
37 ID uint64
38 Owner address
39 GameType string
40 Status GameWorldStatus
41 CreatedHeight int64
42 RestrictedHeight int64
43}
44
45type GameTypePolicy struct {
46 CreationCost int64
47 CreationMode CreationMode
48}
49
50func CreateGameWorld(cur realm, gameType string) uint64 {
51 owner := accesscontrol.MustGetUserCaller(0, cur)
52 policy := mustGetGameTypePolicy(gameType)
53 assertCreationAllowed(gameType, owner, policy.CreationMode)
54 assertExactUgnotPayment(policy.CreationCost)
55
56 id := createGameWorld(owner, gameType)
57 if policy.CreationCost > 0 {
58 bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
59 bnk.SendCoins(cur.Address(), admin.GetFeeCollector(), chain.Coins{{"ugnot", policy.CreationCost}})
60 }
61 emitCreateGameWorld(owner, id, gameType, policy.CreationCost)
62 return id
63}
64
65func RestrictGameWorld(cur realm, gameWorldID uint64) {
66 executor := accesscontrol.MustGetAdminOrOperatorCaller(0, cur, admin.IsAdmin, admin.IsOperator)
67 world := GetGameWorld(gameWorldID)
68 if world.Status == GameWorldStatusRestricted {
69 return
70 }
71 world.Status = GameWorldStatusRestricted
72 world.RestrictedHeight = runtime.ChainHeight()
73 gameWorlds.Set(uint64Key(gameWorldID), world)
74 removeOwnerGameWorldID(world.Owner, gameWorldID)
75 restrictedGameWorldIDs.Set(uint64Key(gameWorldID), true)
76 chain.Emit(
77 RestrictGameWorldEvent,
78 "executor", executor.String(),
79 "id", strconv.FormatUint(gameWorldID, 10),
80 "oldStatus", strconv.Itoa(int(GameWorldStatusExists)),
81 "newStatus", strconv.Itoa(int(GameWorldStatusRestricted)),
82 "restrictedHeight", strconv.FormatInt(world.RestrictedHeight, 10),
83 )
84}
85
86func HasGameWorld(gameWorldID uint64) bool { return gameWorlds.Has(uint64Key(gameWorldID)) }
87
88func GetGameWorld(gameWorldID uint64) GameWorld {
89 value := gameWorlds.Get(uint64Key(gameWorldID))
90 if value == nil {
91 panic("game world not found: " + strconv.FormatUint(gameWorldID, 10))
92 }
93 return value.(GameWorld)
94}
95
96func GetLatestGameWorldID() uint64 { return latestGameWorldID }
97
98func GetListLimit() int { return gameWorldListLimit }
99
100func GetRestrictedGameWorldCount() int { return restrictedGameWorldIDs.Size() }
101
102func ListRestrictedGameWorldIDs(page, count int) []uint64 {
103 if page < 1 {
104 panic("page must be at least 1")
105 }
106 if count < 1 || count > gameWorldListLimit {
107 panic("count must be between 1 and 100")
108 }
109 result := []uint64{}
110 restrictedGameWorldIDs.IterateByOffset((page-1)*count, count, func(key string, _ any) bool {
111 result = append(result, parseUint64Key(key))
112 return false
113 })
114 return result
115}
116
117func ListLatestGameWorldsByOwner(owner address) []GameWorld {
118 ids, found := getOwnerGameWorldIDs(owner)
119 if !found {
120 return []GameWorld{}
121 }
122 offset := ids.Size() - gameWorldListLimit
123 if offset < 0 {
124 offset = 0
125 }
126 result := []GameWorld{}
127 ids.IterateByOffset(offset, gameWorldListLimit, func(key string, _ any) bool {
128 result = append(result, GetGameWorld(parseUint64Key(key)))
129 return false
130 })
131 return result
132}
133
134func createGameWorld(owner address, gameType string) uint64 {
135 if latestGameWorldID == uint64(18446744073709551615) {
136 panic("game world id exhausted")
137 }
138 latestGameWorldID++
139 id := latestGameWorldID
140 gameWorlds.Set(uint64Key(id), GameWorld{
141 ID: id,
142 Owner: owner,
143 GameType: gameType,
144 Status: GameWorldStatusExists,
145 CreatedHeight: runtime.ChainHeight(),
146 })
147 addOwnerGameWorldID(owner, id)
148 return id
149}
150
151func assertCreationAllowed(gameType string, creator address, mode CreationMode) {
152 switch mode {
153 case CreationModeOperatorOnly:
154 if !admin.IsAdmin(creator) && !admin.IsOperator(creator) {
155 panic("game type creation is operator only")
156 }
157 case CreationModeApprovedCreator:
158 if !isApprovedCreator(gameType, creator) {
159 panic("creator is not approved for game type")
160 }
161 case CreationModePublicCreator:
162 return
163 default:
164 panic("invalid creation mode")
165 }
166}
167
168func assertExactUgnotPayment(expected int64) {
169 coins := unsafe.OriginSend()
170 if expected == 0 {
171 if len(coins) != 0 {
172 panic("exact payment required: expected no coins, got " + coins.String())
173 }
174 return
175 }
176 if len(coins) != 1 || coins[0].Denom != "ugnot" || coins[0].Amount != expected {
177 panic("exact payment required: expected " + strconv.FormatInt(expected, 10) + "ugnot, got " + coins.String())
178 }
179}
180
181func emitCreateGameWorld(owner address, id uint64, gameType string, creationCost int64) {
182 chain.Emit(
183 CreateGameWorldEvent,
184 "executor", owner.String(),
185 "id", strconv.FormatUint(id, 10),
186 "owner", owner.String(),
187 "gameType", gameType,
188 "creationCost", strconv.FormatInt(creationCost, 10),
189 "createdHeight", strconv.FormatInt(runtime.ChainHeight(), 10),
190 )
191}