Skip to content

Framework Reference

Everything the framework registers with SMAPI and the game: console commands for testing, item queries for shops, unlock keys for progression, Content Patcher tokens for pack authors, tile actions for mappers and the maps it adds.

All strings on this page are exact unless marked otherwise.


Console commands

Type these into the SMAPI console while the game is running.

Command Arguments Description
campgrounds_startcamp <CAMPGROUND_DATA_ID> [TENT_DATA_ID] [GUEST_NAME] Starts a camping trip immediately. The fastest way to test a campground without playing to it.
campgrounds_addrations <AMOUNT> Adds Camp Rations to the player. Negative values subtract, but see the note on overspending.
campgrounds_setcampingtotaldays <TOTAL_DAYS> Sets the total nights camped, stored in the vanilla stats system under PeacefulEnd.Campgrounds_TotalNightsGoneCamping.
campgrounds_opententmenu (none) Opens the TentListMenu directly.
campgrounds_startcamp YourName.MyPack_RiverClearing YourName.MyPack_CanvasTent Abigail

Pair with Content Patcher's commands

patch reload YourName.MyPack re-applies your pack without restarting. The framework picks up the invalidated data automatically. Then campgrounds_startcamp to look at the result. That loop takes seconds.

patch summary tells you whether your patch applied at all. Check it first when nothing seems to happen.


Item queries

Use these anywhere the game accepts an item query, most usefully in shop data, so players can buy their way into your content.

Query Argument Returns
(O)PeacefulEnd.Campgrounds.Items.CampsiteMap <CAMPGROUND_DATA_ID> A campsite map that unlocks that campground.
(O)PeacefulEnd.Campgrounds.Items.CampfireRecipe <CAMPFIRE_FOOD_DATA_ID> A recipe that unlocks that campfire food.
(O)PeacefulEnd.Campgrounds.Items.TentSchematic <CAMPING_TENT_DATA_ID> A schematic that unlocks that tent.
PeacefulEnd.Campgrounds.Currency.CampRations.Id (none) A Camp Rations item.
PeacefulEnd.Campgrounds_UNKNOWN_COOKING_RECIPES (none) The day's unknown cooking recipes. Used by the visitor shops.

Selling a campsite map in a shop:

{
  "Action": "EditData",
  "Target": "Data/Shops",
  "TargetField": [ "SomeShopId", "Items" ],
  "Entries": {
    "{{ModId}}_RiverClearingMap": {
      "Id": "{{ModId}}_RiverClearingMap",
      "ItemId": "(O)PeacefulEnd.Campgrounds.Items.CampsiteMap {{ModId}}_RiverClearing",
      "Price": 5000
    }
  }
}

Sell them in the built-in camp shop

The framework's own shop, PeacefulEnd.Campgrounds.Shops.CampShop, is an ordinary Data/Shops entry, so you can EditData it to stock your campsite maps, tent schematics and recipes right at the caretaker's counter, using the item queries below. That puts your unlockables where players already look for camping supplies.

The argument is not optional

The first three queries share their name with the item they create, which is a neat trick but has a sharp edge: writing the bare "ItemId": "(O)PeacefulEnd.Campgrounds.Items.CampsiteMap" resolves as a query with no arguments, not as a plain item. That fails and logs a shop error.

Always pass the Id. And pass a real one. All three queries look the argument up in loaded data and return an error result if it doesn't resolve, so a typo shows up as a shop error rather than silently stocking a broken item. The message names the Id you gave:

No campground found with the ID "{your Id}".
No campfire food found with the ID "{your Id}".
No camping tent found with the ID "{your Id}".

What these items actually do

They aren't keepsakes. The moment one enters the player's inventory the framework consumes it, plays the "hold above head" animation with a discovery message and sets a world state flag, the same pattern the game uses for special items. The item is then removed.

Which means: the item is a delivery mechanism for an unlock flag and the flag is what your content should actually check.


Unlock keys

When one of the items above is picked up, the framework sets a world state flag built from the data Id:

Item Flag set
Campsite map MAP_UNLOCKED_CAMPGROUND:<CAMPGROUND_DATA_ID>
Campfire recipe RECIPE_UNLOCKED_CAMPFIRE_FOOD:<CAMPFIRE_FOOD_DATA_ID>
Tent schematic SCHEMATIC_UNLOCKED_TENT:<CAMPING_TENT_DATA_ID>

