Skip to content Search Apps Documentation Source Content File Folder Download Copy Actions State String Boolean Number Struct Map Slice Pointer Function Closure Reference Nil Package Type Interface Unknown

Community realm. Read the code first. Community realm, deployed by its author. Read the code before you interact or send coins.

garden.gno

140.69 Kb · 2882 lines
   1// Package garden is Gnotchi's pet realm, v4: garden/v3 plus a permanent face.
   2//
   3// WHY THIS EXISTS, in one paragraph, because a realm cannot be edited and the reason has to
   4// travel with the code. garden/v2 accepts `plant:0:<ts>` and `harvest:0:<ts>`, enforces the
   5// grow timer, moves the acorns -- and exposes no way to read a plot back. Its whole garden
   6// is write-only on chain. The app therefore hides the garden from any player who connects a
   7// wallet (app/js/home.js gates the garden card on `!gnome.chainProof`), and since harvesting
   8// is the only acorn income, a connected player's acorns can only ever go down. The live site
   9// now apologises for this in six places (app/js/config.js's WALLET_TRADEOFF_SENTENCE). There
  10// is no fix that is not a new realm. GardenState below is that fix; everything else here is
  11// either the same correction applied to eight more unreadable fields, or deliberately
  12// unchanged from v2.
  13//
  14// Full design, including every v2 limitation with line numbers, the migration argument, and
  15// the list of things deliberately left out: realms/garden/v3/DESIGN.md, which this realm
  16// inherits whole, and realms/garden/v4/DESIGN.md, which records only what v4 changes and why.
  17// Read both before changing anything here.
  18//
  19// v4 IS v3 PLUS A PERMANENT FACE, AND THREE SMALL REPAIRS. garden/v3 was deployed to onyx at
  20// block 336496 on 2026-10-11 and nobody adopted on it -- TotalGnomes() read 0 on 2026-10-12 --
  21// so replacing it costs the deploy gas and nothing else: no migration, no gnome to move. What
  22// changes: every gnome carries two more PERMANENT appearance ids, eyes and nose, each a small
  23// integer validated against a compiled-in range and written exactly once, at Adopt. That is
  24// SPRITES.md's "Phase 1 -- faces" (ten eyes, eight noses: sixteen looks become 1,280), which
  25// v3's DESIGN.md section 7.3 had argued away by applying a correct argument against OPEN-ENDED
  26// appearance to a closed list nobody had asked to be open. The realm records what it is given
  27// and validates it; whether a face is CHOSEN by the player or ROLLED by the app is deliberately
  28// not decided here, because a realm cannot be edited and that decision should not be frozen in
  29// one (DESIGN.md v4 section 3). The repairs, each cheap only while nobody has adopted: the
  30// acorn softlock AUDIT-DEPLOYED.md calls G9 is made unreachable by an acorn reserve in SaveDay;
  31// a reserved buyer typed in upper case no longer creates a listing nobody can buy; and the
  32// test-only fee override is gone from production code. Everything else -- the SetRule ratchet,
  33// the garden-settles-to-seller rule, the IsUserCall gates on Adopt and Buy, the sale, the wire
  34// format, every read -- is v3's, unchanged, with v3's reasoning kept in place below.
  35//
  36// GNOMES ARE TRADEABLE HERE, AND THE SALE COUNTER IS THE ONLY DOOR. TRADING-RULES.md records
  37// the owner's ruling of 2026-10-10 -- "gnomes AND items are tradeable, and a cut on every
  38// trade of both is a must" -- and MARKETPLACE-V1.md records the structural fact that decides
  39// the shape: a royalty implemented as a transfer hook sees transfers, not payments, so the
  40// only way to make the cut unavoidable is for the contract holding the asset to refuse every
  41// transfer path except its own sale function. That is what this realm does. There is no
  42// Approve, no TransferFrom, no gift, no burn, no withdrawal; a registered extension panics on
  43// every ledger movement unless Buy below is moving exactly that token right now. See the
  44// SALE section and DESIGN.md section 9. The fee and its recipient are the three SALE
  45// CONSTANTS at the very top of this file, decided by the owner on 2026-10-11 for the onyx
  46// testnet; init still gates them and refuses a value that could not work.
  47//
  48// DEPLOY STATUS IS DELIBERATELY NOT RECORDED IN THIS FILE, and neither is a claim about
  49// whether anyone has decided to deploy it. This comment ships on chain exactly as written
  50// and cannot be edited afterwards, so any status asserted here is frozen at authoring time
  51// and becomes a lie the moment it changes. garden/v2/gnomod.toml said "NOT deployed" in just
  52// such a line, was deployed hours later, and now carries that false claim on chain
  53// permanently. Live status lives in the repo root's STATUS.md and DECISIONS.md, which can
  54// still be corrected; the authority above both is the chain itself (`vm/qfile` on this path).
  55//
  56// ============================================================================
  57// THE ONE RULE THAT REPLACES v2's MISTAKE
  58// ============================================================================
  59//
  60// v2's bug class is not "a getter was forgotten". It is that a field was added to persisted
  61// state without a reader, on a contract that can never grow one. So:
  62//
  63//	EVERY PERSISTED FIELD HAS AN EXPORTED READER, AT DEPLOY TIME. THERE IS NO SECOND CHANCE.
  64//
  65// Anyone adding a field to `gnome` below must add it to a reader in the same change.
  66// TestEveryStoredFieldIsReadable pins the current field list against the current readers,
  67// value by value, so the failure is at least loud and local. Gno has no reflection to assert
  68// over, so that test cannot *detect* a new field -- the rule is still a discipline and this
  69// comment is not pretending otherwise.
  70//
  71// The corollary, which is why this file has no "extensible" blob getter: EXTENSIBILITY IS A
  72// FICTION ON AN IMMUTABLE CONTRACT. A versioned self-describing string cannot carry a field
  73// that did not exist at deploy time any more than a positional tuple can. This surface is
  74// designed for completeness now, not for growth later, because there is no later.
  75//
  76// ============================================================================
  77// SECURITY: NO recover(), EVER. Carried over from v1/v2/hollowmere/arena verbatim in intent.
  78// ============================================================================
  79//
  80// No code in this realm, now or later, may recover() around a call into ledger or token
  81// (grc721's PrivateLedger/Token). The sale-gate guarantee below is real, but it is NOT that
  82// the ledger write never happens -- it does, before the panic -- it is that an *uncaught*
  83// panic aborts the whole enclosing transaction, so that write (and everything else the
  84// transaction did) is discarded and never committed on chain. p/nt/grc721/v0/types.gno says
  85// this outright about extension hooks like saleGateExtension below: "Hooks run after the
  86// ledger write so they observe the post-movement state; only a caller that recovers from the
  87// panic keeps that write." A recover() anywhere between a ledger call and the transaction
  88// boundary -- even one added for an unrelated reason, even several call-frames away -- would
  89// silently convert every refused transfer into a permanent, successful one.
  90//
  91// This is not limited to code textually inside this file: a pure `p/` package this realm
  92// calls runs in garden's own call frame too, so a helper as innocuous as
  93// `Try(func(){ ledger.TransferFrom(...) })` imported from some future `p/` dependency would
  94// break the same guarantee just as surely as a recover() written here directly.
  95//
  96// The same rule applies to SaveDay's, ChooseType's and Buy's validate-then-commit pattern
  97// below: none recovers from its own internal panics, by design. Every check runs against a
  98// local value copy and nothing is written back until the whole call has validated; a panic
  99// before the commit leaves persisted state completely untouched, with no partial effect to
 100// undo. Buy extends the rule to money: the ledger write, the index writes and the two coin
 101// sends all happen after the last check that can fail, so a sale either happens whole or not
 102// at all -- there is no reachable state in which the gnome moved and the coins did not, or
 103// the reverse.
 104package garden
 105
 106import (
 107	"strconv"
 108	"strings"
 109	"time"
 110	"unicode"
 111	"unicode/utf8"
 112
 113	"gno.land/p/nt/avl/v0"
 114	"gno.land/p/nt/grc721/v0"
 115	"gno.land/p/nt/seqid/v0"
 116	"gno.land/p/nt/ufmt/v0"
 117
 118	"chain"
 119	"chain/banker"
 120	"chain/runtime/unsafe"
 121)
 122
 123// ============================================================================
 124// THE THREE SALE CONSTANTS. THE OWNER DECIDED THESE ON 2026-10-11 ("Go, yes proceed"), FOR THE
 125// ONYX TESTNET. NOTHING ELSE IN THIS FILE IS A NUMBER THE OWNER HAS NOT ALREADY SEEN.
 126// ============================================================================
 127//
 128// These were committed as placeholders that could not work until the owner chose them; they
 129// are now his values and they are the ones this realm deploys with. init() below still gates
 130// them -- assertFeeConfig refuses an invalid recipient, this realm's own address, the
 131// admin/deploy key, a rate outside 1..10000 and a floor outside 1..maxPriceUgnot -- so a
 132// future edit to a value that could not work still fails at deploy rather than succeeding
 133// with the wrong fee forever. TestDeployGateStillRefusesBadFeeValues proves the gate, and
 134// TestCommittedFeeConstantsAreTheOwnersDecision pins the values below to the decision.
 135//
 136// A realm cannot be edited after deploy, so these are permanent for THIS realm's life. They
 137// are testnet values: mainnet would be a fresh deploy with fresh numbers decided then (DESIGN.md
 138// section 7.5), and DECISIONS.md already requires a different key than the deploy key for
 139// anything real.
 140//
 141//	saleFeeRecipient  -- the bech32 address every sale's fee is sent to. Units: an address.
 142//	                     The owner chose the project's `gnotchi-opponent` key, which is NOT the
 143//	                     deploy/admin key (init refuses that one). It must not be this realm's
 144//	                     own address either: coins sent there are burned, because there is no
 145//	                     withdrawal path.
 146//	saleFeeRateBps    -- the proportional cut, in BASIS POINTS of the declared price
 147//	                     (100 bps = 1%). 500 is 5%, the figure ECONOMY.md:231 floated. Legal
 148//	                     range 1..10000.
 149//	saleFeeFloorUgnot -- the flat floor every sale pays regardless of price, in UGNOT
 150//	                     (1 GNOT = 1,000,000 ugnot). 1 GNOT. Legal range 1..maxPriceUgnot. This
 151//	                     is the whole defence against "list at 0, settle off-chain": a sale at a
 152//	                     declared price of zero still pays this much. Zero is refused because
 153//	                     the owner's ruling is a cut on EVERY trade.
 154//
 155// The buyer pays price + floor + price*rate; the seller receives exactly price.
 156//
 157// WHY THREE CONSTANTS AND NOT TWO: the brief for this work spoke of "the fee and the recipient
 158// address". A fee that is a percentage alone is dodged completely by listing at a price of 0,
 159// so a percentage cannot be "the fee" on its own; the floor is the part that makes the cut
 160// unavoidable and ECONOMY.md already describes the fee as floor plus percentage. Splitting the
 161// fee into its two numbers is the honest shape, not an extra decision.
 162const (
 163	saleFeeRecipient  = "g1ylpe9juk8cafgf9rt09wzuz85ug2lyfk5metyv" // gnotchi-opponent, owner's choice
 164	saleFeeRateBps    = int64(500)                                 // 5% of the declared price
 165	saleFeeFloorUgnot = int64(1_000_000)                           // 1 GNOT flat minimum per sale
 166)
 167
 168// ============================================================================
 169// Constants that are NOT tunable. See the ruleBands table below for the ones that are, and
 170// DESIGN.md section 5.3 for why the tunable/fixed split falls exactly here.
 171// ============================================================================
 172
 173const (
 174	collectionName   = "Gnotchi gnomes"
 175	collectionSymbol = "GNOME"
 176
 177	// adminAddress is the only address SetRule and FreezeRules will accept. Compiled in,
 178	// with no transfer function, deliberately -- an admin-transfer path is a second
 179	// authority path, and a lost admin key merely freezes the config, which is v2's status
 180	// quo and not a disaster.
 181	//
 182	// THIS IS THE DISPOSABLE ONYX TESTNET DEPLOY KEY (DECISIONS.md: "Never a mainnet key.").
 183	// It is correct for onyx and wrong for anywhere real. WHOEVER DEPLOYS THIS SOMEWHERE
 184	// THAT MATTERS MUST CHANGE THIS ONE CONSTANT FIRST; it cannot be changed afterwards.
 185	// See DESIGN.md section 5.6.
 186	adminAddress = "g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar"
 187
 188	// maxNameLen: unchanged from v1/v2 -- kept in step with app/js/config.js's NAME_MAX_LEN
 189	// by hand, because Gno has no way to read that file at build time.
 190	maxNameLen = 24
 191
 192	// numEyes / numNoses: THE TWO PERMANENT FACE TRAITS, new in v4. A gnome's eyes id is an
 193	// integer in [0, numEyes) and its nose id an integer in [0, numNoses), both supplied to Adopt,
 194	// validated there, written once, and never written again by any function in this file.
 195	//
 196	// WHY IDS AND NOT NAMES OR PIXELS. The realm needs to know only WHICH face a gnome has, not
 197	// what it looks like: art never touches the chain (SPRITES.md). An integer is the smallest
 198	// thing that says "which", it needs no allowlist of spellings that could drift from the art's
 199	// own names, and the art side (the generated app/js/parts.js catalogue, built from
 200	// pack/04-pixel-art/grids/) indexes its parts by position anyway. The contract between the
 201	// two halves is therefore: eyes id i is the i-th eye part in catalogue order, nose id j the
 202	// j-th nose part, 0-based, and THAT ORDER IS FROZEN THE MOMENT THIS REALM DEPLOYS. A part
 203	// may be redrawn; it may never be reordered, removed or renumbered.
 204	//
 205	// WHY EXACTLY TEN AND EIGHT, WITH NO HEADROOM. These are SPRITES.md's Phase 1 numbers,
 206	// chosen before this realm for the economy (ECONOMY.md: a weighted table over discrete parts
 207	// so that rarity is population frequency counted on chain). Headroom -- accepting, say, 0..15
 208	// so art could grow -- would let a raw caller mint a face no catalogue entry exists for, which
 209	// the app would then have to draw as SOMETHING, forever, for a gnome that could be sold on
 210	// the strength of being the only one. An unknown id refused at Adopt is the only way the realm
 211	// can guarantee every gnome it has ever minted is drawable. The price is that an eleventh eye
 212	// is a v5, exactly as a fifth hat always was. Rules() publishes both counts so a client can
 213	// read the range off the chain instead of holding a copy.
 214	numEyes  = int64(10)
 215	numNoses = int64(8)
 216
 217	// acornReserve is the fix for the acorn softlock (AUDIT-DEPLOYED.md G9, found against v2 and
 218	// present in v3 too, worse: v3's once-per-address purse means selling and re-adopting cannot
 219	// refill it either). The only way acorns ever increase after adoption is a harvest; a harvest
 220	// needs a planted plot; planting costs plantCost and needs belly above the hunger gate; feeding
 221	// costs feedCost. So a gnome with NOTHING planted and FEWER than plantCost+feedCost acorns can
 222	// be one decayed belly away from a state no sequence of calls ever leaves: pats only, 10 XP a
 223	// day, forever, on a realm whose own rule 9 says the gnome never loses progress.
 224	//
 225	// SaveDay therefore refuses any batch that SPENDS acorns and ends with nothing planted and
 226	// fewer than acornReserve in the purse. It is a gate, not a grant: no acorn is ever created,
 227	// nothing becomes purchasable, and the number is derived from two the owner has already seen
 228	// rather than invented. The one residual, a purse carried across a sale at exactly one acorn,
 229	// is closed by the hunger-gate waiver in SaveDay's plant branch; see there. Rules() publishes
 230	// it, because like the hunger gate this exists only on chain and a client that does not
 231	// pre-check it loses gas (LOOP.md:26-29 records that happening once already).
 232	acornReserve = plantCost + feedCost
 233
 234	hourSeconds = int64(3600)
 235	daySeconds  = int64(86400)
 236
 237	// ---- values that must agree with app/js/rules.config.json and are NOT tunable. Each is
 238	// cited against the exact key it mirrors. Published by Rules() so a drift check against
 239	// rules.config.json is possible at all -- which it is not against v2, where nothing on
 240	// chain can be asked what any of these are. See DESIGN.md section 4.
 241	numPlots    = 3         // acorns.plots -- see DESIGN.md 4.4 for why this can never be tunable
 242	plantCost   = int64(1)  // acorns.plantCost
 243	feedCost    = int64(1)  // acorns.feedCost
 244	acornsStart = int64(10) // acorns.start
 245	meterMax    = int64(10) // meters.max
 246	patCheer    = int64(3)  // meters.patCheer
 247
 248	// hungryAtOrBelow is NOT tunable, and it is the one key I removed from the band table
 249	// after finding my own counterexample to the table's safety claim. It gates two different
 250	// things: planting (lower is kinder -- fewer refusals) and the `hungry` branch of moodNow
 251	// (where lower is NOT kinder). Because moodNow is first-match-wins with hungry checked
 252	// before sulky, and because hungry carries no XP modifier while sulky carries a penalty,
 253	// BEING HUNGRY CURRENTLY SHIELDS A NEGLECTED GNOME FROM THE SULKY PENALTY. Lowering this
 254	// threshold from 2 to 0 therefore moves a gnome with belly 1-2 and no pat for 72h out of
 255	// hungry and into sulky, costing it 10% of that day's care XP. LOOP.md does not ask for
 256	// this number to change, so removal costs nothing and shrinks the permanent admin surface.
 257	hungryAtOrBelow = int64(2) // meters.hungryAtOrBelow
 258
 259	// happyBonusPct is NOT tunable either, and the ratchet in SetRule is why. Its default (10)
 260	// is already its kindest value, so the only direction available to it was harsher --
 261	// removing a bonus. Under the ratchet it could therefore never move at all, and an
 262	// immovable "tunable" is pure audit surface. LOOP.md does want this at 0; that is a change
 263	// which takes something from a player, so it needs a v4 rather than an admin call. Stated
 264	// here rather than quietly dropped.
 265	happyBonusPct    = int64(10) // xp.happyBonus * 100
 266	happyWindowHours = int64(2)  // time.happyWindowHours
 267	sulkyAfterHours  = int64(72) // time.sulkyAfterHours
 268	maxLevel         = int64(20) // xp.maxLevel
 269	typeAtLevel      = int64(5)  // xp.typeAtLevel -- when ChooseType unlocks
 270
 271	xpPat            = int64(5)  // xp.pat
 272	xpFeed           = int64(5)  // xp.feed
 273	xpHarvestPerPlot = int64(10) // xp.harvestPerPlot
 274	xpTinkerTask     = int64(15) // xp.tinker
 275
 276	// maxActionsPerCall / maxBackfillHours: this realm's own choices, carried over from v2
 277	// unchanged, including v2's reasoning. maxBackfillHours was an explicit owner ruling --
 278	// "a player who ignores the game for a year can't just come back and claim it all in one
 279	// go" (DECISIONS.md) -- and v2 shipped on the back of it. Not tunable: widening it later
 280	// only ever grants more, but it also widens how far back a cared day (and so a streak)
 281	// can be claimed, and that is a game-rule decision rather than a balance dial.
 282	maxActionsPerCall = 32
 283	maxBackfillHours  = int64(48)
 284
 285	// caredWindowDays is the width of the cared-day bitmask. 64 days, 8 bytes, newest-first.
 286	// See DESIGN.md section 2.3 for why the chain stores cared DAYS and never computes the
 287	// STREAK: rules.js's streakFrom stays the single tested implementation of that rule.
 288	caredWindowDays = uint(64)
 289
 290	// maxGnomesPageSize bounds one GnomesPage call's own cost, the same way hollowmere/v1's
 291	// maxClaimantsPageSize does, so a caller cannot make one call scale with the whole realm.
 292	maxGnomesPageSize = 100
 293
 294	// ---- the sale. These are SHAPE constants, not economic ones: none of them is a price,
 295	// a rate or a destination, and none of them is a number the owner has been asked for.
 296	//
 297	// ugnotDenom is the only coin a sale is priced or settled in. ECONOMY.md and
 298	// MARKETPLACE-V1.md section 3: GNOT only, acorns never touch the market.
 299	ugnotDenom = "ugnot"
 300
 301	// maxPriceUgnot caps a declared price at 10^15 ugnot (a billion GNOT, more than exists).
 302	// It exists so that feeFor's arithmetic provably cannot overflow int64 for any legal
 303	// price at any legal rate, not to express an opinion about what a gnome is worth.
 304	maxPriceUgnot = int64(1_000_000_000_000_000)
 305
 306	// maxFeeRateBps: a share of a price cannot exceed the price. The upper bound the deploy
 307	// gate enforces on saleFeeRateBps, nothing more.
 308	maxFeeRateBps = int64(10_000)
 309
 310	// maxListingDays bounds how long a listing stands before Buy refuses it. A seller chooses
 311	// 1..30 days per listing. The hazard this bounds is irreversible: an immutable realm with
 312	// perpetual offers would let a gnome cared for another year sell at a price its owner set
 313	// and forgot. Relisting is one call; a sale at a forgotten price cannot be undone.
 314	maxListingDays = int64(30)
 315
 316	// maxListingsPageSize bounds one ListingsPage call, as maxGnomesPageSize does.
 317	maxListingsPageSize = 100
 318
 319	moodHungry  = "hungry"
 320	moodSulky   = "sulky"
 321	moodHappy   = "happy"
 322	moodContent = "content"
 323
 324	actionPat     = "pat"
 325	actionFeed    = "feed"
 326	actionPlant   = "plant"
 327	actionHarvest = "harvest"
 328	actionTinker  = "tinker"
 329
 330	typeGardener = "gardener"
 331	typeExplorer = "explorer"
 332	typeTinkerer = "tinkerer"
 333
 334	// ---- tunable rule keys. String keys, not Go identifiers, because SetRule takes one from
 335	// a transaction and the band table has to be a single list rather than a switch repeated
 336	// in three places. Names match rules.config.json's own names so a drift diff is mechanical.
 337	keyGrowHours       = "growHours"
 338	keyHarvestPerPlot  = "harvestPerPlot"
 339	keyFeedBelly       = "feedBelly"
 340	keyBellyDrop       = "bellyDropEveryHours"
 341	keyCheerDrop       = "cheerDropEveryHours"
 342	keyPatCooldown     = "patCooldownHours"
 343	keyTinkerCost      = "tinkerCost"
 344	keyDailyCareCap    = "dailyCareCap"
 345	keyLevelBase       = "levelBase"
 346	keyLevelStep       = "levelStep"
 347	keySulkyPenaltyPct = "sulkyPenaltyPct"
 348
 349	// kindEnd values for ruleBand below: which end of a band is KINDER to the player. This is
 350	// load-bearing in two places -- it fixes where the default must sit, AND it fixes which way
 351	// SetRule's ratchet will let the value travel. There is deliberately no "none": a band with
 352	// no kind direction has no safe ratchet, and SetRule rejects one rather than letting it
 353	// through unratcheted.
 354	kindMin = "min" // lower is kinder, so the default sits at max and may only fall
 355	kindMax = "max" // higher is kinder, so the default sits at min and may only rise
 356)
 357
 358// validHats/validCoats: unchanged from v1/v2. v3's DESIGN.md section 7.3 records why appearance
 359// is not the "open-ended list of parts" ECONOMY.md asks for: on an immutable realm an open-ended
 360// list is either validated against a compiled-in allowlist (exactly as closed as this) or it
 361// accepts arbitrary strings, which would undo validNameRune's three audit rounds one field over,
 362// since Render echoes into markdown. That argument is correct and v4 keeps it. What v3 got wrong
 363// was the conclusion drawn from it -- "so keep hat and coat" -- because a face chosen from ten
 364// eyes and eight noses is not open-ended; it is two more fields of exactly this kind, and v4
 365// adds them as integer ranges (numEyes/numNoses above) rather than as a third and fourth map.
 366var validHats = map[string]bool{
 367	"red":    true,
 368	"purple": true,
 369	"green":  true,
 370	"navy":   true,
 371}
 372
 373var validCoats = map[string]bool{
 374	"blue":   true,
 375	"teal":   true,
 376	"orange": true,
 377	"brown":  true,
 378}
 379
 380// validTypes: the three callings ChooseType accepts. All three are reachable in this realm,
 381// which is the point -- v2's assignType was called with explorer XP hard-coded to 0
 382// (garden/v2/garden.gno:823), so Explorer was permanently unreachable there, and for a
 383// connected player who never saw a garden or a tinker bench none of the three was. See
 384// DESIGN.md section 3.4.
 385var validTypes = map[string]bool{
 386	typeGardener: true,
 387	typeExplorer: true,
 388	typeTinkerer: true,
 389}
 390
 391// ============================================================================
 392// The tunable rules, their bands, and the one property that makes an admin defensible
 393// ============================================================================
 394
 395// ruleBand is one tunable number: its key, its deploy default, and the closed interval
 396// SetRule will accept. kindEnd records which end of that interval is kinder to the player,
 397// and is what TestEveryBandIsPlayerSafe checks the default against.
 398type ruleBand struct {
 399	key     string
 400	def     int64
 401	min     int64
 402	max     int64
 403	kindEnd string
 404}
 405
 406// ruleBands is the whole tunable surface of this realm. Nothing outside this table can be
 407// changed after deployment, by anyone, ever.
 408//
 409// WHY THIS TABLE EXISTS AT ALL, since no other realm in this project has one.
 410// DECISIONS.md records that the build brief gave garden and arena a Pause(), that an AI
 411// instruction overrode it, that consequently "no realm has an admin, a pause or a settable
 412// parameter, and every number is a frozen constant", and that "whether the next realms get
 413// an owner-settable config is an open decision for the owner, and it is cheap today".
 414// Meanwhile LOOP.md:89-92 already specifies NINE of the numbers below changing after the
 415// first ten testers answer, and LOOP.md:127-130 says to deploy nothing before they do. A v3
 416// that froze them would be obsolete on the day it shipped: that rebalance would cost a v4
 417// plus a second migration. So the numbers LOOP.md names are tunable, inside bands, in one
 418// direction only, and FreezeRules below makes the frozen outcome reachable too.
 419//
 420// WHY THE RISK IS SMALL, RESTATED NOW THAT GNOMES HAVE A PRICE. An earlier version of this
 421// paragraph said "this realm holds nothing of value"; with a sale function that is no longer
 422// the whole truth, so here is the exact truth instead. The realm holds no coins between
 423// transactions: Buy's banker is BankerTypeOriginSend, which can move at most the coins the
 424// buyer attached to that one call and dies with the call, and there is no RealmSend banker
 425// anywhere in this file. Gnomes DO now have a price, and the admin reaches none of them: no
 426// function here lets adminAddress list, cancel, buy, move, burn or re-price a gnome, change
 427// the fee, change the recipient, or pause sales. The only state the admin can reach is
 428// ruleValues, through SetRule, inside the bands, one way. There is still no theft available
 429// to a compromised admin; what it can do is rebalance the game toward the player.
 430//
 431// THE SAFETY PROPERTY, STATED EXACTLY, BECAUSE AN EARLIER AND LOOSER VERSION OF THIS COMMENT
 432// WAS FALSE AND SHIPPED NOWHERE ONLY BECAUSE AN AUDIT CAUGHT IT. Two parts, and the second
 433// part is the one that was missing:
 434//
 435//  1. Every band is one-directional: kindEnd records which end is kinder to the player, and
 436//     each default sits at the HOSTILE end, so the only place a value can go is toward the
 437//     player. TestEveryBandIsPlayerSafe pins that, per key.
 438//  2. SetRule RATCHETS: it rejects any value less kind than the CURRENT one, not merely any
 439//     value outside the band. Without this, a band being a closed interval meant it was
 440//     freely re-enterable from either side -- levelBase 50 -> 30 -> 50 was permitted, and
 441//     because levelFromXp is recomputed and written back on every SaveDay, that last call
 442//     made STORED LEVELS DROP on gnomes that already existed. See SetRule's own comment for
 443//     why that specific power was the one thing a redeploy could never grant, and for the
 444//     price the ratchet charges (there are no take-backs).
 445//
 446// Together: for this realm's whole life, a level can never go down, a meter can never drain
 447// faster, income can never fall, a cost can never rise and a gate can never tighten. That is
 448// an auditable property of this table plus that check, not a promise about anybody's
 449// intentions. TestTuningIsAOneWayRatchetTowardThePlayer exercises both directions of every
 450// key, which is the test the earlier version of this comment did not have.
 451//
 452// THE ONE RESIDUAL, NAMED RATHER THAN CLAIMED AWAY. moodNow is first-match-wins (hungry,
 453// sulky, happy, content) and only happy and sulky carry an XP modifier, so BEING HUNGRY
 454// SHIELDS A NEGLECTED GNOME FROM THE SULKY PENALTY. feedBelly and bellyDropEveryHours both
 455// raise effective belly, so either can lift a gnome with belly 1-2, unpatted for 72h, out of
 456// hungry and into sulky -- costing it sulkyPenaltyPct of that one day's capped care XP. It is
 457// bounded at 10% of at most 60 XP, it only touches a gnome already being neglected for three
 458// days, and TestRaisingFeedBellyCanCostANeglectedGnomeTenPercent demonstrates it rather than
 459// denying it. Both keys stay tunable because they are the core of LOOP.md's rebalance and the
 460// kindest changes available; hungryAtOrBelow, which was the largest contributor and is on
 461// nobody's list, was removed from this table instead. THE OWNER CAN CLOSE THE RESIDUAL
 462// ENTIRELY IN ONE CALL: SetRule(sulkyPenaltyPct, 0) is permitted by the ratchet and makes the
 463// flip cost exactly nothing. The perverse incentive underneath it (a gnome at belly 2 out-
 464// earning the same gnome at belly 3) exists at the DEFAULT config too and is a faithful port
 465// of app/js/rules.js's own ladder -- correcting it here would create precisely the
 466// chain/browser mood divergence this realm exists to stop, so it is ported, not fixed.
 467var ruleBands = []ruleBand{
 468	// Lower is kinder: default sits at the top of the band and may only fall.
 469	{keyGrowHours, 24, 12, 24, kindMin},      // acorns.growHours; LOOP.md wants 20
 470	{keyPatCooldown, 12, 6, 12, kindMin},     // time.patCooldownHours; LOOP.md wants 8
 471	{keyTinkerCost, 5, 1, 5, kindMin},        // acorns.tinkerCost; LOOP.md wants 3
 472	{keyLevelBase, 50, 30, 50, kindMin},      // xp.levelBase; LOOP.md wants 40
 473	{keyLevelStep, 25, 15, 25, kindMin},      // xp.levelStep
 474	{keySulkyPenaltyPct, 10, 0, 10, kindMin}, // xp.sulkyPenalty * 100; LOOP.md wants 0
 475
 476	// Higher is kinder: default sits at the bottom of the band and may only rise.
 477	{keyHarvestPerPlot, 3, 3, 6, kindMax},   // acorns.harvestPerPlot; LOOP.md wants 4
 478	{keyFeedBelly, 4, 4, 8, kindMax},        // meters.feedBelly; LOOP.md wants 6
 479	{keyBellyDrop, 3, 3, 8, kindMax},        // meters.bellyDropEveryHours; LOOP.md wants 4
 480	{keyCheerDrop, 6, 6, 12, kindMax},       // meters.cheerDropEveryHours; LOOP.md wants 8
 481	{keyDailyCareCap, 60, 60, 120, kindMax}, // xp.dailyCareCap
 482}
 483
 484// ============================================================================
 485// Storage
 486// ============================================================================
 487
 488// plot is one of this gnome's numPlots garden plots.
 489//
 490// TWO DELIBERATE DIFFERENCES FROM v2's plotState (garden/v2/garden.gno:164-167):
 491//
 492//  1. No `growing bool`. plantedAt == 0 IS the empty state. v2 carried both and had to keep
 493//     them consistent by hand in four places; one field cannot disagree with itself.
 494//  2. readyAt is STORED, not computed. This is the only derived value persisted anywhere in
 495//     this realm and it is deliberate: growHours is tunable, so a readiness check against
 496//     TODAY's growHours would silently change the ready time of a plot already in the
 497//     ground. Freezing it at plant time means a rule change can never reach backwards into
 498//     a plant a player has already made. 8 bytes per plot to make a tunable parameter safe.
 499type plot struct {
 500	plantedAt int64 // unix seconds; 0 means empty
 501	readyAt   int64 // plantedAt + growHours*hourSeconds, fixed AT PLANT TIME; 0 when empty
 502}
 503
 504// gnome is the whole persisted pet. EVERY FIELD HERE HAS AN EXPORTED READER -- see the
 505// package doc's one rule, and the table in DESIGN.md section 2.4 mapping field to reader.
 506//
 507// NOT here, on purpose: acorns. They live in `balances`, keyed by owner address, because
 508// ECONOMY.md:65-67 records the owner finding that hole himself -- "Acorns never travel with
 509// a gnome when it is sold. They belong to the player. Without this, money buys a gnome and
 510// inherits its acorns ... it is the kind of thing that is unfixable after deployment." Gnomes
 511// sell here, so this is no longer a theoretical distinction: Buy moves the gnome and no acorn
 512// moves to the buyer, because the purse was never on the gnome to begin with. The one acorn
 513// write Buy makes is TO THE SELLER -- it settles the garden's planted plots into the seller's
 514// own purse, because a planted plot is a claim on acorns and would otherwise have travelled.
 515//
 516// Meters are stored as (last-known value, the unix second it was set) per the brief, and
 517// meterNow works the current value out from elapsed time whenever anything reads it. Nothing
 518// ticks on this realm's clock, there is no scheduled job, and nothing here is written by
 519// anything other than a call the owning address itself signed.
 520type gnome struct {
 521	name string
 522	hat  string
 523	coat string
 524
 525	// eyes and nose are THE PERMANENT PAIR. Written by Adopt, validated against numEyes and
 526	// numNoses there, and never written by any other function in this file: SaveDay copies them
 527	// unchanged through `work := *g`, ChooseType and Buy touch other fields only, and there is no
 528	// setter. SPRITES.md takes from its Aavegotchi research that two permanently unchangeable
 529	// traits are what make an individual gnome worth owning; a face that could be changed would
 530	// be a hat. Readable through Face() and FaceOfToken(). A sale moves them with the gnome,
 531	// because they are the gnome's.
 532	eyes int64
 533	nose int64
 534
 535	level     int64
 536	gnomeType string // "" until ChooseType; never derived from XP -- see DESIGN.md 3.4
 537	adoptedAt int64  // unix seconds
 538
 539	belly   int64
 540	bellyAt int64
 541	cheer   int64
 542	cheerAt int64
 543
 544	// lastPatAt == 0 means "never patted". 0 is an unambiguous sentinel here (unlike in
 545	// rules.js, where timestamps are milliseconds and a test fixture can legitimately hold
 546	// 0): these are only ever written from a real chain's time.Now().Unix(), which is never
 547	// 0 on any chain that has left genesis.
 548	lastPatAt        int64
 549	lastHappyEventAt int64 // last pat or feed; the mood input v2 stored and never exposed
 550
 551	totalXp int64 // mood-adjusted, capped per UTC day; the only input to levelFromXp
 552
 553	// xpGarden/xpTinker/harvestCount are lifetime, UNCAPPED, mood-UNADJUSTED counters. In v2
 554	// the first two decided the type; here they decide nothing (ChooseType does) and exist
 555	// purely to be read. That is not waste: GARDEN-V3.md:95-98 argues that "earned marks" --
 556	// a muddy coat from gardening, a patched coat from long care -- are the appearance
 557	// feature actually worth having, and that if the underlying counters are exposed they
 558	// need no contract work at all, becoming a browser rendering decision over chain data.
 559	// So this realm ships counters and no appearance machinery.
 560	xpGarden     int64
 561	xpTinker     int64
 562	harvestCount int64
 563
 564	// dailyCareXp/dailyCareDay: the per-UTC-day accumulator dailyCareCap actually bounds,
 565	// carried over from v2's audit fix F1. Note this is STRICTER than app/js/rules.js, whose
 566	// careXpForDay caps a single call with no memory of what an earlier call the same day
 567	// already spent. v2's reading is the correct one and v3 keeps it; Progress() exposes the
 568	// spend so a client can subtract it instead of promising XP the chain then refuses.
 569	dailyCareXp  int64
 570	dailyCareDay int64
 571
 572	plots [numPlots]plot
 573
 574	lastTinkerAt int64 // unix seconds of the last completed tinker task; 0 = never
 575
 576	// Cared days. See DESIGN.md section 2.3 for the whole argument; the short version is that
 577	// the chain stores FACTS (which UTC days this address saved care on) and never the STREAK
 578	// RULE, which stays in app/js/rules.js's streakFrom -- the one implementation its own
 579	// tests already pin. Porting that rule here would mean reimplementing ISO-week rest-day
 580	// attribution permanently, and getting it subtly wrong would make the chain and the
 581	// browser disagree about the streak forever, which is the exact failure this realm exists
 582	// to stop.
 583	//
 584	// caredDayCount is the lifetime total and is what app/js/home.js's "Day N" means (it
 585	// increments per distinct day cared, not per day since adoption -- app/js/live.js:384-386).
 586	// lastCaredDay is a UTC day number; 0 means "never cared" and is unambiguous because unix
 587	// day 0 is 1970-01-01. caredMask bit i means "cared on day (lastCaredDay - i)".
 588	caredDayCount int64
 589	lastCaredDay  int64
 590	caredMask     uint64
 591
 592	// lastActionAt is the replay/ordering frontier: the unix second of the most recent action
 593	// any successful SaveDay call has ever applied. Initialised to adoptedAt at Adopt time,
 594	// so "every action strictly after lastActionAt" also means "no action at or before
 595	// adoption" for free. RESET TO THE SALE TIME BY Buy, for the same reason: a buyer must not
 596	// be able to backfill care dated before they owned the gnome, which would otherwise let
 597	// them claim the seller's last 48 hours. See SaveDay's own threat model.
 598	lastActionAt int64
 599
 600	// Provenance. acquiredAt is when the CURRENT owner got this gnome -- adoptedAt at Adopt,
 601	// the sale time after each Buy. saleCount is how many times it has changed hands. Both
 602	// are readable through Provenance(). Everything else on this struct travels with the
 603	// gnome when it sells, deliberately and whole -- except the contents of `plots`, which Buy
 604	// settles to the seller in acorns because a planted plot is a claim on acorns and acorns
 605	// never travel. See Buy's own comment for what the buyer inherits and why.
 606	acquiredAt int64
 607	saleCount  int64
 608}
 609
 610// listing is one gnome offered for sale. Keyed by tokenID in `listings`; at most one per
 611// gnome, and List replaces rather than stacks.
 612//
 613// buyer is the zero address for "anyone may buy", or one named address. A named buyer is the
 614// ONLY gift-shaped path in this realm, and it still pays the floor: see Buy.
 615type listing struct {
 616	seller     address
 617	priceUgnot int64
 618	buyer      address
 619	listedAt   int64
 620	expiresAt  int64
 621}
 622
 623// feeConfig is the three sale constants, as init captured them. See the top of the file.
 624type feeConfig struct {
 625	recipient  address
 626	rateBps    int64
 627	floorUgnot int64
 628}
 629
 630var (
 631	token  *grc721.Token
 632	ledger *grc721.PrivateLedger
 633
 634	nextID seqid.ID
 635
 636	gnomes     = avl.NewTree() // tokenID -> *gnome
 637	ownerIndex = avl.NewTree() // owner address -> tokenID
 638	balances   = avl.NewTree() // owner address -> int64 acorns (per PLAYER, not per gnome)
 639	ruleValues = avl.NewTree() // rule key -> int64
 640	listings   = avl.NewTree() // tokenID -> *listing
 641
 642	// faceCounts: "eyes:nose" -> int64, how many gnomes have ever been minted with that face.
 643	// Written only by Adopt (incremented), never decremented: there is no burn, and a sale moves
 644	// a face without minting one. At most numEyes*numNoses keys. ECONOMY.md's whole rarity
 645	// argument is that "rarity becomes population frequency counted on chain -- the chain itself
 646	// proves how few share a part"; this is that count, and FaceCount/FaceCensus are its readers
 647	// (every persisted field has a reader). A client could rebuild it by paging GnomesPage and
 648	// calling Face on each owner, at a cost that grows with the population; this costs one tree
 649	// write per adoption and one read per question.
 650	faceCounts = avl.NewTree()
 651
 652	rulesFrozen bool
 653
 654	// fee is the three sale constants as init captured them. WRITTEN EXACTLY ONCE, IN init,
 655	// AND NOWHERE ELSE IN THIS FILE. There is no setter, no steward, no band and no freeze for
 656	// it because it is frozen from the first block: DESIGN.md section 9.6 argues why a fee is
 657	// not the kind of number the ratchet in SetRule is for.
 658	fee feeConfig
 659
 660	// v3 had a `testFeeOverride *feeConfig` here: a package-level variable in production code
 661	// whose only writer was a test file, left over from when the three sale constants were
 662	// placeholders that init refused. v3's own DESIGN.md section 9.7 named it as residue and
 663	// said removing it was "a clean follow-up if the reviewer prefers the suite to run on exactly
 664	// the deployed config". v4 is that follow-up: there is no override, init reads the three
 665	// constants and nothing else, and the sale tests assert fee balances on the real recipient
 666	// address -- which is only an address in the harness, not a key anyone holds there.
 667
 668	// transferGate names the ONE token the grc721 extension will let move, and only while Buy
 669	// is moving it. It is "" at every transaction boundary: Buy sets it immediately before the
 670	// ledger call and clears it immediately after, and any panic in between aborts the
 671	// transaction and discards the set. It is therefore not persisted state in any meaningful
 672	// sense and has no reader; TestTransferGateIsClosedBetweenCalls pins that it is empty
 673	// after every sale.
 674	transferGate string
 675)
 676
 677func init(cur realm) {
 678	// Fail at deploy, loudly, rather than silently and forever. adminAddress is a compiled-in
 679	// string that this package's own gnomod.toml tells a deployer in capitals to edit before
 680	// deploying anywhere real. A typo fails CLOSED -- no caller can ever equal an invalid
 681	// address -- which is the safe direction but also the silent one: SetRule and FreezeRules
 682	// would be permanently unreachable while AdminState() published the broken value and
 683	// rulesFrozen stayed false forever, so the realm would advertise a live admin it did not
 684	// have. There is no way to notice that after deployment and no way to fix it. One line.
 685	if !address(adminAddress).IsValid() {
 686		panic("garden: adminAddress is not a valid address; fix the constant before deploying")
 687	}
 688
 689	// THE DEPLOY GATE. The three sale constants at the top of this file are checked against
 690	// the ranges and refusals in assertFeeConfig, so a deploy with a value that cannot work is
 691	// impossible rather than unlikely. The test suite runs on exactly these constants too; v3's
 692	// harness override is gone (see the `fee` comment above).
 693	cfg := feeConfig{
 694		recipient:  address(saleFeeRecipient),
 695		rateBps:    saleFeeRateBps,
 696		floorUgnot: saleFeeFloorUgnot,
 697	}
 698	assertFeeConfig(cfg, cur.Address())
 699	fee = cfg
 700
 701	token, ledger = grc721.NewToken(collectionName, collectionSymbol, 0, cur)
 702	ledger.RegisterExtension(&saleGateExtension{})
 703
 704	for _, b := range ruleBands {
 705		ruleValues.Set(b.key, b.def)
 706	}
 707}
 708
 709// assertFeeConfig is init's gate on the three sale constants, split out so a test can prove
 710// what it refuses, and prove that the committed constants pass it, without a failing init
 711// taking the whole test package down. Every message names the constant to edit. realmAddr is
 712// this realm's own address: fees sent there would be burned, because nothing here can ever
 713// withdraw them.
 714func assertFeeConfig(cfg feeConfig, realmAddr address) {
 715	if !cfg.recipient.IsValid() {
 716		panic("garden: DEPLOY GATE: saleFeeRecipient is not a valid address; fix the constant at the top of garden.gno before deploying")
 717	}
 718	if cfg.recipient == realmAddr {
 719		panic("garden: DEPLOY GATE: saleFeeRecipient is this realm's own address, and coins sent there are burned because there is no withdrawal path")
 720	}
 721	// The admin address is the deploy key, and the deploy key is the one address that would end
 722	// up here by default rather than by decision. The whole point of the gate is that the
 723	// recipient is chosen; MARKETPLACE-V1.md section 4.3 asks for exactly this assertion. Audit
 724	// finding Y-1/G-4 of 2026-10-11.
 725	if cfg.recipient == address(adminAddress) {
 726		panic("garden: DEPLOY GATE: saleFeeRecipient is the admin/deploy key; the fee recipient must be an address chosen for that purpose, not the key that happens to deploy")
 727	}
 728	if cfg.rateBps < 1 || cfg.rateBps > maxFeeRateBps {
 729		panic("garden: DEPLOY GATE: saleFeeRateBps must be between 1 and " + strconv.FormatInt(maxFeeRateBps, 10) + " basis points; fix the constant at the top of garden.gno before deploying")
 730	}
 731	if cfg.floorUgnot < 1 || cfg.floorUgnot > maxPriceUgnot {
 732		panic("garden: DEPLOY GATE: saleFeeFloorUgnot must be between 1 and " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot; fix the constant at the top of garden.gno before deploying")
 733	}
 734}
 735
 736// saleGateExtension replaces v1/v2's soulboundExtension. v2 panicked on EVERY transfer; this
 737// panics on every transfer EXCEPT the one Buy is performing right now, identified by
 738// transferGate holding that exact token id. The grc721 PrivateLedger is never exported and
 739// no Approve, SetApprovalForAll or TransferFrom is exposed by this realm, so Buy is the only
 740// code that can reach the ledger at all -- the gate is the second lock on a door that already
 741// has one, and it is the lock whose claim ships on chain where anyone can read it.
 742//
 743// See the package doc for why this relies on the transaction boundary rather than on hook
 744// ordering: the hook fires AFTER the ledger write, so a refused transfer's write is discarded
 745// only because the uncaught panic aborts the whole transaction. No recover(), ever.
 746type saleGateExtension struct{}
 747
 748func (saleGateExtension) ExtensionKind() string { return "sale-gate" }
 749
 750func (saleGateExtension) OnMint(to address, tid grc721.TokenID) {}
 751
 752func (saleGateExtension) OnTransfer(from, to address, tid grc721.TokenID) {
 753	if transferGate == "" || transferGate != tid.String() {
 754		panic("garden: a gnome can only change hands through Buy; every other transfer is permanently disabled")
 755	}
 756}
 757
 758func (saleGateExtension) OnBurn(tid grc721.TokenID) {
 759	panic("garden: gnomes cannot be burned")
 760}
 761
 762// ============================================================================
 763// Adopt
 764// ============================================================================
 765
 766// Adopt mints one gnome to the caller. Every validation v1 and v2 performed is carried over
 767// in the same order with the same messages -- the name charset allowlist, the invisible-letter
 768// denylist, the one-gnome-per-address rule, the OriginSend refusal -- because those checks
 769// are the output of three audit rounds and re-deriving them from memory is how they get lost.
 770//
 771// TWO THINGS CHANGE BECAUSE GNOMES CAN NOW BE SOLD. First, an address that has sold its gnome
 772// has no gnome, so it may adopt again -- "one gnome per address" is a statement about the
 773// present, not a lifetime count, and adoption is free for a fresh address anyway, so refusing
 774// it to a seller would punish only the honest. Second, the starting purse is granted ONCE PER
 775// ADDRESS, EVER: only if this address has never held acorns. A seller keeps their purse
 776// (acorns belong to the player, ECONOMY.md:65-67), so a re-adopting seller must not be
 777// topped back up to acornsStart, and a buyer who already has a purse from an earlier gnome
 778// keeps that one. Without this, sell-and-readopt would refill acorns for free.
 779//
 780// THE CUSTODY QUESTION IS CLOSED: ONLY A WALLET MAY ADOPT. realms/garden/NOTES.md recorded it
 781// in full and reserved it for the owner: cur.Previous().Address() is not restricted to a human
 782// wallet, so a realm that called Adopt would become the permanent, unrecoverable owner of a
 783// gnome, with no burn, admin or transfer to undo it. v1 and v2 left that open. On 2026-10-11
 784// the owner chose the accident case over the sponsor case -- an orphaned gnome permanently
 785// owned by some batching or multicall realm is a real person's lost gnome, and it can never
 786// be undone -- so Adopt now refuses any caller that is not a direct wallet call, with the same
 787// IsUserCall gate Buy already uses (DESIGN.md section 7.5).
 788//
 789// What the gate is, exactly, read from this toolchain's chain/runtime: IsUserCall() is
 790// "pkgPath == """, true for a `gnokey maketx call` (or a wallet's MsgCall) straight from a
 791// user and false for every code realm, INCLUDING the ephemeral realm a `gnokey maketx run`
 792// script executes in (that one is IsUserRun, not IsUserCall). So a sponsor realm, a batching
 793// realm and a run script are all refused, and none of them can ever be let back in on this
 794// realm: a v4 with an explicit "adopt for" path is how that door would reopen. The refusal
 795// comes first, before anything is read or written, so a refused caller gets this reason and
 796// not a later one.
 797//
 798// A consequence worth stating: since Buy is also wallet-only, NO REALM CAN EVER OWN A GNOME
 799// here, by construction. The audit's G-2 worry -- a realm owning and listing a gnome so that
 800// Buy pays a price to an address with no banker, where it is burned -- has no remaining path.
 801//
 802// NEW IN v4: eyes AND nose. Two integers, eyes in [0, numEyes) and nose in [0, numNoses),
 803// checked after the hat and coat so a caller with several mistakes hears them in the order the
 804// arguments are written. An id outside its range aborts the whole call and mints nothing -- an
 805// unknown face is refused, never defaulted, because a default would silently give every
 806// mistyped adoption the same face and a face is permanent. THE REALM DOES NOT CARE HOW THE
 807// NUMBERS WERE ARRIVED AT. A client may offer the player a choice, or roll on a weighted table
 808// and submit the result, or anything in between; this function records what it is given and
 809// validates it, and nothing on chain can tell the two apart. That neutrality is deliberate and
 810// argued in realms/garden/v4/DESIGN.md section 3: a realm cannot be edited, so the choice-versus-
 811// rarity decision must not be frozen in one. Its honest consequence is stated there too -- rarity
 812// on this realm is a property of client discipline, not of the contract, and a raw gnokey caller
 813// can always ask for the rarest face. The signature change breaks every existing Adopt caller
 814// (the app's live.js builds [name, hat, coat]); v4 DESIGN.md section 4 lists them.
 815func Adopt(cur realm, name, hat, coat string, eyes, nose int64) {
 816	if !cur.Previous().IsUserCall() {
 817		panic("garden: adopt must be called directly by a wallet, not through another realm; a gnome minted to a realm's address could never be recovered")
 818	}
 819
 820	// Refuse attached coins outright rather than strand them: Buy is the only function here
 821	// that takes payment, and nothing else has a way to return a coin. Two honest limits, both
 822	// carried from v1: the check is origin-scoped, not frame-scoped, so a transaction that
 823	// attaches coins to some OTHER realm in the same call chain makes this panic even though
 824	// garden received nothing; and it cannot prevent a direct bank send to garden's address
 825	// with no call at all.
 826	//
 827	// Said exactly, so nobody later mistakes it for recoverable: coins that reach this realm's
 828	// address by a direct bank send are BURNED, not stuck. Buy's banker is BankerTypeOriginSend,
 829	// which can move only the coins attached to that one Buy; there is no RealmSend banker, no
 830	// withdrawal path, no admin reach over coins, and none of those can be added. Do not send
 831	// GNOT here except as a Buy's exact total.
 832	if !unsafe.OriginSend().IsZero() {
 833		panic("garden: adopt accepts no coins; only Buy takes payment, and nothing else here can return them")
 834	}
 835
 836	caller := cur.Previous().Address()
 837	if !caller.IsValid() {
 838		panic("garden: adopt requires a valid caller address")
 839	}
 840
 841	if alreadyAdopted(caller) {
 842		panic("garden: this address has already adopted a gnome")
 843	}
 844
 845	if !validNameLength(name) {
 846		panic("garden: name must be 1-" + strconv.Itoa(maxNameLen) + " characters")
 847	}
 848
 849	if !validNameCharset(name) {
 850		panic("garden: name contains a character that is not an allowed letter, digit, space, hyphen or apostrophe")
 851	}
 852
 853	if !hasVisibleNameContent(name) {
 854		panic("garden: name must contain at least one visible character")
 855	}
 856
 857	if !validHats[hat] {
 858		panic("garden: invalid hat colour")
 859	}
 860
 861	if !validCoats[coat] {
 862		panic("garden: invalid coat colour")
 863	}
 864
 865	if !validEyes(eyes) {
 866		panic("garden: invalid eyes id; must be 0 to " + strconv.FormatInt(numEyes-1, 10))
 867	}
 868
 869	if !validNose(nose) {
 870		panic("garden: invalid nose id; must be 0 to " + strconv.FormatInt(numNoses-1, 10))
 871	}
 872
 873	id := nextID.Next()
 874	tid := grc721.TokenID(id.String())
 875
 876	if err := ledger.Mint(caller, tid); err != nil {
 877		panic(err)
 878	}
 879
 880	now := time.Now().Unix()
 881
 882	// A freshly-adopted gnome starts with full meters. The brief specifies decay rates and
 883	// the starting acorn count but never a starting meter value; v2 chose full and nothing
 884	// since has argued against it. lastPatAt/lastHappyEventAt/lastTinkerAt/lastCaredDay all
 885	// start at 0 ("never"). lastActionAt starts at adoptedAt, which is what makes the replay
 886	// rule also forbid pre-adoption actions with no separate check.
 887	gnomes.Set(tid.String(), &gnome{
 888		name:         name,
 889		hat:          hat,
 890		coat:         coat,
 891		eyes:         eyes,
 892		nose:         nose,
 893		level:        1,
 894		gnomeType:    "",
 895		adoptedAt:    now,
 896		belly:        meterMax,
 897		bellyAt:      now,
 898		cheer:        meterMax,
 899		cheerAt:      now,
 900		lastActionAt: now,
 901		acquiredAt:   now,
 902	})
 903	ownerIndex.Set(caller.String(), tid.String())
 904	grantStartingPurseIfNew(caller)
 905	faceCounts.Set(faceKey(eyes, nose), faceCount(eyes, nose)+1)
 906
 907	chain.Emit(
 908		"Adopt",
 909		"owner", caller.String(),
 910		"tokenId", tid.String(),
 911		"name", name,
 912		"hat", hat,
 913		"coat", coat,
 914		"eyes", strconv.FormatInt(eyes, 10),
 915		"nose", strconv.FormatInt(nose, 10),
 916	)
 917}
 918
 919// faceKey / faceCount: the census tree's key and its zero-default read.
 920func faceKey(eyes, nose int64) string {
 921	return strconv.FormatInt(eyes, 10) + ":" + strconv.FormatInt(nose, 10)
 922}
 923
 924func faceCount(eyes, nose int64) int64 {
 925	v := faceCounts.Get(faceKey(eyes, nose))
 926	n, ok := v.(int64)
 927	if !ok {
 928		return 0
 929	}
 930	return n
 931}
 932
 933// validEyes / validNose: the whole face allowlist. A range check rather than a map because the
 934// ids are positions in a catalogue (see numEyes), and a range cannot have a hole in it.
 935func validEyes(id int64) bool { return id >= 0 && id < numEyes }
 936func validNose(id int64) bool { return id >= 0 && id < numNoses }
 937
 938// alreadyAdopted: unchanged from v1/v2, and now also Buy's "one gnome per address" check.
 939// Both ownerIndex and token.BalanceOf are consulted, not either alone, so a disagreement
 940// between this realm's own index and the grc721 ledger can never be used to mint or buy a
 941// second gnome to one address IN THIS REALM. Buy keeps the two in step by writing ownerIndex
 942// in the same call as the ledger move.
 943//
 944// "ONE GNOME PER ADDRESS" IS TRUE PER REALM AND FALSE GLOBALLY, AND THAT IS PERMANENT. This
 945// paragraph is here rather than only in DESIGN.md because DESIGN.md is a repo file: editable,
 946// not deployed, and not available to whoever reads this realm's source off chain with
 947// `vm/qfile`. The caveat that most needs to reach a future realm's author was otherwise
 948// written in the one place that does not ship.
 949//
 950// garden/v2 is deployed and immutable at
 951// gno.land/r/g1r4aw43kagaaquhegxw6xr346h9f2qu2ezf0jar/garden/v2. It keeps its OWN ownerIndex
 952// and its OWN grc721 ledger, neither of which this function can see, and it stays fully
 953// callable forever -- there is no pause and no admin anywhere in it. garden/v3 (block 336496) is
 954// deployed and immutable in the same way, with the one difference that nobody ever adopted on
 955// it, so it stands empty but callable. So one address can hold a v2 gnome AND a v3 gnome AND a
 956// v4 gnome simultaneously, with different names, faces, levels, callings and acorn balances,
 957// and each realm's own SaveDay keeps accruing on its own gnome indefinitely. Nothing on chain
 958// reconciles them and nothing ever can.
 959//
 960// The consequence that matters most to a future author: the deployed arena/v1 imports
 961// garden/v2 by absolute path, immutably, and reads GnomeByAddress live at battle-resolution
 962// time. For a dual adopter it therefore authoritatively reads the V2 gnome -- the state the
 963// app has stopped showing and this realm can never update. ChooseType here cannot touch it.
 964// ANY FUTURE REALM THAT ASSUMES A GLOBAL ONE-GNOME-PER-ADDRESS INVARIANT IS BUILDING ON A
 965// FALSE PREMISE. An arena/v2 written against v3 must pick one realm as the identity of record
 966// and say which.
 967//
 968// v3 deliberately does not import v2 to close this. See DESIGN.md section 6.3 for the full
 969// argument; the part of it that does not expire is that a hard import would pin this realm to
 970// one path on one chain and make it undeployable anywhere v2 is not -- the exact coupling
 971// arena/v1's own gnomod.toml has to warn its deployers about.
 972func alreadyAdopted(addr address) bool {
 973	if ownerIndex.Has(addr.String()) {
 974		return true
 975	}
 976
 977	balance, err := token.BalanceOf(addr)
 978	if err != nil {
 979		panic(err)
 980	}
 981	return balance > 0
 982}
 983
 984// ============================================================================
 985// SaveDay
 986// ============================================================================
 987
 988// saveAction is one parsed entry from a SaveDay call's action list.
 989type saveAction struct {
 990	kind string
 991	at   int64 // unix seconds, as claimed by the caller
 992	plot int   // only meaningful for actionPlant/actionHarvest
 993}
 994
 995// SaveDay is the brief's "one daily save": the day's pats, feeds, plants, harvests and tinker
 996// task submitted as a single list in one signed call.
 997//
 998// THE WIRE FORMAT IS UNCHANGED FROM v2, deliberately. It is proven on chain (TESTNET.md:51-60,
 999// heights 310579/310601/310681) and app/js/live.js already builds it, so changing it would
1000// cost app work and buy nothing. Semicolon-separated entries, each colon-separated:
1001//
1002//	"pat:<unixSeconds>"
1003//	"feed:<unixSeconds>"
1004//	"tinker:<unixSeconds>"
1005//	"plant:<plotIndex>:<unixSeconds>"
1006//	"harvest:<plotIndex>:<unixSeconds>"
1007//
1008// e.g. "pat:1791600000;feed:1791601000;plant:0:1791602000". An empty string is a legal no-op.
1009// A list, rather than a []Action parameter, because gno.land's own call-argument decoder
1010// (gno.land/pkg/sdk/vm/convert.go, convertArgToGno) can only build primitive scalars and
1011// []byte from a transaction -- there is no case there for a slice of structs, and no Gno-side
1012// type cleverness changes what the wire format can carry. No gnomeID parameter either: one
1013// gnome per address, no delegation, so there is no second gnome a caller could mean and no
1014// path by which this could legitimately act on anyone else's. A sale changes which address
1015// that is, and nothing else here: the seller's next SaveDay finds no gnome, and the buyer's
1016// must date every action after the sale (Buy resets lastActionAt).
1017//
1018// TIMESTAMPS ARE BLOCK TIME, NOT WALL CLOCK, AND A CLIENT THAT FORGETS THAT FAILS EVERY TIME.
1019// DECISIONS.md:500-515: the first on-chain SaveDay failed with "action timestamp cannot be in
1020// the future" because the client sent `date -u +%s` while block time ran ~3 seconds behind
1021// wall clock, measured not assumed, and "a browser sending Date.now() will therefore fail
1022// essentially always". The fix is client-side -- read the chain's own latest block time and
1023// subtract a small margin. This realm deliberately does NOT add a future tolerance: DECISIONS'
1024// own warning is that a tolerance near midnight would let a caller claim the next UTC day's
1025// XP allowance early.
1026//
1027// THREAT MODEL. actionsRaw is client-supplied and this realm is the only authority over it.
1028// Carried over from v2 whole, including both of its audit fixes:
1029//
1030//   - Replay, in whole or in part: every timestamp must be strictly greater than
1031//     lastActionAt, the gnome's own high-water mark. A replayed batch's first action is by
1032//     definition <= that mark.
1033//   - Duplicate actions inside one call: the same strict-increase rule applies action to
1034//     action, so two actions sharing a timestamp are rejected whatever kinds they are.
1035//   - Future-dated actions: every timestamp must be <= this call's own block time.
1036//   - Actions at or before adoption: subsumed by the replay check, since lastActionAt starts
1037//     at adoptedAt.
1038//   - Unlimited backfill after a long dormancy (v2's audit fix F2, an owner ruling):
1039//     maxBackfillHours closes this twice over. Any action older than the window is rejected
1040//     outright with a message a player can understand, AND the floor the replay check
1041//     enforces is itself clamped forward to now-maxBackfillHours before the loop runs, so the
1042//     bound does not rest on a single comparison a future edit could weaken.
1043//   - Inflated action counts: pat spacing bounds pats per day arithmetically regardless of
1044//     what the list claims (two a day at the default 12-hour cooldown, four if the owner
1045//     ratchets it to the band floor of 6 -- v2's version of this note said "more than two a
1046//     day is impossible", which was true of v2's fixed constant and is not true here), and
1047//     feed/plant/tinker are each gated by the caller's own acorn balance, which this exact set
1048//     of rules is the only thing that can increase. The economy is the rate limit. Even the
1049//     floor case is far under dailyCareCap, so nothing here is a way around the cap.
1050//   - A thousand actions in one list: rejected on the delimiter COUNT, before strings.Split
1051//     allocates anything proportional to the input (v2's audit fix F5).
1052//   - XP inflation via the daily cap (v2's audit fix F1): the whole call's raw care XP is
1053//     mood-adjusted, then clamped to whatever remains of dailyCareCap FOR THE UTC CALENDAR
1054//     DAY, tracked by the gnome's own accumulator rather than anything scoped to one call.
1055//   - The acorn softlock (new in v4, AUDIT-DEPLOYED.md G9): a batch that spends acorns may not
1056//     end with nothing planted and fewer than acornReserve left. Checked once, after the apply
1057//     pass, against the batch's END state, so "feed then plant" in one call is fine and the
1058//     refusal names the whole batch rather than one action. See acornReserve.
1059//
1060// ATOMICITY: every check runs against local copies -- `work` for the gnome, `acorns` for the
1061// balance -- taken at the top. Nothing is written back until the whole list has validated and
1062// applied cleanly, so any panic before the final commit leaves persisted state completely
1063// untouched. A plain value copy, not a recover()-and-rollback; see the package SECURITY note.
1064func SaveDay(cur realm, actionsRaw string) {
1065	if !unsafe.OriginSend().IsZero() {
1066		panic("garden: save day accepts no coins; only Buy takes payment, and nothing else here can return them")
1067	}
1068
1069	caller := cur.Previous().Address()
1070	if !caller.IsValid() {
1071		panic("garden: save day requires a valid caller address")
1072	}
1073
1074	tid, g, ok := gnomeForAddr(caller)
1075	if !ok {
1076		panic("garden: no gnome adopted for this address")
1077	}
1078
1079	now := time.Now().Unix()
1080	actions := parseActions(actionsRaw)
1081
1082	if len(actions) == 0 {
1083		chain.Emit("SaveDay", "owner", caller.String(), "tokenId", tid, "actions", "0", "xpApplied", "0")
1084		return
1085	}
1086
1087	// Whole-batch ordering/bounds/same-day validation, before any mutation. firstDay anchors
1088	// the "one calendar day per call" rule: a call spanning more than one UTC day is rejected
1089	// outright rather than guessed at how to split. earliestAllowed is the backfill floor, used
1090	// twice on purpose -- see the threat model above.
1091	earliestAllowed := now - maxBackfillHours*hourSeconds
1092
1093	firstDay := dayNumber(actions[0].at)
1094	prevAt := g.lastActionAt
1095	if prevAt < earliestAllowed {
1096		prevAt = earliestAllowed
1097	}
1098	for _, a := range actions {
1099		if a.at <= earliestAllowed {
1100			panic("garden: this action is more than " + strconv.FormatInt(maxBackfillHours, 10) + " hours old; care can only be backfilled for the last " + strconv.FormatInt(maxBackfillHours, 10) + " hours, not longer absences")
1101		}
1102		if a.at <= prevAt {
1103			panic("garden: action timestamp must be strictly after the last saved action")
1104		}
1105		if a.at > now {
1106			panic("garden: action timestamp cannot be in the future")
1107		}
1108		if dayNumber(a.at) != firstDay {
1109			panic("garden: every action in one SaveDay call must fall on the same UTC day")
1110		}
1111		prevAt = a.at
1112	}
1113
1114	// Every rule this function's own body needs is read ONCE here, before the apply pass,
1115	// rather than per action: one tree lookup each instead of one per action, on a loop that
1116	// can legitimately carry 32 entries.
1117	//
1118	// Precisely, because an earlier version of this comment overclaimed twice. It said "rule
1119	// values are read once" full stop, which is not true of the whole call -- levelFromXp
1120	// below reads levelBase and levelStep itself (once each, not per iteration). And it
1121	// justified the caching as protection against "a SetRule landing concurrently", which is a
1122	// threat that does not exist: a transaction does not interleave with another, so nothing
1123	// can land inside one SaveDay. The real and sufficient benefit is gas.
1124	bellyDrop := rule(keyBellyDrop)
1125	cheerDrop := rule(keyCheerDrop)
1126	feedBelly := rule(keyFeedBelly)
1127	patCooldown := rule(keyPatCooldown)
1128	growHours := rule(keyGrowHours)
1129	harvestPerPlot := rule(keyHarvestPerPlot)
1130	tinkerCost := rule(keyTinkerCost)
1131	sulkyPenalty := rule(keySulkyPenaltyPct)
1132	dailyCap := rule(keyDailyCareCap)
1133
1134	// Apply pass: mutate local copies only. work.plots is a fixed-size array (a value type),
1135	// so `work := *g` copies it in full and mutating work.plots[i] never touches g.plots[i]
1136	// until the explicit commit at the end. Acorns are a separate tree, so they get their own
1137	// local and their own single write-back.
1138	work := *g
1139	acorns := balanceOf(caller)
1140	spentThisCall := false // set by feed, plant and tinker; the reserve rule applies only then
1141
1142	// gateWaived: the hunger-gate waiver (see the plant branch), decided ONCE from the state the
1143	// batch STARTED in -- purse below the reserve AND nothing planted -- and not from the running
1144	// balance. Audit pass 1 of v4 showed that judging it on the running balance let an ordinary
1145	// player reach it: tinker from six acorns down to one inside the batch, then plant while
1146	// hungry; or hold one acorn with a plot already growing and plant a second. Neither is the
1147	// carried-purse state the waiver exists for, and the first dodged a pack rule for anyone
1148	// holding tinkerCost+1 acorns. The start state is the only one the waiver was ever about.
1149	gateWaived := acorns < acornReserve && !anyPlotPlanted(g)
1150	rawCareXp := int64(0)
1151	tinkeredThisCall := false
1152
1153	for _, a := range actions {
1154		switch a.kind {
1155		case actionPat:
1156			if !canPat(work.lastPatAt, a.at, patCooldown) {
1157				panic("garden: pat must be at least " + strconv.FormatInt(patCooldown, 10) + " hours after the last one")
1158			}
1159			work.lastPatAt = a.at
1160			work.lastHappyEventAt = a.at
1161			work.cheer = clampMeter(meterNow(work.cheer, work.cheerAt, a.at, cheerDrop) + patCheer)
1162			work.cheerAt = a.at
1163			rawCareXp += xpPat
1164
1165		case actionFeed:
1166			if acorns < feedCost {
1167				panic("garden: not enough acorns to feed")
1168			}
1169			acorns -= feedCost
1170			spentThisCall = true
1171			work.belly = clampMeter(meterNow(work.belly, work.bellyAt, a.at, bellyDrop) + feedBelly)
1172			work.bellyAt = a.at
1173			work.lastHappyEventAt = a.at
1174			rawCareXp += xpFeed
1175
1176		case actionPlant:
1177			// Table 3.3: "Hungry: can't garden until fed." Carried over from v2 as blocking
1178			// new PLANTING only, never harvesting -- rule 9 says the gnome never loses
1179			// progress, and a ready harvest should not be strandable behind a hunger gate.
1180			// This gate exists only on chain; app/js/rules.js has no equivalent, which is why
1181			// hungryAtOrBelow is published by Rules() and why LOOP.md:26-29 records a live bug
1182			// where the client did not check and a whole day's save was rejected with gas paid.
1183			//
1184			// THE WAIVER (v4): the gate does not apply when the batch STARTED with the purse
1185			// below acornReserve and nothing planted (gateWaived, computed once above the loop).
1186			// With the reserve rule below, no SaveDay ever leaves an ordinary player in that
1187			// state, so for them this clause is unreachable and the gate behaves exactly as in
1188			// v3. It exists for the one state the reserve cannot police because no SaveDay
1189			// produced it: a purse carried across a sale -- a seller at zero acorns whose growing
1190			// plot refunded plantCost, who then adopts again (or buys) and holds exactly one acorn
1191			// with an empty garden. Without this clause that player, once hungry, could neither
1192			// feed (the reserve refuses spending the last acorn with nothing planted) nor plant
1193			// (this gate), and the softlock was back by a side door. "Feed first" is only a
1194			// reasonable instruction to someone who can afford to feed AND plant; when they
1195			// cannot, planting the last acorn is the one move that leads anywhere, and the gate
1196			// yields to it. AND ONLY WHILE THE PLAYER STILL CANNOT AFFORD TO FEED: the waiver also
1197			// requires the RUNNING purse to be below the reserve, so if a harvest earlier in the
1198			// same batch has lifted it (reachable in one UTC day once growHours is tuned below 24),
1199			// the next plant is gated normally again. Audit pass 2 of v4 found that shape; without
1200			// this second clause a batch that started waived stayed waived through every plant in
1201			// it. TestTheHungerGateStillHoldsAtTheReserve pins that it yields in that state only.
1202			waived := gateWaived && acorns < acornReserve
1203			if !waived && meterNow(work.belly, work.bellyAt, a.at, bellyDrop) <= hungryAtOrBelow {
1204				panic("garden: too hungry to garden; feed first")
1205			}
1206			p := &work.plots[a.plot]
1207			if p.plantedAt != 0 {
1208				panic("garden: that plot is already planted")
1209			}
1210			if acorns < plantCost {
1211				panic("garden: not enough acorns to plant")
1212			}
1213			acorns -= plantCost
1214			spentThisCall = true
1215			p.plantedAt = a.at
1216			p.readyAt = a.at + growHours*hourSeconds
1217
1218		case actionHarvest:
1219			p := &work.plots[a.plot]
1220			if p.plantedAt == 0 {
1221				panic("garden: that plot has nothing to harvest")
1222			}
1223			// Against the plot's OWN stored readyAt, never against today's growHours. See
1224			// plot's doc comment: this is what makes growHours safe to tune.
1225			if a.at < p.readyAt {
1226				panic("garden: that plot is not ready to harvest yet")
1227			}
1228			p.plantedAt = 0
1229			p.readyAt = 0
1230			acorns += harvestPerPlot
1231			work.xpGarden += xpHarvestPerPlot
1232			work.harvestCount++
1233			rawCareXp += xpHarvestPerPlot
1234
1235		case actionTinker:
1236			if tinkeredThisCall {
1237				panic("garden: only one tinker task per day")
1238			}
1239			if work.lastTinkerAt != 0 && dayNumber(work.lastTinkerAt) == dayNumber(a.at) {
1240				panic("garden: already did a tinker task today")
1241			}
1242			if acorns < tinkerCost {
1243				panic("garden: not enough acorns to tinker")
1244			}
1245			acorns -= tinkerCost
1246			spentThisCall = true
1247			work.xpTinker += xpTinkerTask
1248			work.lastTinkerAt = a.at
1249			tinkeredThisCall = true
1250			rawCareXp += xpTinkerTask
1251
1252		default:
1253			// Unreachable: parseActions rejects any kind outside this set before we get here.
1254			panic("garden: unknown action kind")
1255		}
1256	}
1257
1258	// THE ACORN RESERVE (v4). Judged on the batch's end state, once, so a player can feed and
1259	// plant in one call and only the net result is held to the rule. Only a batch that SPENT
1260	// acorns is held to it: a pure pat batch on a gnome that arrived below the reserve (see the
1261	// waiver in the plant branch for how that can happen) must not be refused for a state it did
1262	// not cause. "Spent" means a feed, plant or tinker RAN, not that the balance went down: an
1263	// earlier draft compared the end balance to the start balance, and a batch that harvested a
1264	// plot (+harvestPerPlot) and then fed the same number of times netted zero, counted as no
1265	// spend, and could end at zero acorns with nothing planted -- the exact state this rule
1266	// exists to make unreachable. Found on the author's own re-read before the audit;
1267	// TestAHarvestCannotMaskAnOverspendInTheSameBatch pins it. See acornReserve for why this
1268	// exists and why it is a gate rather than a grant.
1269	if spentThisCall && acorns < acornReserve && !anyPlotPlanted(&work) {
1270		panic("garden: that would leave fewer than " + strconv.FormatInt(acornReserve, 10) + " acorns with nothing planted, and a gnome with no acorns and no garden can never earn another; plant first, or keep " + strconv.FormatInt(acornReserve, 10) + " back")
1271	}
1272
1273	// One mood snapshot for the whole call's raw care XP, matching app/js/rules.js's
1274	// careXpForDay, which also takes one mood for an entire day's total rather than
1275	// re-deriving it per action. The "now" for that snapshot is the LAST action's own
1276	// timestamp, against the state exactly as this call leaves it.
1277	latestActionAt := actions[len(actions)-1].at
1278	bellyAtEnd := meterNow(work.belly, work.bellyAt, latestActionAt, bellyDrop)
1279	mood := moodNow(bellyAtEnd, work.lastPatAt, work.lastHappyEventAt, work.adoptedAt, latestActionAt, hungryAtOrBelow)
1280
1281	adjustedXp := rawCareXp
1282	switch mood {
1283	case moodHappy:
1284		adjustedXp = adjustedXp * (100 + happyBonusPct) / 100
1285	case moodSulky:
1286		adjustedXp = adjustedXp * (100 - sulkyPenalty) / 100
1287	}
1288
1289	// dailyCareCap bounds a UTC CALENDAR DAY's total care XP, not one call's (v2's audit fix
1290	// F1: before it, two calls the same day got 60 each against a cap of 60). Reset the
1291	// accumulator the moment this call's own day differs from whatever day it covers, clamp to
1292	// what is actually left, and top it up by exactly that much -- never by the pre-clamp
1293	// amount.
1294	if work.dailyCareDay != firstDay {
1295		work.dailyCareDay = firstDay
1296		work.dailyCareXp = 0
1297	}
1298	remainingToday := dailyCap - work.dailyCareXp
1299	if remainingToday < 0 {
1300		remainingToday = 0
1301	}
1302	if adjustedXp > remainingToday {
1303		adjustedXp = remainingToday
1304	}
1305	work.dailyCareXp += adjustedXp
1306
1307	work.totalXp += adjustedXp
1308	work.level = levelFromXp(work.totalXp)
1309
1310	// No assignType. v2 derived the type from xpGarden/xpTinker with explorer hard-coded to
1311	// 0, which made Explorer unreachable and -- per DECISIONS.md:643-646 -- made the other two
1312	// unbalanced by construction, so "nearly everyone becomes a Gardener". ChooseType replaces
1313	// it. See DESIGN.md section 3.4.
1314
1315	recordCaredDay(&work, firstDay)
1316	work.lastActionAt = latestActionAt
1317
1318	// Commit. Both writes are in one transaction, so either both land or neither does -- an
1319	// uncaught panic between them discards the whole transaction's state diff, which is why
1320	// this realm never needs (and must never have) a recover().
1321	//
1322	// The balance goes FIRST on purpose: setBalance is the only write here that can refuse
1323	// (its negative-balance guard), so running it before `*g = work` means the refusal
1324	// happens before any in-place mutation of the stored gnome, rather than relying solely
1325	// on the transaction boundary to undo one.
1326	setBalance(caller, acorns)
1327	*g = work
1328	gnomes.Set(tid, g)
1329
1330	chain.Emit(
1331		"SaveDay",
1332		"owner", caller.String(),
1333		"tokenId", tid,
1334		"actions", strconv.Itoa(len(actions)),
1335		"xpApplied", strconv.FormatInt(adjustedXp, 10),
1336		"mood", mood,
1337	)
1338}
1339
1340// ============================================================================
1341// ChooseType
1342// ============================================================================
1343
1344// ChooseType sets this gnome's calling, once, permanently, at level typeAtLevel or above.
1345//
1346// THIS REPLACES v2's DERIVED TYPE, AND IT IS THE WHOLE FIX FOR "ADVERTISES SOMETHING
1347// UNREACHABLE". v2 called assignType(xpGarden, 0, xpTinker) -- a literal 0 for explorer,
1348// because it had no Visit call -- so Explorer could never be assigned to any gnome, ever; and
1349// because the app hides the garden and the tinker bench from a connected player, such a
1350// player reached level 5 with 0 and 0, tied three ways, and got "" instead. Four independent
1351// sources already said the answer is a chooser rather than a tally:
1352//
1353//   - app/js/rules.js's own assignType returns {choose: tied} on a tie, documented "Ties:
1354//     player chooses".
1355//   - garden/v2/garden.gno's own comment: "the brief says 'the player chooses', and this
1356//     version has no chooser endpoint".
1357//   - LOOP.md:99-101: "at level 5 the player CHOOSES the calling rather than having it
1358//     derived from tallies that can only ever say Gardener."
1359//   - DECISIONS.md:643-646: the tallies are unbalanced by construction, because gardening
1360//     funds the gnome's food and tinkering does not, so the derivation was never a choice.
1361//
1362// WHAT IT COSTS, stated rather than glossed: the type stops being evidence of what a player
1363// did and becomes evidence of what they picked. arena/v1's type edge then reflects a choice
1364// rather than a history -- strictly better than reflecting a history that could only ever
1365// have one answer, but a different claim, and anyone reading that edge should know it changed.
1366//
1367// One choice, no retrain. rules.config.json has a retrainDays key; this realm implements
1368// nothing for it (DESIGN.md section 7.4).
1369func ChooseType(cur realm, kind string) {
1370	if !unsafe.OriginSend().IsZero() {
1371		panic("garden: choose type accepts no coins; only Buy takes payment, and nothing else here can return them")
1372	}
1373
1374	caller := cur.Previous().Address()
1375	if !caller.IsValid() {
1376		panic("garden: choose type requires a valid caller address")
1377	}
1378
1379	tid, g, ok := gnomeForAddr(caller)
1380	if !ok {
1381		panic("garden: no gnome adopted for this address")
1382	}
1383
1384	// Order matters for the message a player sees: "already chosen" before the level check,
1385	// so a level-5 gnome that has already chosen is told the true reason rather than being
1386	// re-offered a choice it cannot make.
1387	if g.gnomeType != "" {
1388		panic("garden: this gnome's calling is already chosen and cannot be changed")
1389	}
1390
1391	// Recomputed from totalXp against the CURRENT curve, not read from g.level. g.level is
1392	// only refreshed on a SaveDay, so after a levelBase/levelStep tune a gnome can be eligible
1393	// by today's curve while its stored level still says otherwise -- and the bands only ever
1394	// move the curve in the player's favour, so the stored value is the one that can be unfairly
1395	// low. The recomputed level is written back here too, so GnomeByAddress (which arena reads)
1396	// stops disagreeing with the curve from this moment rather than from the next save.
1397	level := levelFromXp(g.totalXp)
1398	if level < typeAtLevel {
1399		panic("garden: a calling can only be chosen at level " + strconv.FormatInt(typeAtLevel, 10) + " or above")
1400	}
1401
1402	if !validTypes[kind] {
1403		panic("garden: a calling is one of \"" + typeGardener + "\", \"" + typeExplorer + "\" or \"" + typeTinkerer + "\"")
1404	}
1405
1406	work := *g
1407	work.gnomeType = kind
1408	work.level = level
1409	*g = work
1410	gnomes.Set(tid, g)
1411
1412	chain.Emit(
1413		"ChooseType",
1414		"owner", caller.String(),
1415		"tokenId", tid,
1416		"type", kind,
1417	)
1418}
1419
1420// ============================================================================
1421// SALE. The only way a gnome ever changes hands. DESIGN.md section 9.
1422// ============================================================================
1423//
1424// THE SHAPE IN ONE PARAGRAPH. The owner lists their gnome at a price in ugnot, for a number of
1425// days, optionally reserved for one named buyer. Anyone eligible calls Buy with EXACTLY
1426// price + fee attached, where fee = saleFeeFloorUgnot + price * saleFeeRateBps / 10000. In
1427// that one call the gnome moves to the buyer, the price goes to the seller, and the fee goes
1428// to saleFeeRecipient. The realm holds nothing afterwards that it did not hold before. There
1429// are no bids, no auctions, no escrow, no approvals, no gifts outside a reserved listing, no
1430// burn, no withdrawal, and no admin reach into any of it.
1431//
1432// WHY THE CUT CANNOT BE DODGED ON CHAIN -- AND EXACTLY HOW FAR THAT CLAIM GOES. A royalty as a
1433// transfer hook sees transfers, not payments: list at 0, settle off-chain, royalty is zero.
1434// Here (1) the fee is computed on a price the realm itself RECEIVES in coins in the same call,
1435// never on a declared number; (2) no other transfer path exists, so there is no venue to
1436// settle through; and (3) the floor is charged even at a declared price of 0, so declaring a
1437// low price and settling the rest off-chain saves the seller the PERCENTAGE but never the
1438// FLOOR. That last point is the honest limit: an off-chain side payment is invisible to any
1439// contract, and the floor is the entire defence against it. So "unavoidable" means: every
1440// change of hands pays at least the floor, in this realm, with no exception reachable by any
1441// caller. Selling the wallet's private key dodges everything, as it does for every asset on
1442// every chain, and no contract can see it.
1443//
1444// WHY IsUserCall ON Buy, AND WHAT IT COSTS FOREVER. Coins attached to a transaction belong to
1445// the realm the wallet called, so a realm in the middle could not pay the seller with them.
1446// chain/banker makes this structural: a BankerTypeOriginSend banker can only be constructed
1447// when cur.Previous().IsUserCall() is true. The price is permanent and recorded here rather
1448// than discovered later: no realm can ever buy a gnome. No auction realm settling on a
1449// player's behalf, no gas-sponsored purchase, no bundle -- and no `gnokey maketx run` script
1450// either, since an ephemeral run realm is IsUser() but not IsUserCall(). A wallet calls Buy
1451// directly, or nobody does. It is this, or a dodgeable cut.
1452//
1453// WHAT THE BUYER INHERITS. Everything except the garden's contents and the replay frontier.
1454// Name, hat, coat, level, totalXp, calling, the lifetime counters, the cared-day history, the
1455// raw meters and their timestamps, the pat/tinker cooldowns and this UTC day's care-XP spend
1456// all travel. The owner accepted "money can buy strength from another player" as the cost of
1457// the ruling (TRADING-RULES.md:161-165), and a levelled gnome's level IS the thing being
1458// bought. The history stays with the gnome because it is the gnome's history -- provenance,
1459// not the seller's property -- and because every alternative is worse: resetting the daily cap
1460// would let sell-and-rebuy double a day's XP; resetting cooldowns would let a buyer pat at
1461// once. Two things change hands differently. lastActionAt is set to the sale time, so the
1462// buyer cannot submit care dated before they owned the gnome. And the GARDEN IS SETTLED TO THE
1463// SELLER, not inherited: a planted plot is a claim on acorns, acorns belong to the player and
1464// never travel (ECONOMY.md:65-67, "hole 2"), and an audit showed that plots travelling WAS an
1465// acorn-transfer channel. See the settlement block in Buy. The three empty plots travel.
1466//
1467// ONE GNOME PER ADDRESS, AND A BUYER WHO ALREADY HAS ONE. Refused, before anything moves:
1468// "sell it first". Two players therefore cannot swap directly -- each must sell, and each sale
1469// pays the fee -- which is the rule working as intended rather than a gap in it. A seller is
1470// free to adopt again afterwards and keeps their acorn purse (see Adopt).
1471
1472// List offers the caller's own gnome for sale.
1473//
1474// priceUgnot may be 0: that is the gift shape, and the buyer still pays the floor. buyer is
1475// the zero address ("") for anyone, or one named address other than the seller. days is
1476// 1..maxListingDays. Listing again replaces the standing listing; it is how a price is
1477// changed, and because Buy demands the exact total of the listing it finds, a buyer who
1478// prepared coins for the old price is refused rather than charged the new one.
1479//
1480// Listing locks nothing. The seller keeps caring for the gnome; care only ever adds to what
1481// the buyer gets, and a listed gnome that goes hungry is the seller's own problem to price.
1482func List(cur realm, priceUgnot int64, buyer address, days int64) {
1483	if !unsafe.OriginSend().IsZero() {
1484		panic("garden: list accepts no coins; only Buy takes payment")
1485	}
1486
1487	caller := cur.Previous().Address()
1488	if !caller.IsValid() {
1489		panic("garden: list requires a valid caller address")
1490	}
1491
1492	tid, _, ok := gnomeForAddr(caller)
1493	if !ok {
1494		panic("garden: no gnome adopted for this address")
1495	}
1496
1497	if priceUgnot < 0 {
1498		panic("garden: a price cannot be negative")
1499	}
1500	if priceUgnot > maxPriceUgnot {
1501		panic("garden: price exceeds the maximum of " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot")
1502	}
1503	if days < 1 || days > maxListingDays {
1504		panic("garden: a listing lasts between 1 and " + strconv.FormatInt(maxListingDays, 10) + " days")
1505	}
1506	// Normalise the reserved buyer's case before anything is checked or stored (v4). bech32 is
1507	// case-insensitive by specification (ASCII letters only, which is why lowerASCII and not
1508	// strings.ToLower: a Unicode fold could map a non-ASCII letter such as the Kelvin sign onto
1509	// a bech32 character, which is a wider claim than the spec makes and than this comment
1510	// wants to make). address.IsValid() accepts an all-upper-case string (measured against this
1511	// toolchain, not assumed), but Buy compares the stored buyer to the caller's own address,
1512	// which a wallet always presents in lower case -- so in v3 a seller who typed "G1..."
1513	// created a listing that validated, rendered, and could never be bought until they
1514	// relisted. v3's DESIGN.md section 9.11 left this as an open question; the answer was "yes,
1515	// it happens", and one line closes it. A mixed-case spelling, which the raw validator
1516	// rejects, is accepted after folding: the checksum still decides whether the folded string
1517	// is an address at all. Nothing else here compares addresses that a caller typed: the seller
1518	// and the buyer are always cur.Previous().
1519	buyer = address(lowerASCII(string(buyer)))
1520	if buyer != address("") {
1521		if !buyer.IsValid() {
1522			panic("garden: reserved buyer is not a valid address; pass an empty address to let anyone buy")
1523		}
1524		if buyer == caller {
1525			panic("garden: you cannot reserve your own gnome for yourself")
1526		}
1527	}
1528
1529	now := time.Now().Unix()
1530	listings.Set(tid, &listing{
1531		seller:     caller,
1532		priceUgnot: priceUgnot,
1533		buyer:      buyer,
1534		listedAt:   now,
1535		expiresAt:  now + days*daySeconds,
1536	})
1537
1538	feeUgnot := feeFor(priceUgnot)
1539	chain.Emit(
1540		"List",
1541		"seller", caller.String(),
1542		"tokenId", tid,
1543		"priceUgnot", strconv.FormatInt(priceUgnot, 10),
1544		"feeUgnot", strconv.FormatInt(feeUgnot, 10),
1545		"totalUgnot", strconv.FormatInt(priceUgnot+feeUgnot, 10),
1546		"buyer", buyer.String(),
1547		"expiresAt", strconv.FormatInt(now+days*daySeconds, 10),
1548	)
1549}
1550
1551// Cancel withdraws the caller's own listing. Only the seller can; nobody else -- not the
1552// admin, not a reserved buyer -- can cancel or alter anyone's listing.
1553func Cancel(cur realm) {
1554	if !unsafe.OriginSend().IsZero() {
1555		panic("garden: cancel accepts no coins; only Buy takes payment")
1556	}
1557
1558	caller := cur.Previous().Address()
1559	if !caller.IsValid() {
1560		panic("garden: cancel requires a valid caller address")
1561	}
1562
1563	tid, _, ok := gnomeForAddr(caller)
1564	if !ok {
1565		panic("garden: no gnome adopted for this address")
1566	}
1567	if !listings.Has(tid) {
1568		panic("garden: your gnome is not for sale")
1569	}
1570
1571	listings.Remove(tid)
1572	chain.Emit("Cancel", "seller", caller.String(), "tokenId", tid)
1573}
1574
1575// Buy purchases the listed gnome tokenID, paying its seller and the fee recipient in the same
1576// call. The caller must be a wallet, must not own a gnome, must be the reserved buyer if there
1577// is one, and must attach EXACTLY the listing's price + fee in ugnot and nothing else.
1578//
1579// VALIDATE, THEN COMMIT -- AND THE COMMIT PHASE CANNOT FAIL. Every refusal below happens
1580// before the first write. After the "commit" line nothing remains that can panic on chain:
1581// the ledger move is to an owner we have just verified with a gate we have just opened; the
1582// two coin sends are for amounts we have just checked equal the envelope (so the
1583// BankerTypeOriginSend cap cannot refuse them), from this realm's own address (which the
1584// envelope has credited before this code runs), to addresses validated at List time and at
1585// init. Payment is therefore atomic with transfer by construction, and not only because an
1586// uncaught panic would abort the transaction -- though it would, and that is the second lock.
1587//
1588// NO REENTRANCY IS POSSIBLE AND THIS IS WHY, STATED SO IT CAN BE CHECKED RATHER THAN
1589// TRUSTED: the only calls Buy makes outside this file are grc721's TransferFrom (a /p/
1590// package running in this realm's own frame, whose only hook is saleGateExtension above) and
1591// banker.SendCoins (a native that credits a bank balance and runs no code at the recipient).
1592// Neither can call back into this realm. State is nonetheless fully written before the sends,
1593// so even if a future chain gave coin recipients a hook, re-entering Buy for this token would
1594// find no listing and a different owner.
1595//
1596// EXACTLY, NOT AT LEAST. Accepting more than the total would make this realm a bank for the
1597// difference, with no withdrawal path; accepting a different denomination would strand it.
1598// The abort returns the whole envelope to the buyer, so a wrong amount costs gas and nothing
1599// else. Listing() publishes the exact total a wallet must attach.
1600func Buy(cur realm, tokenID string) {
1601	// Direct wallet call only. chain/banker would refuse the OriginSend banker further down
1602	// anyway; checking here first gives the caller the real reason instead of a banker error.
1603	if !cur.Previous().IsUserCall() {
1604		panic("garden: buy must be called directly by a wallet, not through another realm; the coins attached to a transaction belong to the realm the wallet called")
1605	}
1606	buyer := cur.Previous().Address()
1607	if !buyer.IsValid() {
1608		panic("garden: buy requires a valid caller address")
1609	}
1610
1611	l, ok := listingFor(tokenID)
1612	if !ok {
1613		panic("garden: that gnome is not for sale")
1614	}
1615
1616	now := time.Now().Unix()
1617	if now > l.expiresAt {
1618		panic("garden: that listing has expired; the seller must list again")
1619	}
1620	if l.seller == buyer {
1621		panic("garden: you cannot buy your own gnome")
1622	}
1623	if l.buyer != address("") && l.buyer != buyer {
1624		panic("garden: that gnome is reserved for a different buyer")
1625	}
1626	if alreadyAdopted(buyer) {
1627		panic("garden: this address already has a gnome; one gnome per address, so sell it before buying another")
1628	}
1629
1630	// Total function: the listing's seller must still be the ledger's owner and the gnome must
1631	// exist. Both are invariants every path in this file maintains (nothing but Buy moves a
1632	// gnome, and Buy deletes the listing), so these are unreachable today; they are here so
1633	// this function is correct on its own terms rather than correct because of code elsewhere.
1634	tid := grc721.TokenID(tokenID)
1635	owner, err := token.OwnerOf(tid)
1636	if err != nil || owner != l.seller {
1637		panic("garden: that listing is stale; the seller no longer owns that gnome")
1638	}
1639	gv := gnomes.Get(tokenID)
1640	g, isGnome := gv.(*gnome)
1641	if !isGnome || g == nil {
1642		panic("garden: that listing names a gnome this realm does not hold")
1643	}
1644
1645	feeUgnot := feeFor(l.priceUgnot)
1646	total := l.priceUgnot + feeUgnot
1647	// Read here, above the commit line, so the commit phase below contains no tree lookup that
1648	// could (even unreachably) panic on a missing key: rule() panics on absence by design.
1649	harvestPerPlot := rule(keyHarvestPerPlot)
1650	send := unsafe.OriginSend()
1651	if !exactUgnot(send, total) {
1652		panic("garden: buy requires exactly " + strconv.FormatInt(total, 10) + ugnotDenom +
1653			" attached (" + strconv.FormatInt(l.priceUgnot, 10) + " price + " +
1654			strconv.FormatInt(feeUgnot, 10) + " fee); got " + coinsString(send))
1655	}
1656
1657	// ---- commit. Nothing below this line can fail on chain. See the function comment. ----
1658
1659	transferGate = tokenID
1660	if err := ledger.TransferFrom(l.seller, l.seller, buyer, tid); err != nil {
1661		panic(err)
1662	}
1663	transferGate = ""
1664
1665	ownerIndex.Remove(l.seller.String())
1666	ownerIndex.Set(buyer.String(), tokenID)
1667	listings.Remove(tokenID)
1668
1669	// Settle the garden to the SELLER as the gnome changes hands (same transaction; the ledger
1670	// move above and this block commit or abort together). Audit finding Y-2 of 2026-10-11: a
1671	// planted plot is a bearer claim on acorns, so if plots travelled, a fresh address could
1672	// plant three, list at 0 to a confederate who pays only the floor, and hand over 9-18
1673	// acorns -- an acorn-transfer channel in a realm whose on-chain rule is that acorns never
1674	// travel. So: a ready plot pays the seller harvestPerPlot (the harvest they grew, judged
1675	// against the plot's own readyAt exactly as SaveDay's harvest is), a growing plot refunds
1676	// the seller plantCost (the acorn they planted), and the buyer receives an empty garden.
1677	// Nothing is destroyed and nothing moves between players. This is a settlement, not play:
1678	// no XP, no harvestCount, no cared day -- strictly worse for a seller than harvesting first,
1679	// so nobody is paid to prefer it. The seller always has a purse (Adopt and Buy both grant one
1680	// on acquisition), both summands are non-negative, and harvestPerPlot was read above the
1681	// commit line, so nothing here can fail. The Sale event carries settledAcorns.
1682	settled := int64(0)
1683	for i := 0; i < numPlots; i++ {
1684		p := &g.plots[i]
1685		if p.plantedAt == 0 {
1686			continue
1687		}
1688		if now >= p.readyAt {
1689			settled += harvestPerPlot
1690		} else {
1691			settled += plantCost
1692		}
1693		p.plantedAt = 0
1694		p.readyAt = 0
1695	}
1696	if settled > 0 {
1697		setBalance(l.seller, balanceOf(l.seller)+settled)
1698	}
1699
1700	g.acquiredAt = now
1701	g.saleCount++
1702	g.lastActionAt = now
1703	gnomes.Set(tokenID, g)
1704
1705	grantStartingPurseIfNew(buyer)
1706
1707	bank := banker.NewBanker(banker.BankerTypeOriginSend, cur)
1708	me := cur.Address()
1709	if l.priceUgnot > 0 {
1710		bank.SendCoins(me, l.seller, chain.Coins{{Denom: ugnotDenom, Amount: l.priceUgnot}})
1711	}
1712	bank.SendCoins(me, fee.recipient, chain.Coins{{Denom: ugnotDenom, Amount: feeUgnot}})
1713
1714	chain.Emit(
1715		"Sale",
1716		"tokenId", tokenID,
1717		"seller", l.seller.String(),
1718		"buyer", buyer.String(),
1719		"priceUgnot", strconv.FormatInt(l.priceUgnot, 10),
1720		"feeUgnot", strconv.FormatInt(feeUgnot, 10),
1721		"feeRecipient", fee.recipient.String(),
1722		"settledAcorns", strconv.FormatInt(settled, 10),
1723	)
1724}
1725
1726// Listing is one gnome's standing offer, including the EXACT total a wallet must attach to
1727// Buy. found is false when the gnome is not listed. An expired listing is still returned,
1728// with its expiresAt, so a client can tell "expired" from "never listed"; Buy refuses it.
1729func Listing(tokenID string) (seller address, priceUgnot int64, buyer address, listedAtUnix, expiresAtUnix, feeUgnot, totalUgnot int64, found bool) {
1730	l, ok := listingFor(tokenID)
1731	if !ok {
1732		return address(""), 0, address(""), 0, 0, 0, 0, false
1733	}
1734	f := feeFor(l.priceUgnot)
1735	return l.seller, l.priceUgnot, l.buyer, l.listedAt, l.expiresAt, f, l.priceUgnot + f, true
1736}
1737
1738// TotalListings / ListingsPage: the board. Same shape and same reasoning as TotalGnomes /
1739// GnomesPage -- this project has paid for a missing enumerator once already. The page is
1740// tokenIDs, oldest key first; resolve each with Listing() and OwnerOfToken(). Expired
1741// listings are included until their seller lists again or cancels.
1742func TotalListings() int64 {
1743	return int64(listings.Size())
1744}
1745
1746func ListingsPage(offset, limit int) []string {
1747	if offset < 0 {
1748		panic("garden: offset must not be negative")
1749	}
1750	if limit <= 0 {
1751		panic("garden: limit must be positive")
1752	}
1753	if limit > maxListingsPageSize {
1754		limit = maxListingsPageSize
1755	}
1756
1757	ids := make([]string, 0, limit)
1758	listings.IterateByOffset(offset, limit, func(key string, value any) bool {
1759		ids = append(ids, key)
1760		return false
1761	})
1762	return ids
1763}
1764
1765// QuoteFee is what a sale at priceUgnot costs the buyer, before anyone lists anything.
1766func QuoteFee(priceUgnot int64) (feeUgnot, totalUgnot int64) {
1767	if priceUgnot < 0 || priceUgnot > maxPriceUgnot {
1768		panic("garden: price must be between 0 and " + strconv.FormatInt(maxPriceUgnot, 10) + " ugnot")
1769	}
1770	f := feeFor(priceUgnot)
1771	return f, priceUgnot + f
1772}
1773
1774// FeeRules publishes the fee exactly as this realm will charge it, for the same reason Rules()
1775// publishes the game numbers: the chain's own value is the one that applies, and a client that
1776// can read it has no reason to hold a copy. Everything here is frozen for the realm's life.
1777func FeeRules() string {
1778	return "feeRateBps=" + strconv.FormatInt(fee.rateBps, 10) +
1779		";feeFloorUgnot=" + strconv.FormatInt(fee.floorUgnot, 10) +
1780		";feeRecipient=" + fee.recipient.String() +
1781		";maxPriceUgnot=" + strconv.FormatInt(maxPriceUgnot, 10) +
1782		";maxListingDays=" + strconv.FormatInt(maxListingDays, 10)
1783}
1784
1785// Provenance is the reader for the two fields a sale adds to the gnome.
1786func Provenance(addr address) (adoptedAtUnix, acquiredAtUnix, saleCount int64, found bool) {
1787	_, g, ok := gnomeForAddr(addr)
1788	if !ok {
1789		return 0, 0, 0, false
1790	}
1791	return g.adoptedAt, g.acquiredAt, g.saleCount, true
1792}
1793
1794// Face is the reader for the two permanent traits v4 adds, by owner address -- the shape every
1795// other per-gnome read here has, so a client that already resolves an address keeps doing so.
1796// GnomeByAddress is NOT extended to carry them: its signature is byte-identical to v2's so that
1797// arena/v1's call and the app's positional tuple parser repoint with an import change, and a
1798// face is not something a battle or a village roster needs. found is false for an address with
1799// no gnome; eyes and nose are then 0, which is also a legal face, so check found.
1800func Face(addr address) (eyes, nose int64, found bool) {
1801	_, g, ok := gnomeForAddr(addr)
1802	if !ok {
1803		return 0, 0, false
1804	}
1805	return g.eyes, g.nose, true
1806}
1807
1808// FaceCount is how many gnomes have ever been minted with this exact face -- the population
1809// frequency ECONOMY.md's rarity argument rests on, read off the chain rather than rebuilt by a
1810// client. Out-of-range ids are refused rather than answered 0, because 0 would be a true
1811// statement about a face that cannot exist and a client could mistake it for "rarest".
1812func FaceCount(eyes, nose int64) int64 {
1813	if !validEyes(eyes) || !validNose(nose) {
1814		panic("garden: face count asked for an id outside the catalogue; eyes are 0 to " + strconv.FormatInt(numEyes-1, 10) + " and noses 0 to " + strconv.FormatInt(numNoses-1, 10))
1815	}
1816	return faceCount(eyes, nose)
1817}
1818
1819// FaceCensus is every face's count in one read: "eyes:nose=count;..." for every face with at
1820// least one gnome, eyes-major, noses-minor, so a client can draw the whole rarity table from one
1821// qeval. Empty while nobody has adopted. At most numEyes*numNoses entries, so the cost is
1822// bounded by the catalogue and not by the population.
1823func FaceCensus() string {
1824	pairs := make([]string, 0, int(numEyes*numNoses))
1825	for e := int64(0); e < numEyes; e++ {
1826		for n := int64(0); n < numNoses; n++ {
1827			c := faceCount(e, n)
1828			if c == 0 {
1829				continue
1830			}
1831			pairs = append(pairs, faceKey(e, n)+"="+strconv.FormatInt(c, 10))
1832		}
1833	}
1834	return strings.Join(pairs, ";")
1835}
1836
1837// FaceOfToken is the same read keyed by tokenID, because the one place a face is looked up
1838// without an owner in hand is the market: ListingsPage returns tokenIDs, and a buyer deciding
1839// between two listings should not need OwnerOfToken first. The face is the gnome's, not the
1840// owner's, so the tokenID is in fact the more honest key.
1841func FaceOfToken(tokenID string) (eyes, nose int64, found bool) {
1842	gv := gnomes.Get(tokenID)
1843	g, ok := gv.(*gnome)
1844	if !ok || g == nil {
1845		return 0, 0, false
1846	}
1847	return g.eyes, g.nose, true
1848}
1849
1850// ============================================================================
1851// Reads. One per cluster of stored state, and between them they cover every persisted field
1852// -- see the package doc's one rule and DESIGN.md section 2.4.
1853// ============================================================================
1854
1855// GnomeByAddress has the BYTE-IDENTICAL signature to garden/v2's, on purpose: anything
1856// already written against v2 -- arena/v1's own GnomeByAddress call, app/js/adventure-
1857// villagers.js, app/js/live.js's qeval tuple parser, which reads these eight values
1858// positionally -- repoints at this realm with a one-line import change and no rewrite.
1859//
1860// That matters because arena/v1 is deployed and imports garden/v2 immutably, so a gnome
1861// adopted here cannot use arena/v1 at all and battle needs an arena/v2. Keeping this
1862// signature stable is the one thing this realm can do to make that a one-line change rather
1863// than a port. See DESIGN.md section 6.3.
1864func GnomeByAddress(addr address) (tokenID, name, hat, coat string, level int64, gnomeType string, adoptedAtUnix int64, found bool) {
1865	tid, g, ok := gnomeForAddr(addr)
1866	if !ok {
1867		return "", "", "", "", 0, "", 0, false
1868	}
1869	return tid, g.name, g.hat, g.coat, g.level, g.gnomeType, g.adoptedAt, true
1870}
1871
1872// GardenState is the read garden/v2 does not have, and the reason this realm exists.
1873//
1874// plantedAtCsv and readyAtCsv are each numPlots comma-separated unix seconds in plot order,
1875// with 0 for an empty plot -- e.g. "1791600000,0,0" / "1791686400,0,0". Two parallel
1876// fixed-width lists rather than one encoded blob, because the app's qeval reader already
1877// splits plain strings and because a reader that has to parse a schema is a reader that can
1878// parse it wrongly.
1879//
1880// readyCount is how many plots are harvestable AT THIS MOMENT, computed by the realm against
1881// each plot's own stored readyAt. The caller could derive it, but only by also knowing the
1882// chain's current block time, which is exactly the thing a browser gets wrong (see SaveDay's
1883// comment on DECISIONS.md:500-515). So the chain answers it.
1884func GardenState(addr address) (plantedAtCsv, readyAtCsv string, readyCount int64, found bool) {
1885	_, g, ok := gnomeForAddr(addr)
1886	if !ok {
1887		return "", "", 0, false
1888	}
1889
1890	now := time.Now().Unix()
1891	planted := make([]string, 0, numPlots)
1892	ready := make([]string, 0, numPlots)
1893	for i := 0; i < numPlots; i++ {
1894		p := g.plots[i]
1895		planted = append(planted, strconv.FormatInt(p.plantedAt, 10))
1896		ready = append(ready, strconv.FormatInt(p.readyAt, 10))
1897		if p.plantedAt != 0 && now >= p.readyAt {
1898			readyCount++
1899		}
1900	}
1901	return strings.Join(planted, ","), strings.Join(ready, ","), readyCount, true
1902}
1903
1904// CareState is v2's CareState plus the two things it left out: lastHappyEventAt (the mood
1905// input v2 stored at its own line 197 and exposed nowhere, so the app substituted
1906// lastActionAt, which is a different number -- a harvest-only day advances lastActionAt and
1907// not lastHappyEventAt), and acorns now read from the player's balance rather than the gnome.
1908//
1909// belly and cheer are returned already decayed to time.Now(), as v2's were. MeterState below
1910// returns the raw stored pair for a caller that wants to run app/js/rules.js's own meterNow
1911// against this realm's state and check the two agree.
1912//
1913// The signature is NOT positionally compatible with v2's CareState, unlike GnomeByAddress
1914// above. Nothing cross-realm reads CareState -- only the app does -- and inserting
1915// lastHappyEventAt where it belongs was worth more than preserving a tuple shape for a single
1916// caller that has to change anyway.
1917func CareState(addr address) (belly, cheer, acorns, totalXp, lastPatAtUnix, lastHappyEventAtUnix, lastActionAtUnix int64, found bool) {
1918	_, g, ok := gnomeForAddr(addr)
1919	if !ok {
1920		return 0, 0, 0, 0, 0, 0, 0, false
1921	}
1922	now := time.Now().Unix()
1923	return meterNow(g.belly, g.bellyAt, now, rule(keyBellyDrop)),
1924		meterNow(g.cheer, g.cheerAt, now, rule(keyCheerDrop)),
1925		balanceOf(addr), g.totalXp, g.lastPatAt, g.lastHappyEventAt, g.lastActionAt, true
1926}
1927
1928// MeterState returns the RAW stored meter pairs, undecayed. v2 exposed only the decayed
1929// values, which is lossy in a way that matters here: a caller cannot check the chain's decay
1930// against app/js/rules.js's meterNow if it is only ever handed the chain's answer. With this,
1931// the browser can run its own formula over the chain's own inputs -- which is how a repeat of
1932// the mood divergence in DESIGN.md section 1.5 gets caught in a day instead of in a year.
1933func MeterState(addr address) (belly, bellyAtUnix, cheer, cheerAtUnix int64, found bool) {
1934	_, g, ok := gnomeForAddr(addr)
1935	if !ok {
1936		return 0, 0, 0, 0, false
1937	}
1938	return g.belly, g.bellyAt, g.cheer, g.cheerAt, true
1939}
1940
1941// Progress is the rest of the stored state: the lifetime counters, the tinker clock and this
1942// UTC day's care-XP spend. Every one of these is a field garden/v2 writes and cannot read
1943// back.
1944//
1945// dailyCareXpSpent is the one a client most needs and v2 most lacked: the chain caps care XP
1946// per UTC DAY and app/js/rules.js caps it per CALL, so a browser that does not subtract this
1947// will promise XP the chain then refuses. dailyCareDay says which UTC day that spend belongs
1948// to, so a client can tell "60 spent today" from "60 spent on a day that has since rolled over".
1949func Progress(addr address) (xpGarden, xpTinker, harvestCount, lastTinkerAtUnix, caredDayCount, dailyCareXpSpent, dailyCareDay int64, found bool) {
1950	_, g, ok := gnomeForAddr(addr)
1951	if !ok {
1952		return 0, 0, 0, 0, 0, 0, 0, false
1953	}
1954	return g.xpGarden, g.xpTinker, g.harvestCount, g.lastTinkerAt,
1955		g.caredDayCount, g.dailyCareXp, g.dailyCareDay, true
1956}
1957
1958// CaredDays returns the UTC day numbers this address saved care on, newest first, comma
1959// separated -- e.g. "20371,20370,20368". At most caredWindowDays of them; empty if never.
1960//
1961// THE STREAK RULE IS DELIBERATELY NOT HERE. app/js/rules.js's streakFrom is the single tested
1962// implementation of it and takes exactly this list (as "YYYY-MM-DD", one line of JS from a day
1963// number -- rules.js:233 already does that conversion). Porting its ISO-week rest-day
1964// attribution onto an immutable contract would mean two implementations of one rule, and
1965// getting it subtly wrong would make the chain and the browser disagree about the streak
1966// forever. The chain stores facts; the browser owns the rule. DESIGN.md section 2.3.
1967//
1968// The encoding (a uint64 mask relative to lastCaredDay) is internal and no caller needs to
1969// know it; TestCaredMaskRoundTrips pins it.
1970func CaredDays(addr address) (daysCsv string, found bool) {
1971	_, g, ok := gnomeForAddr(addr)
1972	if !ok {
1973		return "", false
1974	}
1975	return caredDaysCsv(g), true
1976}
1977
1978// Mood returns the realm's OWN mood for this gnome, right now.
1979//
1980// This exists because the chain's mood is what applies the XP modifier, and because v2 proves
1981// a divergence here is not hypothetical: v2 ported rules.js's mood faithfully, rules.js was
1982// then fixed so a never-patted gnome is "new" rather than "neglected", and the deployed realm
1983// and the shipped browser have returned different moods for the same gnome ever since, with
1984// no way to notice from outside. v3 matches the current rules.js (see moodNow) and publishes
1985// its answer so the next divergence is observable.
1986func Mood(addr address) (mood string, found bool) {
1987	_, g, ok := gnomeForAddr(addr)
1988	if !ok {
1989		return "", false
1990	}
1991	now := time.Now().Unix()
1992	belly := meterNow(g.belly, g.bellyAt, now, rule(keyBellyDrop))
1993	return moodNow(belly, g.lastPatAt, g.lastHappyEventAt, g.adoptedAt, now, hungryAtOrBelow), true
1994}
1995
1996// CollectionInfo / TotalSupply / BalanceOf / OwnerOfToken: the GRC721 read surface.
1997//
1998// THESE EXIST BECAUSE AN AUDIT FOUND THE ONE PLACE THIS REALM'S OWN RULE DID NOT HOLD. Every
1999// field of the gnome struct has a reader; `token` and `ledger` did not. They are persisted
2000// realm state (the token is created inside garden's own storage, so there is no separate
2001// grc721 realm an indexer could query instead), and before these four functions the only read
2002// of any of it was token.TotalSupply() interpolated into Render's markdown, where no caller
2003// can consume it as a typed value.
2004//
2005// What that cost, permanently, if it had shipped: no wallet could list the gnome, no indexer
2006// could enumerate holders, no other realm could verify ownership except through garden's own
2007// GnomeByAddress, and -- the sharpest one -- there was NO tokenID-to-owner reader at all,
2008// because ownerIndex is owner-to-tokenID only. A tokenID observed in an Adopt event could not
2009// be resolved back to an owner without brute-forcing GnomesPage. garden/v2 has the identical
2010// gap and can never close it.
2011//
2012// nextID is the one piece of persisted state with no direct reader, and that is deliberate
2013// rather than missed: it is a seqid counter equal to the number of adoptions (there is no
2014// burn, and a sale moves a token without minting one), so TotalSupply() below is the same
2015// number by construction. Recorded so the next reader does not have to work that out.
2016func CollectionInfo() (name, symbol string) {
2017	return collectionName, collectionSymbol
2018}
2019
2020func TotalSupply() int64 {
2021	return token.TotalSupply()
2022}
2023
2024// BalanceOf is 0 or 1 for every address, forever: one gnome per address, no burn, and Buy
2025// refuses a buyer who already holds one. Exposed anyway rather than replaced by a bool,
2026// because a GRC721 consumer expects this shape.
2027//
2028// An invalid address owns nothing, so 0 is the CORRECT answer here and not an error signal --
2029// this function deliberately cannot distinguish "invalid address" from "owns none", because
2030// there is nothing to distinguish. Acorns(addr) below is the reader that does draw a
2031// distinction (absent versus zero), and it draws it about something where the difference is
2032// real. An earlier version of this comment claimed the opposite of the line underneath it;
2033// corrected here, before it could ship and say that permanently.
2034func BalanceOf(addr address) int64 {
2035	n, err := token.BalanceOf(addr)
2036	if err != nil {
2037		return 0
2038	}
2039	return n
2040}
2041
2042// OwnerOfToken resolves a tokenID back to its owner -- the lookup that was impossible without
2043// it. found is false for a tokenID this realm never minted.
2044func OwnerOfToken(tokenID string) (owner address, found bool) {
2045	if !gnomes.Has(tokenID) {
2046		return address(""), false
2047	}
2048	o, err := token.OwnerOf(grc721.TokenID(tokenID))
2049	if err != nil {
2050		return address(""), false
2051	}
2052	return o, true
2053}
2054
2055// TotalGnomes / GnomesPage: the enumerator garden/v2 has no equivalent of.
2056//
2057// This project has already paid for a missing enumerator once. DECISIONS.md:780-791 records
2058// that arena/v1 "exposes no way to list challenges -- confirmed with vm/qfuncs against the
2059// deployed realm, not assumed from the source -- and a realm cannot be edited", so there is no
2060// challenge inbox and there cannot be one; "if an inbox is ever wanted, it is an arena/v2
2061// plus a migration". hollowmere/v1 learned that and shipped TotalClaimants/ClaimantsPage.
2062// garden/v2 did not, so no leaderboard, no village roster and no "who is playing" query is
2063// possible against it at all. Twenty lines, unavailable forever if omitted.
2064func TotalGnomes() int64 {
2065	return int64(ownerIndex.Size())
2066}
2067
2068// GnomesPage returns up to limit owner addresses in ownerIndex's own stable key order,
2069// starting at offset. limit is CLAMPED to maxGnomesPageSize rather than rejected, so a caller
2070// asking for too much still gets a gas-bounded page instead of an aborted call; a negative
2071// offset or a non-positive limit are caller mistakes with no sane page to return.
2072//
2073// Paging with offset 0, then offset+len(page), until a page shorter than its requested limit
2074// comes back, covers every owner exactly once with no gaps and no duplicates for any snapshot
2075// where the tree does not grow between calls. TestGnomesPagePagesEveryOwnerExactlyOnce proves
2076// that directly rather than assuming it from avl.Tree's doc comment.
2077func GnomesPage(offset, limit int) []string {
2078	if offset < 0 {
2079		panic("garden: offset must not be negative")
2080	}
2081	if limit <= 0 {
2082		panic("garden: limit must be positive")
2083	}
2084	if limit > maxGnomesPageSize {
2085		limit = maxGnomesPageSize
2086	}
2087
2088	owners := make([]string, 0, limit)
2089	ownerIndex.IterateByOffset(offset, limit, func(key string, value any) bool {
2090		owners = append(owners, key)
2091		return false
2092	})
2093	return owners
2094}
2095
2096// Acorns is the player's own acorn balance, read directly rather than only through
2097// CareState, because acorns are a PLAYER's property here and not a gnome's (ECONOMY.md:65-67)
2098// and the read should say so. An address that has never held a purse gets 0, false -- never
2099// a bare 0 that a caller could mistake for a real empty purse. A player who has SOLD their
2100// gnome still has a purse, and this says so: it is keyed on the purse, not on the gnome,
2101// because the purse outlives the gnome and is what a later Adopt or Buy will find waiting.
2102func Acorns(addr address) (acorns int64, found bool) {
2103	if !balances.Has(addr.String()) {
2104		return 0, false
2105	}
2106	return balanceOf(addr), true
2107}
2108
2109// ============================================================================
2110// Rules: publishing them, tuning them inside bands, and freezing them for good
2111// ============================================================================
2112
2113// Rules publishes every number this realm actually uses, as "key=value;key=value;...", fixed
2114// values first in the order of DESIGN.md's agreement table, then the tunables in ruleBands
2115// order.
2116//
2117// THIS IS THE PIECE v2 HAD NO ANSWER TO. v2 kept thirty-odd values hand-copied from
2118// app/js/rules.config.json with no reader, so no drift check between the chain and the
2119// browser was possible even in principle -- nothing on chain could be asked what growHours
2120// was. With this, anyone can: gnoweb, curl, or a node test that compares each key against
2121// rules.config.json and fails on any mismatch.
2122//
2123// AND IT FIXES THE DIRECTION OF CORRECTION, which is the part that matters on an immutable
2124// contract. v2's two copies could drift and neither was authoritative. Here the chain is
2125// authoritative and, for a tunable, is the only place a value can change -- so a client should
2126// READ this and use it, not hold a second copy and hope.
2127//
2128// WHAT IS NOT DONE, stated rather than implied: the node-side half of that check does not
2129// exist. The follow-up is one test comparing this output against rules.config.json, plus
2130// making app/js/rules-bridge.js prefer these values for a connected player. Neither is
2131// written. Until they are, this is a capability and not a check.
2132func Rules() string {
2133	pairs := []string{
2134		"plots=" + strconv.Itoa(numPlots),
2135		"plantCost=" + strconv.FormatInt(plantCost, 10),
2136		"feedCost=" + strconv.FormatInt(feedCost, 10),
2137		"acornsStart=" + strconv.FormatInt(acornsStart, 10),
2138		"meterMax=" + strconv.FormatInt(meterMax, 10),
2139		"hungryAtOrBelow=" + strconv.FormatInt(hungryAtOrBelow, 10),
2140		"happyBonusPct=" + strconv.FormatInt(happyBonusPct, 10),
2141		"patCheer=" + strconv.FormatInt(patCheer, 10),
2142		"happyWindowHours=" + strconv.FormatInt(happyWindowHours, 10),
2143		"sulkyAfterHours=" + strconv.FormatInt(sulkyAfterHours, 10),
2144		"maxLevel=" + strconv.FormatInt(maxLevel, 10),
2145		"typeAtLevel=" + strconv.FormatInt(typeAtLevel, 10),
2146		"xpPat=" + strconv.FormatInt(xpPat, 10),
2147		"xpFeed=" + strconv.FormatInt(xpFeed, 10),
2148		"xpHarvestPerPlot=" + strconv.FormatInt(xpHarvestPerPlot, 10),
2149		"xpTinker=" + strconv.FormatInt(xpTinkerTask, 10),
2150		"maxBackfillHours=" + strconv.FormatInt(maxBackfillHours, 10),
2151		"maxActionsPerCall=" + strconv.Itoa(maxActionsPerCall),
2152		"caredWindowDays=" + strconv.FormatInt(int64(caredWindowDays), 10),
2153		"faceEyes=" + strconv.FormatInt(numEyes, 10),
2154		"faceNoses=" + strconv.FormatInt(numNoses, 10),
2155		"acornReserve=" + strconv.FormatInt(acornReserve, 10),
2156	}
2157	for _, b := range ruleBands {
2158		pairs = append(pairs, b.key+"="+strconv.FormatInt(rule(b.key), 10))
2159	}
2160	return strings.Join(pairs, ";")
2161}
2162
2163// RuleBands publishes the tunable surface itself: "key=min..max" for every band, in the same
2164// order Rules() uses.
2165//
2166// Without this, "the admin can only tune inside bands" is a claim in a document. With it,
2167// anyone can read the actual limits off the chain and check what this realm will and will not
2168// accept, without trusting DESIGN.md, this comment, or the owner.
2169func RuleBands() string {
2170	pairs := make([]string, 0, len(ruleBands))
2171	for _, b := range ruleBands {
2172		pairs = append(pairs, b.key+"="+strconv.FormatInt(b.min, 10)+".."+strconv.FormatInt(b.max, 10))
2173	}
2174	return strings.Join(pairs, ";")
2175}
2176
2177// AdminState is who may tune, and whether tuning is over. Readable by anyone, deliberately:
2178// an admin nobody can see is worse than an admin.
2179func AdminState() (admin address, frozen bool) {
2180	return address(adminAddress), rulesFrozen
2181}
2182
2183// SetRule changes one tunable number, inside its band, if the rules are not frozen.
2184//
2185// Only adminAddress. No admin transfer. Nothing here can touch a gnome, a balance, an XP
2186// total or a level -- the only state this function can reach is ruleValues, and only for a key
2187// that is in ruleBands, and only to a value inside that key's band. See ruleBands' own comment
2188// for why this exists at all and why the bands make it defensible; DESIGN.md section 5 for
2189// the full argument, including the explicit note that this is the one decision in v3 the owner
2190// should actively approve or veto before deploy, and that vetoing it is a deletion rather than
2191// a redesign.
2192func SetRule(cur realm, key string, value int64) {
2193	if !unsafe.OriginSend().IsZero() {
2194		panic("garden: set rule accepts no coins; only Buy takes payment, and nothing else here can return them")
2195	}
2196
2197	caller := cur.Previous().Address()
2198	if caller != address(adminAddress) {
2199		panic("garden: only the admin can change a rule")
2200	}
2201
2202	if rulesFrozen {
2203		panic("garden: the rules are frozen and can never be changed again")
2204	}
2205
2206	b, ok := bandFor(key)
2207	if !ok {
2208		panic("garden: \"" + key + "\" is not a tunable rule; see RuleBands()")
2209	}
2210
2211	if value < b.min || value > b.max {
2212		panic("garden: " + key + " must be between " + strconv.FormatInt(b.min, 10) + " and " + strconv.FormatInt(b.max, 10))
2213	}
2214
2215	before := rule(key)
2216
2217	// THE RATCHET. This is the check that makes the one-directional claim actually true, and
2218	// it is here because an audit found that without it the claim was false after the second
2219	// tune. A band is a CLOSED INTERVAL, freely re-enterable from either side: bounding the
2220	// interval bounds where a rule may sit, not which way it may travel. So
2221	// SetRule(levelBase, 30) followed by SetRule(levelBase, 50) was permitted, and since
2222	// levelFromXp is recomputed from totalXp on every SaveDay and written back, that second
2223	// call made STORED LEVELS DROP for gnomes that already existed.
2224	//
2225	// That is the one capability redeploying could never give the owner -- a v4 is a fresh
2226	// namespace and cannot reach into v3's stored gnomes -- so it was exactly the power the
2227	// whole "this realm holds nothing of value, an admin can only rebalance" argument did not
2228	// cover. FreezeRules made it worse rather than better: an admin could push every key to
2229	// the harsh end of its band and then freeze there, permanently, with no unfreeze and no
2230	// SetRule left to undo it.
2231	//
2232	// With this check each rule is a monotone one-way ratchet toward the player, for the
2233	// realm's whole life. The price, stated plainly because it is real: THERE ARE NO
2234	// TAKE-BACKS. Tune growHours to 20 and 22 is gone forever. That is not a side effect of
2235	// the fix, it IS the property -- "can only ever move toward the player" and "can be moved
2236	// back" are the same sentence negated. An owner who wants a harsher number after trying a
2237	// kinder one needs FreezeRules and a v4, which is the honest path for a change that takes
2238	// something from a player.
2239	switch b.kindEnd {
2240	case kindMin:
2241		if value > before {
2242			panic("garden: " + key + " can only be lowered, never raised; it is " + strconv.FormatInt(before, 10) + " and tuning is a one-way ratchet toward the player")
2243		}
2244	case kindMax:
2245		if value < before {
2246			panic("garden: " + key + " can only be raised, never lowered; it is " + strconv.FormatInt(before, 10) + " and tuning is a one-way ratchet toward the player")
2247		}
2248	default:
2249		// Total rather than defensive: a band with no stated kind direction has no safe
2250		// ratchet, so it must not be tunable at all. Rejecting here means adding such a band
2251		// later fails loudly instead of silently escaping the ratchet.
2252		panic("garden: " + key + " has no safe tuning direction and must not be tunable")
2253	}
2254	ruleValues.Set(key, value)
2255
2256	chain.Emit(
2257		"SetRule",
2258		"key", key,
2259		"from", strconv.FormatInt(before, 10),
2260		"to", strconv.FormatInt(value, 10),
2261	)
2262}
2263
2264// FreezeRules ends tuning permanently. One admin-only, irreversible call; afterwards SetRule
2265// panics forever and this realm is exactly as frozen as garden/v2.
2266//
2267// This is what makes the whole tunable-config decision safe to defer rather than get right
2268// before deploy: the owner can deploy tunable, run Check A, apply LOOP.md's numbers and then
2269// freeze -- or freeze on day one and never tune. Both outcomes are reachable from this realm.
2270// Only one is reachable from v2.
2271//
2272// There is no unfreeze, on purpose. A reversible freeze is not a freeze.
2273func FreezeRules(cur realm) {
2274	if !unsafe.OriginSend().IsZero() {
2275		panic("garden: freeze rules accepts no coins; only Buy takes payment, and nothing else here can return them")
2276	}
2277
2278	caller := cur.Previous().Address()
2279	if caller != address(adminAddress) {
2280		panic("garden: only the admin can freeze the rules")
2281	}
2282
2283	if rulesFrozen {
2284		panic("garden: the rules are already frozen")
2285	}
2286
2287	rulesFrozen = true
2288	chain.Emit("FreezeRules", "rules", Rules())
2289}
2290
2291// ============================================================================
2292// Render
2293// ============================================================================
2294
2295// Render is the gnoweb page. Unlike v2's, it shows the garden -- which is the whole point of
2296// this realm, and a player should be able to see their plots without a client at all.
2297//
2298// path is bech32-validated before anything derived from it is echoed back into the page, the
2299// same guard v1, v2, hollowmere and arena all use, for the same reason: path is whatever a URL
2300// put after the realm path, so it is attacker-controlled, and gnoweb renders this return value
2301// as markdown. A rejected path is never reflected at all, which is strictly safer than
2302// escaping it.
2303func Render(path string) string {
2304	if path == "" {
2305		return ufmt.Sprintf(
2306			"# %s\n\nGnome collection. A gnome changes hands only through Buy, and every sale pays the fee below.\n\n"+
2307				"* **Total adopted**: %d\n* **For sale**: %d\n* **Fee**: `%s`\n* **Rules**: `%s`\n* **Tunable within**: `%s`\n",
2308			collectionName, token.TotalSupply(), TotalListings(), FeeRules(), Rules(), RuleBands(),
2309		)
2310	}
2311
2312	// Lower-cased for the same reason List lower-cases a reserved buyer: an upper-case bech32
2313	// string validates, but ownerIndex is keyed by the lower-case form every wallet presents, so
2314	// without this a correct address typed in capitals read "no gnome adopted yet". The echo
2315	// below is still of the VALIDATED value, never of the raw path.
2316	addr := address(lowerASCII(path))
2317	if !addr.IsValid() {
2318		return ufmt.Sprintf("# %s\n\nNo gnome adopted for an invalid address.\n", collectionName)
2319	}
2320
2321	tid, g, ok := gnomeForAddr(addr)
2322	if !ok {
2323		return ufmt.Sprintf("# %s\n\nNo gnome adopted yet for `%s`.\n", collectionName, addr.String())
2324	}
2325
2326	gnomeType := g.gnomeType
2327	if gnomeType == "" {
2328		gnomeType = "(not yet chosen)"
2329	}
2330
2331	now := time.Now().Unix()
2332
2333	str := ufmt.Sprintf("# %s\n\n", g.name)
2334	str += ufmt.Sprintf("* **Owner**: %s\n", addr.String())
2335	str += ufmt.Sprintf("* **Token ID**: %s\n", tid)
2336	str += ufmt.Sprintf("* **Level**: %d\n", g.level)
2337	str += ufmt.Sprintf("* **Calling**: %s\n", gnomeType)
2338	str += ufmt.Sprintf("* **Look**: %s hat, %s coat\n", g.hat, g.coat)
2339	str += ufmt.Sprintf("* **Face**: eyes %d, nose %d (permanent)\n", g.eyes, g.nose)
2340	str += ufmt.Sprintf("* **Acorns**: %d\n", balanceOf(addr))
2341	str += ufmt.Sprintf("* **Belly**: %d/%d\n", meterNow(g.belly, g.bellyAt, now, rule(keyBellyDrop)), meterMax)
2342	str += ufmt.Sprintf("* **Cheer**: %d/%d\n", meterNow(g.cheer, g.cheerAt, now, rule(keyCheerDrop)), meterMax)
2343	belly := meterNow(g.belly, g.bellyAt, now, rule(keyBellyDrop))
2344	str += ufmt.Sprintf("* **Mood**: %s\n", moodNow(belly, g.lastPatAt, g.lastHappyEventAt, g.adoptedAt, now, hungryAtOrBelow))
2345	str += ufmt.Sprintf("* **Days cared**: %d\n", g.caredDayCount)
2346
2347	str += "\n## Garden\n\n"
2348	for i := 0; i < numPlots; i++ {
2349		p := g.plots[i]
2350		switch {
2351		case p.plantedAt == 0:
2352			str += ufmt.Sprintf("* **Plot %d**: empty\n", i)
2353		case now >= p.readyAt:
2354			str += ufmt.Sprintf("* **Plot %d**: ready to harvest\n", i)
2355		default:
2356			str += ufmt.Sprintf("* **Plot %d**: growing, ready in %d hours\n", i, (p.readyAt-now+hourSeconds-1)/hourSeconds)
2357		}
2358	}
2359
2360	// The listing, so an owner who forgot they listed sees it here without a client. Every
2361	// value is this realm's own stored data or an address it validated at List time; nothing
2362	// from the URL path reaches this section.
2363	if l, ok := listingFor(tid); ok {
2364		f := feeFor(l.priceUgnot)
2365		str += "\n## For sale\n\n"
2366		if now > l.expiresAt {
2367			str += "* **Status**: EXPIRED (the owner must list again)\n"
2368		} else {
2369			str += "* **Status**: for sale\n"
2370		}
2371		str += ufmt.Sprintf("* **Price**: %d ugnot to the seller\n", l.priceUgnot)
2372		str += ufmt.Sprintf("* **Buyer pays**: %d ugnot (%d price + %d fee)\n", l.priceUgnot+f, l.priceUgnot, f)
2373		if l.buyer != address("") {
2374			str += ufmt.Sprintf("* **Reserved for**: %s\n", l.buyer.String())
2375		} else {
2376			str += "* **Reserved for**: anyone\n"
2377		}
2378		str += "* **Garden**: NOT included -- planted plots are settled to the seller in acorns at the sale; the buyer receives three empty plots\n"
2379		str += ufmt.Sprintf("* **Expires**: %d (unix seconds)\n", l.expiresAt)
2380	}
2381
2382	return str
2383}
2384
2385// ============================================================================
2386// Helpers
2387// ============================================================================
2388
2389func gnomeForAddr(addr address) (tid string, g *gnome, found bool) {
2390	v := ownerIndex.Get(addr.String())
2391	if v == nil {
2392		return "", nil, false
2393	}
2394	tid, ok := v.(string)
2395	if !ok {
2396		return "", nil, false
2397	}
2398	gv := gnomes.Get(tid)
2399	if gv == nil {
2400		return "", nil, false
2401	}
2402	gp, ok := gv.(*gnome)
2403	if !ok {
2404		return "", nil, false
2405	}
2406	return tid, gp, true
2407}
2408
2409// lowerASCII folds A-Z to a-z and leaves every other byte alone. Used on a typed address
2410// before validation (List's reserved buyer, Render's path): bech32's case-insensitivity is
2411// an ASCII rule, so this is exactly the fold the specification licenses and no more. A
2412// non-ASCII byte is left for address.IsValid() to reject.
2413func lowerASCII(s string) string {
2414	b := []byte(s)
2415	for i, c := range b {
2416		if c >= 'A' && c <= 'Z' {
2417			b[i] = c + ('a' - 'A')
2418		}
2419	}
2420	return string(b)
2421}
2422
2423// anyPlotPlanted is the reserve rule's "is income on its way" test: a planted plot, ready or
2424// growing, is a claim on at least plantCost acorns (Buy's settlement makes that literal), so
2425// a purse below the reserve is survivable while one exists.
2426func anyPlotPlanted(g *gnome) bool {
2427	for i := 0; i < numPlots; i++ {
2428		if g.plots[i].plantedAt != 0 {
2429			return true
2430		}
2431	}
2432	return false
2433}
2434
2435// balanceOf: a missing key is a zero balance, not an error. Only Adopt creates a key, so a
2436// missing key means "no gnome here", which every caller has already checked.
2437func balanceOf(addr address) int64 {
2438	v := balances.Get(addr.String())
2439	n, ok := v.(int64)
2440	if !ok {
2441		return 0
2442	}
2443	return n
2444}
2445
2446// setBalance refuses to store a negative balance. Nothing above should ever produce one --
2447// every spend is guarded by its own "not enough acorns" check -- so this is not a guard that
2448// is expected to fire; it is a guard that makes a future arithmetic mistake abort the
2449// transaction instead of silently minting a debt the economy has no concept of.
2450func setBalance(addr address, v int64) {
2451	if v < 0 {
2452		panic("garden: acorn balance can never go negative")
2453	}
2454	balances.Set(addr.String(), v)
2455}
2456
2457// grantStartingPurseIfNew gives an address acornsStart the FIRST time a gnome arrives in its
2458// hands, by Adopt or by Buy, and never again. See Adopt's comment: a seller keeps their
2459// purse, so neither re-adopting nor buying may refill it.
2460func grantStartingPurseIfNew(addr address) {
2461	if balances.Has(addr.String()) {
2462		return
2463	}
2464	balances.Set(addr.String(), acornsStart)
2465}
2466
2467// feeFor is the whole fee formula: floor + price * rate / 10000, rounded down.
2468//
2469// Written as (price/10000)*rate + ((price%10000)*rate)/10000 rather than price*rate/10000.
2470// The two are identical for every input (write price = 10000q + r; then floor(price*rate/
2471// 10000) = q*rate + floor(r*rate/10000) because q*rate is an integer), but the first form
2472// cannot overflow int64 for any legal input: price <= maxPriceUgnot = 10^15 and rate <=
2473// maxFeeRateBps = 10^4, so (price/10000)*rate <= 10^15 and the remainder term is < 10^4 * 10^4.
2474// The naive form would overflow at price*rate > 9.2*10^18, i.e. a 10^15 price at any rate
2475// above 9223 bps. TestFeeArithmeticMatchesTheNaiveFormula pins the identity.
2476func feeFor(priceUgnot int64) int64 {
2477	return fee.floorUgnot + (priceUgnot/10_000)*fee.rateBps + ((priceUgnot%10_000)*fee.rateBps)/10_000
2478}
2479
2480// exactUgnot is Buy's payment check: one coin, in ugnot, of exactly want. Not "at least".
2481func exactUgnot(send chain.Coins, want int64) bool {
2482	return len(send) == 1 && send[0].Denom == ugnotDenom && send[0].Amount == want
2483}
2484
2485// coinsString renders an envelope for a refusal message; "nothing" for an empty one.
2486func coinsString(c chain.Coins) string {
2487	if len(c) == 0 {
2488		return "nothing"
2489	}
2490	return c.String()
2491}
2492
2493func listingFor(tokenID string) (*listing, bool) {
2494	v := listings.Get(tokenID)
2495	l, ok := v.(*listing)
2496	if !ok || l == nil {
2497		return nil, false
2498	}
2499	return l, true
2500}
2501
2502// rule reads a tunable. A missing key is a bug in this file, not a runtime condition, so it
2503// panics rather than returning a silent zero -- a zero growHours or a zero levelBase would be
2504// a far worse outcome than an aborted call.
2505func rule(key string) int64 {
2506	v := ruleValues.Get(key)
2507	n, ok := v.(int64)
2508	if !ok {
2509		panic("garden: missing rule value for \"" + key + "\"")
2510	}
2511	return n
2512}
2513
2514func bandFor(key string) (ruleBand, bool) {
2515	for _, b := range ruleBands {
2516		if b.key == key {
2517			return b, true
2518		}
2519	}
2520	return ruleBand{}, false
2521}
2522
2523// validNameLength/validNameCharset/hasVisibleNameContent/validNameRune/invisibleLetterRunes:
2524// unchanged from v1 and v2, including v2's audit fix F3 (\uXXXX escapes rather than literal
2525// invisible glyphs, so the denylist is readable in an editor and cannot be silently emptied by
2526// a formatter that strips default-ignorable characters). See v1's own comments for the
2527// exhaustive scan proving the list complete against this toolchain's Unicode version, and
2528// realms/garden/NOTES.md for the open product question about combining marks -- that allowlist
2529// excludes every combining mark, which makes zalgo names structurally impossible and also makes
2530// NFD Latin, Devanagari, Thai, vocalised Hebrew and Arabic unspellable. That trade-off is
2531// recorded as the owner's to revisit and is carried forward here unchanged rather than
2532// silently re-decided.
2533func validNameLength(name string) bool {
2534	n := utf8.RuneCountInString(name)
2535	return n > 0 && n <= maxNameLen
2536}
2537
2538func validNameCharset(name string) bool {
2539	for _, r := range name {
2540		if !validNameRune(r) {
2541			return false
2542		}
2543	}
2544	return true
2545}
2546
2547func hasVisibleNameContent(name string) bool {
2548	for _, r := range name {
2549		if r != ' ' {
2550			return true
2551		}
2552	}
2553	return false
2554}
2555
2556var invisibleLetterRunes = map[rune]bool{
2557	'ᅟ': true, // HANGUL CHOSEONG FILLER
2558	'ᅠ': true, // HANGUL JUNGSEONG FILLER
2559	'ㅤ': true, // HANGUL FILLER
2560	'ᅠ': true, // HALFWIDTH HANGUL FILLER
2561}
2562
2563func validNameRune(r rune) bool {
2564	switch r {
2565	case ' ', '-', '\'':
2566		return true
2567	}
2568	if invisibleLetterRunes[r] {
2569		return false
2570	}
2571	return unicode.IsLetter(r) || unicode.IsDigit(r)
2572}
2573
2574// parseActions parses and validates SaveDay's wire format, enforcing maxActionsPerCall on the
2575// delimiter COUNT before strings.Split ever runs (v2's audit fix F5: Split allocates the full
2576// result slice before any length check could inspect it, so a megabyte of semicolons allocated
2577// half a million short-lived strings before being rejected. strings.Count scans the raw bytes
2578// without allocating a result, so the oversized case now costs nothing proportional to the
2579// input. The caller always paid their own gas for this, so it was never a cross-user DoS --
2580// the fix makes the claim true, it does not close a vulnerability).
2581// quoteAction renders an offending entry for a panic message, TRUNCATED. Without this, the
2582// four malformed-action panics below interpolated the caller's own bytes at full length, so a
2583// semicolon-free, colon-free megabyte passed both delimiter guards and then built a
2584// megabyte-long panic string -- one remaining path that allocated proportional to the input,
2585// which is precisely what this function's own comment claims cannot happen. Harmless (about
2586// 2x amplification of data already in the transaction, entirely caller-paid, and a transaction
2587// abort message is not a markdown sink) but it made the claim false, and the claim ships.
2588//
2589// 48 bytes is comfortably longer than any legal entry -- "harvest:2:1791600000" is 20 -- so a
2590// real player's error message is never truncated and only an abusive one is.
2591func quoteAction(part string) string {
2592	const maxQuoted = 48
2593	if len(part) > maxQuoted {
2594		return "\"" + part[:maxQuoted] + "...\" (truncated)"
2595	}
2596	return "\"" + part + "\""
2597}
2598
2599func parseActions(raw string) []saveAction {
2600	if raw == "" {
2601		return nil
2602	}
2603
2604	if n := strings.Count(raw, ";") + 1; n > maxActionsPerCall {
2605		panic("garden: too many actions in one call (max " + strconv.Itoa(maxActionsPerCall) + ")")
2606	}
2607
2608	// The COLON count, bounded for the same reason and in the same way -- and this half was
2609	// missing. v2's fix F5 (ported above) guards the ';' count before the outer Split, but the
2610	// INNER strings.Split(part, ":") below was unguarded: a single semicolon-free entry with a
2611	// megabyte of colons passes the check above with n == 1, and then allocates one string per
2612	// colon before len(fields) != 2 can reject it. That is precisely the allocation pattern F5
2613	// exists to eliminate, which made this function's own claim about allocating nothing
2614	// proportional to the input false for the inner split.
2615	//
2616	// The legal maximum is two colons per entry ("plant:<plot>:<ts>"), so the whole string can
2617	// never legitimately carry more than 2*maxActionsPerCall of them. Rejected on a byte scan
2618	// with no allocation, like the check above. As with F5 the caller always paid their own gas
2619	// for this, so it was never a cross-user DoS -- this makes the comment true rather than
2620	// closing a vulnerability.
2621	if strings.Count(raw, ":") > 2*maxActionsPerCall {
2622		panic("garden: malformed action list (too many fields)")
2623	}
2624
2625	parts := strings.Split(raw, ";")
2626
2627	actions := make([]saveAction, 0, len(parts))
2628	for _, part := range parts {
2629		fields := strings.Split(part, ":")
2630		switch fields[0] {
2631		case actionPat, actionFeed, actionTinker:
2632			if len(fields) != 2 {
2633				panic("garden: malformed action " + quoteAction(part))
2634			}
2635			actions = append(actions, saveAction{
2636				kind: fields[0],
2637				at:   parseUnixField(fields[1], part),
2638			})
2639		case actionPlant, actionHarvest:
2640			if len(fields) != 3 {
2641				panic("garden: malformed action " + quoteAction(part))
2642			}
2643			actions = append(actions, saveAction{
2644				kind: fields[0],
2645				plot: parsePlotField(fields[1], part),
2646				at:   parseUnixField(fields[2], part),
2647			})
2648		default:
2649			panic("garden: unknown action kind in " + quoteAction(part))
2650		}
2651	}
2652	return actions
2653}
2654
2655func parseUnixField(s, part string) int64 {
2656	v, err := strconv.ParseInt(s, 10, 64)
2657	if err != nil || v <= 0 {
2658		panic("garden: invalid timestamp in action " + quoteAction(part))
2659	}
2660	return v
2661}
2662
2663func parsePlotField(s, part string) int {
2664	v, err := strconv.Atoi(s)
2665	if err != nil || v < 0 || v >= numPlots {
2666		panic("garden: invalid plot index in action " + quoteAction(part))
2667	}
2668	return v
2669}
2670
2671// dayNumber is the UTC calendar day index for a unix-second timestamp. Integer division by
2672// 86400 lands exactly on 00:00 UTC boundaries, matching brief 3.1 with no timezone handling,
2673// because unix time is UTC by definition.
2674func dayNumber(unixSeconds int64) int64 { return unixSeconds / daySeconds }
2675
2676// recordCaredDay folds one cared UTC day into the gnome's lifetime count and its 64-day
2677// window. See the gnome struct's own comment for the storage shape and DESIGN.md section 2.3
2678// for why the chain records days and never computes the streak.
2679//
2680// This handles a day arriving AT OR BEFORE lastCaredDay even though SaveDay's replay rule
2681// (every action strictly after lastActionAt) makes that unreachable today. Two extra lines to
2682// make this function correct on its own terms rather than correct only because of an invariant
2683// enforced two hundred lines away. On a contract that can never be edited, a total function is
2684// worth more than the lines it costs -- and a reader of this function alone should not have to
2685// go and verify SaveDay's ordering rule to know it is sound.
2686func recordCaredDay(g *gnome, day int64) {
2687	if g.lastCaredDay == 0 {
2688		g.lastCaredDay = day
2689		g.caredMask = 1
2690		g.caredDayCount = 1
2691		return
2692	}
2693
2694	if day > g.lastCaredDay {
2695		shift := uint(day - g.lastCaredDay)
2696		if shift >= caredWindowDays {
2697			// Everything in the window is now older than the window. Go defines a shift of
2698			// >= the operand width on an unsigned value as 0, so `g.caredMask <<= shift` would
2699			// do this anyway -- written out because relying on that is a reader's trap.
2700			g.caredMask = 0
2701		} else {
2702			g.caredMask <<= shift
2703		}
2704		g.caredMask |= 1
2705		g.lastCaredDay = day
2706		g.caredDayCount++
2707		return
2708	}
2709
2710	// day <= lastCaredDay.
2711	back := uint(g.lastCaredDay - day)
2712	if back >= caredWindowDays {
2713		// Older than the window: it still happened, so it counts toward the lifetime total,
2714		// but there is no bit left to record it in.
2715		g.caredDayCount++
2716		return
2717	}
2718	bit := uint64(1) << back
2719	if g.caredMask&bit != 0 {
2720		// Already recorded. Two saves on one UTC day are one cared day.
2721		return
2722	}
2723	g.caredMask |= bit
2724	g.caredDayCount++
2725}
2726
2727// caredDaysCsv decodes the window, newest first.
2728func caredDaysCsv(g *gnome) string {
2729	if g.lastCaredDay == 0 {
2730		return ""
2731	}
2732	days := make([]string, 0, caredWindowDays)
2733	for i := uint(0); i < caredWindowDays; i++ {
2734		if g.caredMask&(uint64(1)<<i) == 0 {
2735			continue
2736		}
2737		days = append(days, strconv.FormatInt(g.lastCaredDay-int64(i), 10))
2738	}
2739	return strings.Join(days, ",")
2740}
2741
2742// meterNow ports app/js/rules.js's meterNow exactly: work the current value out from a
2743// last-known value, the unix second it was set, and how many whole dropEveryHours periods have
2744// elapsed since, clamped to [0, meterMax].
2745//
2746// A tunable decay rate re-derives history under the new rate, which is harmless precisely
2747// because meters saturate: the affected span is bounded in practice by the 48-hour backfill
2748// window, and a long-dormant gnome's belly is already clamped at 0 under either rate. See
2749// DESIGN.md section 5.4.
2750func meterNow(value, setAt, now, dropEveryHours int64) int64 {
2751	// Total rather than defensive, and held to this file's own stated standard: recordCaredDay
2752	// argues that "a reader of this function alone should not have to go and verify SaveDay's
2753	// ordering rule to know it is sound", and this function was the counter-example -- it
2754	// divided by a parameter whose non-zeroness was guaranteed only by a band table twelve
2755	// hundred lines away. Unreachable today (both decay bands have non-zero minima and the
2756	// ratchet only raises them), and a division panic would brick every read and every save at
2757	// once, so the guard costs three lines and removes a whole-realm failure mode from the
2758	// set of things a future band edit could cause.
2759	if dropEveryHours <= 0 {
2760		panic("garden: a meter decay rate must be positive")
2761	}
2762	if now < setAt {
2763		now = setAt
2764	}
2765	drops := (now - setAt) / (dropEveryHours * hourSeconds)
2766	return clampMeter(value - drops)
2767}
2768
2769func clampMeter(v int64) int64 {
2770	if v < 0 {
2771		return 0
2772	}
2773	if v > meterMax {
2774		return meterMax
2775	}
2776	return v
2777}
2778
2779// canPat ports app/js/rules.js's canPat. lastPatAt == 0 means never patted; see the gnome
2780// struct's comment for why 0 is an unambiguous sentinel here and is not in the JS.
2781func canPat(lastPatAt, now, cooldownHours int64) bool {
2782	return lastPatAt == 0 || now-lastPatAt >= cooldownHours*hourSeconds
2783}
2784
2785// moodNow ports app/js/rules.js's CURRENT moodNow, in brief table 3.3's first-match-wins order:
2786// hungry, then sulky, then happy, then content. "Asleep" is display-only and has no on-chain
2787// representation, exactly as in rules.js.
2788//
2789// THE adoptedAt FLOOR IS THE FIX FOR A DIVERGENCE v2 CANNOT BE FIXED FOR, and it is the reason
2790// this function takes a parameter v2's did not. v2 ported rules.js's mood as it then stood,
2791// where a never-patted gnome's "last attention" fell back to the unix epoch, so "no pat for 72
2792// hours" was trivially true and every brand-new gnome read as sulky -- v2's own comment flags
2793// this and notes rules.test.js never exercised it. rules.js:103-119 has since been fixed, for
2794// a reported bug: "EVERY newly adopted gnome was instantly sulky" and "the very first thing a
2795// new player read was '<name>'s in a huff'". v2 cannot be edited, so the deployed realm and the
2796// shipped browser now disagree about the mood of the same gnome, and the chain's answer is the
2797// one that applies the XP penalty. This matches the fixed version:
2798//
2799//	neverPatted -> floor "last attention" at adoptedAt, because a gnome has been cared for, by
2800//	definition, at the moment somebody chose it.
2801//
2802// rules.js also maxes in lastVisitReceivedAt. This realm has no visit mechanic (DESIGN.md
2803// section 7.1), so that input is permanently absent, and rules.js with lastVisitReceivedAt ===
2804// null gives the identical answer. The floor applies ONLY to a gnome that has genuinely never
2805// been patted: rules.js's own comment records that an earlier attempt applied it
2806// unconditionally and quietly made neglect unreachable, so a gnome last patted 73 hours ago
2807// still sulks here, as it should.
2808//
2809// hungryThreshold is a PARAMETER rather than a package-constant reference inside the body, so
2810// this function is pure in its arguments and its formula can be checked against rules.js
2811// without knowing any realm state. It is deliberately NOT named hungryAtOrBelow: that is the
2812// name of the package constant every caller passes, and shadowing it here would leave a reader
2813// of this body alone unable to tell whether the threshold is fixed or caller-supplied -- on a
2814// file whose stated standard is that reading one function should be enough. The older note on
2815// why the parameter exists at all:
2816//
2817// the threshold was once a rule(...) lookup inside the body, so this
2818// function is pure in its arguments. It used to read the rule itself while SaveDay had already
2819// cached that same value for the planting gate -- harmless today, but it made the answer depend
2820// on realm state the signature did not mention, which is exactly the kind of hidden input that
2821// makes a formula hard to check against rules.js.
2822func moodNow(belly, lastPatAt, lastHappyEventAt, adoptedAt, now, hungryThreshold int64) string {
2823	if belly <= hungryThreshold {
2824		return moodHungry
2825	}
2826
2827	lastAttention := lastPatAt
2828	if lastPatAt == 0 {
2829		lastAttention = adoptedAt
2830	}
2831	if now-lastAttention >= sulkyAfterHours*hourSeconds {
2832		return moodSulky
2833	}
2834
2835	if lastHappyEventAt != 0 && now-lastHappyEventAt <= happyWindowHours*hourSeconds {
2836		return moodHappy
2837	}
2838	return moodContent
2839}
2840
2841// xpToNext/totalXpForLevel/levelFromXp port app/js/rules.js's functions of the same name:
2842// levelBase + levelStep*level to reach the next level, maxLevel the ceiling. levelBase and
2843// levelStep are tunable, so a level is recomputed from totalXp on every save -- the bands only
2844// permit them to fall, so a level can rise from a tune and can never drop. arena/v1 reads this
2845// level for ranked eligibility, so a tune can make a gnome newly eligible and never newly
2846// ineligible. DESIGN.md section 5.4.
2847// xpToNextWith/totalXpForLevelWith take the curve's two numbers as ARGUMENTS rather than
2848// reading them per call, and levelFromXp below reads them exactly once.
2849//
2850// This is not style. The obvious shape -- xpToNext() reading rule(keyLevelBase) and
2851// rule(keyLevelStep) itself -- puts two avl lookups inside a loop that is inside another loop
2852// bounded by maxLevel, so one SaveDay paid on the order of 800 tree reads to work out a level
2853// that needs two numbers and some arithmetic. garden/v2 had the numbers as compile-time
2854// constants and paid nothing; making them tunable must not quietly cost that.
2855func xpToNextWith(level, base, step int64) int64 { return base + step*level }
2856
2857func totalXpForLevelWith(level, base, step int64) int64 {
2858	var t int64
2859	for l := int64(1); l < level; l++ {
2860		t += xpToNextWith(l, base, step)
2861	}
2862	return t
2863}
2864
2865// xpToNext/totalXpForLevel are the single-argument forms the tests and any future reader
2866// want, kept so the curve can be asked about without threading the two numbers by hand.
2867func xpToNext(level int64) int64 {
2868	return xpToNextWith(level, rule(keyLevelBase), rule(keyLevelStep))
2869}
2870
2871func totalXpForLevel(level int64) int64 {
2872	return totalXpForLevelWith(level, rule(keyLevelBase), rule(keyLevelStep))
2873}
2874
2875func levelFromXp(totalXp int64) int64 {
2876	base, step := rule(keyLevelBase), rule(keyLevelStep)
2877	level := int64(1)
2878	for level < maxLevel && totalXp >= totalXpForLevelWith(level+1, base, step) {
2879		level++
2880	}
2881	return level
2882}