Skip to content

Instantly share code, notes, and snippets.

@zappybiby
Last active July 12, 2025 13:17
Show Gist options
  • Save zappybiby/9f6a65168a9e79c991e1a6c6cd32c18e to your computer and use it in GitHub Desktop.
Save zappybiby/9f6a65168a9e79c991e1a6c6cd32c18e to your computer and use it in GitHub Desktop.
## Pretext

You can’t attach `Affliction_FasterBoi` directly to a prefab (“The script needs to derive from MonoBehaviour.”). `Affliction_FasterBoi` is a non-component class, not a Unity component, so the Editor can’t serialize it.

- **Action_ApplyAffliction**: A `MonoBehaviour` that actually applies any `Affliction` instance to a character.  
- **`[SerializeReference]` on affliction**: Lets `Action_ApplyAffliction` hold a concrete `Affliction` subclass (e.g. `FasterBoi`) at runtime.  
- **Item Component as Orchestrator**: The `Item` component tracks the `holderCharacter` and, when used, invokes its child `ItemAction` components (like `Action_ApplyAffliction`), passing along the character.

```text
You can’t attach Affliction_FasterBoi directly to a prefab (“The script needs to derive from MonoBehaviour.”). Affliction_FasterBoi is a non-component class, not a Unity component, so the Editor can’t serialize it.

Action_ApplyAffliction: A MonoBehaviour that actually applies any Affliction instance to a character.

[SerializeReference] on affliction: Lets Action_ApplyAffliction hold a concrete Affliction subclass (e.g. FasterBoi) at runtime.

Item Component as Orchestrator: The Item component tracks the holderCharacter and, when used, invokes its child ItemAction components (like Action_ApplyAffliction), passing along the character.
```

## The Idea

### Design-time (Editor)

1. Create a prefab with:  
   - A “Missing Script” placeholder named `Item`  
   - A second “Missing Script” placeholder named `Action_ApplyAffliction`  
   - Your own `SetupCustomEnergyDrink` `MonoBehaviour`  
2. In the Inspector, fill in public fields (`duration`, `speed modifiers`, etc.) on `SetupCustomEnergyDrink`.

### Run-time (Game)

1. `SetupCustomEnergyDrink.Awake()` finds the `Action_ApplyAffliction` component.  
2. Reflectively creates and configures an `Affliction_FasterBoi` instance using your Inspector values.  
3. Assigns it into the action’s private `affliction` field.  
4. Destroys itself, leaving a perfectly configured item that the game’s own `Item` code handles.

SetupCustomEnergyDrink.cs
```csharp
using UnityEngine;
using System;
using System.Reflection;

public class SetupCustomEnergyDrink : MonoBehaviour
{
    // You can configure these values in the Unity Inspector for your item
    [Header("FasterBoi Affliction Settings")]
    public float totalTime = 30f;
    public float moveSpeedMod = 1.5f;
    public float climbSpeedMod = 1.2f;
    public float drowsyOnEnd = 0.3f;
    public float climbDelay = 5f;
    
    // This runs when the game loads your prefab
    void Awake()
    {
        // Configure the item, then self-destruct as this script is no longer needed
        ConfigureAffliction();
        Destroy(this);
    }

    private void ConfigureAffliction()
    {
        // Find the placeholder component we will add in the next steps
        Component actionComponent = GetComponent("Action_ApplyAffliction");
        if (actionComponent == null) return;

        // Find the affliction type from the game's loaded code
        Type fasterBoiType = Type.GetType("Peak.Afflictions.Affliction_FasterBoi, Assembly-CSharp");
        if (fasterBoiType == null) return;
        
        // Create an instance of the affliction in memory
        object fasterBoiInstance = Activator.CreateInstance(fasterBoiType);

        // Use Reflection to set the fields on the new instance with our configured values
        fasterBoiType.GetField("totalTime").SetValue(fasterBoiInstance, this.totalTime);
        fasterBoiType.GetField("moveSpeedMod").SetValue(fasterBoiInstance, this.moveSpeedMod);
        fasterBoiType.GetField("climbSpeedMod").SetValue(fasterBoiInstance, this.climbSpeedMod);
        fasterBoiType.GetField("drowsyOnEnd").SetValue(fasterBoiInstance, this.drowsyOnEnd);
        fasterBoiType.GetField("climbDelay").SetValue(fasterBoiInstance, this.climbDelay);
        
        // Find the public "affliction" field on the Action_ApplyAffliction component
        FieldInfo afflictionField = actionComponent.GetType().GetField("affliction");
        if (afflictionField == null) return;

        // Assign our fully configured affliction instance to the component
        afflictionField.SetValue(actionComponent, fasterBoiInstance);
    }
}
```

