About this mod
Some configurable fish tweaks, like visible fish sprites.
- Requirements
- Permissions and credits
- Changelogs
- Donations
I've come to realise that I enjoy mining & farming because I can pick and choose what what I mine / farm.
I don't dislike the minigame, but since the game spawns the fish as entities...
You basically pick from 1-5 'unknowns' while waiting for them to be replaced by the next batch of RNG 'unknowns' to appear...
Only to discover that none of them end up being that specific one you're missing before the season ends...
So now you can choose to see what those 'unknowns' are before deciding to catch them, among other configurable tweaks (F1):
- Sprite Setting:
1: Don't use modded sprites till the 'Keen Eye' skill. Show dark modded sprites after.
2: Show dark modded sprites till the 'Keen Eye' skill. Show modded sprites after.
3: Always show dark modded sprites.
4: Always show modded sprites.
- Apply Shine:
This shine has also been improved to properly draw above water and reflect the rarity colour scheme (instead of being glitchy and single colour).
- Chance:
A small bonus to fish spawns is applied based on any bait applied.
Multiplier applied to overall fish spawn chance.
This is something I threw together quickly for my own playthrough, I'm unlikely to develop it further / maintain it.
Installation:
- Download and Install the latest version of BepInEx.
- Download this mod from Files, and unzip the mod .dll into your BepInEx\plugins folder.
Compatibility:
- Should be compatible with most fishing mods.
- Unless they edit the same place, in which case turning off certain features can help.
Source:
using BepInEx;
using BepInEx.Configuration;
using BepInEx.Logging;
using DG.Tweening;
using HarmonyLib;
using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reflection;
using UnityEngine;
using Wish;
[BepInPlugin(MOD_GUID, MOD_NAME, MOD_VER)]
public class GenericPortraitsPlugin : BaseUnityPlugin
{
private const string MOD_GUID = "barteke22.sunhaven.fish_tweaks";
private const string MOD_VER = "1.0.2";
private const string MOD_NAME = "Fish Tweaks";
private Harmony _harmony = new Harmony("barteke22.sunhaven.fish_tweaks");
public static ManualLogSource logger;
private static ConfigEntry<bool> conf_visual_override;
private static ConfigEntry<SpriteMode> conf_visual_applySprite;
private static ConfigEntry<bool> conf_visual_applyShine;
private static ConfigEntry<bool> conf_visual_keepUncaughtDark;
private static ConfigEntry<bool> conf_chance_levelScale;
private static ConfigEntry<bool> conf_chance_baitBonus;
private static ConfigEntry<float> conf_chance_mult;
private static Dictionary<MeshGenerator, FishEntry> _meshToFishDict = new Dictionary<MeshGenerator, FishEntry>();
private void Awake()
{
logger = Logger;
try
{
Config.Bind("Info", "Info", "<-- Hover for info.", "Most of these settings will only apply once you change locations. Disabling everything under 'Chance' will disable any Prefixes to FishSpawnManager Initialize() and SpawnRoutine() code.");
conf_visual_override = Config.Bind("Visual", "Override Biting", true, "Prefixes BiteRoutine to apply bite animation to the fish sprites. It's recommended to leave this enabled, unless you have another fishing mod that patches the BiteRoutine.");
conf_visual_applySprite = Config.Bind("Visual", "Apply Sprite", SpriteMode.Dark_KeenEye_Light, "0: Don't use modded sprites ever." + Environment.NewLine
+ "1: Don't use modded sprites till the 'Keen Eye' skill. Show dark modded sprites after." + Environment.NewLine
+ "2: Show dark modded sprites till the 'Keen Eye' skill. Show modded sprites after." + Environment.NewLine
+ "3: Always show dark modded sprites." + Environment.NewLine
+ "4: Always show modded sprites.");
conf_visual_applyShine = Config.Bind("Visual", "Apply Shine", true, "Apply the 'rarity shine' from the 'Keen Eye' skill. It might be pointless if you have the non-dark fish sprites enabled.");
conf_visual_keepUncaughtDark = Config.Bind("Visual", "Keep Uncaught Dark", true, "Keep fish you've never caught dark regardless of 'Keen Eye'.");
conf_chance_levelScale = Config.Bind("Chance", "Level Bonus", true, "A small bonus to fish spawns is applied based on your Fishing Level.");
conf_chance_baitBonus = Config.Bind("Chance", "Bait Bonus", true, "A small bonus to fish spawns is applied based on any bait applied.");
conf_chance_mult = Config.Bind("Chance", "Multiplier", 1f, "Multiplier applied to overall fish spawn chance. Very high multiplier can cause more fish than normal to spawn. To balance this, spawn delays might be increased at certain values.");
_harmony.PatchAll();
logger.LogInfo(MOD_GUID + " v" + MOD_VER + " loaded.");
}
catch (Exception e)
{
logger.LogError("** Awake FATAL - " + e);
}
}
public static void LogError(Exception e)
{
logger.LogError(e.Message + Environment.NewLine + e.StackTrace);
}
[HarmonyPatch(typeof(Fish), "Start")]
class HarmonyPatch_Fish_Start
{
private static bool Prefix(Fish __instance)
{
try
{
FishData fishData = __instance.fishData;
if (conf_visual_applyShine.Value)
{
if (fishData.rarity > ItemRarity.Uncommon && GameSave.Fishing.GetNode("Fishing8b"))
{
var particles = SingletonBehaviour<Prefabs>.Instance.fishShineParticles;
particles.GetComponent<ParticleSystemRenderer>().sortingOrder = 1000;
var main = particles.main;
switch (fishData.rarity)
{
case ItemRarity.Rare:
main.startColor = new Color(0f, 50f, 255f);
break;
case ItemRarity.Epic:
main.startColor = new Color(144f, 0f, 255f);
break;
case ItemRarity.Legendary:
main.startColor = new Color(255f, 200f, 0f);
break;
}
Instantiate(particles, __instance.transform.position + new Vector3(0f, -1f, -1f), Quaternion.identity, __instance.transform);
}
}
typeof(Fish).GetProperty("Caught").SetValue(__instance, false);
if (conf_visual_applySprite.Value != 0 && (conf_visual_applySprite.Value > SpriteMode.Disabled_KeenEye_Dark || GameSave.Fishing.GetNode("Fishing8b")))//attach fishVisualPrefab above original fish sprite
{
var offsetBugFix = "ElvenForest2".Equals(ScenePortalManager.ActiveSceneName, StringComparison.Ordinal) ? -1.2f : 0.1f;//fix for invisible fish (vanilla bug)
var obj = Instantiate(fishData.fishVisualPrefab, __instance.transform.position + new Vector3(0f, 0f, offsetBugFix), Quaternion.identity, __instance.transform);
var meshGen = obj.GetComponentInChildren<MeshGenerator>();
meshGen.transform.localScale = new Vector3(0.6f, 0.6f, 0.6f);
meshGen.GetComponent<MeshRenderer>().material.color = new Color(0f, 0f, 0f, 0.65f);
_meshToFishDict[meshGen] = new FishEntry() { fish = __instance, oldDirection = __instance.FacingDirection };
__instance.Graphics.position = new Vector3(0f, 0f, -99999f);//move original sprite's graphic down
}
#if DEBUG
logger.LogWarning(fishData.Name + " (" + fishData.rarity + ") in " + ScenePortalManager.ActiveSceneName);
#endif
return false;
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(FishSpawnManager), "Initialize")]
class HarmonyPatch_FishSpawnManager_Initialize
{
private static bool Prefix(FishSpawnManager __instance)
{
try
{
if (conf_chance_levelScale.Value || conf_chance_baitBonus.Value || conf_chance_mult.Value != 1f)
{
var _spawnRoutine = typeof(FishSpawnManager).GetField("_spawnRoutine", BindingFlags.NonPublic | BindingFlags.Instance);
var _fishSpawners = typeof(FishSpawnManager).GetField("_fishSpawners", BindingFlags.NonPublic | BindingFlags.Instance);
var currentFishDictionary = (Dictionary<int, Fish>)typeof(FishSpawnManager).GetField("currentFishDictionary", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (_spawnRoutine.GetValue(__instance) is Coroutine r)
{
__instance.StopCoroutine(r);
}
_fishSpawners.SetValue(__instance, UnityEngine.Object.FindObjectsOfType<FishSpawner>());
var _fishSpawnersVal = (FishSpawner[])_fishSpawners.GetValue(__instance);
for (int i = 0; i < _fishSpawnersVal.Length; i++)
{
FishSpawner fishSpawner = _fishSpawnersVal[i];
fishSpawner.ID = (ushort)(i + ScenePortalManager.ActiveSceneIndex * 100);
if (currentFishDictionary.TryGetValue(fishSpawner.ID, out var value))
{
fishSpawner.CurrentFish = value;
}
}
_spawnRoutine.SetValue(__instance, __instance.StartCoroutine(FishSpawnManager_SpawnRoutine.SpawnRoutine(__instance)));
return false;
}
}
catch (Exception e)
{
LogError(e);
}
return true;
}
}
[HarmonyPatch(typeof(MeshGenerator), "ChangeSprite")]
class HarmonyPatch_MeshGenerator_ChangeSprite
{
private static void Postfix(MeshGenerator __instance)
{
try
{
if (_meshToFishDict.TryGetValue(__instance, out var fishEntry))
{
var curDir = fishEntry.fish.FacingDirection;
var oldDir = fishEntry.oldDirection;
fishEntry.oldDirection = curDir;
if (curDir < Direction.East)
{
if (curDir != oldDir) __instance.flip = UnityEngine.Random.Range(0, 2) == 1;
if ((__instance.flip && curDir == Direction.North) || (!__instance.flip && curDir == Direction.South))
{
__instance.transform.eulerAngles = new Vector3(-5f, -80f, -60f);
}
else __instance.transform.eulerAngles = new Vector3(-5f, 80f, 60f);
}
else
{
//if ((__instance.flip && curDir == Direction.West) || (!__instance.flip && curDir == Direction.East))
//{
__instance.flip = curDir == Direction.West;
__instance.transform.eulerAngles = Vector3.zero;//upside down looked bad since the view is semi-top-down
//}
//else __instance.transform.eulerAngles = new Vector3(-90, 0, 180);
}
if (conf_visual_applySprite.Value == SpriteMode.Disabled_KeenEye_Dark
|| conf_visual_applySprite.Value == SpriteMode.Always_Dark
|| (conf_visual_applySprite.Value == SpriteMode.Dark_KeenEye_Light && !GameSave.Fishing.GetNode("Fishing8b"))
|| (conf_visual_keepUncaughtDark.Value && !GameSave.CurrentCharacter.Encylopdeia.ContainsKey((short)fishEntry.fish.fishData.id)))
{
__instance.GetComponent<MeshRenderer>().material.color = new Color(0f, 0f, 0f, 0.65f);
}
}
}
catch (Exception e)
{
LogError(e);
}
}
}
[HarmonyPatch(typeof(MeshGenerator), "OnDestroy")]
class HarmonyPatch_MeshGenerator_OnDestroy
{
private static void Postfix(MeshGenerator __instance)
{
try
{
_meshToFishDict.Remove(__instance);
}
catch (Exception e)
{
LogError(e);
}
}
}
[HarmonyPatch(typeof(Fish), "BiteRoutine")]
class HarmonyPatch_Fish_BiteRoutine
{
private static bool Prefix(Fish __instance, ref IEnumerator __result)
{
try
{
if (conf_visual_override.Value)
{
__result = BiteRoutine(__instance);
return false;
}
}
catch (Exception e)
{
LogError(e);
}
return true;
}
private static IEnumerator BiteRoutine(Fish __instance)
{
if (!__instance.isActiveAndEnabled)
{
yield break;
}
float seconds = UnityEngine.Random.Range(0.1f, 0.2f);
yield return new WaitForSeconds(seconds);
var _targetBobber = typeof(Fish).GetField("_targetBobber", BindingFlags.NonPublic | BindingFlags.Instance);
var _targetBobberInstance = (Bobber)_targetBobber.GetValue(__instance);
Transform graphics = null;
if (_meshToFishDict.FirstOrDefault(f => f.Value.fish == __instance) is KeyValuePair<MeshGenerator, FishEntry> fishEntry)
{
graphics = fishEntry.Value.fish.transform;
}
else graphics = __instance.Graphics;
if (Wish.Utilities.Chance(0.65f))
{
_targetBobberInstance.SmallBite();
float num = UnityEngine.Random.Range(1f, 2f);
switch (__instance.FacingDirection)
{
case Direction.North:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, -0.35f, 0f), num);
break;
case Direction.South:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, 0.35f, 0f), num);
break;
case Direction.East:
graphics.DOBlendableLocalMoveBy(new Vector3(-0.35f, 0f, 0f), num);
break;
case Direction.West:
graphics.DOBlendableLocalMoveBy(new Vector3(0.35f, 0f, 0f), num);
break;
}
yield return new WaitForSeconds(num);
switch (__instance.FacingDirection)
{
case Direction.North:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, 0.35f, 0f), 0.275f);
break;
case Direction.South:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, -0.35f, 0f), 0.275f);
break;
case Direction.East:
graphics.DOBlendableLocalMoveBy(new Vector3(0.35f, 0f, 0f), 0.275f);
break;
case Direction.West:
graphics.DOBlendableLocalMoveBy(new Vector3(-0.35f, 0f, 0f), 0.275f);
break;
}
yield return new WaitForSeconds(0.275f);
}
if (Wish.Utilities.Chance(0.4f))
{
_targetBobberInstance.SmallBite();
float num2 = UnityEngine.Random.Range(0.7f, 2.6f);
switch (__instance.FacingDirection)
{
case Direction.North:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, -0.35f, 0f), num2);
break;
case Direction.South:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, 0.35f, 0f), num2);
break;
case Direction.East:
graphics.DOBlendableLocalMoveBy(new Vector3(-0.35f, 0f, 0f), num2);
break;
case Direction.West:
graphics.DOBlendableLocalMoveBy(new Vector3(0.35f, 0f, 0f), num2);
break;
}
yield return new WaitForSeconds(num2);
switch (__instance.FacingDirection)
{
case Direction.North:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, 0.35f, 0f), 0.275f);
break;
case Direction.South:
graphics.DOBlendableLocalMoveBy(new Vector3(0f, -0.35f, 0f), 0.275f);
break;
case Direction.East:
graphics.DOBlendableLocalMoveBy(new Vector3(0.35f, 0f, 0f), 0.275f);
break;
case Direction.West:
graphics.DOBlendableLocalMoveBy(new Vector3(-0.35f, 0f, 0f), 0.275f);
break;
}
yield return new WaitForSeconds(0.275f);
}
_targetBobberInstance.Bite();
_targetBobberInstance.FishingRod.HasFish(__instance);
typeof(Fish).GetField("fishRunAwayTween", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(__instance, DOVirtual.DelayedCall(1f, delegate
{
if (!(_targetBobber != null) || !_targetBobberInstance.FishingRod.Reeling)
{
__instance.Flee();
var canBite = typeof(Fish).GetField("canBite", BindingFlags.NonPublic | BindingFlags.Instance);
canBite.SetValue(__instance, false);
DOVirtual.DelayedCall(4f, delegate
{
canBite.SetValue(__instance, true);
});
if (_targetBobber != null)
{
if (_targetBobberInstance.FishingRod.CurrentFish == __instance)
{
_targetBobberInstance.FishingRod.CurrentFish = null;
}
if (_targetBobberInstance.FishingRod.TargetFish == __instance)
{
_targetBobberInstance.FishingRod.TargetFish = null;
}
}
__instance.SetTarget(__instance.transform.position + new Vector3(UnityEngine.Random.Range(-1f, 1f), UnityEngine.Random.Range(-1f, 1f)).normalized * 2.25f, 0f);
_targetBobberInstance?.FailMiniGame();
_targetBobber.SetValue(__instance, null);
}
}));
}
}
private class FishSpawnManager_SpawnRoutine
{
public static IEnumerator SpawnRoutine(FishSpawnManager __instance)
{
var time = Math.Max(0.25f, 0.25f * (conf_chance_mult.Value < 3f ? conf_chance_mult.Value / 1.5f : conf_chance_mult.Value / 3));
var ResetCurrentFishDictionary = typeof(FishSpawnManager).GetMethod("ResetCurrentFishDictionary", BindingFlags.NonPublic | BindingFlags.Instance);
var _fishSpawners = (FishSpawner[])typeof(FishSpawnManager).GetField("_fishSpawners", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
//var spawnLimit = typeof(FishSpawnManager).GetField("spawnLimit", BindingFlags.NonPublic | BindingFlags.Instance);
var numFish = typeof(FishSpawnManager).GetField("numFish", BindingFlags.NonPublic | BindingFlags.Instance);
var buffs = (Dictionary<BuffType, Buff>)typeof(Player).GetField("_buffs", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(Player.Instance);
while (true)
{
var skillBonus = (conf_chance_levelScale.Value ? (SingletonBehaviour<GameSave>.Instance.CurrentSave.characterData.Professions[ProfessionType.Fishing].level * 0.0003f - 0.0006f) : 0f);
ResetCurrentFishDictionary.Invoke(__instance, null);
if (_fishSpawners.Length != 0)
{
var numFishVal = (int)numFish.GetValue(__instance);
float fishSpawnMultiplier = 1f;
if (GameSave.Fishing.GetNode("Fishing9b"))
{
fishSpawnMultiplier = 1.05f + 0.05f * (float)GameSave.Fishing.GetNodeAmount("Fishing9b");
}
if (numFishVal < 10)//(int)spawnLimit.GetValue(__instance))
{
List<int> list = Enumerable.Range(0, _fishSpawners.Length).ToList();
list.Shuffle();
foreach (int item in list)
{
if (item >= 0 && item < _fishSpawners.Length)
{
FishSpawner fishSpawner = _fishSpawners[item];
if (fishSpawner == null)
{
continue;
}
float num = (0.03f * fishSpawnMultiplier * FishSpawnManager.fishSpawnGlobalMultiplier + skillBonus) * conf_chance_mult.Value;
if (conf_chance_baitBonus.Value)
{
num += 0.05f * buffs.Count(f => nameof(f.Key).EndsWith("Bait", StringComparison.Ordinal) && !f.Value.Complete());
}
if ((bool)Player.Instance)
{
FishingRod fishingRod = Player.Instance.FishingRod;
if ((bool)fishingRod)
{
switch (fishingRod.fishingRodType)
{
case FishingRodType.Withergate:
if (SceneSettingsManager.Instance.GetCurrentSceneSettings != null && SceneSettingsManager.Instance.GetCurrentSceneSettings.townType == TownType.Withergate)
{
num *= 1.15f;
}
break;
case FishingRodType.Nelvari:
if (SceneSettingsManager.Instance.GetCurrentSceneSettings != null && SceneSettingsManager.Instance.GetCurrentSceneSettings.townType == TownType.Nelvari)
{
num *= 1.15f;
}
break;
}
}
}
float num2 = ((numFishVal == 0) ? 1.25f : (1f / numFishVal));
var currentFishDictionary = (Dictionary<int, Fish>)typeof(FishSpawnManager).GetField("currentFishDictionary", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(__instance);
if (FishSpawnManager.CanSpawnFish && fishSpawner.CurrentFish == null && (!currentFishDictionary.TryGetValue(fishSpawner.ID, out var value) || value == null) && UnityEngine.Random.value < num * num2)
{
FishData fish = fishSpawner.GetFish();
if ((bool)fish)
{
Vector3 position = fishSpawner.transform.position;
__instance.SpawnFish(fish.name, position, fishSpawner.ID);
}
}
if ((bool)fishSpawner.CurrentFish)
{
numFishVal++;
numFish.SetValue(__instance, numFishVal);
}
}
yield return new WaitForSeconds(time + skillBonus);
}
}
}
yield return new WaitForSeconds(time + (skillBonus * 10f));
}
}
}
enum SpriteMode
{
[Description("0: Disabled.")]
Disabled,
[Description("1: Disabled before, dark after.")]
Disabled_KeenEye_Dark,
[Description("2: Dark before, light after.")]
Dark_KeenEye_Light,
[Description("3: Always dark.")]
Always_Dark,
[Description("4: Always light.")]
Always_Light
}
private class FishEntry
{
public Fish fish;
public Direction oldDirection;
}
}