Search Apps Documentation Source Content File Folder Download Copy Actions Download

world.gno

5.18 Kb ยท 246 lines
  1package chunk
  2
  3import (
  4	"chain"
  5	"chain/runtime"
  6	"strconv"
  7
  8	"gno.land/p/nt/avl"
  9	"gno.land/p/nt/ufmt"
 10)
 11
 12const (
 13	CreateWorldEvent      = "CreateWorld"
 14	UpdateWorldEvent      = "UpdateWorld"
 15	SetWorldPropertyEvent = "SetWorldProperty"
 16)
 17
 18var (
 19	worlds    = avl.NewTree()            // worldID -> map[string]string
 20	slugIndex = make(map[string]uint32)  // slug -> worldID
 21	nameIndex = make(map[string]uint32)  // name -> worldID
 22)
 23
 24func CreateWorld(cur realm, id uint32, biomeName, name, slug string, seed int, options string) {
 25	caller := validateUser()
 26	assertIsAdmin(caller)
 27
 28	validateWorldID(id)
 29	validateBiomeName(biomeName)
 30	validateName(name)
 31	validateSlug(slug)
 32	validateSeed(seed)
 33
 34	// Check uniqueness
 35	if _, found := worlds.Get(worldIDToKey(id)); found {
 36		panic("world already exists: " + ufmt.Sprintf("%d", id))
 37	}
 38
 39	if _, found := nameIndex[name]; found {
 40		panic("name already exists: " + name)
 41	}
 42
 43	if _, found := slugIndex[slug]; found {
 44		panic("slug already exists: " + slug)
 45	}
 46
 47	height := ufmt.Sprintf("%d", runtime.ChainHeight())
 48
 49	world := map[string]string{
 50		"id":        ufmt.Sprintf("%d", id),
 51		"name":      name,
 52		"slug":      slug,
 53		"seed":      ufmt.Sprintf("%d", seed),
 54		"biome":     biomeName,
 55		"createdAt": height,
 56		"updatedAt": height,
 57		"options":   options,
 58	}
 59
 60	worlds.Set(worldIDToKey(id), world)
 61	slugIndex[slug] = id
 62	nameIndex[name] = id
 63
 64	chain.Emit(
 65		CreateWorldEvent,
 66		"id", ufmt.Sprintf("%d", id),
 67		"biome", biomeName,
 68		"name", name,
 69		"slug", slug,
 70		"seed", ufmt.Sprintf("%d", seed),
 71		"options", options,
 72	)
 73}
 74
 75func UpdateWorld(cur realm, worldID uint32, biomeName, name, slug, options string) {
 76	caller := validateUser()
 77	assertIsAdmin(caller)
 78
 79	world := mustGetWorld(worldID)
 80
 81	odlBiomeName := world["biome"]
 82	oldName := world["name"]
 83	oldSlug := world["slug"]
 84
 85	if len(biomeName) != 0 && biomeName != odlBiomeName {
 86		validateBiomeName(biomeName)
 87		world["biome"] = biomeName
 88	}
 89
 90	if len(name) != 0 && name != oldName {
 91		validateName(name)
 92		if _, found := nameIndex[name]; found {
 93			panic("name already exists: " + name)
 94		}
 95		delete(nameIndex, oldName)
 96		nameIndex[name] = worldID
 97		world["name"] = name
 98	}
 99
100	if len(slug) != 0 && slug != oldSlug {
101		validateSlug(slug)
102		if _, found := slugIndex[slug]; found {
103			panic("slug already exists: " + slug)
104		}
105		delete(slugIndex, oldSlug)
106		slugIndex[slug] = worldID
107		world["slug"] = slug
108	}
109
110	world["options"] = options
111	world["updatedAt"] = ufmt.Sprintf("%d", runtime.ChainHeight())
112
113	// Emit event
114	chain.Emit(
115		UpdateWorldEvent,
116		"id", ufmt.Sprintf("%d", worldID),
117		"biome", biomeName,
118		"name", name,
119		"slug", slug,
120		"options", options,
121	)
122}
123
124func GetWorld(worldID uint32) map[string]string {
125	return mustGetWorld(worldID)
126}
127
128func GetWorldBySlug(slug string) map[string]string {
129	worldID, found := slugIndex[slug]
130	if !found {
131		panic("world not found by slug: " + slug)
132	}
133	return mustGetWorld(worldID)
134}
135
136func GetTotalWorldSize() int {
137	return worlds.Size()
138}
139
140func ListWorlds(page, count int) []map[string]string {
141	if page < 1 {
142		panic("page must be at least 1")
143	}
144	if count < 1 {
145		panic("count must be at least 1")
146	}
147	if count > listLimit {
148		panic("count exceeds listLimit")
149	}
150
151	var result []map[string]string
152	offset := (page - 1) * count
153	worlds.IterateByOffset(offset, count, func(_ string, value any) bool {
154		result = append(result, value.(map[string]string))
155		return false
156	})
157	return result
158}
159
160func SetWorldProperty(cur realm, worldID uint32, key string, value string) {
161	caller := validateUser()
162	assertIsAdmin(caller)
163
164	if key == "id" || key == "seed" || key == "createdAt" || key == "updatedAt" {
165		panic("reserved key: " + key)
166	}
167
168	world := mustGetWorld(worldID)
169	world[key] = value
170
171	chain.Emit(
172		SetWorldPropertyEvent,
173		"worldID", ufmt.Sprintf("%d", worldID),
174		"key", key,
175		"value", value,
176	)
177}
178
179func mustGetWorld(worldID uint32) map[string]string {
180	value, found := worlds.Get(worldIDToKey(worldID))
181	if !found {
182		panic("world not found: " + ufmt.Sprintf("%d", worldID))
183	}
184	return value.(map[string]string)
185}
186
187// worldIDToKey converts a world ID to a padded string key for AVL tree
188func worldIDToKey(worldID uint32) string {
189	s := strconv.FormatUint(uint64(worldID), 10)
190	// Pad to 10 digits for proper AVL tree sorting
191	for len(s) < 10 {
192		s = "0" + s
193	}
194	return s
195}
196
197func validateBiomeName(biomeName string) {
198	if len(biomeName) == 0 {
199		panic("biome name cannot be empty")
200	}
201}
202
203func validateName(name string) {
204	if len(name) == 0 {
205		panic("name cannot be empty")
206	}
207	if len(name) > 100 {
208		panic("name too long (max 100)")
209	}
210}
211
212func validateSlug(slug string) {
213	if len(slug) == 0 {
214		panic("slug cannot be empty")
215	}
216
217	if len(slug) > 50 {
218		panic("slug too long (max 50)")
219	}
220
221	// Allowed characters: a-z, 0-9, hyphen, underscore
222	for _, ch := range slug {
223		if !((ch >= 'a' && ch <= 'z') ||
224			(ch >= '0' && ch <= '9') ||
225			ch == '-' || ch == '_') {
226			panic("slug contains invalid characters (allowed: a-z, 0-9, -, _)")
227		}
228	}
229
230	// Cannot start or end with hyphen
231	if slug[0] == '-' || slug[len(slug)-1] == '-' {
232		panic("slug cannot start or end with hyphen")
233	}
234}
235
236func validateSeed(seed int) {
237	if seed <= 0 {
238		panic("seed must be positive")
239	}
240}
241
242func validateWorldID(id uint32) {
243	if id == 0 {
244		panic("world id must be positive")
245	}
246}