Skip to content

Latest commit

 

History

History
199 lines (167 loc) · 15.6 KB

ARCHITECTURE.md

File metadata and controls

199 lines (167 loc) · 15.6 KB

How to Fish — Game Architecture Report

All findings below come from the installed build (Steam App 4001890, How to Fish_Data/Managed/Assembly-CSharp.dll, 870,400 bytes, dated 2026-09-04), decompiled with ILSpy 11.1 (ilspycmd). Nothing here is guessed; every class, field and method name was read from the decompiled source.

1. Engine and runtime

Item Finding Evidence
Engine Unity 6000.4.4f1 globalgamemanagers header; Player.log "Initialize engine version: 6000.4.4f1 (360f97ecca93)"
Scripting backend Mono (not IL2CPP) Managed/ folder with Assembly-CSharp.dll, MonoBleedingEdge/, no GameAssembly.dll / global-metadata.dat
Render pipeline URP Unity.RenderPipelines.Universal.Runtime.dll, GameInfo._urpAsset, GameInfo._globalVolume
Networking FishNet (FishNet.Runtime.dll), transports: Steam (Heathen/Steamworks.NET) + FishyUnityTransport via Multipass ConnectionManager.SetTransport, FishNet.SDK.Id
Input New Input System and legacy UnityEngine.Input both active PlayerInput actions (PlayerMove, PlayerLeftClick, …) plus Input.GetKeyDown(KeyCode.M) in MoneyManager.Update
Content Addressables (StreamingAssets/aa), Localization tables, Resources.LoadAll<Item>("Items") GameInfo.Awake
Save system JSON text files in %USERPROFILE%\AppData\LocalLow\Dazed Games\How to Fish\Saves\<SteamID>\ (local.txt = per-player journal/skins, <Server name>.txt = world) SaveSystem._saveRootFolder
Mod loaders present None (no BepInEx/, MelonLoader/, winhttp.dll, version.dll) directory listing
Anti-cheat None found (no EAC/BattlEye files, no integrity checks in code) directory listing, code search

Modding approach chosen: BepInEx 5.4.23.5 (x64, Mono) + HarmonyX, with game assemblies referenced through BepInEx.AssemblyPublicizer so private fields are accessed directly instead of by string reflection. No original game file is modified; BepInEx adds winhttp.dll, doorstop_config.ini, .doorstop_version and BepInEx/.

2. Networking / authority model

FishNet with a listen-server host. Server (NetworkBehaviour, singleton Server.Instance) is the central RPC hub: ~60 [ServerRpc(RequireOwnership = false)] methods.

Data Authority Where
Player position/rotation Client (sent each tick) Player.SendPosRot → Server.UpdatePlayerPosRot
Projectile spawn + hit detection Client (shooter simulates, then tells server) Weapon.Shoot → ProjectileManager.AddProjectile(isLocal:true) → Hit → Creature.LocalHit → Server.HitCreature
Creature HP Host (Creature._hp SyncVar) but damage value is supplied by the client Server.RpcLogic___HitCreature → Creature.ServerChangeHp(damage)
Killscore multiplier Client computes, host stores (only if still 1.0) Server.SetItemMultiplier → Item.SetKillscoreMultiplier
Player health/hunger Host PlayerVitals.TakeDamage, LowerFullness gate on IsServerInitialized
Damage to players Sent by whoever simulates the attacker AttackingFish.DamageOnCollision → Server.HitPlayer
Money Host (MoneyManager._money SyncVar) MoneyManager.AddMoney/SellItem/RemoveMoney
Fish bite / selection / weight / drip Host CreatureManager.FindFishForBait (server tick)
Island Host (OnlineIslandManager._curIsland SyncVar) SpawnIsland

Consequence: aim/weapon/movement features work in any role; god mode, hunger, money, fish manipulation and spawning require being the host (single-player is always host: ConnectionManager.CreateOfflineLobby starts server + client).

3. Class / function map

GameInfo (MonoBehaviour, static facade)
 ├─ LevelLayer / ItemLayer / ProjectileHitLayer / NpcLayer / BoatLayer …  (LayerMasks)
 ├─ HeadShotDamageMulti (default 1.5), CooknessWorthCurve
 ├─ _nameToSpawnable / _idToSpawnable / _allItems   ← Resources.LoadAll<Item>("Items")   (item database)
 ├─ _allCreatures                                    ← Resources.LoadAll<Creature>("Items/Creatures")
 ├─ AllBaits (BaitInfo SOs), AllAttachments
 └─ Seed (random seed; lobby id when on Steam)