Flags are set everywhere (for all players), not per player.

The buy-to-unlock pattern

This is the main progression loop the framework is built around and nothing in the models hints at it. Check the flag with the vanilla WORLD_STATE_ID query in your entry's UnlockCondition:

{
  "Id": "{{ModId}}_RiverClearing",
  "UnlockCondition": "WORLD_STATE_ID MAP_UNLOCKED_CAMPGROUND:{{ModId}}_RiverClearing",
  "UnlockHint": "Maybe someone in town sells maps.",
  "PlayerSpawnTile": { "X": 12, "Y": 18 },
  "GuestSpawnTile": { "X": 14, "Y": 18 }
}

Sell the matching map in a shop and the campground unlocks when the player buys it. The same shape works for tents (SCHEMATIC_UNLOCKED_TENT:) and foods (RECIPE_UNLOCKED_CAMPFIRE_FOOD:).

The Id appears four times, keep them identical

The Entries key, the entry's Id, the item query argument and the UnlockCondition flag all embed the same Id. Using {{ModId}} in each keeps them in sync.

Casing is forgiving where it's looked up, strict where it's compared. The item query and event command match your Id case-insensitively and the flag they set always uses the canonical casing from your entry's Id, so campsitemap yourname.mypack_riverclearing still unlocks YourName.MyPack_RiverClearing. But WORLD_STATE_ID compares the flag as plain text, so the Id in your UnlockCondition must match your entry's Id exactly, casing included.

In practice both live in the same file and both use {{ModId}}, so they agree by construction. It helps to know which half is which if you ever hand-write one of them.

Don't put spaces in your Ids

Game state query arguments are space-delimited, so a campground Id containing a space would split the flag across two arguments and never match. Ids shouldn't have spaces anyway. This is one more reason.


Currency

Constant Value Purpose
Currency / item query ID PeacefulEnd.Campgrounds.Currency.CampRations.Id Registered both as the specialCurrencyDisplay key and as an item query.
Balance storage key PeacefulEnd.Campgrounds.Currency.CampRations.Balance Where the balance lives in Game1.player.modData.

The balance is clamped at a minimum of 0. Spending more than the player has fails rather than going negative.

The currency icon

The Camp Rations icon is drawn from a patchable asset:

Data/PeacefulEnd.Campgrounds/Campgrounds/Textures/CurrencyIcons

Camp Rations use the 16×16 sprite at (0, 0). You can retexture it with an EditImage patch:

{
  "Action": "EditImage",
  "Target": "Data/PeacefulEnd.Campgrounds/Campgrounds/Textures/CurrencyIcons",
  "FromFile": "assets/rations_icon.png",
  "ToArea": { "X": 0, "Y": 0, "Width": 16, "Height": 16 }
}

You can also read a player's ration balance from a Game State Query without any framework support, since it's stored in modData. But mind the comparison:

PLAYER_MOD_DATA Current PeacefulEnd.Campgrounds.Currency.CampRations.Balance 25

This is an exact string match, not a threshold

The balance is stored as the integer's text form (_campRations.ToString()) and PLAYER_MOD_DATA compares it as a string. The query above matches a balance of exactly 25, not "25 or more". There's no built-in way to express "at least N rations" as a GSQ against this key. If you need a threshold, gate on something else (a purchased unlock item, the nights-camped stat) instead.

Inside the framework's own menus the balance is compared numerically, so affordability checks there behave as you'd expect. This caveat is only about GSQs reading the raw stored value.


Stats

The framework records several stats in the game's vanilla stats system. Each is a plain integer you can read from a Game State Query or display through a Content Patcher token. The first three are cumulative tallies. The last three are live counts of how much content is currently unlocked.

Stat ID Meaning
PeacefulEnd.Campgrounds_TotalNightsGoneCamping Total nights the player has spent camping.
PeacefulEnd.Campgrounds_TotalGuestsInvited Total guests brought on overnight trips. Counted per guest when a trip is completed, so re-inviting the same guest on a later trip adds to it again. Inviting a guest without camping overnight doesn't count.
PeacefulEnd.Campgrounds_TotalCampMealsMade Total meals cooked at the campfire across all trips. Cooking happens once per trip but a single session can produce several meals at once. Each meal made adds to the total.
PeacefulEnd.Campgrounds_TotalCampsitesUnlocked How many campgrounds are currently unlocked.
PeacefulEnd.Campgrounds_TotalTentsUnlocked How many tent schematics are currently unlocked.
PeacefulEnd.Campgrounds_TotalRecipesUnlocked How many campfire recipes are currently unlocked.

