package personal_world import ( "chain" "chain/banker" "chain/runtime" "math/rand" "strconv" "strings" "gno.land/p/demo/entropy" "gno.land/p/nt/avl" "gno.land/p/nt/ufmt" ) const ( CreateWorldEvent = "CreateWorld" UpdateWorldEvent = "UpdateWorld" ExpandWorldEvent = "ExpandWorld" DeleteWorldEvent = "DeleteWorld" ) var ( nextWorldID uint32 = 1 worlds = avl.NewTree() // worldID -> map[string]string slugIndex = make(map[string]uint32) // slug -> worldID nameIndex = make(map[string]uint32) // name -> worldID ownerIndex = avl.NewTree() // owner -> map[uint32]bool ) func CreateWorld(cur realm, biomeName string, name string, slug string, seed int, isVisible bool, options string) uint32 { caller := validateUser() // Validate inputs validateBiomeName(biomeName) validateName(name) validateSlug(slug) validateSeed(seed) // Check uniqueness if _, found := nameIndex[name]; found { panic("name already exists") } if _, found := slugIndex[slug]; found { panic("slug already exists") } // Get creation info biomeInfo := mustGetBiomeInfo(biomeName) requiredCost, _ := strconv.ParseInt(biomeInfo["cost"], 10, 64) // Check if this is user's first world isFirstWorld := !ownerIndex.Has(caller.String()) // Check payment sent with transaction coinsSent := banker.OriginSend() amountUgnot := coinsSent.AmountOf("ugnot") finalRequiredCost := int64(0) // First world is free only if it's default biome if !isFirstWorld || biomeName != defaultBiome { finalRequiredCost = requiredCost } // Validate payment if amountUgnot < finalRequiredCost { panic(ufmt.Sprintf("insufficient payment: required %d, got %d", finalRequiredCost, amountUgnot)) } id := getNextWorldID() // Get default size defaultSizeInfo := mustGetSizeInfo(0) size, _ := strconv.Atoi(defaultSizeInfo["size"]) height := ufmt.Sprintf("%d", runtime.ChainHeight()) // Create world as map[string]string world := map[string]string{ "id": ufmt.Sprintf("%d", id), "owner": caller.String(), "name": name, "slug": slug, "sizeId": "0", "size": ufmt.Sprintf("%d", size), "seed": ufmt.Sprintf("%d", seed), "biome": biomeName, "isVisible": ufmt.Sprintf("%t", isVisible), "createdAt": height, "updatedAt": height, "totalPaid": ufmt.Sprintf("%d", finalRequiredCost), "options": options, } worlds.Set(worldIDToKey(id), world) slugIndex[slug] = id nameIndex[name] = id addToOwnerIndex(id, caller) // Distribute funds to operator and protocol (only if payment was made) var operatorShare, protocolShare int64 if finalRequiredCost > 0 { operatorShare, protocolShare = distributeFunds(finalRequiredCost) } // Refund excess payment var refundAmount int64 if amountUgnot > finalRequiredCost { refundAmount = amountUgnot - finalRequiredCost bnk := banker.NewBanker(banker.BankerTypeRealmSend) realmAddr := runtime.CurrentRealm().Address() refundCoins := chain.NewCoins(chain.NewCoin("ugnot", refundAmount)) bnk.SendCoins(realmAddr, caller, refundCoins) } // Emit event with distribution details chain.Emit( CreateWorldEvent, "id", ufmt.Sprintf("%d", id), "owner", caller.String(), "name", name, "slug", slug, "seed", ufmt.Sprintf("%d", seed), "isVisible", ufmt.Sprintf("%t", isVisible), "options", options, "isFirstWorld", ufmt.Sprintf("%t", isFirstWorld), "required", ufmt.Sprintf("%d", finalRequiredCost), "sent", ufmt.Sprintf("%d", amountUgnot), "refund", ufmt.Sprintf("%d", refundAmount), "operatorShare", ufmt.Sprintf("%d", operatorShare), "protocolShare", ufmt.Sprintf("%d", protocolShare), ) return id } func UpdateWorld(cur realm, worldID uint32, name string, slug string, isVisible bool, options string) { caller := validateUser() // Get world properties world := mustGetWorld(worldID) // Check world:update permission (owner always has this) if !HasPermission(worldID, caller, "world:update") { panic("caller must be admin or world owner to update world " + ufmt.Sprintf("%d", worldID)) } // Get current values oldName := world["name"] oldSlug := world["slug"] if len(name) != 0 && name != oldName { validateName(name) if _, found := nameIndex[name]; found { panic("name already exists") } delete(nameIndex, oldName) nameIndex[name] = worldID world["name"] = name } if len(slug) != 0 && slug != oldSlug { validateSlug(slug) if _, found := slugIndex[slug]; found { panic("slug already exists") } delete(slugIndex, oldSlug) slugIndex[slug] = worldID world["slug"] = slug } world["isVisible"] = ufmt.Sprintf("%t", isVisible) // Update options (empty string is allowed to reset options) world["options"] = options world["updatedAt"] = ufmt.Sprintf("%d", runtime.ChainHeight()) // Emit event chain.Emit( UpdateWorldEvent, "id", ufmt.Sprintf("%d", worldID), "name", name, "slug", slug, "options", options, ) } func ExpandWorld(cur realm, worldID uint32) { caller := validateUser() world := mustGetWorld(worldID) // Check world:expand permission (owner always has this) if !HasPermission(worldID, caller, "world:expand") { panic("caller must be admin or world owner to expand world " + ufmt.Sprintf("%d", worldID)) } // Get current size ID oldSizeID, _ := strconv.Atoi(world["sizeId"]) newSizeID := oldSizeID + 1 // Get new size info newSizeInfo := mustGetSizeInfo(newSizeID) newSizeCost, _ := strconv.ParseInt(newSizeInfo["cost"], 10, 64) // Get biome info biomeInfo := mustGetBiomeInfo(world["biome"]) multiple, _ := strconv.ParseFloat(biomeInfo["priceMultiplier"], 64) expansionCost := int64(float64(newSizeCost) * multiple) coinsSent := banker.OriginSend() amountUgnot := coinsSent.AmountOf("ugnot") if amountUgnot < expansionCost { panic(ufmt.Sprintf("insufficient payment: required %d, got %d", expansionCost, amountUgnot)) } // Get old totalPaid oldTotalPaid, _ := strconv.ParseInt(world["totalPaid"], 10, 64) // Update properties newTotalPaid := oldTotalPaid + expansionCost world["sizeId"] = ufmt.Sprintf("%d", newSizeID) world["size"] = newSizeInfo["size"] world["totalPaid"] = ufmt.Sprintf("%d", newTotalPaid) world["updatedAt"] = ufmt.Sprintf("%d", runtime.ChainHeight()) // Distribute funds operatorShare, protocolShare := distributeFunds(expansionCost) // Refund excess payment var refundAmount int64 if amountUgnot > expansionCost { refundAmount = amountUgnot - expansionCost bnk := banker.NewBanker(banker.BankerTypeRealmSend) realmAddr := runtime.CurrentRealm().Address() refundCoins := chain.NewCoins(chain.NewCoin("ugnot", refundAmount)) bnk.SendCoins(realmAddr, caller, refundCoins) } chain.Emit( ExpandWorldEvent, "id", ufmt.Sprintf("%d", worldID), "oldSizeID", ufmt.Sprintf("%d", oldSizeID), "newSizeID", ufmt.Sprintf("%d", newSizeID), "required", ufmt.Sprintf("%d", expansionCost), "sent", ufmt.Sprintf("%d", amountUgnot), "refund", ufmt.Sprintf("%d", refundAmount), "totalPaid", ufmt.Sprintf("%d", newTotalPaid), "operatorShare", ufmt.Sprintf("%d", operatorShare), "protocolShare", ufmt.Sprintf("%d", protocolShare), ) } func DeleteWorld(cur realm, worldID uint32) { caller := validateUser() assertIsAdminOrOperator(caller) // Get world properties world := mustGetWorld(worldID) // Remove from all indexes worlds.Remove(worldIDToKey(worldID)) delete(slugIndex, world["slug"]) delete(nameIndex, world["name"]) removeFromOwnerIndex(worldID, address(world["owner"])) // Remove chunk verifiers deleteWorldChunkVerifiers(worldID) // Remove all role assignments for this world delete(worldAuthorization, worldID) // Emit event chain.Emit( DeleteWorldEvent, "id", ufmt.Sprintf("%d", worldID), "caller", caller.String(), "slug", world["slug"], "name", world["name"], ) } func GetWorld(worldID uint32) map[string]string { return mustGetWorld(worldID) } func GetWorldBySlug(slug string) map[string]string { worldID, found := slugIndex[slug] if !found { panic("world not found by slug: " + slug) } return mustGetWorld(worldID) } func GetTotalWorldSize() int { return worlds.Size() } func IsNameAvailable(name string) bool { _, found := nameIndex[name] return !found } func IsSlugAvailable(slug string) bool { _, found := slugIndex[slug] return !found } func ListWorlds(page, count int) []map[string]string { if page < 1 { panic("page must be at least 1") } if count < 1 { panic("count must be at least 1") } if count > listLimit { panic("count exceeds listLimit") } var results []map[string]string offset := (page - 1) * count worlds.IterateByOffset(offset, count, func(_ string, value any) bool { results = append(results, value.(map[string]string)) return false }) return results } func GetWorldSizeByOwner(owner address) int { ownerKey := owner.String() worldIDs, found := ownerIndex.Get(ownerKey) if !found { return 0 } return len(worldIDs.(map[uint32]bool)) } func ListWorldsByOwner(owner address, page, count int) []map[string]string { if page < 1 { panic("page must be at least 1") } if count < 1 { panic("count must be at least 1") } if count > listLimit { panic("count exceeds listLimit") } ownerKey := owner.String() worldIDsVal, found := ownerIndex.Get(ownerKey) if !found { return []map[string]string{} } worldIDs := worldIDsVal.(map[uint32]bool) offset := (page - 1) * count if offset >= len(worldIDs) { return []map[string]string{} } // Convert map keys to slice for pagination var ids []uint32 for id := range worldIDs { ids = append(ids, id) } end := offset + count if end > len(ids) { end = len(ids) } var results []map[string]string for _, worldID := range ids[offset:end] { if world, found := getWorld(worldID); found { results = append(results, world) } } return results } func ListRandomWorlds(count int) []map[string]string { if count < 1 { panic("count must be at least 1") } if count > listLimit { panic("count exceeds listLimit") } if nextWorldID <= 1 { return []map[string]string{} } e := entropy.New() seed := uint64(e.Seed()) r := rand.New(rand.NewPCG(seed, 0xdeadbeef)) var results []map[string]string seen := make(map[uint32]bool) for i := 0; i < count; i++ { // Random ID in range [1, nextWorldID) id := uint32(r.IntN(int(nextWorldID-1))) + 1 if seen[id] { continue } seen[id] = true world, found := getWorld(id) if !found { continue } if world["isVisible"] != "true" { continue } results = append(results, world) } return results } // worldIDToKey converts a world ID to a padded string key for AVL tree func worldIDToKey(worldID uint32) string { s := strconv.FormatUint(uint64(worldID), 10) // Pad to 10 digits for proper AVL tree sorting for len(s) < 10 { s = "0" + s } return s } // getNextWorldID generates next world ID func getNextWorldID() uint32 { id := nextWorldID nextWorldID++ return id } // addToOwnerIndex adds world ID to owner's index func addToOwnerIndex(worldID uint32, owner address) { ownerKey := owner.String() existingIDs, found := ownerIndex.Get(ownerKey) if !found { ownerIndex.Set(ownerKey, map[uint32]bool{worldID: true}) return } worldIDs := existingIDs.(map[uint32]bool) worldIDs[worldID] = true } // removeFromOwnerIndex removes world ID from owner's index func removeFromOwnerIndex(worldID uint32, owner address) { ownerKey := owner.String() existingIDs, found := ownerIndex.Get(ownerKey) if !found { return } worldIDs := existingIDs.(map[uint32]bool) delete(worldIDs, worldID) if len(worldIDs) == 0 { ownerIndex.Remove(ownerKey) } } // mustGetWorld gets world properties with panic on not found func mustGetWorld(worldID uint32) map[string]string { properties, found := worlds.Get(worldIDToKey(worldID)) if !found { panic("world not found: " + ufmt.Sprintf("%d", worldID)) } return properties.(map[string]string) } // getWorld gets world properties without panic func getWorld(worldID uint32) (map[string]string, bool) { properties, found := worlds.Get(worldIDToKey(worldID)) if !found { return nil, false } return properties.(map[string]string), true } // validateName validates name format func validateName(name string) { // Empty check if len(name) == 0 { panic("name cannot be empty") } // Length check (allow longer than slug for unicode characters) if len(name) > 100 { panic("name too long (max 100)") } // Check if name is all whitespace (must be before trim check) trimmed := strings.TrimSpace(name) if len(trimmed) == 0 { panic("name cannot be only whitespace") } // Check for leading/trailing whitespace if name != trimmed { panic("name cannot have leading or trailing whitespace") } // Check for control characters (0x00-0x1F) for _, ch := range name { if ch < 0x20 { panic("name cannot contain control characters") } } } // validateSlug validates slug format func validateSlug(slug string) { if len(slug) == 0 { panic("slug cannot be empty") } if len(slug) > 50 { panic("slug too long (max 50)") } // Allowed characters: a-z, 0-9, hyphen, underscore for _, ch := range slug { if !((ch >= 'a' && ch <= 'z') || (ch >= '0' && ch <= '9') || ch == '-' || ch == '_') { panic("slug contains invalid characters (allowed: a-z, 0-9, -, _)") } } // Cannot start or end with hyphen if slug[0] == '-' || slug[len(slug)-1] == '-' { panic("slug cannot start or end with hyphen") } } // validateSeed validates seed value func validateSeed(seed int) { if seed <= 0 { panic("seed must be positive") } }