## Create the Base Object

1. In the Hierarchy window, create a new Empty GameObject (`Ctrl+Shift+N`). Name it something like `MyEnergyDrink_Prefab`.  
2. Add the necessary visual and physical components: `Mesh Filter`, `Mesh Renderer`, `Rigidbody`, and a Collider (e.g., `Capsule Collider`).

### 2a. Adding the Item Component

1. With `EnergyDrink_Prefab` selected, click **Add Component**.  
2. Type **Item**. When the “New Script” option appears, press **Enter**.  
3. Dismiss any “Create Script” dialogs.  
4. A “Missing Script” component slot labeled **Item** will appear.

### 2b. Adding the Action_ApplyAffliction Component

Repeat the above process, typing **Action_ApplyAffliction** instead of **Item**.

## Step 3: Add Your Custom Setup Script

1. With `EnergyDrink_Prefab` selected, click **Add Component**.  
2. Type **SetupCustomEnergyDrink** and add it.  
3. No errors should appear.

## Step 4: Configure Your Item's Effect

In the Inspector, under **FasterBoi Affliction Settings**, configure:  
- **Total Time**: 30  
- **Move Speed Mod**: 1.5  
- **Climb Speed Mod**: 1.2  
- **Drowsy On End**: 0.3  
- **Climb Delay**: 5  

## Step 5: Save as a Prefab

Drag `EnergyDrink_Prefab` from the Hierarchy into your Project folder (e.g., `Assets/MyMod/Prefabs`).

---

This works because the prefab file stores the “Missing Script” components by name. When PEAKLib loads the prefab, the game engine attaches the actual scripts.

---

## Energy Mod Script