Because these are vanilla stats, you can gate content on them from a Game State Query with PLAYER_STAT, for example unlocking a campground only after the player has camped a few times:

{ "UnlockCondition": "PLAYER_STAT Current PeacefulEnd.Campgrounds_TotalNightsGoneCamping 5" }

Content Patcher tokens

The framework registers six custom Content Patcher tokens, one for each stat above. Use them to drop the current value straight into any text field.

Token Value
PeacefulEnd.Campgrounds/TotalNightsGoneCamping Nights the player has spent camping.
PeacefulEnd.Campgrounds/TotalGuestsInvited Guests brought on completed overnight trips (repeat invites count).
PeacefulEnd.Campgrounds/TotalCampMealsMade Meals cooked at the campfire across all trips.
PeacefulEnd.Campgrounds/TotalCampsitesUnlocked Campgrounds currently unlocked.
PeacefulEnd.Campgrounds/TotalTentsUnlocked Tent schematics currently unlocked.
PeacefulEnd.Campgrounds/TotalRecipesUnlocked Campfire recipes currently unlocked.

Each token is prefixed with the framework's mod ID (PeacefulEnd.Campgrounds/) and resolves to the stat's current value as a number. Any pack that already lists Campgrounds as a dependency can use them with no extra setup:

{
  "Action": "EditData",
  "Target": "Characters/Dialogue/Robin",
  "Entries": {
    "Mon": "You've camped {{PeacefulEnd.Campgrounds/TotalNightsGoneCamping}} nights now? Sounds like you've earned a rest."
  }
}

Token to display, PLAYER_STAT to gate

These tokens are for showing a number in text. To gate content on a threshold (such as only after ten nights), use the PLAYER_STAT Game State Query instead.

Availability and freshness

The tokens return nothing on the title screen (before a save loads), so any patch relying on them is inactive there. Their value refreshes on Content Patcher's context updates, not continuously. For nights, guests and meals that makes no difference, since those only change on a day change. The three unlock counts are the exception: the framework recomputes them about once a second, so their real value can change mid-day the moment the player unlocks something, but a token displaying one won't catch up until the next context update. If you need an unlock count the instant it changes, read the stat via C#.


Player modData keys

Key Contents
PeacefulEnd.Campgrounds.Currency.CampRations.Balance The player's Camp Rations balance.
Campgrounds.NextVisit.Cooldown.Id A JSON object mapping each VisitorData.Id to the day its visit cooldown expires, as a days-since-start number. Written on save, read each morning.
Campgrounds.NextInvite.Cooldown.<VillagerName> The day a villager's invite cooldown expires, as a days-since-start number. One key per villager, written when a trip is taken.

The intro event

Three of the tile actions below refuse to do anything until the player has seen:

PeacefulEnd.Campgrounds.Events.CampingIntro

Until then they show a "talk to Clem" message instead. If you're testing and the camp list won't open, this is why.

Check it yourself with PLAYER_HAS_SEEN_EVENT Current PeacefulEnd.Campgrounds.Events.CampingIntro.


Tile actions

For map authors. Set as the Action tile property on the Buildings layer.

Action Arguments Description
PeacefulEnd.Campgrounds_CampingSiteList (none) Opens CampListMenu, where the player chooses a campground. Requires the intro event.
PeacefulEnd.Campgrounds_CarRepair (none) Offers the garage car repair. Requires the intro event, plus the materials below.
PeacefulEnd.Campgrounds_RepairVisitorSite <SITE_ID> Offers to repair visitor site 1, 2 or 3. Requires the intro event, plus the materials below.
PeacefulEnd.Campgrounds_CampShopCounter (none) Opens the camp shop counter, a dialogue offering supplies, the tent catalogue or leaving. The supplies option opens the shop PeacefulEnd.Campgrounds.Shops.CampShop, run by the caretaker PeacefulEnd.Campgrounds.Characters.Caretaker.
PeacefulEnd.Campgrounds_RepairVisitorSite 1

