Limitless (mod analysis)
v1.4.8
Reverse-engineering notes for the community Limitless module: how it raises battle agent capacity beyond the stock native pool. Independent of any other total-conversion content.
Related: Agents, Missions, MissionLogic, MBSubModuleBase, Harmony.
What it is
Limitless increases the native agent slot pool used in single-player missions (soldiers, player, mounts). Stock capacity is exposed to C# as:
// DefaultBattleMissionAgentSpawnLogic
public static int MaxNumberOfAgentsForMission
{
get
{
if (_maxNumberOfAgentsForMissionCache == 0)
_maxNumberOfAgentsForMissionCache = MBAPI.IMBAgent.GetMaximumNumberOfAgents();
return _maxNumberOfAgentsForMissionCache;
}
}
public static int MaxNumberOfTroopsForMission => MaxNumberOfAgentsForMission / 2;
GetMaximumNumberOfAgents() reads the engine limit baked into TaleWorlds.Native.dll. Harmony on managed spawn code alone cannot grow that pool β Limitless patches the native binary in memory, then retargets campaign spawn math to spend the new slots.
It is not a skill/attribute-cap mod.
Layout on disk
Under Modules\Limitless\:
| Path | Role |
|---|---|
SubModule.xml |
Registers Limitless.dll / Limitless.SubModule |
ModConfig.json |
Player settings (JSON) |
bin\Win64_Shipping_Client\Limitless.dll |
Managed submodule + Harmony |
bin\Win64_Shipping_Client\0Harmony.dll |
Bundled Harmony |
System\Limitless.ini |
Native-facing copy of a subset of settings |
System\Limitless.Injector.exe |
Loads the patcher into the live game process |
System\Limitless.Patcher.dll |
Native recipes against TaleWorlds.Native.dll |
SubModule.xml dependencies on the analyzed build (optional CustomBattle):
<DependedModules>
<DependedModule Id="Native" DependentVersion="v1.4.8" />
<DependedModule Id="SandBoxCore" DependentVersion="v1.4.8" />
<DependedModule Id="CustomBattle" Optional="true" DependentVersion="v1.4.8" />
<DependedModule Id="Sandbox" DependentVersion="v1.4.8" />
</DependedModules>
Settings
ModConfig.json (managed) drives the Options UI and is rewritten to System\Limitless.ini before native init.
| Key | Meaning |
|---|---|
AgentLimit |
Total native slots (soldiers + player + horses). Multiple of 30, range 2040β32760 |
SiegeAgentLimit |
Cap for siege / sally-out spawn budgets (β€ AgentLimit) |
RealUnlimitedCorpses |
Raises corpse visual/pool caps (native policy uses 16380) |
CustomAnimationSampling |
Overrides distant animation sampling |
DistantAnimationIntervalMs |
Far-distance interval (ms), 1β1000 when custom sampling is on |
DespawnHorses |
Mission behavior fades eligible riderless mounts |
DespawnHorseDelaySeconds |
Minimum riderless time before fade eligibility |
Native INI only carries the keys the patcher needs (AgentLimit, corpse/anim flags). Horse despawn stays managed-only.
Changing settings requires a full game restart. Mid-session retries are refused.
Boot sequence
- Game loads the module. Startup runs from
OnBeforeInitialModuleScreenSetAsRoot(fresh main menu). It aborts if aGameorMissionalready exists, or in multiplayer. - Managed code loads
ModConfig.jsonand writesSystem\Limitless.ini. - If
Limitless.Patcher.dllis not already loaded in-process, it starts:Limitless.Injector.exe --pid <current PID> "<β¦>\System\Limitless.Patcher.dll". - Injector waits until
TaleWorlds.Native.dllis mapped, then loads the patcher (remoteLoadLibraryW). Target must be Bannerlord / launcher; a per-PID mutex blocks duplicate loads. - Managed code resolves patcher exports and calls
Limitless_InitializeSinglePlayerFromMenu. - Capacity / feature bits / animation interval are checked against config and against
DefaultBattleMissionAgentSpawnLogic.MaxNumberOfAgentsForMission. - Harmony installs campaign spawn budgeting and Performance Options entries. Optional
HorseCleanupLogicis added on mission start when despawn is enabled.
If inject/init fails, campaign expansion is disabled for that session; any native patches that already applied stay until restart.
Native patcher
Limitless.Patcher.dll is unmanaged (x64). Public exports:
| Export | Purpose |
|---|---|
Limitless_Initialize |
Generic init entry |
Limitless_InitializeSinglePlayerFromMenu |
Menu-gated SP init (what the submodule calls) |
Limitless_GetCapacity |
Active agent-slot capacity after patch |
Limitless_GetFeatures |
Bit flags (corpses / custom anim) |
Limitless_GetAnimationIntervalMs |
Active distant-anim interval |
Limitless_GetStatus |
ANSI status string for diagnostics |
Observed init behavior (from status strings / imports):
- SHA256 of on-disk
TaleWorlds.Native.dll(BCrypt). Unknown hash βUnsupported engine SHA256β recipes are build-specific. - Confirm loaded
.textmatches the file (refuse if another native patcher already mutated it). - Suspend threads, apply a recipe/manifest of immediate and RIP-relative operand patches (stock sizes such as 2040 β configured
AgentLimit), restore protection, resume threads. - Optional corpse and animation recipes when those INI flags are set.
Recipe labels seen in the binary include Mission allocation, primary/second agent pools, link tables and counters, slot init/fill counts, Agent-ID bit packing (stock: min=-1, max=2046, bits=11), bone matrix/mapping block growth, corpse renderer pool sizes, and private RIP-relative animation denominators.
Why so many sites?
Community confirmation ([BUTR] Aragas): the C++ engine hardcodes the battle max-size / agent-pool length as an inlined constant, not a single runtime variable. Compilers duplicate that immediate across allocations, loops, ID packing, and related geometry β on the order of ~1600 occurrences in a typical build (ballpark, not a fixed contract). Limitless (and equivalent experiments) locate each relevant immediate and rewrite it.
That is exactly why the mod is one game version at a time:
- Every Bannerlord / native rebuild reshuffles code and immediates β new offsets / recipes.
- There is no stable Harmony-style hook for βchange one capacity field and inherit forever.β
- Backwards-compatible tooling that must survive patches generally cannot ship this approach; maintainers re-run the find-and-patch pass per update (community estimate: often under an hour with modern AI-assisted scanning, still mandatory each release).
A cleaner engine-side fix would be a mutable static (or config-driven) capacity instead of a compile-time constant, so one write updates all consumers. Whether TaleWorlds adopts that is unknown; until then, inlined-constant relocation remains the only practical path for raising the native ceiling.
Managed Harmony layer
After native capacity exists, C# must budget those slots for campaign / custom battles.
Targets (campaign / mission names verified in 1.4.8 decompiled game source):
SandBoxMissions.CreateCampaignMissionAgentSpawnLogicDefaultBattleMissionAgentSpawnLogic(_battleSize,InitWithSinglePhase, quota helpers)MissionBattleSideSpawnContext(TryReinforcementSpawn,GetReservedTroopQuota,SpawnTroops)BannerlordMissions.OpenCustomBattleMission/OpenSiegeMissionWithDeployment- Custom Battle UI:
ArmyCompositionGroupVM.MaxArmySizewhenTaleWorlds.MountAndBlade.CustomBattleis loaded (shipping assembly; not always present in every source dump)
Behavior summary:
- Registers a per-logic slot budget =
AgentLimitorSiegeAgentLimit, clamped to native capacity; writes_battleSize. - Transpilers replace reads of
MaxNumberOfAgentsForMissionwith that budget where quota = capacity βNumberOfAgents. - Mounted troops cost 2 slots when horses spawn; foot cost 1.
- Initial deployment binary-searches a battle size so defender + attacker composition fits remaining slots.
- Custom Battle army size β
AgentLimit / 2.
Options UI: Harmony adds a Limitless (restart required) group under Performance (OptionsProvider.GetPerformanceOptionCategory and related OptionsVM hooks).
Horse cleanup: a MissionLogic scans Mission.MountsWithoutRiders, skips mounts near mount interactions / reserved riders, then FadeOut after the configured delay β only on supported campaign battles that have a registered budget.
Architecture
ModConfig.json
β
βΌ
Limitless.dll (main menu)
β
βββ write System\Limitless.ini
βββ spawn Limitless.Injector.exe --pid β¦
β βββ LoadLibrary β Limitless.Patcher.dll
β βββ patch TaleWorlds.Native.dll in memory
β (pools / IDs / corpses / anim)
βββ Harmony: spawn budgets + Options + horse fade
Practical limits
- SP only. Multiplayer paths are refused.
- Init only at the idle main menu (no live
Mission). - Hostile to other native patchers that rewrite
TaleWorlds.Native.dll.text. - High
AgentLimitcosts RAM/CPU; siege scenes can still trap agents when the scene cannot hold the budget. AgentLimitmust stay a multiple of 30; that matches how pool geometry is scaled in the recipes.
Takeaways for modders
- Managed battle-size mods that only change spawn counts still hit
MBAPI.IMBAgent.GetMaximumNumberOfAgents(). - Raising that ceiling means native work (or shipping a forked engine), not Harmony alone β and specifically relocating an inlined constant across a large recipe set (~thousand-scale sites), not flipping one global.
- Any native recipe set is tied to a specific
TaleWorlds.Native.dllhash β plan for per-patch maintenance; this will not be backwards-compatible across game updates. - After growing the pool, still retarget campaign spawn / reinforcement math so mounts and both sides fit the new budget.
- Prefer asking TW for a static / configurable max-agent capacity if the goal is long-lived modding APIs; patching inlined immediates is workable but forever version-locked.