Search Apps Documentation Source Content File Folder Download Copy Actions Download

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}