Market shops
What it is
A market shop is a stall in the game that an NPC runs. The player puts the goods they want on the stall’s conveyor and pays at its till. This API lets your mod list the stalls in the scene, see the name, the lock and the price at the till, lock or unlock a stall, and hear when the player pays.
The shop the player runs themselves is a different thing. It is called a player shop and it is on the economy page. This API does not buy or sell anything. See What you can’t do (yet).
When to use it
- Show a message or reward something when the player pays at a stall.
- Lock a stall so the player cannot pay, or unlock it, for example during a quest.
- Show the current price at a stall’s till.
Quick example
This mod writes a log line each time the player pays at a market stall. It also logs each stall’s name and price when a save is loaded.
using ObenseuerCore;
[ModInfo("example.marketwatch", "Market Watch", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public class MarketWatch : FrameworkMod
{
protected override void OnLoad()
{
if (!MarketShops.IsAvailable)
Log.Warn("Market shop payments are off, so Paid will not fire. See the limits below.");
MarketShops.Paid += args => Log.Info(args.Name + ": paid " + args.Price + " " + args.Currency);
Events.WhenReady(() =>
{
foreach (MarketShopInfo shop in MarketShops.All())
Log.Info(shop.Name + " asks " + shop.Price + " " + shop.Currency + (shop.Locked ? " (locked)" : ""));
});
}
}
Paid is an event: the framework calls your code each time it happens. Currency is OC (Open Sewer Coins) or RM (Real Money), as described in the glossary. See Using events. The limits are in the Limits section below.
Common tasks
Each example below is a complete mod, and each one runs once a save is loaded, because the stalls are in the scene.
Lock every stall
This locks every stall in the scene, so the player cannot pay at any till. SetLocked returns false for an unknown id, a stall with no lock, and while a save is busy, as the Limits section below says.
using ObenseuerCore;
[ModInfo("example.lockstalls", "Lock Stalls", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public class LockStalls : FrameworkMod
{
protected override void OnLoad()
{
Events.WhenReady(() =>
{
int locked = 0;
foreach (MarketShopInfo shop in MarketShops.All())
{
if (MarketShops.SetLocked(shop.Id, true))
locked++;
else
Log.Warn("Could not lock " + shop.Id + ". See the limits below.");
}
Log.Info("Locked " + locked + " stalls.");
});
}
}
Unlock one stall
This unlocks the first stall in the scene and checks that it worked.
using ObenseuerCore;
[ModInfo("example.unlockstall", "Unlock Stall", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public class UnlockStall : FrameworkMod
{
protected override void OnLoad()
{
Events.WhenReady(() =>
{
foreach (MarketShopInfo shop in MarketShops.All())
{
if (MarketShops.SetLocked(shop.Id, false))
Log.Info(shop.Name + " is unlocked.");
else
Log.Warn("Could not unlock " + shop.Name + ". See the limits below.");
break;
}
});
}
}
Show the stalls with something to pay for
This lists the stalls whose till is waiting for a payment.
using ObenseuerCore;
[ModInfo("example.stallprices", "Stall Prices", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public class StallPrices : FrameworkMod
{
protected override void OnLoad()
{
Events.WhenReady(() =>
{
foreach (MarketShopInfo shop in MarketShops.All())
if (shop.Price > 0)
Log.Info(shop.Name + ": " + shop.Price);
});
}
}
Reference
This is the full list of what the API gives you. Text in brackets at the end of a note is extra detail for experienced modders, and you can skip it on a first read.
Market shops
Each stall is a MarketShopInfo, a snapshot taken when you asked. The snapshot does not update by itself, so ask again for fresh values.
MarketShops lists stalls, locks them and reports payments
Call All() to list the stalls and SetLocked to lock one. Paid fires when the player pays at a till.
public static class MarketShops
{
public static List<MarketShopInfo> All();
public static bool SetLocked(string id, bool locked);
public static event Action<MarketPayArgs> Paid;
public static bool IsAvailable { get; }
}
| Method | Returns | What it does |
|---|---|---|
All() | List<MarketShopInfo> | Every market stall in the scene the player is in. |
SetLocked(id, locked) | bool | Locks or unlocks a stall. Returns true when it is now as you asked. |
| Property | Type | What it means |
|---|---|---|
IsAvailable | bool | false when the framework could not set up the Paid event. |
| Event | Gives you | When it fires |
|---|---|---|
Paid | MarketPayArgs: the stall’s id, name, the amount charged and the currency. | The player has paid at a till. |
MarketShopInfo a snapshot of one stall
All() gives you one of these for each stall. Price is the total on the conveyor now, in Currency.
public sealed class MarketShopInfo
{
public string Id { get; }
public string Name { get; }
public bool Locked { get; }
public int Price { get; }
public MoneyCurrency Currency { get; }
}
| Property | Type | What it means |
|---|---|---|
Id | string | The id to pass to SetLocked. The number the game saves the stall under, or a path through the scene. |
Name | string | The name of the stall. |
Locked | bool | true when the stall’s lock is locked. |
Price | int | The total at the till right now. 0 when nothing is on the conveyor. |
Currency | MoneyCurrency | The currency the stall charges in: OC or RM. |
MarketPayArgs what Paid tells you
Paid gives you one of these after the game has taken the money. Price and Currency are what was charged.
public sealed class MarketPayArgs
{
public string Id { get; }
public string Name { get; }
public int Price { get; }
public MoneyCurrency Currency { get; }
}
| Property | Type | What it means |
|---|---|---|
Id | string | The stall’s id. |
Name | string | The name of the stall. |
Price | int | The amount charged, at least 1. |
Currency | MoneyCurrency | The currency it was charged in: OC or RM. |
Things to know
All()lists the stalls that are switched on in the scene the player is in. A stall that is switched off is not listed. If no save is loaded, you get an empty list. (It searches the active scene withFindObjectsByType, which skips inactive objects. The id is the object’sGUID, else the GUID of theSavableScripton its object, else a path counted from the scene root.)Priceis the total of the goods on the stall’s conveyor, in the stall’s currency. It is0when the conveyor is empty. You can read it, but you can’t change it. (PriceisMarketShop.GetCurrentPrice().Currencyis the shop’scurrency, converted byCash.FromGame.)SetLocked(id, locked)locks or unlocks the stall’s lock, as the game does. A locked stall’s till refuses to open. It gives backtruewhen the lock is now as you asked. (It calls the game’sLock()orUnlock(), which call the lock’s_Lock()or_Unlock(). The save check isSaveData.Busy.)Paidfires after the player pays at the till and the game has taken the money.Priceis the amount the game charged. It is rounded, and at least 1, the same as the game’s own minimum.Paidalso fires when a script pays at a stall, not only the player at the till. The framework listens to the game’s own pay method, which any script can call. (The change is a prefix and a postfix onMarketShop.Pay, which is public.)IsAvailableisfalsewhen thePaidevent could not be set up. See If something fails to set up.
What you can’t do (yet)
- You can’t read or change the goods on the conveyor. The conveyor holds the player’s own goods while they wait to be paid for, so
All()does not list them. ReadPricefor the total at the till. (The conveyor’s item list is a privateList<ConveyorItem>.) - You can’t use the game’s
Marketclass. It has no working state to read. (ItscheckInventorythrowsNotImplementedExceptionwheninv.slotAmountis above zero, andbuyandreturnStuffthrow the same way.) - You can’t buy anything from a stall. A stall has no stock of its own. The till charges the goods the player has already put on the conveyor, and the conveyor takes them from the player’s own inventory. So there is nothing on the stall’s side to buy, and a
Buycall would take money for nothing. The economy page lists this under the things it leaves out for market shops. (The conveyor is filled byMarketShop.LoadConveyorandIsMarketShopItem.) - You can’t load, scan or change the conveyor. (
MarketShop.stateand its items are not written by this API.) - You can’t change prices or ownership. (This means
priceFactor,retainOwnerandowner.) - Base shops are on another page. See Base shops.
Limits
SetLockedreturnsfalsefor an unknown id, for a stall with no lock, and while a save is loading or being written.Paiddoes not fire when the payment is refused, for example when the player has too little money, or when the stall is not waiting for a payment.- The price is read just before the game takes the money. The game charges the same value, but if the price changes in between, the framework does not notice.
Details
Target: Obenseuer 0.5.08 (buildid 25969064), Unity 6000.0.77f1 Mono, BepInEx 6 Unity.Mono (be.788, the tested build) or BepInEx 5 (5.4.23 or later). Namespace: ObenseuerCore.
Everything not listed in this reference is internal and may change without notice.
The framework changes one of the game’s methods when it starts, so it can tell your mod when the player pays. This page calls that a feature group. Its name, the one you will see in the log, is obenseuer.moddingframework.marketshops.
When you can call it
- Call everything from your mod’s normal code, not from a background thread you started. The game and Unity only work safely on one thread, the main thread (see the glossary). That includes the code that handles
Paid. - It is safe to call at any time. With no save loaded,
Allgives back an empty list. Nothing throws, and a null or emptyidfinds nothing. Paidis not held back while a save loads. If your handler should ignore payments while a save loads, checkSaveData.Busy, which istruewhile a save is loading or being written.
If something fails to set up
The framework changes some of the game’s methods when it starts. If a game update breaks one of them, the framework removes all the changes of the feature group that depends on it, turns the group off, and writes error lines to the group’s log file in BepInEx\logs. The rest of the framework keeps working.
- If the payment change cannot be made, the feature group is turned off and
Paidstops firing.MarketShops.IsAvailablebecomesfalse.AllandSetLockedkeep working. (The game method involved isMarketShop.Pay, patched with a prefix and a postfix.)