An invalid site ID does nothing, quietly

_RepairVisitorSite switches on 1, 2 and 3. Any other number falls through the switch and the action returns success with no dialogue, no repair and no error. If your repair tile does nothing when interacted with, check the number.

Repair costs

Each repair is only offered if the player is carrying the materials:

Repair Materials
Garage car 5× Battery Pack (O)787, 10× Iron Bar (O)335, 100× Wood (O)388
Visitor site 1 2× Iron Bar (O)335, 100× Wood (O)388
Visitor site 2 5× Iron Bar (O)335, 25× Hardwood (O)709, 200× Wood (O)388
Visitor site 3 5× Pine Tar (O)726, 15× Iron Bar (O)335, 50× Hardwood (O)709, 200× Stone (O)390

Otherwise the player gets a "missing items" message. Note site 3 wants Stone, not Wood, unlike the other two.

Touch actions

Set as the TouchAction tile property.

Action Description
PeacefulEnd.Campgrounds_ParkClosed Shows the "park pathway closed" message.
PeacefulEnd.Campgrounds_CampingExit Ends the camping trip. Offers to head back to the park, return to the farm or cancel.

The leave-early warning

_CampingExit has an extra confirmation step that protects the player from wasting a meal.

Cooking a campfire meal doesn't apply its buffs immediately. The buffs are cached and applied the next morning (they're the reward for staying the night). So if the player has cooked but then tries to leave the same day, the framework shows a "leave without camping?" Yes/No prompt first:

  • Yes: the cached meal is discarded (ClearBuffs) and the player exits. They cooked, but they leave before the buff pays out, so they lose it.
  • No: the player stays at the campsite.

If the player hasn't cooked yet, there's nothing to lose, so this prompt is skipped and they go straight to the exit menu below.

The exit menu

Once past that check (or straight away, if nothing was cooked), the player chooses:

Option Result
Head back to the park Ends the trip and warps to Cindersap Park.
Return to the farm Ends the trip and warps to the farmhouse front door.
Cancel Stays at the campsite.

Map tile properties

For map authors and for anyone writing an advanced visitor, because this is how those spawn their NPCs.

The framework scans the park map every morning and reads these properties.

Back layer

Property Value Purpose
IsVisitorSpawn (any, presence is what counts) Marks a spawn point for a standard visitor. Must be paired with VisitorSpot.
VisitorSpot SW, NW or SE Which site this spawn point belongs to. Parsed case-insensitively.
SpawnVisitorName An NPC's internal name Spawns that NPC on this tile, unconditionally.
VisitorFacingDirection North, East, South, West Which way the spawned NPC faces. Case-insensitive, defaults to North if absent or unparseable.

IsVisitorSpawn + VisitorSpot and SpawnVisitorName are two separate mechanisms:

  • IsVisitorSpawn + VisitorSpot is a slot. It's only used if a standard visitor is active at that site. The framework decides who fills it. Place several per site (they're shuffled) and the visitor is placed on one of them at random.
  • SpawnVisitorName is a named placement. It spawns that specific NPC every day, regardless of which visitor was selected or whether any site is repaired. This is how an advanced visitor's patched map puts its own characters on the map.

A bad VisitorSpot value silently degrades

The two mechanisms are checked as if/else if on the same tile. A tile with IsVisitorSpawn whose VisitorSpot doesn't parse falls through to the SpawnVisitorName branch and if that's absent too, the tile does nothing at all. No warning is logged. If a spawn point is being ignored, check the spelling of VisitorSpot first.

Spawned NPCs are frozen in place for the day. Any NPC in the park that isn't spawned this way is returned to its normal schedule each morning, except the park's caretaker, PeacefulEnd.Campgrounds.Characters.Caretaker, which is left alone.

Campground maps

The tile properties above are read on the park map. A separate set is read on each campground map (the map a visitor travels to) to place the tents and campfire. All are on the Back layer, and all three positions are mandatory: a campground map missing any of them fails setup, logs a warning and the trip is aborted.

Property Value Purpose
IsCampingSpot (any, presence counts) Marks where a tent is pitched. Read twice (see IsForGuest).
IsForGuest T on the guest's tent tile, absent on the player's Distinguishes the two IsCampingSpot tiles.
CampingDirection Direction Which way that tent faces. Defaults to South if absent or unparseable.
IsCookingSpot (any, presence counts) Marks the campfire / cooking spot.

