Search Apps Documentation Source Content File Folder Download Copy Actions Download

render.gno

11.22 Kb · 374 lines
  1package personal_world
  2
  3import (
  4	"net/url"
  5	"strconv"
  6	"strings"
  7
  8	"gno.land/p/jeronimoalbi/pager"
  9	"gno.land/r/akkadia/v0/admin"
 10)
 11
 12// Render handles RESTful routing and returns Markdown responses
 13func Render(iUrl string) string {
 14	assertMigrationStateAvailable()
 15	u, err := url.Parse(iUrl)
 16	if err != nil {
 17		return "404\n"
 18	}
 19
 20	query := u.Query()
 21
 22	switch u.Path {
 23	case "":
 24		return renderHome()
 25	case "world":
 26		idStr := query.Get("id")
 27		if idStr == "" {
 28			return renderError("World ID required")
 29		}
 30		return renderWorld(iUrl, idStr)
 31	case "worlds":
 32		owner := query.Get("owner")
 33		if owner != "" {
 34			return renderWorldsByOwner(iUrl, owner)
 35		}
 36		return renderWorldsList(iUrl)
 37	case "biomes":
 38		return renderBiomes()
 39	case "sizes":
 40		return renderSizes()
 41	default:
 42		return renderError("Not found")
 43	}
 44}
 45
 46// renderHome renders the home page
 47func renderHome() string {
 48	output := `# Personal World
 49
 50Welcome to the Personal World system on Akkadia.
 51
 52## What is Personal World?
 53
 54Personal World is a decentralized world creation and management system where users can:
 55
 56* Create and customize their own worlds
 57* Manage world editors and permissions
 58* Expand world sizes dynamically
 59* Set custom world types and properties
 60
 61## Getting Started
 62
 631. **Create Your First World**: World creation costs the selected biome price.
 642. **Customize**: Set name, slug and visibility
 653. **Manage**: Add editors, expand size, update properties
 664. **Explore**: Browse all worlds and discover unique creations
 67
 68## Config
 69
 70`
 71	metadata := worldConfigStore
 72	worlds := worldStore
 73	output += "* **Default Biome**: " + metadata.DefaultBiome() + "\n"
 74	output += "* **Fee Collector BPS**: " + strconv.Itoa(feeCollectorBPS) + " (" + strconv.Itoa(feeCollectorBPS/100) + "." + strconv.Itoa(feeCollectorBPS%100) + "%)\n"
 75	output += "* **List Limit**: " + strconv.Itoa(listLimit) + "\n"
 76	output += "* **Batch Limit**: " + strconv.Itoa(batchLimit) + "\n"
 77
 78	output += `
 79## Stats
 80
 81`
 82	output += "* **Total Worlds**: " + strconv.Itoa(worlds.Total()) + "\n"
 83
 84	output += `
 85## Quick Links
 86
 87* [Browse All Worlds](./personal_world:worlds)
 88* [View Biomes](./personal_world:biomes)
 89* [View Sizes](./personal_world:sizes)
 90
 91## Search by Owner
 92
 93<gno-form path="worlds">
 94  <gno-input name="owner" placeholder="Enter owner address" />
 95</gno-form>
 96`
 97
 98	return output
 99}
