SpongeTweaks
All-in-one: giant fish, catch records, rod upgrades, boss/fish HP, boss timer, ammo count, swimming, stacking, sticky items, save backups, void beam, laser sight, more players, fair slots.
By SpongeMods
| Last updated | 37 minutes ago |
| Total downloads | 20 |
| Total rating | 0 |
| Categories | |
| Dependency string | SpongeMods-SpongeTweaks-1.0.1 |
| Dependants | 0 other packages depend on this package |
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
SpongeMods - SpongeTweaks
One BepInEx plugin that merges the functionality of fourteen How to Fish community mods into a single DLL, rewritten and renamed, with every non-English string and identifier translated to English.
Built and verified against the current game build (Assembly-CSharp.dll from the
Steam install), BepInEx 5.4.2305.
IMPORTANT: disable the fourteen original mods first
This plugin patches the same game methods as the mods it replaces. Running it alongside any of them applies both patches and will double-count ammo text, roll sizes twice, write two sets of backups and generally misbehave.
In the r2modman UI, untick these fourteen in the Speedrunners profile:
| Disable | Replaced by feature |
|---|---|
| AtomicStudio-Stackable_Items | Stackable Items |
| Azumatt-BetterSwimming | Better Swimming |
| CMax-AmmoCount | Ammo Counter |
| evansvl-BossHP | Boss HP |
| evansvl-BossTimer | Boss Timer |
| evansvl-FishHP | Fish HP |
| Hatsune_EXO-HTFHollowPurple | Void Beam |
| hiccup-SaveBackups | Save Backups |
| Kalamies-MorePlayers | More Players |
| ReyDev-PumpJump | Shotgun Jump |
| ReyshersU-Better_How_To_Fish | Giant Catches |
| welffi-FairerSlots | Fairer Slots |
| welffi-StickyItems | Sticky Items |
| yepokay-LaserSightEditor | Laser Sight Editor |
BepInEx-BepInExPack must stay enabled: it is the loader.
Leave them installed but disabled. mods.yml is r2modman's own database and is
deliberately not edited by this project.
Features and their keybinds
Every keybind is unchanged from the mod it came from.
| Key | Action | Feature |
|---|---|---|
H |
Charge and fire the void beam | Void Beam |
F9 |
Cycle laser sight colour | Laser Sight Editor |
F10 |
Cycle laser glow intensity | Laser Sight Editor |
E |
Buy a rod upgrade at the bench | Giant Catches |
Fourteen features, each independently switchable via Enabled in its config
section:
- Giant Catches - random fish sizes (1x to 25x) with scaled model, health and contact damage; a persistent biggest-catch leaderboard; catch cards with a rendered fish preview; a 25-level rod upgrade track (luck, reel speed, sale bonus) sold from a workbench placed near the sell box; world-space creature health bars; melee and bullet upgrade tables extended to 255 levels.
- Boss HP - exact boss hit points beside the native bar, optional bar recolour.
- Boss Timer - countdown until the boss leaves, read from networked ticks.
- Fish HP - stacked boss-style bars for creatures you have damaged.
- Ammo Counter - "loaded / magazine" on weapon inventory slots.
- Better Swimming - hold-to-rise, buoyancy, dive, speed clamp, configurable drowning.
- Shotgun Jump - recoil knockback on multi-projectile weapons.
- Fairer Slots - the drip machine rerolls skins you already own.
- Laser Sight Editor - recolour the laser, cycle presets with hotkeys.
- More Players - raise the hosted lobby size (default 32, up to 250).
- Stackable Items - identical non-weapon items share a slot, saved per player.
- Sticky Items - dropped items and dead fish persist across save/load.
- Save Backups - versioned save snapshots, with a Backups button on the load screen for restoring, editing, pinning and per-mod companion files.
- Void Beam - charge two orbs and fire an annihilating beam, networked so everyone sees it.
Configuration
One file: BepInEx/config/spongemods.howtofish.anglerscompendium.cfg, written on
first launch.
All 65 config keys from the original mods are preserved by name. Section headers
were renamed so fourteen mods can share one file without colliding (for example
BetterSwimming's 2 - Swimming is now Swimming - Movement, and each mod's
generic HUD section is now Boss HP, Boss Timer or Fish HP). Key names,
defaults and value ranges are unchanged, so settings are easy to carry over by
hand.
Live reload. Editing the .cfg while the game is running reloads it
immediately, no restart needed. BetterSwimming was the only one of the fourteen
to do this; here it covers every feature. Most settings are read fresh each frame
so they take effect at once. Values only read during startup, notably each
feature's own Enabled flag, still need a restart.
Saved data lives in:
BepInEx/config/SpongeMods/Stacks/<save>.json- stacked item overflow<persistentDataPath>/SaveBackups/<save>/- save snapshots<persistentDataPath>/Saves/<save>_stickyitems.json- dropped world items- Rod levels and catch records are stored inside the
.cfgitself
Building
Needs the .NET 8 SDK. The game path is baked into the csproj and can be overridden:
dotnet build -c Release
dotnet build -c Release -p:GameManaged="/path/to/How to Fish_Data/Managed"
Output: bin/Release/SpongeMods.SpongeTweaks.dll. Deploy that DLL plus Sounds/ to
BepInEx/plugins/SpongeMods-SpongeTweaks/.
Private game members
The originals reached private fields (Weapon._attachments, PlayerMovement._rig,
Item._syncedRandomWeight, ...) via publicized reference assemblies. This project
uses the Krafs.Publicizer NuGet package, which rewrites the reference copies at
compile time only. The game DLLs on disk are never modified, and at runtime the
real, untouched assemblies are loaded. Restoring NuGet packages therefore requires
network access on a clean checkout.
Naming
Everything internal carries the SpongeMods identifier:
| Thing | Value |
|---|---|
| Plugin GUID | spongemods.howtofish.anglerscompendium |
| Assembly | SpongeMods.SpongeTweaks.dll |
| Namespace | SpongeMods.SpongeTweaks.* |
| Config file | spongemods.howtofish.anglerscompendium.cfg |
| Companion data | BepInEx/config/SpongeMods/Stacks/ |
| Runtime objects | SpongeMods_BossHpText, SpongeMods_RankingHud, ... |
| Embedded icons | SpongeMods.SpongeTweaks.edit.png, ... |
The runtime object prefix matters beyond branding: the backup UI skips buttons
whose name starts with SpongeMods_ when measuring the menu's own layout, so
renaming it without updating that check would make the panel re-measure its own
button every time it opened.
Performance work
Profiling by inspection of the per-frame paths found several real costs. Each was fixed and the reasoning is in the code:
| Where | Problem | Fix |
|---|---|---|
RodUpgradeSystem.Tick |
Two full scene scans per frame whenever no upgrade bench existed, because proximity was resolved twice and each miss rescanned | Resolve once per frame; throttle the fallback scan to 2s; squared distance |
CreatureHealthBar.LateUpdate |
Runs per creature per frame: LINQ lambda allocated a closure, Vector3.Distance took a square root, and MaxHp (Harmony-patched, so not a field read) was re-read |
Allocation-free loops, squared-distance culling, single MaxHp read, early-outs before the expensive work |
FishScaleState.LateUpdate |
Called IsScalableCatch (a GetComponent plus property reads) every frame for every fish |
Cached; only re-checked while still false |
| Three size checks | GetType().Name == "Albatross" allocated a string per fish per frame |
Single IsFlyingCreature type test |
VoidAnnihilation.At |
Physics.OverlapSphere allocates a fresh Collider[] each call, and the beam sweeps every frame of its flight (~160 arrays per shot) |
OverlapSphereNonAlloc into a reused buffer |
CreatureHealthModule.EnsureHud |
Rescanned the scene on every hit when the boss UI had never existed, so rapid punching meant repeated full scans | Throttled to 1s between failed searches |
Code reuse
Duplication removed rather than left to drift:
HudBuildermerged intoUiSupport: one UI helper, not two.AddPanel/AddTextexisted in three near-identical copies across the HUDs; now one pair taking the anchor as a parameter.ColumnHeader,CellandCaptionin the backup browser differed only in font size, alpha and clipping; collapsed intoFixedLabel.Ensure<T>,WithAlpha,SetLayoutWidthandStripLocalisationreplace idioms that were repeated a dozen-plus times.
Verifying against the game
The compiler cannot check strings like [HarmonyPatch(typeof(Weapon), "Shoot")]
or AccessTools.Field(typeof(Creature), "_hp"). If the game renames a member,
those silently become no-op patches at runtime rather than build errors. Harmony
also matches patch parameters to the target method by name, and a wrong name
throws at patch time and takes the whole plugin down.
Run everything for both mods with one command, from the parent directory:
../verify-all.sh
# override paths if your install differs:
GAME_MANAGED="/path/to/How to Fish_Data/Managed" ../verify-all.sh
That builds both plugins and runs five Mono.Cecil checkers:
| Tool | Checks | Current result |
|---|---|---|
tools/VerifyGameApi |
Every [HarmonyPatch] / AccessTools / Traverse string names a member that exists |
113 refs (AC), 22 (DS) |
tools/VerifyPatchSignatures |
Every patch parameter is a Harmony injection or a real parameter of the target | 66 signatures (AC), 14 (DS) |
tools/VerifyModInterop |
SpongeSpeedRunners's reflective hooks into this mod still resolve, with the right shape and staticness | contract intact |
tools/VerifyNoConflicts |
Shared Harmony targets, duplicate GUIDs, clashing runtime object names or resources between the two mods | conflict-free |
| BepInEx discovery | Exactly one loadable plugin with valid metadata | passes |
Run this after any game update; it is the fastest way to find what an update broke.
Both checkers have been fault-tested. Renaming a patch target to a nonexistent method, and renaming a patch parameter to a name the target does not have, both still compile cleanly and are correctly reported by the respective tool. A checker that never fails is worth nothing, so if you change one, re-confirm it can fail.
What the verifiers cannot see
They compare declarations: Harmony attributes, AccessTools strings, config
binds. They are structurally blind to ordinary C# behaviour.
This is not hypothetical. An initial audit reported "66/66 patch targets, zero
missing" and was used to claim complete parity, while BetterSwimming's
FileSystemWatcher-based live config reload had been dropped entirely. It
declared no patches and touched no game types, so nothing flagged it.
When checking parity against the originals, also diff:
Awakeside-effects that are not patches (watchers,DontDestroyOnLoad,SaveOnConfigSetbatching)- soft dependencies and other-mod interop (
Chainloader.PluginInfos,Type.GetType("OtherMod.Thing, OtherMod"),AccessTools.TypeByName) - game APIs called directly rather than patched
- dead code: HollowPurple binds
BeamSecondsandUseGameExplosionEffectsand definesSpawnBeam, but never calls it. Faithful parity means keeping the config keys (so existing configs still load) without inventing behaviour the original never had.
Numeric constants
Names and signatures can all match while a transcribed number is wrong, and a mistyped threshold (0.28 vs 0.82, 4.5 vs 45) changes behaviour without failing any check above. Every numeric literal was therefore diffed per feature against the original, ignoring version strings and comments. All eight features with tuned constants match. The only differences are deliberate equivalences:
MathF.PI / 180f->Mathf.Deg2Rad(proven bit-identical as float)2147483647.0->int.MaxValue(identical in double comparison)- the duplicated
1920x1080canvas reference now lives once inUiSupport.CreateOverlayCanvas
The commands used for these sweeps are in the git history for this README.
Architecture
src/Core/ CompendiumPlugin (single entry point), IFeatureModule, UiSupport
src/Hud/ Boss HP, Boss Timer, Fish HP, Ammo Counter
src/Gameplay/ Better Swimming, Shotgun Jump, Fairer Slots
src/Cosmetic/ Laser Sight Editor
src/Multiplayer/ More Players
src/Inventory/ Stackable Items
src/Persistence/ Sticky Items, Save Backups (+ companion registry)
src/Abilities/ Void Beam
src/Progression/ Giant Catches
tools/ Mono.Cecil verifiers (standalone console apps)
verify.sh Build + run both verifiers
Each former mod is an IFeatureModule. CompendiumPlugin.Awake binds every
module's config, then initialises only the enabled ones, and pumps Tick /
LateTick for those that asked for it. A module that throws during startup is
logged and skipped, so one broken feature cannot stop the other thirteen loading.
There is one Harmony instance for the whole plugin, and one Update loop
rather than fourteen.
Build trap: tools/ and the default glob
The plugin csproj is SDK-style, so it globs **/*.cs by default and will happily
compile tools/*/Program.cs into the plugin, failing with duplicate Main and
missing Mono.Cecil. The csproj therefore carries an explicit
<Compile Remove="tools/**/*.cs" />. Keep that if you add more tooling.
This is easy to miss because the plugin builds fine until the moment a second project is added under the plugin's own directory.
Traps found the hard way
Things that looked correct and were not. Read this before touching patches.
A Harmony prefix runs before the game's own null guard. SlotMachine.Roll
has its entire body wrapped in if ((bool)_instance), and it is reachable over
the network (SlotMachineManager line 159 is the RPC handler) as well as
locally. A prefix therefore executes with arguments straight off the wire, on a
client where the machine may not exist yet. The FairSlots patch originally
indexed itemIDs[rolled] and dereferenced GameInfo.IDToItem(id).SkinPreset
immediately. IDToItem returns null for an unknown id, so a malformed or
early packet would have thrown inside the patch. Any prefix on a networked
method must treat every parameter as untrusted.
SkinPreset.Skins is an IReadOnlyList<ItemSkin>, not an array. It is a
_skins.AsReadOnly() property, so it is .Count, not .Length, and it
allocates on every access. Do not call it in a per-frame loop.
Godmode's "player only" and "single sink" claims are verifiable, so verify
them rather than asserting them. Evidence in the decompiled assembly:
ServerDie has exactly one call site in the whole game (inside
PlayerVitals.TakeDamage); _syncedHealth is written in only four places, and
the three besides TakeDamage (spawn reset, Heal, LoadFromSave) cannot be
lethal because SaveManager clamps stored Health to a minimum of 10; and
PlayerDying.LocalDie has one caller, in the SyncVar change handler, which is
a reaction to health already being 0 rather than an independent damage source.
Suppressing TakeDamage does not strand status effects. Poison and fire
decrement their own counters before calling TakeDamage, so they still tick
down and clear. Passive regen returns early once health is at 100, before its
LowerFullness call, so godmode does not silently drain hunger. Both were
hypothesised as bugs and both were disproved by reading the code.
Harmony needs one method per patch. The four store-context patch pairs in
StackingPatches look like copy-paste and cannot be collapsed into a loop, but
their bodies now route through shared Push/Pop/HeldItem helpers.
ilspycmd 8.2 silently empties yield return state machine bodies. Files
look plausible while missing the real logic. Use 9.0.0.7889; v11 fails with a
DotnetToolSettings.xml error.
Notes for future work
Points where the merge is not a literal transcription, and why:
InventorySlot.SetItemis patched by two features. Ammo Counter writes the ammo readout; Stackable Items writes the stack count. Stacking runs atPriority.Lowso it goes last, and it returns early for weapons rather than clearing the label, which is what preserves the ammo text. Changing either patch means rechecking the other.ItemToSavedItemis private. Sticky Items reuses the game's own item serialisation reflectively rather than duplicating it, so saved items stay compatible with the game's format.- Stacking registers as a Save Backups companion. Restoring a snapshot also
restores that save's stack file, and clears stack data the snapshot predates.
This is the same public API SaveBackups exposed, so third-party mods that
integrated with it can register against
Persistence.CompanionRegistry. - Size is stored in the game's
_syncedRandomWeightSyncVar, not a parallel field, so it replicates and persists for free. The cost is reflection. - Rod upgrades are charged server-side. The purchase request is resolved from the network connection, not from the Steam ID in the message, so a client cannot buy an upgrade for someone else.
- The void beam damages only on the firing client. Everyone spawns the orb
visually;
dealsDamageis false on remote clients so one shot is not applied several times.
Translation
HollowPurple and PumpJump were written in Spanish, and GiantFish had Spanish UI
text. All of it is now English: identifiers (MultiplicadorRetroceso ->
KnockbackPower, ParcheInicializacionEscopeta -> ShotgunRecoilPatch), log
messages, and on-screen strings (MAYORES CAPTURAS -> BIGGEST CATCHES,
Pescador -> Angler, MESA DE MEJORAS DE CAÑA -> ROD UPGRADE BENCH).
Accented characters in UI strings are written as \u escapes where they are
symbols (\u00d7 for the multiplication sign, \u2605 for the filled star) so
the source stays ASCII and cannot be mangled by an editor's encoding.
Status
Built, statically verified, and installed to
BepInEx/plugins/SpongeMods-SpongeTweaks/ in the Speedrunners profile.
Not yet run in-game. Everything below is static verification: the build is
clean, all 113 game references and 66 patch signatures resolve, and BepInEx's own
plugin-discovery logic finds exactly one loadable plugin with correct metadata.
That rules out the failure modes that would stop it loading, but it is not a
substitute for playing. Launch the game once, then check
BepInEx/LogOutput.log for the line:
SpongeTweaks 1.0.0 loaded ... features.
followed by one "active" line per enabled feature. Anything that failed to start
is logged as <feature> failed to start and was skipped.
Provenance
Decompiled sources for all fourteen originals and the game itself are in
../decompiled/. See ../decompiled/README.md for how they were produced and
for the toolchain traps involved.