Player (NetworkBehaviour)  static Player.LocalPlayer
 ├─ Movement      PlayerMovement      custom Rigidbody controller
 ├─ Camera        PlayerCamera        _rot (x=pitch,y=yaw) → CamTransform
 ├─ AimAssist     PlayerAimAssist     GetRotationDelta() added to _rot every frame (controller only)
 ├─ Holding       PlayerHolding       HeldItem, _grabRange, pickup/interact scan
 ├─ Inventory     PlayerInventory     slots, CurBait, owned baits
 ├─ Vitals        PlayerVitals        _syncedHealth/_syncedFullness/_syncedPoison/_syncedFire
 ├─ ToolMovement  PlayerToolMovement  weapon sway / bob / recoil rig
 ├─ KillScore     PlayerKillScore     killscore UI queue
 ├─ Punching      PlayerPunching      fists (melee without an item)
 └─ LocalTeleport(pos, rot, instant) → PlayerMovement.Teleport + flag sent to server

Item (NetworkBehaviour)                       ItemManager.Items : Dictionary<Transform, Item>
 ├─ ID (byte), GetName() (localized), _worth, _weight, _syncedRandomWeight, _cookness,
 │   _bettingMultiplier, _killScoreMultiplier, _rodAttachedTo, IsQuestItem
 ├─ TotalWorth = (int)((int)(_worth × weight × CooknessWorthCurve(cookness) × betting) × killScoreMultiplier)
 ├─ LocalHit(...) virtual
 ├─ Tool : Item   (_anim : Animation)
 │   ├─ Weapon : Tool        (guns, see §4)
 │   ├─ Melee : Tool         (bat/knife/etc., damage = _sharpnessUpgrades[idx].Damage)
 │   └─ FishingRod : Tool → FishingRodCast / FishingRodCrab
 ├─ Explosive : Item         (dynamite; ExplosionManager.ServerExplode)
 ├─ Creature : Item          (_hp SyncVar, _isDrip SyncVar, HeadPos, BossType, IsEndangered)
 │   ├─ Fish : Creature → AttackingFish → Piranha;  Pufferfish;  RunningFish
 │   ├─ Crab, Bird (seagull), Albatross
 │   └─ (BowheadWhale is a boss creature)
 ├─ DeadPlayer, Radio, Map, Disc

4. Weapon architecture

Weapon : Tool fields (serialized per prefab): _weaponInfo : WeaponInfo, _attachments : Attachments, _fullAuto, _canAds, _noShootingDuringShootAnim, _noQueueingShots, _timeBetweenShots, _projSpeed, _spread, _projectileCountPerShot, _recoilKnockback. Runtime: Ammo (private set), IsAds, _aimPercent, _isReloading, _hasCoolDown.

WeaponInfo (serializable class, sent over network): ProjectileType (byte), ProjectileDamage, ProjectileForce, ProjectileGravity, ShootVFX, BoatForceOverride.

Attachments (per-weapon NetworkBehaviour): sights (AdsFov, AimSwayMulti, UseSniperUi), barrel (FirePoint, ScreenRecoilAmount, ModelRecoilPos/Rot, WeaponRecoilMulti, spring values), bullets (Damage = _bulletUpgrades[idx].Damage), AmmoPerMag (default/extended mag).

Fire pipeline (all weapons share it; differences are data):

PlayerHolding.PrimaryInput (Input action "PlayerLeftClick", ignored if Player.BlockInputs)
 → Weapon.PrimaryInput → Weapon.Shoot()                       [also Update() for full-auto / queued]
     ├─ HasCooldown() (_hasCoolDown, or Fire anim playing if _noShootingDuringShootAnim)
     ├─ Recoil: AddModelRecoil (ToolMovement recoil rig), Camera.Recoil(ScreenRecoilAmount),
     │          ToolMovement.Recoil, Movement.Knockback(-cam.forward × _recoilKnockback)
     ├─ Ammo--; cooldown Invoke("CoolDown", _timeBetweenShots)
     ├─ direction = Attachments.FirePoint.forward   (the *weapon model* muzzle; sway & model recoil move it)
     │     or camera forward when sniper-scoped (_aimPercent > 0.9)
     ├─ GunClippingRay(): camera→muzzle raycast; if blocked, hit applied directly and projectile spawned at y=-10000
     ├─ spread: per pellet Quaternion.Euler(random unit vector × _spread) × forward × _projSpeed
     └─ ProjectileManager.AddProjectile / AddProjectiles(owner, weaponInfo, isLocal:true, pos, velocity[])
           ├─ Server.AddProjectile(s) (RPC → other clients simulate a copy)
           ├─ Damage = OneShot ? 99999 : Weapon.Damage (Attachments.Damage)
           ├─ ProjectileType.IsHitScan → HitScan(): SphereCast(∞)   else FixedUpdate: SphereCast per step
           │     gravity: Velocity += down × GravityForce × dt;  removed beyond _maxProjectileRange from owner
           └─ Hit(): Item → item.LocalHit(...);  Player → Vitals.LocalHit;  Explosive → ForceExplode

Damage / hit target selection: ProjectileManager.Hit uses ItemManager.Get(hit.collider) (collider → Item map). Creature.LocalHit:

  • headshot if transform.InverseTransformPoint(point).z > HeadPos → damage × GameInfo.HeadShotDamageMulti
  • Server.HitCreature(this, player, damage, point, dir); hitmarker; if lethal locally → killscore (§7)

Melee (Melee.HitTarget) and fists (PlayerPunching.HitTarget) call the same Item.LocalHit(..., rangedHit:false).

5. Fishing architecture

Input: right mouse (PlayerRightClick) held  → FishingRod.SecondaryInput → FishingRodCast.ReelOutInput
        → FishingRod.ReelOutInput: _isPullingBack = true, OnPullBackStarted (bail open)
        rod rotation _curRodRot rises at _castingRodRotBackSpeed (FishingRod.RotateRod)
Release → ReelOutCancelled → OnPullBackReleased(_curRodRot) → _releasePoint
        → rod swings forward; UpdateReleasedPullBack: when _curRodRot/_releasePoint < _releaseBaitRotPercent → StartCast()
Cast  : _isReelingOut = true; line length follows bait distance (ReelOut) up to _maxLineLength
Water : Bait.UnderwaterCheck (y < WaterManager.WaterHeight) → Bait.BaitsUnderWater, RandomizedCatchTime = Random(CatchTimeMinMax)
        TimeUnderWater grows only while the bait moves horizontally (>√5 m/s) when BaitInfo.RequireReelingToCatch
Bite  : [HOST] CreatureManager.TickUpdate → FindFishForBait(bait):
            requires bait.y < _minBaitLevel (-2.5), no ServerItemOnBait, TimeUnderWater ≥ RandomizedCatchTime
        → GetRandomItem(pos, BaitInfo.ItemWeights)   weighted random over Fishable.ItemToSpawn
        → HookItem(prefab, bait): Instantiate; Random(0,100) < _shinyCreatureChance → SetDrip();
            item.SetAttachedRod(rod) (SyncVar) ; Spawn()
Hooked: Item.OnAttachedRodChange → InitializeBait → FixedJoint to bait; client UI "caught"
Reel  : left mouse (PrimaryInput) → ReelInInput → StartReel(); FixedUpdate shortens line (_holdReelSpeed × _curReelSpeedMulti)
Land  : right mouse while item on bait → FishingRod.ReleaseItem → Server.ReleaseItemFromBait → SetAttachedRod(null), bait may be consumed (LostOnBaitChance)
        (also auto-release after 5 s above water: Bait.IncreaseAboveWaterTime)
Kill  : any LocalHit (punch / melee / gun / explosion) → Creature dies → CanPickUp
Pickup: "PlayerPickUp" → PlayerHolding.PickUpInput → Item.PickUp(calledFromLocal, sendToServer)
Sell  : [HOST] SellBox.OnTriggerStay (dead creature, not held) → MoneyManager.SellItem(item) (+TotalWorth)
        or NPC money quest: NPC.EatItem → MoneyManager.SellItem + DestroyItem(NPC)

No line tension / line-break mechanic exists in this build (no tension variable, no break code). No Line Break / Infinite Line Strength therefore have nothing to act on.

6. Fish generation — when characteristics are decided

