## 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.");
}
}
```
Last active
July 12, 2025 13:17
-
-
Save zappybiby/9f6a65168a9e79c991e1a6c6cd32c18e to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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