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}