RoguelikeUnlimitedLevelProgression
Removes the level 20 cap of the Roguelike mode in singleplayer: one level per victory, extended XP past 20 and Skills/Items/Attributes rewards that keep working when a pool runs out. Singleplayer only.
| Date uploaded | a week ago |
| Version | 0.1.2 |
| Download link | DefRuivo_StolenRealmMods-RoguelikeUnlimitedLevelProgression-0.1.2.zip |
| Downloads | 19 |
| Dependency string | DefRuivo_StolenRealmMods-RoguelikeUnlimitedLevelProgression-0.1.2 |
This mod requires the following mods to function
BepInEx-BepInExPack
BepInEx pack for Mono Unity games. Preconfigured and ready to use.
Preferred version: 5.4.2305README
RoguelikeUnlimitedLevelProgression
Lets a Roguelike run go past level 20 — tested on singleplayer; multiplayer is not yet tested.
- GUID:
com.gumatos.roguelikeunlimitedlevelprogression - Harmony owner id: the same GUID (the mod unpatches by id, never by type).
- Version: 0.1.2
- Works with: Stolen Realm (Roguelike mode).
- Needs: BepInEx 5 (r2modman installs it for you) and nothing else — no ModAPI, no BetterStats.
- BepInEx display name:
Roguelike Unlimited Level Progression
What it does
In the Roguelike mode the game stops granting a level once
character.Level < settings.RoguelikeMaxLevel is false (RoguelikeSettings.RoguelikeMaxLevel
ships as 20 in the game's asset). This mod replaces that single comparison and keeps the
rest of the flow — the XP curve, the level-up queue, the UI — coherent above 20.
- One level per victory. Above 20 the reward stage is still one entry per victory: the
comparison that decides whether the character levels becomes
Policy.CanAdvance(Character, RoguelikeSettings). - Loading a saved character never loses levels.
Character.Load(int)clamps the saved level withMathf.Min(Character.MaxLevel, CharacterSaveFile.ExperienceLevel), andCharacter.MaxLevelis exactly the getter this mod patches — before that assignment the character's own level is still ~1, so it cannot protect the save. While a character is loading (IsLoaded == false) the patched getter is therefore floored by the savedCharacterSaveFile.ExperienceLevel, on every path including the vanilla fallback (so a save above level 30 is never truncated to 30). - Extended XP after 20 (linear, O(1)).
F(20)/D(20)are read from the game itself (Character.GetExperienceFloorForLevel(20)and(21)), and forlevel > 20F(L) = F20 + D20 * (L - 20). Nothing is hardcoded: if those numbers are unusable (NaN, non-finite,D20 <= 0) the mod stays vanilla instead of guessing. - Skills, Items, Attributes stay eligible. When the skill pool is exhausted the stage is
skipped and a durable receipt (S) is written under
<BepInEx config>/com.gumatos.roguelikeunlimitedlevelprogression/receipts/<character-guid>.json; S is subtracted from the level-up queue count so the skipped skill is not silently lost. If the receipt cannot be written, the stage is not skipped (no reward is consumed). Items/attributes keep coming after that. No fake skills are invented. - Item/skill reward queries stay at level 20 (
Rewards.RewardQueryLevel) so loot quality does not extrapolate past the vanilla curve. - UI: the level-up bar and the "Max" label are recalculated only for the extended scope,
so they do not show a false maximum or
NaN.
Nothing else changes. No new areas, no endless mode, no invented gold, no meta powerup increase, no item/fortune cap removal, no Bard/music (its tree arrives with a later game update), no forced achievements, no campaign changes.
Configuration
| Key | Default | Meaning |
|---|---|---|
General.Enabled |
true |
Master switch. false reproduces the vanilla behaviour exactly. |
Progression.MaxLevel |
0 |
Voluntary ceiling. 0 = no gameplay ceiling. A value >= 21 caps the extended levels; lowering it never lowers a character that is already above it. |
Rewards.RewardQueryLevel |
20 |
Level used for item/skill reward queries (1..30). |
Diagnostics.Verbose |
false |
Extra logging. |
Scope and fail-closed behaviour
- Tested on singleplayer, not yet tested for multiplayer. In a network session this is not a
party feature: the mod keeps the vanilla progression branch whenever the game reports
PlayingMultiplayerand logs a refusal (refuse("rede", …)), so no party-wide extension happens: the game grants level/XP per owner, and a peer only ever writes its own characters — never the host's party state. If a network session does extend anything (for example, if that instance does not reportPlayingMultiplayer), it applies only to the characters of the player running the mod, in that player's own instance; players without the mod keep vanilla behaviour, and extended saves/ledger are not carried across peers. Testing multiplayer — whole party on the same mod version, authority and save/reload across peers — is pending (MP-BENCH-1is the bench). - Campaign and AI characters are outside the extended scope and keep the exact vanilla comparison.
- The gameplay patch group is all-or-nothing: if any Harmony target is missing, if any
patch throws or if the critical transpiler refuses the target, every patch of the mod is
reverted (
Harmony.UnpatchID(guid)), so the game never runs half-patched. - The critical patch matches the IL structurally (member signatures + a unique window) and requires exactly one match; labels/exception blocks are preserved. No local index or IL offset is hardcoded.
Build
dotnet build RoguelikeUnlimitedLevelProgression/RoguelikeUnlimitedLevelProgression.csproj -p:DeployToBepInEx=false
dotnet build tests/RoguelikeUnlimitedLevelProgression.Harness/RoguelikeUnlimitedLevelProgression.Harness.csproj -p:DeployToBepInEx=false
The game/BepInEx assemblies are not versioned: they are read from the private lib/ folder at the
root of the checkout (<checkout>/lib, gitignored — see docs/ESTRUTURA.md).
Both the mod and the harness resolve that folder relative to the checkout they belong to, so the
same source builds at any depth: no absolute path of the owner's machine, no dependency on where a
worktree happens to live. A canonical checkout (C:/dev/stolen-realm) therefore builds with no extra
argument.
There is no silent fallback: an isolated clone/worktree that does not have its own lib/ fails
with a single clear error and needs the owner-authorized override —
dotnet build RoguelikeUnlimitedLevelProgression/RoguelikeUnlimitedLevelProgression.csproj -p:CommonLibDir=<absolute path of a lib folder>
LEVEL-LIB: lib de referencias nao encontrada em '<...>\lib'. Rode o build na raiz do checkout
canonico (lib/ e local, gitignored) ou passe o override autorizado: -p:CommonLibDir=<caminho>.
Never copy the game DLLs into the repository or into a worktree to satisfy a build.
Deploying into the r2modman profile is opt-in: dotnet build -p:DeployToBepInEx=true
copies the DLL to .../profiles/Default/BepInEx/plugins/RoguelikeUnlimitedLevelProgression/.
Not exercised yet
The v0.1 code was built and exercised in an isolated harness (the mod's own sources compiled
into a test executable that feeds the real transpiler the real IL of
Root.SendGameCompleteFromServer read from the reference Assembly-CSharp.dll), plus the
policy/XP/ledger suites. It has not been run inside the game yet: applying the patches at
runtime, gameplay at levels 21..31, the real runtime F20/D20, the owner's save/reload and
multiplayer remain to be verified in game.
RoguelikeUnlimitedLevelProgression (português)
Remove o teto de nível 20 do modo Roguelike em singleplayer (v0.1): um nível por vitória,
XP estendido depois do 20 (linear, F(L) = F20 + D20·(L-20), com F20/D20 lidos do próprio
jogo) e recompensas de Skills/Itens/Atributos que continuam funcionando quando um pool esgota
— o skill pulado vira um recibo durável (sidecar JSON por personagem) e é subtraído da
contagem da fila, para não ser perdido em silêncio.
Nada mais muda: sem áreas novas, sem endless, sem ouro inventado, sem remover teto de item ou de fortune, sem Bard/música, sem conquista forçada e sem alteração de campanha. Campanha e IA ficam fora do escopo ampliado e mantêm a comparação vanilla. Testado em singleplayer; multiplayer ainda não testado. No multiplayer o branch vanilla é preservado com aviso e não existe ampliação para a party: cada jogador só escreve os próprios personagens, nunca o estado da partida do host; a medição de rede ainda está pendente. O grupo de patches de gameplay é tudo-ou-nada: qualquer alvo ausente, exceção ou recusa do transpiler reverte o mod inteiro.
Build: as DLLs do jogo ficam em lib/ na raiz do checkout (gitignored, nunca versionadas nem
copiadas para o repositório/worktree) e são resolvidas por caminho relativo ao próprio checkout,
sem caminho absoluto do computador do dono. Um clone/worktree isolado sem lib/ própria falha com
LEVEL-LIB: lib de referencias nao encontrada em '<...>\lib' e exige o override autorizado
-p:CommonLibDir=<caminho absoluto>; não existe fallback silencioso.
O código foi compilado e exercitado em bancada isolada (harness que compila os fontes reais
do mod e alimenta o transpiler real com a IL real de Root.SendGameCompleteFromServer). A
execução dentro do jogo — aplicar os patches, jogar nos níveis 21..31, F20/D20 reais,
save/reload do dono e multiplayer — ainda é NAO_EXERCITADO nesta fase.
Source, issues and forks: https://github.com/DefRuivo/stolenRealmMods — contributions are welcome. Código-fonte, issues e forks: https://github.com/DefRuivo/stolenRealmMods — contribuições são bem-vindas.
CHANGELOG
Changelog — RoguelikeUnlimitedLevelProgression
Dates use ISO 8601 (BRT, UTC−03:00). Implementation/audit dates are not publication dates. CHG-REV documentation audit: 2026-10-16. This mod remains unpublished.
0.1.2 — in preparation (2026-10-19)
- Source and contributions: the README now links to the source repository, so users can report issues, contribute or make a fork: https://github.com/DefRuivo/stolenRealmMods.
- The README also clarifies the existing validation scope: tested on singleplayer, not yet tested for multiplayer. This is a documentation change, not multiplayer support; the existing network refusal remains unchanged.
- Bumped the project, manifest, plugin constant and README version together; rebuilt the Release package. No intentional progression, reward or save-format change in this bump.
0.1.1 — in preparation (revised on 2026-10-16)
Lifecycle and diagnostics (RLP-1/RLP-2) — package rebuilt from the current source on 2026-10-16
- Root cause:
Plugin.OnDestroyitself calledUnpatchSelf()and turned offGameplayGroupAppliedwhen the game destroyed the chainloader host on the first scene. The boot announced the patches, but the plugin removed them before use; it was not proof that the structural window or the engine guards were wrong. - Fix:
OnDestroypreserves patches and session state, recording the lifecycle and the ownership of the targets. Removal on install failure/rollback remains explicit inAwake/PatchRegistry; it was not eliminated globally. - Instrumentation: Harmony ownership checked in Awake/OnDestroy/first hook/post-battle, plus refusal reasons and effective policy entries. Reading the existing log on 2026-10-16 records
gameplay=20/20after OnDestroy andpolitica: allow=True ... nivel=20 capVanilla=20. That proves retention of the patches and passage through the policy, and does not replace the whole save/reload, rewards and multiplayer matrix. - Post-battle failsafe: a separate, best-effort prefix in
PostBattleManager.OpenPostBattleMenu; it only tries to grant victory advancement to eligible controlled characters still absent from the vanilla list, using the samePolicy.CanAdvance. It does not duplicate the advancement of those already in the list and does not replace the critical transpiler. It is existing local implementation, not the root cause of the patches disappearing.
Package identity — 2026-10-13 (commit 941e3c3)
- Definitive name:
RoguelikeUnlimitedLevelProgression; display name:Roguelike Unlimited Level Progression. - Project, assembly, namespace, package and harness follow the definitive name.
GUID/Harmony owner and receipts directory:
com.gumatos.roguelikeunlimitedlevelprogression. - Distribution icon 256×256 added in
8cabbe5; the RU icon entered in the rename941e3c3. The local art was replaced again after that commit and must not be described as the old monogram without re-auditing. The version remains 0.1.1; the rename itself changed no gameplay and did not publish the mod. lib/paths of the project/harness resolved from the checkout root (06564ce, 2026-10-12); boot marker fixed tocarregado.(931bcc4, 2026-10-12).- Since the mod was not published, there is no config/receipt migration. The evidence from earlier rounds remains immutable with the identity of its time.
Load protection — 2026-10-11 (commit d1de4ea)
Fix for the silent loss of the saved level on load (R3-01, HIGH finding of the LEVEL-R3 review).
Historical R3-01 fixes
Character.Load(int)demoted the saved level. The load clampExperienceLevel = Mathf.Min(MaxLevel, CharacterSaveFile.ExperienceLevel)uses exactly theCharacter.MaxLevelproperty that this mod patches, and runs beforeExperienceLevelis assigned — at that instantCharacter.Levelis still ~1 and does not protect the saved level. With a voluntary capProgression.MaxLevel = N(≥ 21) a save aboveNwas demoted toN; and the fallback path (group not applied / network unavailable / backend unavailable) returned the vanilla cap 30, truncating saves above 30. The clamped value was persisted (CharacterSaveFile.ExperienceLevel), that is, the loss was silent and durable. Fix: while the character is not loaded (IsLoaded == false) the patched getter never returns a value belowCharacterSaveFile.ExperienceLevel— on every path, including the vanilla fallback. Outside the load path behaviour does not change; campaign/AI/out-of-scope remain vanilla. TheGiveExperiencepath still does not bounce below the current level.- New coverage on the bench (12 checks,
loadclampsuite): a structural anchor in the real IL ofCharacter.Load(int)(single windowMathf.Min(get_MaxLevel, CharacterSaveFile.get_ExperienceLevel)→set_ExperienceLevel), proof that the clamp getter is the same target as the postfix, and a reproduction of the clamp with the pair (effective MaxLevel, saved level) — green with the fix and red with the defect re-planted.
Compatibility
- No behaviour change for campaign, AI, multiplayer or already-loaded characters. The configuration, GUID and scope decisions of 0.1.0 remain intact.
0.1.0
First implementation in preparation (commit cd8e4ce, 2026-10-11), never published. A new mod (no existing mod file was changed), standalone
BepInEx 5 + Harmony, no ModAPI, no BetterStats/RSTV.
Gameplay
- Level 20 cap removed in Roguelike mode (singleplayer). The comparison
character.Level < settings.RoguelikeMaxLevelof the battle result block (Root.SendGameCompleteFromServer) becomesPolicy.CanAdvance(Character, RoguelikeSettings). The patch is structural: it recognises the window by the member signatures (Character::get_Level,GlobalSettingsManager::get_instance,GlobalSettingsManager::roguelikeManager,RoguelikeSettings::RoguelikeMaxLevel,bge), requires exactly one match and preserves labels/exception blocks; there is no local index or hard-coded IL offset.bge→brfalseto the same destination, so a refusal by the policy skips reward and list together. - One level per victory, with the vanilla attributes derived from
Leveland skills/items while eligible. - XP extended past 20, linear and O(1), derived from
F20/D20read from the game itself (Character.GetExperienceFloorForLevel(20)/(21)); atL = 20andL = 21the extension coincides with vanilla by construction. The inverse is analytic (no unbounded loop) and the XP queries of the 5 static converters only hold inside an explicit per-character scope — campaign and AI remain intact. The legacy XP mirror is limited to level 20 for an extended character (the realExperienceLevelremains the source and keeps being saved). - Rewards with an exhausted pool. Empty skill pool: the stage is skipped after
writing a durable receipt (S) and the S is subtracted from the queue count; if the receipt does not
write, the stage is not skipped (the reward is not consumed). Empty item pool: the
nullfallback is blocked and the flow moves on to attributes. A reroll withforcedTierand no option is cancelled (the original skill is preserved). The sharedLevelUpSequenceOrderorder is not changed. - Sidecar v1 in
Paths.ConfigPath/com.gumatos.roguelikeunlimitedlevelprogression/receipts/<guid>.json(schema_version,character_guid,run_id,preset_guid,skipped_skill_receipts[]), atomic write (temporary + replace +.bakbackup), dedupe by ordinal. The run is only reset after a validSyncCharacterToPreset; a null preset does not reset; a corrupted/unreadable ledger does not consume a reward. - Multiplayer: the vanilla branch is kept, with a warning that extended saves/ledger are not supported in this version.
Historical validation baseline (0.1.0 harness snapshot; not the current total)
- Harness
tests/RoguelikeUnlimitedLevelProgression.Harness(net6.0) that compiles the real sources of the mod and exercises: the real transpiler over the real IL of the referenceAssembly-CSharp.dll(hash8bb4a7c5…7855a), with 12 counterproofs (single window, removal ofget_Level,bgE→brfalsewith the same destination, preserved labels, refusal on ambiguity/unsafe target/missing reward tail, absence of partial mutation); the policy matrix (19→20, 20→21, 29→30, 30→31, guards for AI/campaign/multiplayer/mode/voluntary cap/representation limit/missing target); the XP curve (monotonicity, inverse, precision, budget inlong); the ledger (reload, dedupe, corrupted, unknown schema, divergent identity, write failure without consuming a reward, reset only with a valid preset, atomic backup); and the scope guards of the static patch entries. - 91 PASS checks, 0 FAIL, 14
NAO_EXERCITADOitems declared with a reason (real application of the patches in Harmony, game open, owner's save/reload, multiplayer, serialized realF20/D20, UI at runtime).
Fixes found by the bench
ReceiptLedgerStore.Save: the directory creation was outside thetry. A disk/permission failure escaped as a rawIOExceptioninstead ofLedgerWriteException, that is, the write failure could cross the reward flow instead of being treated as "do not consume the reward". It moved inside thetry— exercised by a real test.- Member resolution without
AccessTools. The targets are now resolved by direct reflection (Patches/Reflect.cs): HarmonyX'sAccessToolsstatic initializer breaks on modern runtimes (MethodInvoker.GetHandler(null), becauseException::PrepForRemoting/FixRemotingExceptiondo not exist outside old Mono) and that is an initialization dependency the mod does not need. A missing target still returnsnull→ fail-closed. - Patch plans keep
MethodInfoinstead ofHarmonyMethod(theHarmonyMethodis created only at apply time), which made the resolution of the 20 targets exercisable on the bench.