So a campground map needs three tagged tiles: a player IsCampingSpot, a guest IsCampingSpot with IsForGuest = T and an IsCookingSpot.

The guest tent tile is required even for solo sites

Setup checks for the player tile, the guest tile and the cooking tile in that order and bails on the first one missing. The guest tile is required whether or not a guest ever comes. Omit it and the site won't load even when the player travels alone. The exact warnings are:

The campgrounds map with name {id} is missing the player's tent spot (IsCampingSpot tile property on Back layer)
The campgrounds map with name {id} is missing the guest's tent spot (IsCampingSpot and IsForGuest tile property on Back layer)
The campgrounds map with name {id} is missing a cooking spot (IsCookingSpot tile property on Back layer)

If a campground silently refuses to load, check the SMAPI log for one of these three.

This is what CampgroundMapDetails is

The framework reads these tiles into a CampgroundMapDetails object at runtime. That model isn't something you write in JSON. See the model index.

Buildings layer

Property Value Purpose
IsCampfire (any, presence is what counts) Adds a campfire light source on this tile.
HasSmoke (any, presence is what counts) Emits a smoke puff roughly once a second. Only read on a tile that also has IsCampfire.

The light is a sconce-type source with radius 2.5 and colour (0, 80, 160). Smoke puffs are offset +24 px right and -16 px up from the tile's top-left corner, so they rise from roughly the middle of the tile.

HasSmoke without IsCampfire does nothing

HasSmoke is only checked inside the IsCampfire branch. A smoking chimney with no campfire beneath it needs both properties on the same tile.

Both are rescanned each morning after the map is invalidated, so a campfire your visitor's map patch brings in will light up on the day that visitor arrives.


Event commands

For use in event scripts.

Command Arguments Description
PeacefulEnd.Campgrounds_GiveRations <AMOUNT> Changes the player's Camp Rations balance by <AMOUNT>.
PeacefulEnd.Campgrounds_GiveCampsiteMap <CAMPGROUND_DATA_ID> Unlocks that campground, with the discovery animation and message.
PeacefulEnd.Campgrounds_GiveCampfireRecipe <CAMPFIRE_FOOD_DATA_ID> Unlocks that campfire recipe, with the discovery animation and message.
PeacefulEnd.Campgrounds_GiveTentSchematic <CAMPING_TENT_DATA_ID> Unlocks that tent schematic, with the discovery animation and message.
PeacefulEnd.Campgrounds_GiveRations 25
PeacefulEnd.Campgrounds_GiveCampsiteMap YourName.MyPack_RiverClearing
PeacefulEnd.Campgrounds_GiveCampfireRecipe YourName.MyPack_ToastedMarshmallow
PeacefulEnd.Campgrounds_GiveTentSchematic YourName.MyPack_CanvasTent

A missing argument, a malformed one or a campground Id that doesn't match any loaded data logs an error and skips the command. The event carries on from the next line.

_GiveCampsiteMap doesn't give an item

Despite the name, this command never puts a campsite map in the player's inventory. It does what picking one up would do, minus the item: sets the MAP_UNLOCKED_CAMPGROUND:<id> world state flag, plays the hold-above-head animation and shows the discovery message.

So it's the event-scripting equivalent of the item. Reach for it when a character hands the player a campsite in a cutscene and for the item query when they buy one.

The Id is matched case-insensitively, consistent with campgrounds_startcamp and the item queries. An Id that matches nothing is reported as:

No campground found with the ID "{your Id}".

The command is then skipped rather than half-applied. The player gets no unlock and no message and the event continues.

_GiveCampfireRecipe and _GiveTentSchematic behave the same way for their content, setting the matching RECIPE_UNLOCKED_CAMPFIRE_FOOD:<id> or SCHEMATIC_UNLOCKED_TENT:<id> world state flag with the same animation and message and likewise never hand over an item. To place an actual item in a cutscene, spawn the item instead.


Spending rations

campgrounds_addrations and PeacefulEnd.Campgrounds_GiveRations both go through the same balance change and both accept negative amounts.

An overspend is rejected, not clamped