Attribute Decided Code
Species On bite, host CreatureManager.GetRandomItem (weights from current BaitInfo.ItemWeights)
Variant ("Drip"/shiny) On bite, host HookItem: Random.Range(0,100) < _shinyCreatureChance → Creature.SetDrip() (_isDrip SyncVar)
Weight On network spawn, host Creature.OnStartServer → CreatureUtils.GetWeightWithStandardDeviation(ObjectId): Gaussian(mean 1, σ 0.15) seeded by GameInfo.Seed + ObjectId, clamped 0.25–1.75 → _syncedRandomWeight
Value Derived on demand Item.TotalWorth (formula in §3)
Killscore multiplier On kill, client→host Server.SetItemMultiplier
Rarity Not a fish attribute. enum Rarity {Default, Common, Rare, Legendary} is only used by ItemSkin (weapon/boat skins, slot machine).
enum FishType Unused (no references)

Displayed weight in-game = _weight × _syncedRandomWeight kg (Creature.InspectInput).

7. Trickshot / killscore calculation

KillScoreCalculator (static list builder) — called in Creature.LocalHit on the local killing blow: bonuses = rangedHit ? GetRangedBonuses(creature, point, damage) : GetMeleeBonuses(creature); multiplier = product of Bonus.Worth; Server.SetItemMultiplier(item, multiplier); PlayerKillScore.AddKillScore(name, TotalWorth, bonuses).

Bonus Worth Condition (exact)
Endangered 1.25 creature.IsEndangered
Double/Triple/Quadra/Penta/Multikill lerp(1,1.5,n×0.1) PlayerSkills.Multikill = kills within 3 s
360 1.5 PlayerSkills.RecentlyDid360: ≥320° accumulated yaw, gaps ≤0.5 s, within last 1 s
No-scope 1.2 not ADS and ≥0.25 s since leaving ADS
Quick-scope 1.1 ADS for ≤0.5 s
Longshot 1.3 distance(target, camera) ≥ 25
Headshot 1.25 local z of hit point > HeadPos
Dogfight 1.5 player airborne and no ground within 1.5 m below target
Aerial 1.25 player airborne only
Fly fishing 1.25 target airborne only
One shot one kill 1.25 target at full HP and damage ≥ HP
Point blank 1.1 distance(target, player) < 2
Overkill 1.25 HP − damage < −70 (not boss)
Last bullet 1.25 held weapon Ammo == 0 after the shot
Finally 1.1 Σ _timeBetweenShots since last kill > 5
Killsteal 1.3 LocalHitsTaken == 0 and HP ≤ 30%
Noob kill 1.01 no other bonus
Impressive 2.0 ≥ 5 bonuses already
Melee 1.05 melee kill; Explosion 1.25 for explosions

Note: CheckScope and CheckNoob fire achievement checks, so the mod predicts bonuses with its own side-effect-free copy of this logic.

8. Player architecture

  • Movement: custom Rigidbody controller (PlayerMovement, not CharacterController). Move() in FixedUpdate rewrites _rig.linearVelocity from _walkSpeed/_sprintSpeed, _acceleration, _decceleration, _extraGravityForce. Jump sets velocity.y = _jumpForce; JumpInput allows jumping if grounded/coyote/swimming/touching boat or PlayerManager.InGodMode.
  • No fall damage exists in the game. Water damage: head underwater > 2 s → Server.HitPlayer(_player, 10, …, damageType 0) every 0.5 s.
  • Vitals (host): health/fullness 0–100, poison & fire DoT, regen needs ≥80% fullness, invulnerability 0.25 s after damage.
  • Camera: PlayerCamera._rot Euler (pitch clamped ±90); recoil via _recoilTar/_recoilCur; FOV from ADS.
  • Interaction: PlayerHolding capsule-overlap scan of _grabRange × _grabRadius; interactions are instant (no hold timers exist).

9. World

  • IslandManager / OnlineIslandManager (host _curIsland SyncVar, TpToSpecificIsland, TpToNextIsland); Island.IslandPos, SpawnManager.PlayerSpawnPos.
  • BossManager.Boss (Creature), IsImmortal, BossMaxHp.
  • NPCManager._idToNpc (NPCs are MonoBehaviours tagged "NPC"; shooting them only plays effects).
  • BoatManager.Boat (VisualBoat, Driver, Velocity).
  • WaterManager.WaterHeight.
  • Built-in dev tooling DazedCommands (chat /spawn, /addmoney, /godmode, /oneshot, …) is gated by ClientSettings.CheatsEnabled, which only a developer Steam ID can toggle (ButtonManager.CheatsButton → SteamManager.IsDev). The mod calls the same underlying APIs directly and never flips CheatsEnabled (it would also enable dev hotkeys M/N/G/H/O/T and 99,999 starting money).