Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

12.27 Kb · 408 lines
  1package chunk
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/akkadia/v0/grc721"
  9	"gno.land/p/jeronimoalbi/pager"
 10	"gno.land/r/akkadia/v0/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}