If a subtraction would take the balance below zero, the framework rejects the whole change and leaves the balance untouched. It does not clamp to 0 and it does not partially spend.

So PeacefulEnd.Campgrounds_GiveRations -100 against a balance of 50 leaves the player with 50, not 0. The event command reports this and skips:

Unable to change the player's Camp Rations balance by -100, as their current balance is 50.

The console command doesn't report it. It's a testing tool, so check the balance yourself if a campgrounds_addrations with a negative number appears to do nothing.

If an event charges the player rations, gate it on the balance rather than assuming the charge landed. The balance is readable from a Game State Query via PLAYER_MOD_DATA, subject to the caveat noted there.


Items and tools

Id Description
(O)PeacefulEnd.Campgrounds.Items.CampsiteMap Campsite map. Consumed on pickup, sets MAP_UNLOCKED_CAMPGROUND:.
(O)PeacefulEnd.Campgrounds.Items.CampfireRecipe Campfire recipe. Consumed on pickup, sets RECIPE_UNLOCKED_CAMPFIRE_FOOD:.
(O)PeacefulEnd.Campgrounds.Items.TentSchematic Tent schematic. Consumed on pickup, sets SCHEMATIC_UNLOCKED_TENT:.
PeacefulEnd.Campgrounds.Tools.WalkieTalkie Walkie-talkie tool. Using it asks whether to start the cutscene showing who's at the park. The prompt differs before and after 12:00.

Each of the three unlock items carries its target Id in modData under Campgrounds.Items.CampsiteMap.Data.Id, Campgrounds.Items.CampfireRecipe.Data.Id or Campgrounds.Items.TentSchematic.Data.Id respectively. The item's displayed name and description are generated from that Id at runtime, so a map for your campground reads as your campground's name without you defining an item for it.


Maps and locations

Asset / location Description
Maps/PeacefulEnd.Campgrounds.ContentPatcher_CindersapPark The park map. Edit this to change the park itself.
Maps/PeacefulEnd.Campgrounds.ContentPatcher_CindersapParkOvergrown The overgrown source map, patched over any unrepaired visitor site.
Maps/PeacefulEnd.Campgrounds.ContentPatcher_CindersapParkActive The active park map.
PeacefulEnd.Campgrounds.ContentPatcher_CindersapParkGarage The garage location, where the car is repaired.

The park's visitor sites

The framework edits the park map on load, patching overgrown tiles over each visitor site the player hasn't repaired. The areas it uses, in tile coordinates:

Site ID RequiredSpot Area patched when unrepaired Position
1 SW X: 0, Y: 25, Width: 22, Height: 26 Bottom-left
2 NW X: 0, Y: 0, Width: 35, Height: 25 Top-left
3 SE X: 40, Y: 0, Width: 29, Height: 51 Right-hand side

The site ID is what PeacefulEnd.Campgrounds_RepairVisitorSite takes as its argument. The RequiredSpot column is what an advanced visitor writes to claim the same site. Repairing a site sets the world state flag CINDERSAP_PARK_VISITOR_SITE_<id>, so you can gate content on a site being open with, for example, WORLD_STATE_ID CINDERSAP_PARK_VISITOR_SITE_1 for the SW site.

How a site's map is assembled

Each site's appearance is the result of up to two patches layered onto the base park map:

Site state What's patched over it
Not repaired The overgrown map, with Replace.
Repaired, no visitor available today Nothing (the base park map shows through).
Repaired, standard visitor today The active map, with Replace.
Repaired, advanced visitor today That visitor's own MapPatches, each with its own PatchMode.

The park map is invalidated every morning, so this is re-evaluated daily.

That's the practical difference between the two visitor types: a standard visitor borrows the park's built-in campsite, an advanced visitor brings its own.

Overgrowth patches use Replace

The overgrowth is applied with PatchMapMode.Replace, so it clears the site completely. Anything your pack adds to an unrepaired site's area on the base park map is wiped until the player repairs it. Patch the site through MapVisitorSettings rather than editing the park map directly.


Other behaviour to know

  • Bushes at the park entrance are removed on save load. The framework clears Bush large terrain features in the Forest location within X: 8, Y: 0, Width: 5, Height: 12 and logs how many it removed. Only bushes are removed, so anything else you place there survives.
  • The garage car is repaired on save load if the player has already earned it.