Kind Realm (renderable)
Name craft
Namespace akkadia / beta
Exported functions 14
Module gno.land/r/akkadia/beta/craft
gno 0.9 validation.gno gno
⧉
1 package craft 2 3 import ( 4 "net/url" 5 "strconv" 6 ) 7 8 Craft (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, recipe string) uint64
DeleteRecipe (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, recipe string)
DeleteRecipes (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, recipesCSV string)
GetCraftFee () int64
GetListLimit () int
GetRecipe (recipe string) uint32
GetRecipeCount () int
HasRecipe (recipe string) bool
ListRecipes (page int, count int) []map[string]string
Render (iURL string) string
SetCraftFee (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, fee int64)
SetListLimit (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, limit int)
SetRecipe (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, recipe string, itemID uint32)
SetRecipes (cur interface {.seal func(); Address func() .uverse.address; IsCode func() bool; IsCurrent func() bool; IsEphemeral func() bool; IsUser func() bool; IsUserCall func() bool; IsUserRun func() bool; PkgPath func() string; Previous func() .uverse.realm; String func() string; Sub func(string) .uverse.realm; Subpath func() string}, recipesCSV string, itemIDsCSV string)
Signatures reconstructed verbatim from vm/qfuncs — interface params keep their inline definitions.
vm/qrender output, sanitized (docs/render-security.md) and displayed in an empty-sandbox iframe — scripts, forms and popups cannot run. Links stay inert in-preview; right-click to open.
const
(
9 recipeBatchLimit = 100
10 maxRecipeIngredients = 16
11 defaultRecipeListLimit = 100
12 defaultCraftFee int64 = 1_000_000
13 )
14
15 type craftIngredient struct {
16 Slot uint
17 TokenID string
18 Amount int64
19 }
20
21 func mustParseRecipeCSV(recipesCSV, itemIDsCSV string ) ([] string , [] uint32 ) {
22 if recipesCSV == "" {
23 panic( "recipes is required" )
24 }
25 if itemIDsCSV == "" {
26 panic( "item ids is required" )
27 }
28
29 canonicalRecipes := [] string {}
30 itemIDs := [] uint32 {}
31 recipeStart, recipeEnd := 0 , 0
32 itemIDStart, itemIDEnd := 0 , 0
33 for {
34 for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
35 recipeEnd++
36 }
37 for itemIDEnd < len(itemIDsCSV) && itemIDsCSV[itemIDEnd] != ',' {
38 itemIDEnd++
39 }
40
41 recipeToken := recipesCSV[recipeStart:recipeEnd]
42 itemIDText := itemIDsCSV[itemIDStart:itemIDEnd]
43 if recipeToken == "" {
44 panic( "empty recipe not allowed" )
45 }
46 if itemIDText == "" {
47 panic( "empty item id not allowed" )
48 }
49
50 canonicalRecipe, _ := mustParseRecipe(recipeToken)
51 itemIDValue, err := strconv.ParseUint(itemIDText, 10 , 32 )
52 if err != nil {
53 panic( "invalid item id: " + itemIDText)
54 }
55
56 canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
57 itemIDs = append(itemIDs, uint32 (itemIDValue))
58 if len(canonicalRecipes) > recipeBatchLimit {
59 panic( "recipes exceeds 100" )
60 }
61
62 recipeDone := recipeEnd >= len(recipesCSV)
63 itemIDDone := itemIDEnd >= len(itemIDsCSV)
64 if recipeDone != itemIDDone {
65 panic( "recipes and item ids count mismatch" )
66 }
67 if recipeDone {
68 return canonicalRecipes, itemIDs
69 }
70
71 recipeEnd++
72 itemIDEnd++
73 recipeStart = recipeEnd
74 itemIDStart = itemIDEnd
75 }
76 }
77
78 func mustParseRecipeKeyCSV(recipesCSV string ) [] string {
79 if recipesCSV == "" {
80 panic( "recipes is required" )
81 }
82
83 canonicalRecipes := [] string {}
84 recipeStart, recipeEnd := 0 , 0
85 for {
86 for recipeEnd < len(recipesCSV) && recipesCSV[recipeEnd] != ',' {
87 recipeEnd++
88 }
89
90 recipeToken := recipesCSV[recipeStart:recipeEnd]
91 if recipeToken == "" {
92 panic( "empty recipe not allowed" )
93 }
94
95 canonicalRecipe, _ := mustParseRecipe(recipeToken)
96 canonicalRecipes = append(canonicalRecipes, canonicalRecipe)
97 if len(canonicalRecipes) > recipeBatchLimit {
98 panic( "recipes exceeds 100" )
99 }
100
101 if recipeEnd >= len(recipesCSV) {
102 return canonicalRecipes
103 }
104
105 recipeEnd++
106 recipeStart = recipeEnd
107 }
108 }
109
110 func mustParseRecipe(recipe string ) ( string , []craftIngredient) {
111 canonical, ingredients, errMsg := parseRecipe(recipe)
112 if errMsg != "" {
113 panic(errMsg)
114 }
115 return canonical, ingredients
116 }
117
118 // A valid recipe is "<4-hex occupancy>|<ingredient>|...". Occupancy bits
119 // choose slots 0-15 in order; each ingredient is "tokenId:amount" or
120 // "slot:tokenId:amount". Example: "0013|iron:1|iron:2|stick:2" (bits 0,1,4)
121 // canonicalizes to "0013|0:iron:1|1:iron:2|4:stick:2".
122 func parseRecipe(recipe string ) ( string , []craftIngredient, string ) {
123 if len(recipe) < 7 || recipe[ 4 ] != '|' {
124 return "" , nil , "invalid recipe"
125 }
126
127 occupancyValue, err := strconv.ParseUint(recipe[: 4 ], 16 , 16 )
128 if err != nil || occupancyValue == 0 {
129 return "" , nil , "invalid recipe occupancy"
130 }
131 occupancy := uint16 (occupancyValue)
132 slots := occupiedSlots(occupancy)
133
134 ingredients := []craftIngredient{}
135 entryStart, entryEnd := 5 , 5
136 for {
137 for entryEnd < len(recipe) && recipe[entryEnd] != '|' {
138 entryEnd++
139 }
140
141 if len(ingredients) == len(slots) {
142 return "" , nil , "occupied slots and ingredients count mismatch"
143 }
144
145 ingredient, errMsg := parseRecipeIngredient(
146 recipe[entryStart:entryEnd],
147 slots[len(ingredients)],
148 )
149 if errMsg != "" {
150 return "" , nil , errMsg
151 }
152 ingredients = append(ingredients, ingredient)
153 if len(ingredients) > maxRecipeIngredients {
154 return "" , nil , "recipe ingredients exceeds 16"
155 }
156
157 if entryEnd >= len(recipe) {
158 break
159 }
160
161 entryEnd++
162 entryStart = entryEnd
163 }
164
165 if len(ingredients) != len(slots) {
166 return "" , nil , "occupied slots and ingredients count mismatch"
167 }
168
169 canonicalRecipe := formatOccupancy(occupancy)
170 for _, ingredient := range ingredients {
171 canonicalRecipe += "|" +
172 strconv.FormatUint( uint64 (ingredient.Slot), 10 ) + ":" +
173 url.PathEscape(ingredient.TokenID) + ":" +
174 strconv.FormatInt(ingredient.Amount, 10 )
175 }
176 return canonicalRecipe, ingredients, ""
177 }
178
179 func parseRecipeIngredient(entry string , expectedSlot uint ) (craftIngredient, string ) {
180 amountSeparator := len(entry) - 1
181 for amountSeparator > 0 && entry[amountSeparator] != ':' {
182 amountSeparator--
183 }
184 if amountSeparator == 0 || entry[amountSeparator] != ':' {
185 return craftIngredient{}, "invalid recipe ingredient"
186 }
187
188 tokenField := entry[:amountSeparator]
189 amountText := entry[amountSeparator+ 1 :]
190 encodedTokenID := tokenField
191 slotPrefixEnd := 0
192 for slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] != ':' {
193 slotPrefixEnd++
194 }
195 if slotPrefixEnd < len(tokenField) && tokenField[slotPrefixEnd] == ':' {
196 slotValue, err := strconv.ParseUint(tokenField[:slotPrefixEnd], 10 , 8 )
197 if err == nil {
198 if uint (slotValue) != expectedSlot {
199 return craftIngredient{}, "ingredient slot mismatch"
200 }
201 encodedTokenID = tokenField[slotPrefixEnd+ 1 :]
202 }
203 }
204
205 tokenID, err := url.PathUnescape(encodedTokenID)
206 if err != nil || tokenID == "" {
207 return craftIngredient{}, "invalid item token id"
208 }
209
210 amount, err := strconv.ParseInt(amountText, 10 , 64 )
211 if err != nil {
212 return craftIngredient{}, "invalid ingredient amount: " + amountText
213 }
214 if amount <= 0 {
215 return craftIngredient{}, "ingredient amount must be positive"
216 }
217
218 return craftIngredient{
219 Slot: expectedSlot,
220 TokenID: tokenID,
221 Amount: amount,
222 }, ""
223 }
224
225 func occupiedSlots(occupancy uint16 ) [] uint {
226 slots := [] uint {}
227 for slot := uint ( 0 ); slot < 16 ; slot++ {
228 if occupancy&( uint16 ( 1 )<<slot) != 0 {
229 slots = append(slots, slot)
230 }
231 }
232 return slots
233 }
234
235 func formatOccupancy(occupancy uint16 ) string {
236 value := strconv.FormatUint( uint64 (occupancy), 16 )
237 return "0000" [: 4 -len(value)] + value
238 }
239
240 func assertRecipeListPageCount(page, count int ) {
241 if page < 1 {
242 panic( "page must be at least 1" )
243 }
244 if count < 1 {
245 panic( "count must be at least 1" )
246 }
247 if count > listLimit {
248 panic( "count exceeds listLimit (max: " + strconv.Itoa(listLimit) + ")" )
249 }
250 }
251