Gameplay and economy
This guide shows the parts of the game your mod can read and change: the player, survival, NPCs, time and weather, money, shops and jobs. Each section names the main calls and what they do in plain words. For short recipes you can copy, see the cookbook. For every member, see the API reference.
Try it first
This small mod pays the player 10 OC at the start of every new day and counts how many days it has done that. It shows the three habits the rules below ask for: set things up in OnLoad, keep your own data in the save, and check what a call returns. Use the project from Your first mod and put this in Plugin.cs.
using System;
using ObenseuerCore;
[Serializable]
public class PayState // the data this mod keeps in each save
{
public int daysPaid; // how many days the mod has paid the player
}
[ModInfo("example.dailypay", "Daily Pay", "0.1.0")]
[ModDependency("obenseuer.moddingframework", "0.11.0")]
public class DailyPay : FrameworkMod
{
private PayState state = new PayState(); // the current data
protected override void OnLoad() // runs once when the framework starts your mod
{
SaveData.Register<PayState>(Info.Id, () => state, s => state = s ?? new PayState()); // what to save, and what to do with the loaded data
Events.DayPassed += day => // runs when a new day starts
{
if (!Cash.Give(MoneyCurrency.OC, 10)) return; // false means the game could not take it now, so try again tomorrow
state.daysPaid++;
Log.Info("paid 10 OC, days paid so far: " + state.daysPaid);
};
}
}
Build it, start the game and load a save. When a new day starts you get 10 OC, and the line paid 10 OC appears in BepInEx\logs\example.dailypay.log. The payment only happens when Cash.Give returns true.
Rules for every call
These rules hold for almost every call on this page.
- Call the framework from your mod’s normal code, and set up in the right place. Normal code means
OnLoad, event handlers andUpdate, not a background thread you started (see main thread). Subscribe to events and register save data once, when your mod starts: inOnLoadfor a framework mod, or inAwakefor a BepInEx plugin. Registering save data later also works, and its data is loaded at once if a save is already open. A second registration with the same id throws.- Technical:
SaveData.Registerloads the entry immediately whenSaveData.Folderis set and no load is running; otherwise it waits for the load that is coming.
- Technical:
- Calls that change money, banking, invoices, the inventory or movement return
falsewhile the game saves or loads. Check the result and try again on a later hour or event. - Most calls that change survival, health, the clock, the weather or the wallet also return
falsewhile a save loads or saves. The same condition isSaveData.Busy, which istrueduring that time.Survival.Decay.Setis the one that does not check it. Check the result, or testSaveData.Busyfirst. - A bad argument throws
ArgumentExceptionorArgumentNullException. Most other problems return a fallback value (such asfalse, 0 ornull) and do not throw. - Lists such as
Shops.All()andJobs.All()show only what is loaded right now. - You can use newer C# than the framework itself. The framework is built as C# 5, and the samples avoid
?.,$""andnameof. Your project from the Starter uses the latest C# version. - Tell BepInEx or the framework which framework version you need. A framework mod has
[ModDependency("obenseuer.moddingframework", "0.11.0")], as in Your first mod. A BepInEx plugin has[BepInDependency("obenseuer.moddingframework", ">=0.11.0")]instead. On BepInEx 5, write"0.11.0"without>=, because BepInEx 5 cannot read a version range. See Supporting BepInEx 5 too.
Item ids are explained in Finding item ids.
The player
PlayerStatereads the player.Health,IsAlive,PositionandFirstNamedo what their names say.PlayerState.Damage(amount, reason)hurts the player the way the game does. It returnsfalsewhen the player is already dead.PlayerState.Heal(amount)adds to the pool the game heals from. Health comes back over time, not at once.- Technical: it adds to the game’s regeneration pool.
PlayerMovement.WalkSpeedMultiplierandJumpMultiplierchange how fast and how high the player moves. They stay between 0.25 and 4, and they are applied again after each scene load.PlayerMovement.Lock(modId)andUnlock(modId)stop and free the player’s movement. Each mod counts on its own, so eachLockneeds oneUnlock. A load, or a scene change that replaces the player’s controller, releases every lock.PlayerInventoryreads and changes the main inventory and the equipment slots. The calls areHas,Count,Give,Take,FreeSlotsandItems().Givecan refuse. It refuses item ids 0 and 1, refuses when the amount does not fit in the main grid, and refuses during a save.FreeSlotsalso counts the backpack and equipment, so it can be above 0 whileGivestill returnsfalse.Has,GiveandTakethrowArgumentExceptionwhen the count is 0 or less.ItemAddedandItemRemovedfire when the game adds or removes an item.
Survival
Survival.Get(Stat)andSurvival.Set(Stat, value)read and write a need. Hunger runs from -100 (full) to 100 (starving).Survival.Decay.Set(stat, modId, multiplier, offsetPerHour)changes how fast a need grows. It acceptsHunger,Thirst,Hygiene,AlcoholNeed,MushroomNeed,SmokingNeedandGamblingNeed. Each mod has one entry per need, and a second call replaces the first.Survival.Consumables.Intercept(modId, handler)lets you step in before the player uses food, drink or another consumable. An interceptor is a function the framework calls before the action happens. Setargs.Cancel = trueto stop the use. A stopped use does nothing, not even the sound. Handlers run in the order the mods registered them, anyCancelwins, and the same handler registered twice by one mod runs once.Survival.Consumables.Remove(modId, handler)removes one handler.- Decay settings are not saved. They live in memory, so set them again each time the game starts.
NPCs
Npcsgives you information about characters in the game.Npcs.All(),Npcs.Get(id)andNpcs.Find(name)returnNpcInfoobjects, which are copies of an NPC’s data at that moment. They do not change afterwards.Npcs.Spawned,DespawnedandDiedare events that carry anNpcInfo.NpcSchedules.CurrentActivity(npcId)returns the name of what the NPC is doing now, ornull.NpcSchedules.ActivityChangedfires when an NPC changes activity. It carriesNpcId,FromandTo.NpcSchedules.Timetable(npcId)returns the NPC’s daily plan as data. It only reads the plan. It does not change it.Npcs.All()is empty until the game has loaded its NPCs. Call it from an event handler, not from your startup code.
Time, weather and power
GameTimereads the game clock.GameTime.IsDaytimeistruefrom hour 6 through 20.WorldClock.SetTimeOfDay(hour, minute)sets the clock. It throws for an hour outside 0 to 23 or a minute outside 0 to 59. It returnsfalsewhen the clock is not ready, a save is busy, or the time is earlier than the current time (time only moves forward). It raises no event.WorldWeather.WeatherChangedfires when the weather changes.WorldWeather.Set(index, transitionSeconds)changes it, andIndexOf(name)finds an index from a name.Setthrows for an index outside0..Count-1or a negative transition time.WorldPower.BlackoutStartedandGeneratorToggledtell you about power changes.WorldPower.ScheduleWakeUpandWakeUpreturnfalseduring a save.- Water and location are on the world page.
Interactions
An interaction is the “Use” prompt that shows when the player looks at an object.
Interactions.Add(target, modId, label, use)adds a “Use” prompt to a game object.targetneeds aCollider(the invisible shape the game uses to know what the player is looking at). The five-argumentAddtakes a verb in place of “Use”. For exampleAdd(target, modId, "Treadmill", "Run", use)shows(Run).Interactions.SetReason(target, modId, reason)shows a red reason in place of the verb and blocks the use while the reason is set.Interactions.Remove(target, modId)removes only a prompt that your mod added.Interactions.Usedfires after the player uses any interaction.Interactions.Looking()returns what the crosshair is on, ornull.Addcan say no. It returnsfalsewhile a save is busy, when the target has noCollider, when it already has a game interaction, when it belongs to another mod, or when patching is not allowed. It throwsArgumentNullExceptionfor anulltarget, label oruse.
Tweaks
Tweaks are built-in gameplay settings: multipliers and on/off switches, such as the game’s time speed. The player sets them in the config file BepInEx\config\obenseuer.moddingframework.cfg, or in the Mods menu. Your mod cannot set them from code, and it should not edit the player’s config file.
- Each tweak is a section of the config file, for example
[Tweaks.Time]withTimeScale = 2. Multipliers are kept between 0.01 and 100. - When a value is changed to fit those limits, the change is written to
BepInEx\logs\obenseuer.moddingframework.tweaks.log. TweakState.RenovationsMoneyOnlyis the only tweak value you can read from code.- Tweak values live in the config file, not in the save.
Money
The game has two currencies. OC is Open Sewer Coins, the everyday money. RM is Real Money (see the glossary entries OC and RM). Use Cash for either one. Wallet covers OC only.
Cashreads and changes the player’s money. The calls areCash.Balance(currency),Cash.Has(currency, amount),Cash.Give(currency, amount)andCash.TrySpend(currency, amount).MoneyCurrencyisOCorRM. OC amounts are whole numbers. RM amounts are rounded to 0.1.Cash.MoneyChangedfires when a balance changes.GiveandTrySpendreturnfalseduring a save or load.Wallet.BalanceandCash.Balancereturn 0 on the main menu.
Bank accounts:
Banking.Accounts()lists the player’s bank accounts. Each has aBankNameand aFrozenflag.Banking.Refund(bankName, currency, amount)adds money to an account. It works without the bank window open. Its ledger entry has the kindRefund.Banking.Charge(bankName, currency, amount)takes money from an account.Banking.DepositWithdrawworks only while the bank’s window is open. Do not use it for payments you schedule.BankLedger.Entries(bankName, max, beforeId)lists ledger entries, newest first, up to 500 per call. The ledger is the account’s list of past transactions.BankLedger.Addedfires for each new entry.
Invoices:
Invoices.Create(debtor, currency, amount, days)makes a bill the player pays.daysis how long they have, and it defaults to 7. It returns abool, not an id, so get the id fromInvoices.Issuedif you may cancel the invoice later.Invoices.Paidfires when the player pays.Invoices.CanPay(id)checks whether they can.Invoices.Cancel(id)cancels one.Createthrows for an empty debtor, an unknown currency, an amount outside1..16000000, or days outside0..36500.
Shops:
Shops.All()lists the shops.Shops.Open()returns the trader whose panel is open.Shops.Stock(shopId)lists items and prices.Shops.PriceQuerylets a handler setargs.Pricefor a trade. It is not raised for vending machines. It runs on every price update, so keep the handler quick.Shops.BoughtandShops.Soldfire after a trade.- Your mod can make a trade itself.
Traders.Buy(modId, traderId, itemId, count, out refusal)andTraders.Selltrade with a trader (traders). At a furniture shop,BaseShops.Buy(id, furnitureId, count)andBaseShops.Sell(id, furnitureId, count)do the same (furniture shops). The player uses the game’s own trade panel.
Jobs
Jobs.All()andJobs.Get(id)list the job stations, the places where the player waits to earn pay.Jobs.Workedfires after the game has paid for a finished wait.WorkedArgscarriesId,Hours,PayOcandPayRm. It only reports the pay. It cannot change it.- The framework has no call to set pay. To change a wage, change the rule, as the next section describes.
Changing a game rule
Use the least powerful tool that does the job. Go down this list and stop at the first one that works.
- A tweak in the config, when one exists for the rule.
- A framework call that sets, cancels or scales the rule, such as
Survival.Decay.Set,Survival.Consumables.Intercept,PlayerMovement.WalkSpeedMultiplierorShops.PriceQuery. - An event that tells you the rule ran, such as
Events.DayPassed,Cash.MoneyChangedorInvoices.Paid. You react after the fact. CorePatcher.Patch, only when nothing above covers the rule. It changes a game method directly. See Patching.
Check the method name in the game’s code before you patch it.
Common mistakes
- Ignoring the result. Money, inventory and survival calls return
falseduring a save or load. If you ignore the result, your change quietly does not happen. - Calling
Npcs.All()at startup. It is empty until the NPCs have loaded. Call it from an event handler. - Setting a need’s decay and expecting it to last. Decay settings are not saved. Set them again each time the game starts.
- Calling from a background thread. Run framework calls from your mod’s normal code. See main thread.
Where to go next
- Player: player, player-movement, inventory, items.
- Survival: API reference, section “Survival”.
- NPCs: npcs, npc-schedules.
- World: world. Interactions: interaction.
- Money: economy, invoices. Jobs: jobs.
- Rule changes: API reference, section
CorePatcher. - Smaller areas: saunas, dishwashers, showers, hotels, breeding, casino, cardgames, trains, traintickets, fuel, medical, weapons, doors, lights, televisions, mailboxes, parcellockers, janitors, cleaning, global-flags.
- When your mod works, read Using the framework alongside other mods before you share it.