render.gno
12.36 Kb · 408 lines
1package chunk
2
3import (
4 "net/url"
5 "strconv"
6 "strings"
7
8 "gno.land/p/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/grc721"
9 "gno.land/p/jeronimoalbi/pager"
10 "gno.land/r/g1nqnrt3aldzhu6zzeg75yw97wvavqy7wr77g56q/deploy-test/v2/admin"
11)
12
13const renderChunkPageSize = 10
14
15// Render
16func Render(iUrl string) string {
17 assertMigrationStateAvailable()
18 u, err := url.Parse(iUrl)
19 if err != nil {
20 return "404\n"
21 }
22
23 path := u.Path
24 query := u.Query()
25
26 switch {
27 case path == "":
28 return renderHome()
29 case path == "chunk":
30 id := query.Get("id")
31 if id == "" {
32 return renderChunkForm()
33 }
34 return renderChunk(iUrl, id)
35 case path == "worlds":
36 return renderWorldsList(iUrl)
37 case strings.HasPrefix(path, "worlds/") && strings.HasSuffix(path, "/chunks"):
38 worldIDStr := strings.TrimPrefix(path, "worlds/")
39 worldIDStr = strings.TrimSuffix(worldIDStr, "/chunks")
40 worldID, err := strconv.ParseUint(worldIDStr, 10, 32)
41 if err != nil {
42 return "404\n"
43 }
44 owner := query.Get("owner")
45 if owner != "" {
46 return renderChunksByOwner(iUrl, uint32(worldID), owner)
47 }
48 return renderChunksByWorld(iUrl, uint32(worldID))
49 case strings.HasPrefix(path, "worlds/"):
50 worldIDStr := strings.TrimPrefix(path, "worlds/")
51 worldID, err := strconv.ParseUint(worldIDStr, 10, 32)
52 if err != nil {
53 return "404\n"
54 }
55 return renderWorld(iUrl, uint32(worldID))
56 default:
57 return "404\n"
58 }
59}
60
61func renderHome() string {
62 output := `# Chunk
63
64Welcome to the Chunk management system on Akkadia.
65
66## What is Chunk?
67
68Chunk is an NFT-based land ownership system where each chunk represents a unique piece of land in the game world:
69
70* **GRC721 NFT**: Each chunk is a unique token with ownership and transferability
71* **World-based**: Chunks belong to specific worlds with coordinates (x, y)
72* **Metadata Storage**: Each chunk stores custom metadata for game state
73* **Role-based Access**: Permissions control who can mint, burn, and modify chunks
74
75## How Chunks Work
76
771. **World Creation**: Admins create worlds with biome, name, slug, and seed
782. **Chunk Minting**: Authorized users mint chunks at specific coordinates
793. **Metadata Management**: Chunk owners can update metadata for game state
804. **Ownership Transfer**: Chunks can be transferred between users as NFTs
81
82## Config
83
84`
85 output += "* **List Limit**: " + strconv.Itoa(listLimit) + "\n"
86 output += "* **Batch Limit**: " + strconv.Itoa(batchLimit) + "\n"
87
88 output += `
89## Stats
90
91`
92 output += "* **Total Chunks**: " + strconv.FormatInt(TokenCount(), 10) + "\n"
93 output += "* **Total Worlds**: " + strconv.Itoa(GetTotalWorldSize()) + "\n"
94
95 output += `
96## Quick Links
97
98* [Browse Worlds](./chunk:worlds)
99* [Search Chunk by ID](./chunk:chunk)
100`
101
102 return output
103}
104
105func renderChunkForm() string {
106 return `# View Chunk
107
108[← Home](./chunk:)
109
110<gno-form path="chunk">
111 <gno-input name="id" placeholder="Enter chunk ID (e.g., 1:0_0)" />
112</gno-form>
113`
114}
115
116func renderChunk(iUrl string, id string) string {
117 if !isValidRenderChunkID(id) {
118 if worldID, ok := parseWorldIDLookup(id); ok {
119 if _, found := worldStore.Get(worldID); found {
120 return renderWorldIDLookup(id)
121 }
122 }
123 return "# Chunk Not Found\n\n[← Home](./chunk:)\n\nChunk `" + id + "` does not exist.\n"
124 }
125
126 tokenID := grc721.TokenID(id)
127 meta := GetChunkMetadata(string(tokenID))
128 if meta == nil {
129 return "# Chunk Not Found\n\n[← Home](./chunk:)\n\nChunk `" + id + "` does not exist.\n"
130 }
131
132 u, _ := url.Parse(iUrl)
133 query := u.Query()
134 pageParam := query.Get("page")
135 ownerParam := query.Get("owner")
136
137 worldIDStr := meta["worldId"]
138
139 output := "# Chunk " + id + "\n\n"
140 output += "[[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/chunk?id=" + id + ")]\n\n"
141
142 backLink := "./chunk:worlds/" + worldIDStr + "/chunks"
143 if ownerParam != "" {
144 backLink += "?owner=" + ownerParam
145 if pageParam != "" {
146 backLink += "&page=" + pageParam
147 }
148 } else if pageParam != "" {
149 backLink += "?page=" + pageParam
150 }
151 output += "[← Back to World " + worldIDStr + " Chunks](" + backLink + ")\n\n"
152
153 owner, found := nftStore.OwnerOfSafe(tokenID)
154 if found {
155 ownerStr := owner.String()
156 output += "* **Owner**: [" + ownerStr + "](./chunk:worlds/" + worldIDStr + "/chunks?owner=" + ownerStr + ") [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + ownerStr + ")]\n"
157 } else {
158 output += "* **Owner**: unavailable\n"
159 }
160 output += "* **World**: [" + worldIDStr + "](./chunk:worlds/" + worldIDStr + ")\n"
161 output += "* **Coordinates**: (" + meta["x"] + ", " + meta["y"] + ")\n"
162 output += "* **World Type**: " + meta["worldType"] + "\n"
163 output += "* **Hash**: " + meta["hash"] + "\n"
164
165 return output
166}
167
168func isValidRenderChunkID(id string) bool {
169 parts := strings.SplitN(id, ":", 2)
170 if len(parts) != 2 {
171 return false
172 }
173
174 if _, err := strconv.ParseUint(parts[0], 10, 32); err != nil {
175 return false
176 }
177
178 coords := strings.SplitN(parts[1], "_", 2)
179 if len(coords) != 2 {
180 return false
181 }
182
183 if _, err := strconv.Atoi(coords[0]); err != nil {
184 return false
185 }
186 if _, err := strconv.Atoi(coords[1]); err != nil {
187 return false
188 }
189 return true
190}
191
192func parseWorldIDLookup(id string) (uint32, bool) {
193 worldID, err := strconv.ParseUint(id, 10, 32)
194 if err != nil {
195 return 0, false
196 }
197 return uint32(worldID), true
198}
199
200func renderWorldIDLookup(id string) string {
201 output := "# World ID Entered\n\n"
202 output += "[← Home](./chunk:)\n\n"
203 output += "`" + id + "` is a world ID, not a chunk ID. Chunk IDs use `worldID:x_y` format.\n\n"
204 output += "* [View World " + id + "](./chunk:worlds/" + id + ")\n"
205 output += "* [View World " + id + " Chunks](./chunk:worlds/" + id + "/chunks)\n"
206 return output
207}
208
209// renderWorld renders a single system world detail page
210func renderWorld(iUrl string, worldID uint32) string {
211 world, found := worldStore.Get(worldID)
212 if !found {
213 return "# World Not Found\n\n[← All Worlds](../chunk:worlds)\n\nWorld `" + formatWorldID(worldID) + "` does not exist.\n"
214 }
215
216 worldIDStr := world["id"]
217 output := "# World " + world["name"] + " (#" + worldIDStr + ")\n\n"
218 output += "[← All Worlds](../chunk:worlds)\n"
219 output += "[[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/primary-realm?slug=" + world["slug"] + ")]\n\n"
220
221 output += "## Info\n\n"
222 output += "* **ID**: " + worldIDStr + "\n"
223 output += "* **Name**: " + world["name"] + "\n"
224 output += "* **Slug**: `" + world["slug"] + "` [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/primary-realm?slug=" + world["slug"] + ")]\n"
225 output += "* **Biome**: " + world["biome"] + "\n"
226 output += "* **Seed**: " + world["seed"] + "\n"
227 output += "* **Created**: block #" + world["createdAt"] + "\n"
228 output += "* **Updated**: block #" + world["updatedAt"] + "\n"
229 propCount := len(world) - 7
230 if propCount > 0 {
231 output += "* **Properties**: " + strconv.Itoa(propCount) + "\n"
232 }
233
234 chunkCount := GetChunkMetadataSizeByWorld(worldID)
235 output += "\n## Chunks\n\n* **Total**: " + strconv.Itoa(chunkCount) + " [[View All Chunks](../chunk:worlds/" + worldIDStr + "/chunks)]\n"
236
237 return output
238}
239
240// renderChunksByWorld renders chunks belonging to a specific world with pagination
241func renderChunksByWorld(iUrl string, worldID uint32) string {
242 worldIDStr := formatWorldID(worldID)
243 output := "# Chunks in World " + worldIDStr + "\n\n"
244 output += "[← World " + worldIDStr + "](../../chunk:worlds/" + worldIDStr + ")\n\n"
245
246 chunkCount := GetChunkMetadataSizeByWorld(worldID)
247 if chunkCount == 0 {
248 output += "*No chunks found in this world.*\n"
249 return output
250 }
251
252 output += "**Total Chunks**: " + strconv.Itoa(chunkCount) + "\n\n"
253
254 u, err := url.Parse(iUrl)
255 if err != nil {
256 return output + "Invalid page"
257 }
258 page := 1
259 pageStr := u.Query().Get("page")
260 if pageStr != "" {
261 parsedPage, err := strconv.Atoi(pageStr)
262 if err != nil || parsedPage < 1 {
263 return output + "Invalid page"
264 }
265 page = parsedPage
266 }
267
268 chunks := ListChunkMetadataByWorld(worldID, page, renderChunkPageSize)
269
270 for _, chunk := range chunks {
271 key := chunk["id"]
272 chunkLink := "../../chunk:chunk?id=" + key + "&worldId=" + worldIDStr
273 if page > 1 {
274 chunkLink += "&page=" + strconv.Itoa(page)
275 }
276 output += "### [" + key + "](" + chunkLink + ") [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/chunk?id=" + key + ")]\n\n"
277 owner, found := nftStore.OwnerOfSafe(grc721.TokenID(key))
278 if found {
279 ownerStr := owner.String()
280 output += "* **Owner**: " + ownerStr + " [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/player?address=" + ownerStr + ")] [[Chunks by Owner](../../chunk:worlds/" + worldIDStr + "/chunks?owner=" + ownerStr + ")]\n"
281 } else {
282 output += "* **Owner**: unavailable\n"
283 }
284 for k, v := range chunk {
285 output += "* **" + k + "**: " + v + "\n"
286 }
287 output += "\n---\n\n"
288 }
289
290 if len(chunks) > 0 {
291 nav := []string{}
292 if page > 1 {
293 nav = append(nav, "[← Prev "+strconv.Itoa(renderChunkPageSize)+"](../../chunk:worlds/"+worldIDStr+"/chunks?page="+strconv.Itoa(page-1)+")")
294 }
295 nextPage := ListChunkMetadataByWorld(worldID, page+1, 1)
296 if len(nextPage) > 0 {
297 nav = append(nav, "[Next "+strconv.Itoa(renderChunkPageSize)+" →](../../chunk:worlds/"+worldIDStr+"/chunks?page="+strconv.Itoa(page+1)+")")
298 }
299 if len(nav) > 0 {
300 output += strings.Join(nav, " | ") + "\n"
301 }
302 }
303
304 return output
305}
306
307// renderChunksByOwner renders chunks owned by a specific address in a world
308func renderChunksByOwner(iUrl string, worldID uint32, owner string) string {
309 worldIDStr := formatWorldID(worldID)
310 output := "# Chunks by " + owner + " in World " + worldIDStr + "\n\n"
311 output += "[← World " + worldIDStr + " Chunks](../../chunk:worlds/" + worldIDStr + "/chunks)\n\n"
312
313 ownerAddr := address(owner)
314 totalCount := GetOwnerTokenSize(worldID, ownerAddr)
315
316 if totalCount == 0 {
317 output += "*No chunks found for this owner in this world.*\n"
318 return output
319 }
320
321 output += "**Total Chunks**: " + strconv.Itoa(totalCount) + "\n\n"
322
323 u, err := url.Parse(iUrl)
324 if err != nil {
325 return output + "Invalid page"
326 }
327 page := 1
328 pageStr := u.Query().Get("page")
329 if pageStr != "" {
330 parsedPage, err := strconv.Atoi(pageStr)
331 if err != nil || parsedPage < 1 {
332 return output + "Invalid page"
333 }
334 page = parsedPage
335 }
336
337 chunkKeys := ListChunkKeysByOwner(worldID, ownerAddr, page, renderChunkPageSize)
338 for _, key := range chunkKeys {
339 meta := GetChunkMetadata(key)
340 if meta == nil {
341 continue
342 }
343 chunkLink := "../../chunk:chunk?id=" + key + "&worldId=" + worldIDStr + "&owner=" + owner
344 if page > 1 {
345 chunkLink += "&page=" + strconv.Itoa(page)
346 }
347 output += "### [" + key + "](" + chunkLink + ") [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/chunk?id=" + key + ")]\n\n"
348 for k, v := range meta {
349 output += "* **" + k + "**: " + v + "\n"
350 }
351 output += "\n---\n\n"
352 }
353
354 if len(chunkKeys) > 0 {
355 nav := []string{}
356 if page > 1 {
357 nav = append(nav, "[← Prev "+strconv.Itoa(renderChunkPageSize)+"](../../chunk:worlds/"+worldIDStr+"/chunks?owner="+owner+"&page="+strconv.Itoa(page-1)+")")
358 }
359 nextPage := ListChunkKeysByOwner(worldID, ownerAddr, page+1, 1)
360 if len(nextPage) > 0 {
361 nav = append(nav, "[Next "+strconv.Itoa(renderChunkPageSize)+" →](../../chunk:worlds/"+worldIDStr+"/chunks?owner="+owner+"&page="+strconv.Itoa(page+1)+")")
362 }
363 if len(nav) > 0 {
364 output += strings.Join(nav, " | ") + "\n"
365 }
366 }
367
368 return output
369}
370
371// renderWorldsList renders a list of all worlds with pagination
372func renderWorldsList(iUrl string) string {
373 output := `# All System Worlds
374
375[← Home](./chunk:)
376
377`
378 if GetTotalWorldSize() == 0 {
379 output += "*No worlds created yet.*\n"
380 return output
381 }
382
383 pages, err := pager.New(iUrl, GetTotalWorldSize(), pager.WithPageSize(10))
384 if err != nil {
385 return output + "Invalid page"
386 }
387
388 worlds := worldStore.List((pages.Offset()/pages.PageSize())+1, pages.PageSize())
389 for _, world := range worlds {
390 worldIDStr := world["id"]
391 worldID, _ := strconv.ParseUint(worldIDStr, 10, 32)
392 chunkCount := GetChunkMetadataSizeByWorld(uint32(worldID))
393
394 output += "* **ID**: " + worldIDStr + "\n"
395 output += "* **Name**: [" + world["name"] + "](./chunk:worlds/" + worldIDStr + ")\n"
396 output += "* **Biome**: " + world["biome"] + "\n"
397 output += "* **Seed**: " + world["seed"] + "\n"
398 output += "* **Slug**: `" + world["slug"] + "` [[View on Explorer](" + admin.GetExplorerURL() + "/m/explorer/primary-realm?slug=" + world["slug"] + ")]\n"
399 output += "* **Chunks**: " + strconv.Itoa(chunkCount) + " [[View Chunks](./chunk:worlds/" + worldIDStr + "/chunks)]\n"
400 output += "\n---\n\n"
401 }
402
403 if pages.HasPages() {
404 output += pager.Picker(pages)
405 }
406
407 return output
408}