SpongeMods-SpongeTweaks icon

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.

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-5.4.2305 icon
BepInEx-BepInExPack

BepInEx pack for Mono Unity games. Preconfigured and ready to use.

Preferred version: 5.4.2305

README

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 .cfg itself

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:

  • HudBuilder merged into UiSupport: one UI helper, not two.
  • AddPanel / AddText existed in three near-identical copies across the HUDs; now one pair taking the anchor as a parameter.
  • ColumnHeader, Cell and Caption in the backup browser differed only in font size, alpha and clipping; collapsed into FixedLabel.
  • Ensure<T>, WithAlpha, SetLayoutWidth and StripLocalisation replace 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:

  • Awake side-effects that are not patches (watchers, DontDestroyOnLoad, SaveOnConfigSet batching)
  • 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 BeamSeconds and UseGameExplosionEffects and defines SpawnBeam, 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 1920x1080 canvas reference now lives once in UiSupport.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.SetItem is patched by two features. Ammo Counter writes the ammo readout; Stackable Items writes the stack count. Stacking runs at Priority.Low so 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.
  • ItemToSavedItem is 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 _syncedRandomWeight SyncVar, 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; dealsDamage is 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.