```csharp
using BepInEx;
using PEAKLib.Core;
using PEAKLib.Items;

[BepInPlugin(
    "com.yourname.energymod",    // This MUST match the 'Mod Id' in your UnityModDefinition asset
    "Super Energy Mod",           // <-- Your mod's name
    "1.0.0"                       // <-- Your mod's version
)]
public class EnergyModPlugin : BaseUnityPlugin
{
    public void Awake()
    {
        Logger.LogInfo("Super Energy Mod is loaded and ready. PEAKLib will now search for its bundle.");
    }
}
```
using BepInEx;
using BepInEx.Logging;
using UnityEngine;
using System;
using System.Reflection;
using HarmonyLib;
[BepInPlugin("com.yourname.fullitemtest", "Dummy Item Set Affliction", "1.0.0")]
public class FullItemTestPlugin : BaseUnityPlugin
{
internal static ManualLogSource Log;
private static bool hasRunTest = false;
private void Awake()
{
Log = Logger;
var harmony = new Harmony("com.yourname.fullitemtest.patch");
harmony.PatchAll();
}
internal static void RunFullItemTest()
{
if (hasRunTest) return;
hasRunTest = true;
GameObject dummyItemObject = null;
try
{
// [Step 1] Get the Local Player Character
Character localPlayer = Character.localCharacter;
if (localPlayer == null)
{
Log.LogError("[FAILURE] Could not get Character.localCharacter.");
return;
}
Log.LogInfo($"Found local player: {localPlayer.name}");
// Create a dummy GameObject and add the necessary components
dummyItemObject = new GameObject("FullItemTestObject");
// Add Item component as it would be on the prefab
Type itemType = Type.GetType("Item, Assembly-CSharp");
Component itemComponent = dummyItemObject.AddComponent(itemType);
Log.LogInfo("Added 'Item' component.");
// Add Action_ApplyAffliction component
Type actionType = Type.GetType("Action_ApplyAffliction, Assembly-CSharp");
Component actionComponent = dummyItemObject.AddComponent(actionType);
Log.LogInfo("Added 'Action_ApplyAffliction' component.");
// The Action needs to know its parent Item. This is done automatically in-game, manually done here
BindingFlags fieldFlags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
actionType.BaseType.GetField("item", fieldFlags).SetValue(actionComponent, itemComponent);
Log.LogInfo(" -> Manually linked Action to its parent Item component.");
// This is the core logic from SetupCustomEnergyDrink.cs
Type fasterBoiType = Type.GetType("Peak.Afflictions.Affliction_FasterBoi, Assembly-CSharp");
object fasterBoiInstance = Activator.CreateInstance(fasterBoiType);
fasterBoiType.GetField("totalTime").SetValue(fasterBoiInstance, 15f);
fasterBoiType.GetField("moveSpeedMod").SetValue(fasterBoiInstance, 2.0f);
FieldInfo afflictionField = actionType.GetField("affliction");
afflictionField.SetValue(actionComponent, fasterBoiInstance);
Log.LogInfo(" -> Created FasterBoi instance and assigned it to the action's 'affliction' field.");
object assignedValue = afflictionField.GetValue(actionComponent);
if (assignedValue == null)
{
Log.LogError("[FAILURE] Field assignment failed. The 'affliction' field is still null.");
return;
}
Log.LogInfo("Setup simulation complete. Affliction is configured on the action component.");
// [Step 4] Simulate the player holding the item
Log.LogInfo("Simulating player holding the item...");
PropertyInfo holderCharacterProp = itemType.GetProperty("holderCharacter");
holderCharacterProp.SetValue(itemComponent, localPlayer);
Character assignedHolder = holderCharacterProp.GetValue(itemComponent) as Character;
if (assignedHolder == localPlayer)
{
Log.LogInfo("Successfully set 'holderCharacter' on the Item component.");
}
else
{
Log.LogError("[FAILURE] Failed to set 'holderCharacter' on the Item component.");
return;
}
// [Step 5] Simulate the player using the item by calling FinishCastPrimary
// This method is responsible for triggering the actions.
Log.LogInfo("Calling Item.FinishCastPrimary() to trigger actions...");
// We need to find the OnPrimaryFinishedCast action delegate and invoke it
// because FinishCastPrimary itself is protected. A better way is to invoke the delegate directly.
FieldInfo onFinishCastField = itemType.GetField("OnPrimaryFinishedCast", fieldFlags);
Action onFinishCastDelegate = onFinishCastField.GetValue(itemComponent) as Action;
// simulate ItemActions OnEnable would subscribe its RunAction to this delegate.
MethodInfo runActionMethod = actionType.GetMethod("RunAction");
onFinishCastDelegate += (Action)Delegate.CreateDelegate(typeof(Action), actionComponent, runActionMethod);
onFinishCastField.SetValue(itemComponent, onFinishCastDelegate); // Put the updated delegate back
// run all subscribed actions
onFinishCastDelegate?.Invoke();
Log.LogInfo(" -> OnPrimaryFinishedCast delegate invoked.");
// Verify the result by checking the player's active afflictions
Log.LogInfo("[Step 6 VERIFYING] Checking player's current afflictions...");
var currentAfflictions = localPlayer.refs.afflictions.afflictionList;
bool testSucceeded = false;
foreach (var affliction in currentAfflictions)
{
if (affliction.GetType() == fasterBoiType)
{
Log.LogWarning($"[SUCCESS] Player has affliction: {affliction.GetType().Name}");
testSucceeded = true;
}
}
if (!testSucceeded)
{
Log.LogError("[FAILURE] Test failed. Affliction_FasterBoi was not found on the player after the test.");
}
}
catch (Exception e)
{
Log.LogError($"An exception occurred during the test: {e}");
}
finally
{
if (dummyItemObject != null) Destroy(dummyItemObject);
Log.LogInfo(" FULL ITEM LOGIC TEST FINISHED ");
}
}
}
// Hook into the same spot as before to run the test
[HarmonyPatch(typeof(RunManager), nameof(RunManager.StartRun))]
class RunManager_StartRun_Patch_V4
{
[HarmonyPostfix]
static void Postfix()
{
FullItemTestPlugin.Log.LogInfo("RunManager.StartRun() has finished. Triggering our full item logic test...");
FullItemTestPlugin.RunFullItemTest();
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment