ItemFisherAsset — Fishing Rods and Catch Mechanics
Overview
ItemFisherAsset extends ItemAsset and defines fishing rods. It is one of the mechanically richest item assets in the SDK, supporting a two-mode fishing system: a passive legacy mode where the catch resolves automatically after a bite, and an active catch-challenge mode with a real-time physics-based minigame. The asset carries audio clips for the cast/reel/tug sound stages, a reward spawn table reference, experience rewards, an optional NPC quest reward list, and the full parameter set for the catch-challenge cursor physics.
The companion class FishingCatchableProperties defines per-catchable-item physics parameters that customize how each fish species behaves during the catch-challenge minigame. Water volumes and fishing zones reference these properties to give different fish types distinct movement patterns.
Inheritance Chain
Asset
└── ItemAsset
└── ItemFisherAssetItemFisherAsset does not introduce intermediate base classes. It branches directly from ItemAsset, gaining all shared item fields (id, rarity, size, slot, quality, exchangeability) without layering through ItemWeaponAsset, ItemClothingAsset, or ItemBarricadeAsset. Fishing rods are held as equipable items but do not fire projectiles, deal damage, or place structures.
The EFishingRewardMode Enum
ItemFisherAsset introduces EFishingRewardMode, a two-value enum controlling how catchable items are selected:
csharp
public enum EFishingRewardMode
{
Rod,
WaterVolumes,
}Rod Mode (Default)
The fishing rod's own _rewardID spawn table determines all catchable items. Per-volume reward tables are ignored entirely. This is the backwards-compatible default — all existing fishing rods before the WaterVolumes mode was added behave this way.
In Rod mode, the catch resolution calls SpawnTableTool.Resolve(rewardID) once per successful catch. Every cast from the same rod draws from the same table regardless of where the player is standing.
WaterVolumes Mode
Each water volume (or the level itself if a volume doesn't specify rewards) defines its own reward tables. The game checks the volume the bobber landed in first; if that volume has no reward configuration, it falls back to the _rewardID on the rod. This allows biome-specific fishing: a lake in the forest yields different fish than a swamp pond, even with the same rod.
The fallback chain is:
- Check the specific water volume's catchable table.
- If the volume has no table, check the level's default fishing rewards.
- If the level has no default, fall back to the rod's
_rewardIDspawn table. - If
_rewardIDis zero, nothing is catchable.
Selecting the Mode
The mode is set via the .dat key Fishing_Reward_Mode:
Fishing_Reward_Mode Rod
Fishing_Reward_Mode WaterVolumesThe parse in PopulateAsset defaults to Rod:
csharp
FishingRewardMode = p.data.ParseEnum("Fishing_Reward_Mode", EFishingRewardMode.Rod);Audio Clips
Three AudioClip fields are loaded from the Unity asset bundle during PopulateAsset:
| Field | Bundle Asset Name | Trigger |
|---|---|---|
_cast | "Cast" | Line is cast from the rod |
_reel | "Reel" | Reeling in progress (legacy mode) or during catch-challenge active input |
_tug | "Tug" | Fish bites the line; bobber submerges |
The load calls are straightforward Unity bundle loads:
csharp
_cast = p.bundle.load<AudioClip>("Cast");
_reel = p.bundle.load<AudioClip>("Reel");
_tug = p.bundle.load<AudioClip>("Tug");If the bundle does not contain an asset with the expected name, load<T> returns null. The UseableFisher usage class should null-check before playing. The asset validation system does not flag missing audio as an error — rods without audio simply produce no sound at those stages.
Reward Configuration
Spawn Table Reference
The _rewardID field (type ushort) references a SpawnAsset by its ID. This spawn table defines the weighted list of items that can be caught. A value of zero means no items can be caught via the rod's own table (WaterVolumes mode may still provide rewards).
csharp
_rewardID = p.data.ParseUInt16("Reward_ID");Experience Rewards
Two int fields define the experience range granted per successful catch:
csharp
rewardExperienceMin = p.data.ParseInt32("Reward_Experience_Min", defaultValue: 3);
rewardExperienceMax = p.data.ParseInt32("Reward_Experience_Max", defaultValue: 3);Both default to 3. At runtime, the actual XP granted is:
csharp
int xp = Random.Range(rewardExperienceMin, rewardExperienceMax + 1);Setting Reward_Experience_Min higher than Reward_Experience_Max produces a Random.Range call with a reversed range, which Unity clamps — the result is always Reward_Experience_Min.
NPC Quest Rewards
The rewardsList field (type NPCRewardsList, internal visibility) parses quest rewards from the .dat using the keys Quest_Rewards (count) and Quest_Reward_N (individual reward entries):
csharp
rewardsList.Parse(p.data, p.localization, this, "Quest_Rewards", "Quest_Reward_");Each successful catch grants these quest rewards in addition to the spawn-table item and experience. This is how fishing quests (catch N fish for an NPC) track progress: each catch fires the quest reward logic, incrementing quest conditions.
Catch Challenge: The Minigame Physics System
When EnableCatchChallenge is true, the player must actively participate in a minigame after a fish bites. A cursor moves vertically within a bounded region; the player holds input to push the cursor up and releases to let it fall. A target window moves independently according to per-fish physics. The player must keep the cursor inside the target window to fill a capture progress bar.
Fixed-Point Arithmetic
All physics values use fixed-point integers scaled by FISHING_POINT_SCALE = 10_000. This ensures deterministic client/server simulation — two machines calculating the same initial conditions with the same inputs produce identical results, regardless of floating-point hardware differences.
The fixed-point convention:
- Position, velocity, acceleration values: scaled by 10,000.
- Restitution coefficients: scaled by 10,000.
- Time values for capture/escape: scaled by
TOCK_PER_SECOND * TIME_SCALE(whereTIME_SCALEis also 10,000), making them tick-count values.
To convert a .dat float value to fixed-point:
csharp
int fixedValue = Mathf.RoundToInt(floatValue * 10_000);To read back a fixed-point value as a float for display:
csharp
float displayValue = fixedValue / 10_000.0f;EnableCatchChallenge Flag
Parsed from the .dat key CatchChallenge_Enabled:
csharp
EnableCatchChallenge = p.data.ParseBool("CatchChallenge_Enabled");When false, the legacy catch path is used: the fish is automatically caught after the bite interval with no player input. This is the default for backwards compatibility.
Cursor Physics Parameters
All parameters use the FishingCatchableProperties.FIXED_POINT_SCALE multiplier. The parsed defaults are:
CursorSize
csharp
CatchChallengeCursorSize = Mathf.RoundToInt(
p.data.ParseFloat("CatchChallenge_CursorSize", 0.2f) * FIXED_POINT_SCALE
);
// Default: 2,000 fixed-point (0.2 * 10,000)The size of the target window the cursor must stay within. Smaller = harder.
Gravity
csharp
CatchChallengeGravity = Mathf.RoundToInt(
Mathf.Abs(p.data.ParseFloat("CatchChallenge_Gravity", 1.0f)) * FIXED_POINT_SCALE
);
// Default: 10,000 (1.0 * 10,000)Downward acceleration applied when the player is NOT holding the input. The Mathf.Abs call means negative values in the .dat are treated as positive — gravity always pulls down. The direction is handled in the simulation loop, not in the parameter.
Acceleration
csharp
CatchChallengeAcceleration = Mathf.RoundToInt(
Mathf.Abs(p.data.ParseFloat("CatchChallenge_Acceleration", 1.0f)) * FIXED_POINT_SCALE
);
// Default: 10,000 (1.0 * 10,000)Upward acceleration applied when the player IS holding the input. Also force-absoluted.
Upper and Lower Restitution
csharp
CatchChallengeUpperRestitution = Mathf.RoundToInt(
Mathf.Abs(p.data.ParseFloat("CatchChallenge_UpperRestitution", 0.5f)) * FIXED_POINT_SCALE
);
// Default: 5,000 (0.5 * 10,000)
CatchChallengeLowerRestitution = Mathf.RoundToInt(
Mathf.Abs(p.data.ParseFloat("CatchChallenge_LowerRestitution", 0.5f)) * FIXED_POINT_SCALE
);
// Default: 5,000 (0.5 * 10,000)Velocity preservation on boundary collision. A value of 5,000 means 50% of velocity is preserved on bounce. Higher restitution makes the cursor bouncier, harder to control. The upper and lower values can differ — a common pattern is higher upper restitution (the cursor bounces hard off the top) and lower lower restitution (the cursor slows quickly at the bottom).
Capture and Escape Speed Multipliers
csharp
CatchChallengeCaptureSpeedMultiplier = p.data.ParseFloat(
"CatchChallenge_CaptureSpeed", 1.0f
);
CatchChallengeEscapeSpeedMultiplier = p.data.ParseFloat(
"CatchChallenge_EscapeSpeed", 1.0f
);These are floats, not fixed-point — they multiply the per-fish captureTicks and escapeTicks values. A multiplier of 2.0 doubles the capture speed (halves the time needed). These are rod-level modifiers, distinct from the per-fish captureTicks/escapeTicks.
Animation Validation
When EnableCatchChallenge is true and asset validation is enabled (Assets.shouldValidateAssets), the asset checks that the equipped prefab has two required animations:
csharp
if (EnableCatchChallenge && Assets.shouldValidateAssets)
{
ValidateEquipableHasAnimation("Catch_Loop");
ValidateEquipableHasAnimation("Catch_Failure");
}- Catch_Loop: Plays while the challenge minigame is active — usually a struggle/reeling animation on the character.
- Catch_Failure: Plays when the fish escapes — usually a snap-back or stumble animation.
If either animation is missing, ValidateEquipableHasAnimation logs an error during asset load. This is a development-time check; it does not prevent the asset from loading, but the warning is surfaced in the Unity Editor console.
Note that there is no Catch_Success animation validation — the success state typically transitions to the standard item-receive animation rather than a dedicated catch celebration.
Description UI (BuildDescription)
The BuildDescription override adds modifier lines to the item tooltip when values differ from their defaults:
csharp
public override void BuildDescription(ItemDescriptionBuilder builder, Item itemInstance)
{
base.BuildDescription(builder, itemInstance);
if (!builder.HasFlag(EItemDescriptionFlags.Uncategorized))
return;
if (FishBiteIntervalMultiplier != 1.0f)
builder.Append(..., DescSort_Important + DescSort_LowerIsBeneficial(...));
if (CatchChallengeCaptureSpeedMultiplier != 1.0f)
builder.Append(..., DescSort_Important + DescSort_HigherIsBeneficial(...));
if (CatchChallengeEscapeSpeedMultiplier != 1.0f)
builder.Append(..., DescSort_Important + DescSort_LowerIsBeneficial(...));
}Three rules:
- Only show a modifier line when the value differs from 1.0 (the neutral default).
- Use
DescSort_LowerIsBeneficialfor values where lower is better (bite interval, escape speed). - Use
DescSort_HigherIsBeneficialfor values where higher is better (capture speed).
The DescSort_Important constant ensures these lines sort near the top of the tooltip, before less-critical item properties.
Lines appear only when the EItemDescriptionFlags.Uncategorized flag is set — this is the standard tooltip section for item-specific stats.
FishingCatchableProperties
FishingCatchableProperties is a separate class (not an Asset) that defines per-catchable-item physics behavior. Each fish species can override these properties to create distinct catch difficulties and movement patterns.
Fixed-Point Scale Constants
csharp
public const int FIXED_POINT_SCALE = 10_000;
public const int TIME_SCALE = 10_000;FIXED_POINT_SCALEscales position/velocity/acceleration values.TIME_SCALEscales time values (used withPlayerInput.TOCK_PER_SECONDto convert seconds to tick counts).
Property Table
| Property | Type | .dat Key | Float Default | Description |
|---|---|---|---|---|
minChangeTargetTicks | int | Min_Relocate_Interval | 1.5s | Min ticks before target window repositions |
maxChangeTargetTicks | int | Max_Relocate_Interval | 2.0s | Max ticks before target window repositions |
maxUpwardAcceleration | int | Max_Upward_Acceleration | 1.5 | Max upward accel for target movement |
maxDownwardAcceleration | int | Max_Downward_Acceleration | 1.2 | Max downward accel for target movement |
maxUpwardSpeed | int | Max_Upward_Speed | 0.6 | Max upward speed limit |
maxDownwardSpeed | int | Max_Downward_Speed | 0.45 | Max downward speed limit |
upperRestitution | int | Upper_Restitution | 0.6 | Bounce preservation at top bound |
lowerRestitution | int | Lower_Restitution | 0.4 | Bounce preservation at bottom bound |
minTargetDelta | int | Min_Target_Delta | 0.3 | Min distance when repositioning target |
maxTargetDelta | int | Max_Target_Delta | 0.4 | Max distance when repositioning target |
minTargetPosition | int | Min_Target_Position | 0.1 | Lower bound of target window position |
maxTargetPosition | int | Max_Target_Position | 0.9 | Upper bound of target window position |
captureTicks | int | Capture_Duration | 2.0s | Ticks to fill capture bar while cursor in window |
escapeTicks | int | Escape_Duration | 2.0s | Ticks to fill escape bar while cursor outside window |
springStiffness | int | Spring_Stiffness | 16.0 | Spring stiffness for target movement interpolation |
springDamping | int | Spring_Damping | 4.0 | Spring damping for target movement interpolation |
Parse Method
Each property reads with data.ParseFloat and converts:
csharp
public void Parse(IDatDictionary data)
{
minChangeTargetTicks = Mathf.RoundToInt(
data.ParseFloat("Min_Relocate_Interval", DEFAULT_MIN_CHANGE_TARGET_INTERVAL)
* PlayerInput.TOCK_PER_SECOND
);
// Time values use TOCK_PER_SECOND only (not FIXED_POINT_SCALE)
maxUpwardAcceleration = Mathf.RoundToInt(
data.ParseFloat("Max_Upward_Acceleration", DEFAULT_MAX_UPWARD_ACCELERATION)
* FIXED_POINT_SCALE
);
// Physics values use FIXED_POINT_SCALE
captureTicks = Mathf.RoundToInt(
data.ParseFloat("Capture_Duration", DEFAULT_CAPTURE_DURATION)
* PlayerInput.TOCK_PER_SECOND * TIME_SCALE
);
// Capture/escape durations use BOTH TOCK_PER_SECOND and TIME_SCALE
}The distinction matters:
- Time intervals (
minChangeTargetTicks,maxChangeTargetTicks): multiplied byTOCK_PER_SECONDonly. These are used in a tick-counter loop comparing against the current tick. - Physics values (
maxUpwardAcceleration,maxUpwardSpeed, restitutions, deltas, positions, spring constants): multiplied byFIXED_POINT_SCALEonly. These are used in the spring-physics simulation that advances per tick. - Durations (
captureTicks,escapeTicks): multiplied byTOCK_PER_SECOND * TIME_SCALE. The extraTIME_SCALEfactor allows rod-level multipliers to scale the effective duration at a finer granularity than whole-tick adjustments.
Static Default
csharp
public static FishingCatchableProperties Default = new FishingCatchableProperties()
{
minChangeTargetTicks = Mathf.RoundToInt(DEFAULT_MIN_CHANGE_TARGET_INTERVAL * PlayerInput.TOCK_PER_SECOND),
maxChangeTargetTicks = Mathf.RoundToInt(DEFAULT_MAX_CHANGE_TARGET_INTERVAL * PlayerInput.TOCK_PER_SECOND),
maxUpwardAcceleration = Mathf.RoundToInt(DEFAULT_MAX_UPWARD_ACCELERATION * FIXED_POINT_SCALE),
// ... all properties initialized with their float defaults converted
};The Default static instance provides fallback values for fish that don't have custom FishingCatchableProperties configured. Any water volume or fishing system that doesn't find a specific catchable properties set uses Default.
Spring Physics Model
The target window's movement uses a spring-physics simulation with the following rules per tick:
- Acceleration: A random acceleration is selected within the bounds
[-maxDownwardAcceleration, +maxUpwardAcceleration]. The sign convention: positive = upward, negative = downward. - Speed clamping: After applying acceleration, the velocity is clamped to
[-maxDownwardSpeed, +maxUpwardSpeed]. - Position update:
position += velocity * dt. - Boundary collision: If position exceeds
maxTargetPositionor falls belowminTargetPosition, the velocity is reflected and multiplied by the appropriate restitution coefficient, and the position is clamped to the boundary. - Spring force: A spring force pulls the target toward a randomly chosen equilibrium point:
force = -springStiffness * (position - equilibrium) - springDamping * velocity. This creates smooth oscillation rather than random walk. - Relocation: After a random interval in
[minChangeTargetTicks, maxChangeTargetTicks], a new equilibrium point is chosen within[minTargetPosition, maxTargetPosition], at leastminTargetDeltaaway from the current position and at mostmaxTargetDeltaaway.
The net effect is a target window that drifts and bounces within the vertical space with spring-damped smoothness, occasionally relocating to a new area. Different fish species configure different parameters: a fast fish has high acceleration and speed limits, a bouncy fish has high restitution, and a twitchy fish has frequent relocations (low minChangeTargetTicks).
Fishing Simulation Phases
The runtime fishing simulation (UseableFisher) operates in distinct phases:
Phase 1: Cast
- The player presses the use key with the rod equipped and facing water.
- The cast animation plays (from the equipped prefab's Animator).
- A line projectile is launched with a trajectory defined by the player's aim direction and a configurable cast distance.
- The
_castAudioClip plays. - A bobber GameObject is instantiated at the landing point.
- The player transitions to the "waiting" state — they can look around but not move.
Phase 2: Bite Wait
- A timer starts with duration:
baseBiteInterval * FishBiteIntervalMultiplier. - The base bite interval is defined per water volume (or defaults to a game-mode config value if the volume doesn't specify one).
- During the wait, the bobber floats with a gentle bobbing animation.
- When the timer expires, a bite occurs:
- The bobber submerges (animation or hide).
- The
_tugAudioClip plays. - A UI prompt appears (legacy: "reel in"; challenge: "hold to fight").
Phase 3a: Legacy Catch (EnableCatchChallenge = false)
- After the bite, a brief reeling animation plays.
- The
_reelAudioClip plays. - The reward is resolved:
SpawnTableTool.Resolve(rewardID)selects a random item from the rod's spawn table. - Experience is granted:
Random.Range(rewardExperienceMin, rewardExperienceMax + 1). - The
rewardsListquest rewards are granted (advancing any active fishing quests). - The caught item is force-added to the player's inventory via
ItemTool.tryForceGiveItem. - If inventory is full, the item drops at the player's feet.
Phase 3b: Challenge Catch (EnableCatchChallenge = true)
- A UI overlay appears showing a vertical bar with the cursor and target window.
- The cursor position initializes at the center of the bar.
- Each frame (every tick on the server):
- Player input: If held, apply
CatchChallengeAccelerationupward. If released, applyCatchChallengeGravitydownward. - Velocity update:
velocity += acceleration * dt(where acceleration is positive for input held, negative for gravity). - Boundary check: If
position <= minPosition:position = minPosition,velocity *= -CatchChallengeLowerRestitution / FIXED_POINT_SCALE. Ifposition >= maxPosition:position = maxPosition,velocity *= -CatchChallengeUpperRestitution / FIXED_POINT_SCALE. - Position update:
position += velocity * dt. - Target movement: The target window updates its position using the spring-physics model defined by
FishingCatchableProperties. - Capture/escape check: If the cursor is within
CursorSize / 2of the target center,captureProgress += dt * CatchChallengeCaptureSpeedMultiplier. Otherwise,escapeProgress += dt * CatchChallengeEscapeSpeedMultiplier. - Resolution: If
captureProgress >= captureTicks: catch success! Proceed to reward distribution. IfescapeProgress >= escapeTicks: the fish escapes.
- Player input: If held, apply
- On success: reward distribution as in legacy mode.
- On failure: the
Catch_Failureanimation plays, no reward is granted, and the rod is ready for another cast.
Phase 4: Recast
After success or failure, the rod returns to the ready state. The player can cast again immediately (no cooldown by default; mods may add one).
Performance Considerations
Fixed-Point Math Overhead
Every tick of the catch-challenge performs integer arithmetic on the cursor and target positions. The fixed-point scale of 10,000 means position and velocity values are in the range [0, 10000]. Multiplication of two fixed-point values produces a result in the range [0, 100_000_000] which fits comfortably in a 32-bit int (max ~2.1 billion). The simulation avoids overflow even with extreme parameter values.
Per-Tick Processing
The catch-challenge runs on both client and server. The server is authoritative for capture/escape progress to prevent cheating. The client runs the same simulation in parallel for visual prediction (no latency on cursor movement), but the server's decision is final for the catch/escape resolution. Discrepancies between client and server simulation are possible if the client's tick rate differs from the server's, but the fixed-point arithmetic minimizes drift.
SpawnTableTool.Resolve
Each successful catch calls SpawnTableTool.Resolve(rewardID), which performs a weighted random selection from the spawn table. For tables with hundreds of entries, this is O(n) per call. Fishing rods with large spawn tables should ensure the entries are sorted by weight descending to allow early-out optimization if SpawnTableTool implements one.
Validation and Error States
Missing Animations
When EnableCatchChallenge is true and asset validation is enabled, ValidateEquipableHasAnimation checks the equipped prefab for Catch_Loop and Catch_Failure. If missing, an error is logged. The rod still loads but the catch-challenge will fail to play animations at runtime.
Zero Reward ID
If _rewardID is zero (the default for unset), SpawnTableTool.Resolve(0) typically returns null or a default result. The rod catches nothing. This is valid for rods intended only for quest rewards (where rewardsList handles all rewards) or for pure-sport fishing with no item gain.
Negative Experience
Setting Reward_Experience_Min higher than Reward_Experience_Max causes Random.Range to receive (high, low + 1). Unity's Random.Range(int, int) expects (minInclusive, maxExclusive) and clamps so max >= min. The result is always the min value. This is a common modder mistake.
Missing Audio
If the Unity bundle doesn't contain Cast, Reel, or Tug audio clips, the respective AudioClip fields are null. UseableFisher should null-check before calling Play(). Most implementations use AudioSource.PlayOneShot(clip) which no-ops on null.
Cargo Data Export
ItemFisherAsset does not define a custom BuildCargoData override. It inherits the base ItemAsset cargo export, which writes the shared item fields (GUID, ID, rarity, size, etc.) to the generic item cargo table. No fishing-specific fields are exported to Cargo tables. The catch challenge parameters, audio references, and reward configuration are not part of the public game data wiki tables.
Modding Guide
Creating a Fishing Rod
Minimum .dat for a functional legacy rod:
ID 58000
Rarity Common
Size_X 2
Size_Y 1
Slot Primary
Reward_ID 120
Reward_Experience_Min 5
Reward_Experience_Max 10This creates a 2×1 common rod that catches items from spawn table 120 and grants 5-10 XP per catch.
Adding Catch Challenge
CatchChallenge_Enabled true
CatchChallenge_CursorSize 0.15
CatchChallenge_Gravity 1.2
CatchChallenge_Acceleration 1.3
CatchChallenge_UpperRestitution 0.4
CatchChallenge_LowerRestitution 0.3
CatchChallenge_CaptureSpeed 0.8
CatchChallenge_EscapeSpeed 1.2This makes a harder rod: smaller cursor, stronger gravity, weaker acceleration, lower bounciness, slower capture, and faster escape. The equipped prefab MUST have Catch_Loop and Catch_Failure animation states.
Biome-Specific Fishing with WaterVolumes
Fishing_Reward_Mode WaterVolumes
Reward_ID 121The rod defaults to spawn table 121 if the water volume has no custom rewards. Each water volume's .dat can specify its own Fisher_Reward_ID to override.
Custom Catchable Properties
A water volume's .dat can define per-fish catchable properties using the FishingCatchableProperties keys:
Fish_1_ID 81
Fish_1_Min_Relocate_Interval 0.8
Fish_1_Max_Relocate_Interval 1.2
Fish_1_Max_Upward_Acceleration 2.0
Fish_1_Max_Downward_Acceleration 1.5
Fish_1_Max_Upward_Speed 0.8
Fish_1_Max_Downward_Speed 0.6
Fish_1_Upper_Restitution 0.7
Fish_1_Lower_Restitution 0.5
Fish_1_Min_Target_Delta 0.2
Fish_1_Max_Target_Delta 0.5
Fish_1_Min_Target_Position 0.05
Fish_1_Max_Target_Position 0.95
Fish_1_Capture_Duration 3.0
Fish_1_Escape_Duration 1.5
Fish_1_Spring_Stiffness 20.0
Fish_1_Spring_Damping 5.0This defines a fast, bouncy, aggressive fish (ID 81) with 3-second capture requirement and 1.5-second escape window, active across the full vertical bar range.
Common Pitfalls
Forgetting animations: If
EnableCatchChallengeis true and the prefab has noCatch_Loop/Catch_Failure, the editor logs errors. At runtime, the minigame still functions but the character stays in idle pose.One Reward_Experience_Max but not Min: The default for Min is 3 regardless of Max. If Max is set to 50 and Min is left unset, the range is 3-50, not 50-50.
Fixed-point display mismatch: When debugging, remember to divide by 10,000. A
CatchChallengeCursorSizeof 2000 is 0.2 in float space.WaterVolume mode with no fallback: If a volume has no rewards and the rod's
Reward_IDis 0, nothing can be caught from that volume. Always set a rod-levelReward_IDas fallback when using WaterVolumes mode.Quest rewards without active quests: The
rewardsListgrant is unconditional. If no quest references the reward, the grant is silently ignored. This is by design — it allows rods to grant quest credit without coupling to specific quests.