100
101// renderBiomes renders the biomes page
102func renderBiomes() string {
103	output := `# Biomes
104
105[← Home](./personal_world:)
106
107The system supports multiple biomes with different creation costs.
108
109## How Pricing Works
110
111* **cost**: The base cost to create a world with this biome
112* **priceMultiplierBPS**: Multiplier applied to world expansion costs, where 10000 means 1x
113  * Example: If expansion base cost is 1000 ugnot and priceMultiplierBPS is 15000, actual cost = 1500 ugnot
114
115---
116
117`
118	metadata := worldConfigStore
119	defaultBiome := metadata.DefaultBiome()
120	for _, biome := range metadata.ListBiomeInfos() {
121		output += "### " + escapeMarkdown(biome["name"]) + "\n\n"
122
123		for k, v := range biome {
124			if k == "name" {
125				if v == defaultBiome {
126					output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + " (default)\n"
127				} else {
128					output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + "\n"
129				}
130			} else if k == "cost" {
131				output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + " ugnot\n"
132			} else {
133				output += "* **" + escapeMarkdown(k) + "**: " + escapeMarkdown(v) + "\n"
134			}
135		}
136		output += "\n---\n\n"
137	}
138
139	return output
140}
141
142// renderSizes renders the sizes page
143func renderSizes() string {
144	output := `# World Sizes
145
146[← Home](./personal_world:)
147
148Worlds can be expanded to larger sizes. Each expansion has a base cost.
149
150## How Expansion Works
151
152* **size**: The world dimension (size x size blocks)
153* **cost**: Base cost to expand to this size level
154* Actual expansion cost = base cost × biome priceMultiplierBPS / 10000
155* New worlds start at size level 0
156
157---
158
159`
160	for _, size := range worldConfigStore.ListSizeInfos() {
161		output += "### Level " + size["id"] + "\n\n"
162		output += "* **Size**: " + escapeMarkdown(size["size"]) + " × " + escapeMarkdown(size["size"]) + " blocks\n"
163		output += "* **Base Cost**: " + escapeMarkdown(size["cost"]) + " ugnot\n"
164		output += "\n---\n\n"
165	}
166
167	return output
168}
169
170// renderWorldsList renders a list of all worlds with pagination
171func renderWorldsList(iUrl string) string {
172	output := `# All Worlds
173
174[← Home](./personal_world:)
175
176`
177
178	worlds := worldStore
179	totalSize := worlds.Total()
180	if totalSize == 0 {
181		output += "*No worlds created yet.*\n"
182		return output
183	}
184
185	pages, err := pager.New(iUrl, totalSize, pager.WithPageSize(10))
186	if err != nil {
187		return renderError("Invalid page")
188	}
189
190	// Get current page number for back links
191	currentPage := (pages.Offset() / pages.PageSize()) + 1
192
193	for _, worldID := range worlds.ListIDs(currentPage, pages.PageSize()) {
194		if world, found := worlds.Get(worldID); found {
195			output += renderWorldItem(world, currentPage)
196		}
197	}
198
199	if pages.HasPages() {
200		output += pager.Picker(pages)
201	}
202
203	return output
204}
205
206// renderWorldsByOwner renders worlds owned by a specific address with pagination
207func renderWorldsByOwner(iUrl string, owner string) string {
208	u, _ := url.Parse(iUrl)
209	page := u.Query().Get("page")
210	ownerAddr := address(owner)
211	if !ownerAddr.IsValid() {
212		return renderError("Invalid owner address")
213	}
214	owner = ownerAddr.String()
215
216	output := "# Personal Worlds by " + escapeMarkdown(owner) + "\n\n"
217	if page != "" {
218		output += "[← Back to List](./personal_world:worlds?page=" + url.QueryEscape(page) + ")\n\n"
219	} else {
220		output += "[← Home](./personal_world:)\n\n"
221	}
222
223	totalOwnerWorlds := GetWorldSizeByOwner(ownerAddr)
224	if totalOwnerWorlds == 0 {
225		output += "*No worlds found for this owner.*\n"
226		return output
227	}
228
229	pages, err := pager.New(iUrl, totalOwnerWorlds, pager.WithPageSize(10))
230	if err != nil {
231		return renderError("Invalid page")
232	}
233
234	currentPage := (pages.Offset() / pages.PageSize()) + 1
235	ownerWorldIDs := worldStore.ListIDsByOwner(ownerAddr, currentPage, pages.PageSize())
236
237	for _, worldID := range ownerWorldIDs {
238		if world, found := worldStore.Get(worldID); found {
239			output += renderWorldItem(world, 0)
240		}
241	}
242
243	if pages.HasPages() {
244		output += pager.Picker(pages)
245	}
246
247	return output
248}
249
250// renderWorldItem renders a single world item
251func renderWorldItem(world map[string]string, page int) string {
252	explorerURL := admin.GetExplorerURL()
253	worldID := world["id"]
254	worldName := escapeMarkdown(world["name"])
255	worldOwner := escapeMarkdown(world["owner"])
256	worldOwnerParam := url.QueryEscape(world["owner"])
257	worldSlugParam := url.QueryEscape(world["slug"])
258	output := ""
259	output += "* **ID**: " + escapeMarkdown(worldID) + "\n"
260	if page > 0 {
261		pageStr := strconv.Itoa(page)
262		output += "* **Name**: [" + worldName + "](./personal_world:world?id=" + url.QueryEscape(worldID) + "&page=" + pageStr + ")\n"
263		output += "* **Owner**: " + worldOwner + " [[View on Explorer](" + explorerURL + "/m/explorer/player?address=" + worldOwnerParam + ")] [[Worlds by Owner](./personal_world:worlds?owner=" + worldOwnerParam + "&page=" + pageStr + ")]\n"
264	} else {
265		output += "* **Name**: [" + worldName + "](./personal_world:world?id=" + url.QueryEscape(worldID) + ")\n"
266		output += "* **Owner**: " + worldOwner + " [[View on Explorer](" + explorerURL + "/m/explorer/player?address=" + worldOwnerParam + ")] [[Worlds by Owner](./personal_world:worlds?owner=" + worldOwnerParam + ")]\n"
267	}
268	output += "* **Biome**: " + escapeMarkdown(world["biome"]) + "\n"
269	output += "* **Size**: " + escapeMarkdown(world["size"]) + "\n"
270	output += "* **Slug**: `" + escapeCodeSpan(world["slug"]) + "` [[View on Explorer](" + explorerURL + "/m/explorer/community-realm?slug=" + worldSlugParam + ")]\n"
271	output += "\n---\n\n"
272	return output
273}
274
275// renderWorld renders a single world detail page
276func renderWorld(iUrl string, idStr string) string {
277	worldID64, err := strconv.ParseUint(idStr, 10, 32)
278	if err != nil {
279		return renderError("Invalid world ID: " + idStr)
280	}
281	worldID := uint32(worldID64)
282
283	world, found := worldStore.Get(worldID)
284	if !found {
285		return renderError("World not found: " + idStr)
286	}
287
288	u, _ := url.Parse(iUrl)
289	pageParam := u.Query().Get("page")
290
291	backLink := "./personal_world:worlds"
292	if pageParam != "" {
293		backLink += "?page=" + pageParam
294	}
295
296	explorerURL := admin.GetExplorerURL()
297
298	output := "# World " + escapeMarkdown(world["name"]) + " (#" + escapeMarkdown(world["id"]) + ")\n\n"
299	output += "[← All Worlds](" + backLink + ")\n"
300	output += "[[View on Explorer](" + explorerURL + "/m/explorer/community-realm?slug=" + url.QueryEscape(world["slug"]) + ")]\n\n"
301
302	output += "## Info\n\n"
303	output += "* **ID**: " + escapeMarkdown(world["id"]) + "\n"
304	output += "* **Name**: " + escapeMarkdown(world["name"]) + "\n"
305	output += "* **Slug**: `" + escapeCodeSpan(world["slug"]) + "`\n"
306	output += "* **Owner**: " + escapeMarkdown(world["owner"]) + " [[View on Explorer](" + explorerURL + "/m/explorer/player?address=" + url.QueryEscape(world["owner"]) + ")] [[Worlds by Owner](./personal_world:worlds?owner=" + url.QueryEscape(world["owner"]) + ")]\n"
307	output += "* **Biome**: " + escapeMarkdown(world["biome"]) + "\n"
308	output += "* **Size**: " + escapeMarkdown(world["size"]) + " (Level " + escapeMarkdown(world["sizeId"]) + ")\n"
309	output += "* **Seed**: " + escapeMarkdown(world["seed"]) + "\n"
310	output += "* **Visible**: " + escapeMarkdown(world["isVisible"]) + "\n"
311	output += "* **Total Paid**: " + escapeMarkdown(world["totalPaid"]) + " ugnot\n"
312	output += "* **Created**: block #" + escapeMarkdown(world["createdAt"]) + "\n"
313	output += "* **Updated**: block #" + escapeMarkdown(world["updatedAt"]) + "\n"
314
315	output += "\n## Roles\n\n"
316	worldKey := formatWorldID(worldID)
317	roles := personalWorldAuthzRBAC.ListRoles(1, listLimit)
318	hasRoles := false
319	for _, role := range roles {
320		users := personalWorldAuthzRBAC.ListRoleUsers(worldKey, role.Name, 1, listLimit)
321		if len(users) != 0 {
322			hasRoles = true
323			break
324		}
325	}
326	if hasRoles {
327		output += "| Address | Roles |\n"
328		output += "|---------|-------|\n"
329		for _, role := range roles {
330			users := personalWorldAuthzRBAC.ListRoleUsers(worldKey, role.Name, 1, listLimit)
331			for _, user := range users {
332				output += "| " + escapeMarkdown(user.String()) + " | " + escapeMarkdown(role.Name) + " |\n"
333			}
334		}
335	} else {
336		output += "*No roles assigned.*\n"
337	}
338
339	return output
340}
341
342// renderError renders an error page
343func renderError(message string) string {
344	output := "# Error\n\n"
345	output += "> " + escapeMarkdown(message) + "\n\n"
346	output += "[← Back to Home](./personal_world:)\n"
347	return output
348}
349
350func escapeMarkdown(s string) string {
351	s = strings.ReplaceAll(s, "\\", "\\\\")
352	s = strings.ReplaceAll(s, "\n", "\\n")
353	s = strings.ReplaceAll(s, "`", "\\`")
354	s = strings.ReplaceAll(s, "*", "\\*")
355	s = strings.ReplaceAll(s, "_", "\\_")
356	s = strings.ReplaceAll(s, "{", "\\{")
357	s = strings.ReplaceAll(s, "}", "\\}")
358	s = strings.ReplaceAll(s, "[", "\\[")
359	s = strings.ReplaceAll(s, "]", "\\]")
360	s = strings.ReplaceAll(s, "(", "\\(")
361	s = strings.ReplaceAll(s, ")", "\\)")
362	s = strings.ReplaceAll(s, "#", "\\#")
363	s = strings.ReplaceAll(s, "+", "\\+")
364	s = strings.ReplaceAll(s, "-", "\\-")
365	s = strings.ReplaceAll(s, ".", "\\.")
366	s = strings.ReplaceAll(s, "!", "\\!")
367	s = strings.ReplaceAll(s, "|", "\\|")
368	s = strings.ReplaceAll(s, ">", "\\>")
369	return s
370}
371
372func escapeCodeSpan(s string) string {
373	return strings.ReplaceAll(s, "`", "\\`")
374}