Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Rist
A character level beside the skills, carved into runestones you choose and deepen.
| Date uploaded | a day ago |
| Version | 1.0.1 |
| Download link | Ezomic-Rist-1.0.1.zip |
| Downloads | 14 |
| Dependency string | Ezomic-Rist-1.0.1 |
This mod requires the following mods to function
denikson-BepInExPack_Valheim
BepInEx pack for Valheim. Preconfigured with the correct entry point for mods and preferred defaults for the community.
Preferred version: 5.4.2333README
Rist
A character level beside the skills, carved into runestones you choose and deepen.
Built against the installed game (0.221.12, Unity 6000.0.61, BepInEx 5.4.23.3, Harmony 2.9). Single DLL plus a text file, no asset bundle.
What it is
Valheim has skills but no character level. Rist adds one that runs alongside the skill system: vanilla skills are read, never written, never rescaled, never reskinned.
Every character level earns one pick. Spend it on any runestone in the catalogue and that runestone gains a rank, up to five. Picks bank: nothing expires, and the panel waits.
There are no drawbacks. A runestone is a reward or it is nothing.
The panel
A fifth tab on the compendium bar, beside the raven and the trophy, opens a full screen of runestones - one per rist. Each carries its own outline, its own rock and its own runes, all seeded off the runestone id, so a rist looks the same in every session and on every machine: the stone is part of how you recognise it.
A rank cuts one more mark into the rim, and the mark holds the level that bought it. Five ranks, five marks, five answers to "when did I get this". The column beside the field follows the cursor and says what a stone is worth now, what the next rank would make it, and what waits at the bottom of its track.
Three designs came before it: a draft window dealing three at random, a board of tiles, and that same board dressed in the game's own wooden sprites. The last is why this one exists. Borrowing a window frame is imitation, and it read as a different game no matter how close the sprites got - IMGUI cannot draw with the game's shaders, so every copied sprite had to survive a colour space round trip that was guessed wrong three times. A runestone imitates nothing: it is Valheim's own subject matter, it is a shape rather than a material, and the marks cut into it are text in the game's own rune face.
XP
Skill level-ups, and nothing else. Nearly every activity in Valheim raises some skill, so building, sneaking, sailing, cooking and fishing all pay in without this mod maintaining a table of what counts.
XP is weighted by the skill level reached, not flat per level-up. Vanilla's own skill cost
curve is pow(level+1, 1.5), so early levels are nearly free. A flat rate would rain runestones in
the first hours and dry up exactly when the deep ranks start to matter, and would make
grinding a fresh cheap skill from zero the fastest way to farm picks.
Dying no longer costs skill progress
Valheim already has a world modifier for this and it is not enough. Skills.OnDeath calls
LowerAllSkills(m_DeathLowerFactor * Game.m_skillReductionRate), and inside that method only
the level loss is scaled by the factor:
m_level -= m_level * factor; // scaled: zero factor means no loss
m_accumulator = 0f; // NOT scaled: always wiped
// ...and "$msg_skills_lowered" is shown regardless
Setting the world's skill reduction to zero therefore still throws away partial progress toward the next level in every skill, and still announces that skills were lowered. On a skill in the seventies, where one level is a long grind, the accumulator is the part that actually hurts. Rist skips the method, which removes all three together.
The bar
The experience bar is a clone of one of the game's own upright bars, the eitr bar, so it carries the real frame, track and fill sprites at whatever HUD scale you have set, drains rather than snapping to empty on a level-up, and flashes while a pick is waiting. If a game update ever breaks the clone it falls back to a plain drawn bar rather than to an empty corner.
Joining a server
Progress is kept on the server, keyed by the platform identity of the connection and the character being played. Valheim characters are client-side files, so anything stored on the character is editable by its owner, which rules it out for something that decides rewards.
A joining character is credited for the skills it already carries, so it arrives at the level
those skills are worth with the picks to spend. That is CreditExistingSkills, on by default;
turn it off and only XP earned on that server counts. It is a per-server setting, synced from
the host.
Crediting has a plain cost: a maxed vanilla character walks in at Rist level 219 and takes the whole catalogue. The answer to that is not to withhold XP from it, it is to not let it in, which is Dyrr's job rather than this mod's.
Skill reports come from the client and cannot be verified, so three ceilings bound what a claim can be worth: how many are accepted, how much one may say, and how much anything can be worth over time. None of it detects a cheat, it bounds one, and it withholds rather than punishes.
Singleplayer works with no special casing and the ceilings are inert there.
Runestones
cards.txt sits beside the DLL. One runestone per line:
id | Name | flavour text | effect | value-per-rank
effect is the literal name of a public float field on the game's own SE_Stats. That is
what makes the catalogue a text file rather than a switch statement: Valheim already sums
about forty-five of these across active status effects (carry weight, armour, the six stamina
costs, stealth, fall damage, regen rates, skill gain) so naming one turns it into a runestone with
no code change and no rebuild.
*inventoryrow is the single special, because grid height is not a stat.
An unknown field name is logged and skipped, not thrown, matching how the game treats a prefab name that does not resolve. A typo costs one runestone, not the catalogue.
Core is optional, and here is exactly what that costs
Rist installs and runs on its own. Core is a soft dependency: install Rist by itself and it works, including the extra inventory rows. Solo, you need nothing else and give up nothing.
On a server it is a different question, and Rist gives up more without Core than the rest of this suite does. Three things, all of which matter only in multiplayer:
The catalogue is no longer checked. cards.txt names what every rank is worth, effects are
applied client-side from it, and the server only ever verifies the rank, so an edited line
is simply believed. With Core, a hash of the file travels in the version handshake and two ends
running the same build over different catalogues get reported. Without it, a client that edits
cards.txt gets whatever it wrote.
The host's curve is no longer forced. LevelBaseXp and the rest are synced from the host
with Core. Without it, a client with different settings reads a different level out of the same
XP, and every number on its screen disagrees with the server deciding them. The .cfg files
have to be matched by hand.
Extra inventory rows are claimed without an arbiter. This is the one worth reading twice.
Inventory.m_height is a single private int with no owner. Two mods that both want extra rows
each write it, the last writer wins, and a mod that only writes when its own state changes
loses silently to one that writes every frame. Core exists to add every claim up and write
once, so mods stack instead of fighting. Standalone Rist owns that write alone, correct on its
own, and in conflict with any other mod that grants inventory rows, with the winner decided
by frame ordering rather than by anything either mod can control. If you run another row-adding
mod, install Core.
What is not given up is correctness of the rows themselves. The standalone owner carries the
Player.Load widening in full, which is not a nicety: without it every item sitting in a
granted row is destroyed by loading the game, silently, because AddItem drops any saved
position outside the current grid and the row is not applied until after the load. That bug
cost a real heartwood to find, and the fallback would not have shipped without the fix.
Rist logs a warning at startup when it starts without Core, naming all three.
Config
BepInEx\config\ezomic.valheim.rist.cfg
| Key | Default | What it does |
|---|---|---|
Enabled |
true |
Off keeps records but stops granting and applying |
XpPerSkillLevel |
1 |
XP per skill-up, multiplied by the level reached |
LevelBaseXp |
40 |
Cumulative XP for level 1 |
LevelExponent |
1.4 |
Cumulative XP for level N is base × N^exponent |
MaxRank |
5 |
How deep one runestone goes, and how many slots its track shows |
BonusEvery |
5 |
Ranks between capstones |
ShowInfoTab |
true |
Add the rists tab to the compendium bar |
RemoveDeathSkillLoss |
true |
Skip Skills.OnDeath |
CheckSkillBaseline |
true |
Take a joining character's skills as the baseline it is paid from |
CreditExistingSkills |
true |
Pay a joining character for the skills it already has; off counts only XP gained here |
MaxSkillUpsPerMinute |
30 |
Server-side ceiling on accepted reports per player |
MaxSkillLevelJump |
1 |
Levels above the baseline one report may claim |
MaxXpPerMinute |
600 |
XP paid per minute of connected time; 0 is off |
XpBurst |
1800 |
How much unspent allowance banks, so honest bursts still pay |
Verbose |
false |
Log every grant, rejection and runestone applied |
ShowXpBar |
true |
Show the experience bar at all |
VanillaBar |
true |
Clone one of the game's own bars; off draws the plain fallback |
BarFollowStamina |
true |
Anchor to the stamina bar; off falls back to BarPosX/BarPosY |
BarOffsetX / BarOffsetY |
0 / 70 |
Pixels right of and below the stamina bar's centre |
BarPosX / BarPosY |
172 / 105 |
Screen pixels to the centre, when not following |
BarSize |
240 |
Length in canvas units; 64 is a starting stamina bar |
BarBuildRaise |
155 |
Pixels to lift the bar while the build or ship panel is open |
BarColour |
4FB3A5 |
RRGGBB; the trailing fill is the same hue held back. E4DCC4 for bone |
BarFlashSeconds |
4 |
How often the bar flashes while a runestone is waiting |
BarX / BarBottom / BarThickness / BarLength |
168 / 75 / 10 / 60 |
Place the fallback bar only |
A value already written to the .cfg beats a new default in code. Change the .cfg.
Status: v1.0
Runs on a listen host and on a dedicated server, and both halves have been exercised: the ledger, the gate, the pick path, the level curve, the panel, the bar and the compendium tab. Nineteen runestones, all of them verified to name a field the game actually reads.
What is not done is the part that only play settles. The curve was 60 * N^1.5 and has now been played once and retuned to 40 * N^1.4; beyond that it has not been played - testing ran on a cheapened one - so the pacing of the whole mod is unknown, and no value in the catalogue has been tuned against anything but reasoning. The version says the code is finished, not that the numbers are right.
Known gaps
- The shipping curve has never been played. Everything below it works; how often a stone lights up at 40 * N^1.4 is now measured against one session rather than reasoned about, but one session is not a playthrough. The panel, the bar and the tab have all been seen in game and are correct; this is about pacing, not about whether anything draws.
- Balance rests entirely on the runestones. With a free choice the strongest runestone is
always available, so the ordering of
MaxRankand the per-rank values are doing the work the random offer used to do. That has not been played hard enough to find out which one wins. - Multiplayer has never had a second player. The ledger, the version gate, the config sync and the catalogue hash all work host-side and against a dev server, but no remote client has connected, so the join path is exercised only in the shape where the server is also the client.
- Ranks taken before ledger v3 have no level. The old format recorded a rank and nothing else, and picks were not stored in order, so there is no way to tell which level bought which rank. Those slots read a dash permanently. Everything taken since is exact.
SE_Statshas no max health, stamina or eitr field - those come from food in Valheim - so no max-pool runestone is possible without a different vehicle.
Design notes
Why picks are chosen rather than dealt, why capacity is a capstone, how the bar is built out of a vanilla one, and why the ledger lives where it does: DESIGN.md.
Author
Rist is an original mod by Robbin Thijssen (Thijssen Software).
Copyright (c) 2026 Robbin Thijssen. MIT licensed. See LICENSE.
CHANGELOG
Changelog
Notable changes to Rist. Format follows Keep a Changelog, and the mod uses semantic versioning.
[1.1.0] - 2026-08-18
XP is now weighted by which skill it came from, not only by how far that skill got.
Why
The live server's ledger asked for this rather than a hunch. A character sitting at Rist level 12 held 1346 XP, of which WoodCutting alone was 666 - half the character was felled trees, and everything earned in a fight came to 293, under a quarter of the whole. The level had stopped describing the character and started describing how long they had stood in the Meadows with an axe.
The existing weighting compounded the wrong way. XP being the level reached is right on its own, but the safest repeatable activity in the game is also the one that climbs highest, so it was collecting the most level-ups and the most XP per level-up.
Raising LevelBaseXp or LevelExponent cannot fix that, and it is the obvious reach, so it is
worth saying why not: both scale the whole curve, so the ratio survives untouched and the late
game pays for a problem that lives in the first hour. That is exactly what happened when the
pair was 60 and 1.5, and it had to be walked back.
Added
SkillWeights- a multiplier per skill, asSkill=multiplierpairs. The default table is built on risk: full price for a skill raised by something that can kill you, half for one that is situational, a quarter for repetition that cannot hurt you. Synced from the host, so a server sets it for everyone on it.DefaultSkillWeight- what a skill not named in the table is worth.1, so a skill added by a game update or another mod is paid normally rather than silently going unpaid.WeightGeneration- bump it to any different text and every character is re-priced once, on its next login, from the skill levels this server watched it reach.
Changed
- Existing characters are re-priced on their next login. The bundled generation is
1and no existing ledger line carries a stamp, so this happens once, automatically. The character above goes from level 12 to level 6, with 53% of it now earned fighting. - Runestones already taken are kept. A character re-priced from 12 to 6 holds all twelve and earns no new pick until it passes twelve again. Refunding them would turn a re-pricing into a free rebuild of the whole character.
- The join-time credit is weighted by the same function as the re-pricing, deliberately shared so the two can never drift apart.
Ledger format
The ledger is now v4, carrying the generation each record was last re-priced at. v1-v3
are still read and come back as never re-priced, which is what they are.
This is a one-way door. A v4 line does not parse on 1.0.1 or earlier, which drops the
record rather than erroring, so copy rist-ledger.txt aside before updating a server you care
about.
[1.0.1] - 2026-08-18
Documentation and the text players read, with no change to what the mod does.
Threshold is Dyrr
The companion mod that decides who may join a world was renamed, and Rist pointed at the old
name in two places that matter: the config descriptions, which are read by anyone who opens
rist.cfg, and the notes on what Rist deliberately does not do. Both now say Dyrr.
The README is shorter
The design argument and the technical notes moved to DESIGN.md, leaving the README as what
someone reads before installing. The reasoning is all still there, one link away, rather than
in front of a reader who only wanted to know what the mod is.
Its two links are absolute now. Thunderstore builds the package page from the README alone, so
a sibling-folder path pointed at nothing there and DESIGN.md is not in the zip at all - both
would have rendered as dead links on the store page and nowhere else, which is the kind of
fault you only see after publishing.
Em-dashes are gone from the docs throughout.
[1.0.0] - 2026-08-16
First release. There is no 0.1.0 entry below it because 0.1.0 was never published. It was this same work under a version that had not yet been judged ready, and folding it in beats inventing a release nobody could have installed.
The line this sits on
It adds a level beside the skills. It never reads, writes, rescales or reskins a vanilla skill.
Valheim has skills but no character level. Rist adds one that runs alongside them. Vanilla skills are read and never written.
Levels and picks
- Every character level earns one pick, spent on any runestone in the catalogue to raise it a rank, up to five.
- Picks bank. Nothing expires and the panel waits.
- No drawbacks. A runestone is a reward or it is nothing. A cost attached to a reward makes a pick something you can regret, and regretting a permanent choice is a reason to stop playing rather than a reason to think harder.
Free choice, not a dealt hand
An earlier version dealt three runestones per level, seeded so that quitting on a bad offer and returning re-offered the same three, since otherwise the pick is theatre, since anyone can reroll until they get what they want.
Choosing freely deletes that problem rather than defending against it: no roll to reroll, no
offer to store in the ledger or on the wire, no second window with its own visibility rules.
What it costs is the tension of a hand you are dealt. A free choice means the strongest runestone
is always available, so balance now lives in the runestones themselves and in MaxRank.
The panel carries the weight for this: choosing between nineteen runestones is only a real choice if each stone says both what it is worth now and what the pick would make it.
Every fifth rank is a capstone
Taking a runestone the whole way grants something the earlier ranks did not hint at, rather than one more slice of the same number. It is what makes depth a decision against breadth instead of a rounding error: five ranks of the same small bonus is the same reward as five different small bonuses, so without this there was no reason to ever finish anything.
Capacity is a capstone, not a runestone of its own
This began as an inventory problem: late-biome kit eats the grid and leaves nothing for what you went out to fetch. The cheap fix is more rows, and that is exactly what Hoard argues against.
It was a runestone once, called Deep pack, and it was the strongest thing in the catalogue: taken first, every time, which is not a choice. A row now sits at the bottom of Ox-backed and of Sure hand, so capacity is the reward for taking a hauling runestone all the way rather than a purchase anyone makes on their first pick.
The panel
A fifth tab on the compendium bar, beside the raven and the trophy, opens a full screen of runestones, one per rist, each with its own outline, rock and marks, all seeded off the card id so a rist looks the same in every session and on every machine. A rank cuts one more mark into the rim, and the mark holds the level that bought it.
Three designs came before it: a draft window dealing three at random, a board of tiles, and that same board dressed in the game's own wooden sprites. The last is why this one exists. Borrowing a window frame is imitation, and it read as a different game no matter how close the sprites got. A runestone imitates nothing.
The bar
A fifth bar under stamina, cloned from one of the game's own so it carries the same frame, fill and trailing fill. It follows the stamina bar rather than sitting at fixed pixels, so "below stamina" stays true at any resolution and HUD scale, and it flashes while a pick is waiting.
Verdigris, because the HUD already spends red, yellow, blue and orange between its four bars and a fifth in any of those reads as one of them.
What the server decides
The ledger is held by the server and keyed to the platform identity plus the character, never to the character file. A character file sits on the player's own disk, so nothing that decides rewards may live there. The client reports only that a skill went up; every number is derived server-side.
Three ceilings bound what a report can be worth, because skills live on the client and a report is therefore a claim that cannot be verified, only bounded:
- Reports per minute, which caps how many claims are accepted.
- A one-level step, which caps how much a single claim may say. Skills level one at a time and fire one callback each, so a report naming a level far above the baseline this server watched that skill reach did not come from playing. This also protects the baseline, which would otherwise adopt the forged level and give every later claim an alibi.
- XP per minute of connected time, which is the only cap that does not depend on believing the client at all. It banks three minutes so an honest burst, clearing a crypt levels four skills at once, is paid in full.
Nobody is disconnected and nothing is ever taken away. Rist used to refuse the connection, and a levelling mod deciding who may play means a bug in an XP system locks people out of a server. Refusing at the door moved to Dyrr, where it is the whole job and is done openly.
Cloning vanilla UI, and what it does not hand over
Both the bar and the compendium tab are clones of things the game already draws, which is what
makes them match. What a clone does not bring is its own lifecycle: it has never run
Awake, OnEnable or LateUpdate, because the donors sit inactive.
GuiBar puts something essential in each of those, so the bar failed three times over - the
tint reached nothing, the width was discarded and re-derived from the donor's, and setting a
value stored a number without drawing it. All three are ways of not reaching one line. The
fill is written directly now, and the trailing bar lagged by hand.
The tab had the same shape of problem read from the other end: its gold is not in any sprite
or any Image.color but in the Button's normalColor, applied to whichever graphic is its
targetGraphic. Building a layer alongside it could never match; swapping the sprite on the
graphic already being tinted inherits size, shadow, gold, hover and press at once.
The rule both arrive at: clone for geometry and sprites, drive the state yourself.
Core is optional
Rist installs and runs on its own. Core is a soft dependency; installing Rist no longer
installs Core with it, and manifest.json no longer lists it.
Solo, nothing is given up, including the extra inventory rows, which now have a Rist-owned
implementation used only when Core is absent. On a server, three things are lost, and the
README section "Core is optional, and here is exactly what that costs" sets them out: the
cards.txt hash check, the host-authoritative curve, and the arbitration of
Inventory.m_height between mods. Rist logs a warning at startup naming all three.
The row fallback deliberately copies Core's Player.Load widening rather than simplifying it.
That is not a nicety: without it every item in a granted row is destroyed by loading the game,
silently, because AddItem drops any saved position outside the current grid and the rows are
not applied until after the load. It is patched in only when Core is absent, so the two owners
can never both write the field or both widen the grid.
Mechanically, as elsewhere in the suite: every Ezomic.Core call now sits in its own
[MethodImpl(MethodImplOptions.NoInlining)] method behind a Chainloader.PluginInfos check,
because the JIT resolves a method's assemblies when it first compiles that method, and an inline
call would drag Core in before the check could prevent it. Verified by decompiling the built
DLL: Suite.* and InventoryRows.* appear only inside those isolated methods.
A character arrives worth what its skills are worth
CreditExistingSkills, on by default, pays a joining character for the skills it already
holds: turn up carrying skills worth twenty levels and you arrive at twenty levels with the
picks to spend. XP is the skill level reached, so a skill at N has already produced N(N+1)/2;
summing that over every skill is exactly what the character would hold had all of it happened
here, which also makes it safe to re-run on every login and impossible to double-pay.
Off, only XP gained on this server counts and every character starts at level 0. Config syncs from the host, so a server picks one rule for everyone on it.
There is no untrusted state. A character above the baseline was briefly marked untrusted and paid nothing "until they line up again", which it could never do, because the withholding returned before the baseline was updated, and the login check only adopted a new baseline when it had found nothing wrong, so the baseline froze while the player's real skills climbed away from it.
Keeping a well-travelled character out of a world altogether is a door policy, and the door is Dyrr's.
Known limits
- Never played at the shipping curve. The pick path was exercised against a deliberately
cheapened one so it could be reached without grinding.
LevelBaseXpandLevelExponentare back at60and1.5, and how often a stone lights up at those numbers is still a guess. - Runestone balance is untested by definition. With free choice the strongest runestone is always available, and nothing has been played hard enough to find out which one that is.
- Multiplayer has never had a second player. The version gate, config sync and catalogue hash all work host-side and against a dev server, but no remote client has connected.