PetChests Walkthrough¶
Difficulty: Intermediate Concepts: Projectile interaction, multiple patches, state management
PetChests allows right-clicking on any cosmetic pet projectile to open piggy bank storage.
What It Does¶
When you right-click on any summoned cosmetic pet, it opens your piggy bank storage. This turns every cosmetic pet into a portable piggy bank - no consumables or special items needed.
Key Concepts¶
1. Multiple Harmony Patches¶
This mod patches several methods to intercept projectile interaction:
Note: The code examples below are simplified for clarity. The actual implementation uses a
PetInteractionhelper class with more complex state management. ThePatchMethod()call shown is pseudocode - see the source code or Harmony Basics for actual patching patterns.
private void ApplyPatches()
{
var projectileType = typeof(Projectile);
var playerType = typeof(Player);
// Pseudocode - actual implementation uses Harmony.Patch() directly
// Is the projectile interactable?
PatchMethod(projectileType, "IsInteractable");
// Can we get a container from it?
PatchMethod(projectileType, "TryGetContainerIndex");
// Player proximity check
PatchMethod(playerType, "HandleBeingInChestRange");
// Sound effect override
PatchMethod(typeof(Main), "PlayInteractiveProjectileOpenCloseSound");
}
2. Identifying Cosmetic Pet Projectiles¶
Check if a projectile is a cosmetic pet (not a light pet like fairies):
private static bool IsCosmeticPet(Projectile proj)
{
// Check if it's owned by local player
if (proj.owner != Main.myPlayer) return false;
int type = proj.type;
// Use Main.projPet[] array to identify pets
if (!Main.projPet[type]) return false;
// Filter out light pets (fairies, etc.)
if (ProjectileID.Sets.LightPet[type]) return false;
// Skip Chester (960) and Flying Piggy Bank (525) - vanilla handles these
if (type == 960 || type == 525) return false;
return true;
}
This makes ALL cosmetic pets work as piggy banks, not just specific ones like Chester.
3. Intercepting Interaction¶
Make cosmetic pets appear interactable:
[HarmonyPatch(typeof(Projectile), "IsInteractable")]
public static class IsInteractiblePatch
{
[HarmonyPostfix]
public static void Postfix(Projectile __instance, ref bool __result)
{
// If original returned false, check if it's a cosmetic pet
if (!__result && IsCosmeticPet(__instance))
{
__result = true;
}
}
}
4. Opening Piggy Bank¶
When interaction happens, open piggy bank:
private static void OpenPiggyBank(Player player)
{
// Set chest to piggy bank mode (-2)
player.chest = -2;
// Trigger UI update
Main.playerInventory = true;
// Play sound
SoundEngine.PlaySound(SoundID.MenuOpen);
_log.Info("Opened piggy bank via pet");
}
5. Managing State¶
Track when piggy bank is open via pet:
private static bool _openedViaPet = false;
private static int _closeCooldown = 0;
private static void OnPetInteract(Projectile pet)
{
Player player = Main.LocalPlayer;
if (player.chest == -2) // Already open
{
ClosePiggyBank(player);
}
else
{
OpenPiggyBank(player);
_openedViaPet = true;
}
}
// Close when inventory closes
public static void PlayerUpdate_Postfix(Player __instance, int i)
{
if (i != Main.myPlayer) return;
if (_openedViaPet && !Main.playerInventory)
{
ClosePiggyBank(__instance);
_openedViaPet = false;
}
// Cooldown to prevent immediate reopen
if (_closeCooldown > 0)
_closeCooldown--;
}
6. Handling Edge Cases¶
// Prevent issues with cooldown
private static void ClosePiggyBank(Player player)
{
player.chest = -1;
_closeCooldown = 10; // Frames of cooldown
_log.Info("Closed piggy bank");
}
// Check cooldown before opening
private static bool CanOpen()
{
return _closeCooldown <= 0;
}
Simplified Code Structure¶
public class Mod : IMod
{
private Harmony _harmony;
private static bool _openedViaPet = false;
private static int _cooldown = 0;
public void Initialize(ModContext context)
{
// Patches are applied manually in OnGameReady()
}
private static bool IsCosmeticPet(Projectile proj)
{
return proj.owner == Main.myPlayer &&
Main.projPet[proj.type] &&
!ProjectileID.Sets.LightPet[proj.type] &&
proj.type != 960 && proj.type != 525;
}
// Patch: Make cosmetic pets appear interactable
public static void IsInteractible_Postfix(Projectile __instance, ref bool __result)
{
if (!__result && IsCosmeticPet(__instance))
__result = true;
}
// Patch (Prefix): Return piggy bank container index for cosmetic pets
// Prefix returns false to skip vanilla (which would check for Chester/Piggy Bank types)
public static bool TryGetContainerIndex_Prefix(Projectile __instance,
ref int containerIndex, ref bool __result)
{
if (IsCosmeticPet(__instance))
{
containerIndex = -2; // Piggy bank
__result = true;
return false; // Skip vanilla
}
return true; // Run vanilla for non-pets
}
// Patch: Track state each frame
public static void PlayerUpdate_Postfix(Player __instance, int i)
{
if (i != Main.myPlayer) return;
if (_cooldown > 0) _cooldown--;
if (_openedViaPet && !Main.playerInventory)
{
_openedViaPet = false;
__instance.chest = -1;
}
}
private static void OpenPiggyBank(Player player)
{
player.chest = -2;
Main.playerInventory = true;
}
}
Configuration¶
| Config Key | Type | Default | Description |
|---|---|---|---|
interactionRange |
int | 200 | Max distance in pixels to interact with a pet (range: 100-500) |
Lessons Learned¶
- Multiple patches work together - Intercept at different points
- Check ownership - Only affect local player's pets
- Use cooldowns - Prevent rapid open/close flickering
- Track your state - Know when you opened something
- Clean up on inventory close - Watch for UI state changes
- Pet array identification - Use
Main.projPet[]andProjectileID.Sets.LightPetto identify pet projectiles
For more on coordinating multiple Harmony patches, see Harmony Basics.