personal_world.gno
7.96 Kb · 302 lines
1package personal_world
2
3import (
4 "chain"
5 "chain/banker"
6 "chain/runtime"
7 "chain/runtime/unsafe"
8 "strconv"
9
10 "gno.land/p/akkadia/v0/accesscontrol"
11 "gno.land/r/akkadia/v0/admin"
12)
13
14const (
15 CreateWorldEvent = "CreateWorld"
16 UpdateWorldEvent = "UpdateWorld"
17 ExpandWorldEvent = "ExpandWorld"
18 DeleteWorldEvent = "DeleteWorld"
19 SetWorldMetadataEvent = "SetWorldMetadata"
20)
21
22func CreateWorld(cur realm, propKeys string, propValues string) uint32 {
23 assertNotFrozen()
24 caller := accesscontrol.MustGetUserCaller(0, cur)
25
26 props := parsePropertiesCSV(propKeys, propValues)
27 worldCreateValidator.MustValidate(props)
28
29 biomeName := props["biome"]
30 name := props["name"]
31 slug := props["slug"]
32 seed := mustParseWorldSeed(props["seed"])
33
34 assertBiomeName(biomeName)
35 assertBiomeExists(biomeName)
36 assertWorldName(name)
37 assertWorldSlug(slug)
38
39 if !worldStore.IsNameAvailable(name) {
40 panic("name already exists")
41 }
42 if !worldStore.IsSlugAvailable(slug) {
43 panic("slug already exists")
44 }
45 // MustGetUserCaller guarantees a direct user call, so OriginSend cannot be borrowed through an intermediate realm.
46 payment := unsafe.OriginSend().AmountOf("ugnot")
47 cost, ok := creationCost(biomeName)
48 if !ok {
49 panic("invalid creation cost")
50 }
51 assertExactPayment(cost, payment)
52
53 sizeInfo := worldConfigStore.MustGetSizeInfo("0")
54 heightStr := strconv.FormatInt(runtime.ChainHeight(), 10)
55
56 props["owner"] = caller.String()
57 props["sizeId"] = sizeInfo["id"]
58 props["size"] = sizeInfo["size"]
59 props["seed"] = strconv.Itoa(seed)
60 props["isVisible"] = strconv.FormatBool(true)
61 props["createdAt"] = heightStr
62 props["updatedAt"] = heightStr
63 props["totalPaid"] = strconv.FormatInt(cost, 10)
64
65 id := worldStore.Create(props)
66 idStr := formatWorldID(id)
67 feeCollectorShare, protocolShare := distribute(cur, cost)
68
69 // Emit event with distribution details
70 chain.Emit(
71 CreateWorldEvent,
72 "id", idStr,
73 "owner", props["owner"],
74 "name", props["name"],
75 "slug", props["slug"],
76 "seed", props["seed"],
77 "cost", strconv.FormatInt(cost, 10),
78 "feeCollectorShare", strconv.FormatInt(feeCollectorShare, 10),
79 "protocolShare", strconv.FormatInt(protocolShare, 10),
80 )
81
82 return id
83}
84
85func UpdateWorld(cur realm, worldID uint32, propKeys string, propValues string) {
86 assertNotFrozen()
87 caller := accesscontrol.MustGetUserCaller(0, cur)
88 if !HasUpdatePermission(worldID, caller) {
89 panic("caller must be admin or world owner to update world " + formatWorldID(worldID))
90 }
91
92 props := parsePropertiesCSV(propKeys, propValues)
93 worldUpdateValidator.MustValidate(props)
94 worldStore.AssertWorldExists(worldID)
95
96 if name, found := props["name"]; found {
97 assertWorldName(name)
98 }
99 if slug, found := props["slug"]; found {
100 assertWorldSlug(slug)
101 }
102
103 heightStr := strconv.FormatInt(runtime.ChainHeight(), 10)
104 props["updatedAt"] = heightStr
105
106 updated := worldStore.Update(worldID, props)
107
108 chain.Emit(
109 UpdateWorldEvent,
110 "id", updated["id"],
111 "name", updated["name"],
112 "slug", updated["slug"],
113 )
114}
115
116func ExpandWorld(cur realm, worldID uint32) {
117 assertNotFrozen()
118 caller := accesscontrol.MustGetUserCaller(0, cur)
119 if !HasExpandPermission(worldID, caller) {
120 panic("caller must be admin or world owner to expand world " + formatWorldID(worldID))
121 }
122
123 world := worldStore.MustGet(worldID)
124 oldSizeID := world["sizeId"]
125
126 cost, ok := expansionCost(oldSizeID, world["biome"])
127 if !ok {
128 panic("invalid expansion cost")
129 }
130 // MustGetUserCaller guarantees a direct user call, so OriginSend cannot be borrowed through an intermediate realm.
131 payment := unsafe.OriginSend().AmountOf("ugnot")
132 assertExactPayment(cost, payment)
133
134 nextSizeInfo := mustGetNextSizeInfo(oldSizeID)
135 totalPaid := calculateTotalPaid(world, cost)
136 heightStr := strconv.FormatInt(runtime.ChainHeight(), 10)
137 updates := map[string]string{
138 "sizeId": nextSizeInfo["id"],
139 "size": nextSizeInfo["size"],
140 "totalPaid": totalPaid,
141 "updatedAt": heightStr,
142 }
143
144 worldStore.Update(worldID, updates)
145 feeCollectorShare, protocolShare := distribute(cur, cost)
146
147 chain.Emit(
148 ExpandWorldEvent,
149 "id", world["id"],
150 "oldSizeID", oldSizeID,
151 "newSizeID", nextSizeInfo["id"],
152 "required", strconv.FormatInt(cost, 10),
153 "totalPaid", totalPaid,
154 "feeCollectorShare", strconv.FormatInt(feeCollectorShare, 10),
155 "protocolShare", strconv.FormatInt(protocolShare, 10),
156 )
157}
158
159func calculateTotalPaid(world map[string]string, cost int64) string {
160 oldTotalPaid, _ := strconv.ParseInt(world["totalPaid"], 10, 64)
161 totalPaid := safeAdd(oldTotalPaid, cost, "total paid overflow")
162 return strconv.FormatInt(totalPaid, 10)
163}
164
165func DeleteWorld(cur realm, worldID uint32) {
166 assertNotFrozen()
167 accesscontrol.AssertIsAdminOrOperator(0, cur, admin.IsAdmin, admin.IsOperator)
168
169 worldStore.Delete(worldID)
170
171 // Remove chunk verifiers
172 verifierStore.Delete(worldID)
173
174 // Remove all role assignments for this world
175 cleanupWorldAuthorization(worldID)
176
177 // Emit event
178 chain.Emit(
179 DeleteWorldEvent,
180 "id", formatWorldID(worldID),
181 )
182}
183
184func SetWorldMetadata(cur realm, worldID uint32, metadata string) {
185 assertNotFrozen()
186 accesscontrol.AssertIsAdmin(0, cur, admin.IsAdmin)
187 worldStore.AssertWorldExists(worldID)
188 assertWorldMetadata(metadata)
189 worldStore.SetMetadata(worldID, metadata)
190
191 chain.Emit(
192 SetWorldMetadataEvent,
193 "id", formatWorldID(worldID),
194 )
195}
196
197func GetWorld(worldID uint32) map[string]string {
198 assertMigrationStateAvailable()
199 return worldStore.MustGet(worldID)
200}
201
202func GetWorldMetadata(worldID uint32) string {
203 assertMigrationStateAvailable()
204 return worldStore.GetMetadata(worldID)
205}
206
207func ListWorldMetadataByIDs(worldIDs ...uint32) []string {
208 assertMigrationStateAvailable()
209 assertListLimit("worldIDs", len(worldIDs))
210 return worldStore.ListMetadataByIDs(worldIDs...)
211}
212
213func GetWorldBySlug(slug string) map[string]string {
214 assertMigrationStateAvailable()
215 worldID := worldStore.MustGetIDBySlug(slug)
216 return worldStore.MustGet(worldID)
217}
218
219func GetWorldIDBySlug(slug string) uint32 {
220 assertMigrationStateAvailable()
221 return worldStore.MustGetIDBySlug(slug)
222}
223
224func GetWorldIDByName(name string) uint32 {
225 assertMigrationStateAvailable()
226 return worldStore.MustGetIDByName(name)
227}
228
229func GetTotalWorldSize() int {
230 assertMigrationStateAvailable()
231 return worldStore.Total()
232}
233
234func IsNameAvailable(name string) bool {
235 assertMigrationStateAvailable()
236 return worldStore.IsNameAvailable(name)
237}
238
239func IsSlugAvailable(slug string) bool {
240 assertMigrationStateAvailable()
241 return worldStore.IsSlugAvailable(slug)
242}
243
244func ListWorldIDs(page, count int) []uint32 {
245 assertMigrationStateAvailable()
246 assertListPageCount(page, count)
247 return worldStore.ListIDs(page, count)
248}
249
250func ListWorldsByIDs(worldIDs ...uint32) []map[string]string {
251 assertMigrationStateAvailable()
252 assertListLimit("worldIDs", len(worldIDs))
253
254 if len(worldIDs) == 0 {
255 return []map[string]string{}
256 }
257
258 return worldStore.ListByIDs(worldIDs...)
259}
260
261func GetWorldSizeByOwner(owner address) int {
262 assertMigrationStateAvailable()
263 return worldStore.OwnerCount(owner)
264}
265
266func ListWorldIDsByOwner(owner address, page int, count int) []uint32 {
267 assertMigrationStateAvailable()
268 assertListPageCount(page, count)
269 return worldStore.ListIDsByOwner(owner, page, count)
270}
271
272func assertExactPayment(expected int64, actual int64) {
273 if actual != expected {
274 panic("exact payment required: expected " + strconv.FormatInt(expected, 10) + ", got " + strconv.FormatInt(actual, 10))
275 }
276}
277
278func distribute(cur realm, cost int64) (int64, int64) {
279 if cost <= 0 {
280 return 0, 0
281 }
282
283 feeCollectorShare, protocolShare := calculateBPSShares(cost, int64(feeCollectorBPS))
284 if feeCollectorShare > 0 {
285 send(cur, admin.GetFeeCollector(), feeCollectorShare)
286 }
287 if protocolShare > 0 {
288 send(cur, admin.GetProtocol(), protocolShare)
289 }
290 return feeCollectorShare, protocolShare
291}
292
293func send(cur realm, to address, amount int64) {
294 if amount <= 0 {
295 panic("amount must be greater than 0")
296 }
297 // Send funds to feeCollector and protocol
298 bnk := banker.NewBanker(banker.BankerTypeRealmSend, cur)
299 realmAddr := cur.Address()
300 coins := chain.Coins{chain.Coin{"ugnot", amount}}
301 bnk.SendCoins(realmAddr, to, coins)
302}