Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions S1API/Entities/NPC.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1719,8 +1719,13 @@ public void LerpScale(float scale, float lerpTime) =>
/// <summary>
/// Causes the NPC to become panicked.
/// </summary>
public void Panic() =>
S1NPC.SetPanicked();
public void Panic()
{
if (!SafeIsServer())
return;

S1NPC.SetPanicked_Server();
}

/// <summary>
/// Causes the NPC to stop panicking, if they are currently.
Expand Down
4 changes: 3 additions & 1 deletion S1API/Growing/SeedCreator.cs
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
#if (IL2CPPMELON)
using S1Growing = Il2CppScheduleOne.Growing;
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
using S1Registry = Il2CppScheduleOne.Registry;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1Growing = ScheduleOne.Growing;
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
using S1Registry = ScheduleOne.Registry;
#endif

Expand Down Expand Up @@ -32,7 +34,7 @@ public static SeedDefinition CreateSeed(
seed.Name = name;
seed.Description = description;
seed.StackLimit = stackLimit;
seed.Category = S1ItemFramework.EItemCategory.Agriculture;
seed.Category = S1CoreItemFramework.EItemCategory.Agriculture;

// if (icon != null)
// {
Expand Down
2 changes: 1 addition & 1 deletion S1API/Internal/Patches/LoadingScreenPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ private static void CloseLoadingScreenDirectly(S1UI.LoadingScreen loadingScreen)
{
ReflectionUtils.TrySetFieldOrProperty(loadingScreen, "IsOpen", false);

var musicPlayer = S1DevUtilities.Singleton<S1Audio.MusicPlayer>.Instance;
var musicPlayer = S1DevUtilities.Singleton<S1Audio.MusicManager>.Instance;
if (musicPlayer != null)
{
musicPlayer.SetTrackEnabled("Loading Screen", enabled: false);
Expand Down
32 changes: 30 additions & 2 deletions S1API/Internal/Patches/NPCPatches.cs
Original file line number Diff line number Diff line change
Expand Up @@ -1579,8 +1579,36 @@ private static bool NPCHealth_Revive_Prefix(S1NPCs.NPCHealth __instance)
if (apiNpc == null || !apiNpc.IsCustomNPC)
return true; // use original for base NPCs

// Skip S1API NPCs for now
return false;
try
{
bool healthSet = Utils.ReflectionUtils.TrySetFieldOrProperty(
__instance, "<Health>k__BackingField", __instance.MaxHealth);
bool isDeadSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsDead", false);
bool isKnockedOutSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsKnockedOut", false);

if (!healthSet || !isDeadSet || !isKnockedOutSet)
{
MelonLogger.Warning(
$"[S1API] Revive guard reflection failed for custom NPC '{baseNpc?.ID ?? "<unknown>"}' " +
$"(Health={healthSet}, IsDead={isDeadSet}, IsKnockedOut={isKnockedOutSet}); falling back to original revive.");
return true;
}

// Disable behaviours locally (non-networked equivalent of Disable_Server)
baseNpc.Behaviour.DeadBehaviour?.Disable();
baseNpc.Behaviour.UnconsciousBehaviour?.Disable();

// Fire revive event so downstream listeners still react
__instance.onRevive?.Invoke();

return false; // skip original to avoid SyncVar/networking calls
}
catch (Exception ex)
{
MelonLogger.Warning(
$"[S1API] Revive guard failed for custom NPC '{baseNpc?.ID ?? "<unknown>"}': {ex.Message}. Falling back to original revive.");
return true;
}
}
Comment on lines +1582 to 1612
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Don’t skip original revive when the guard path fails.

On Line 1604 the patch always returns false, even if local mutations fail (exception or unsuccessful reflection set). That can leave the NPC unrecovered while still bypassing the original revive path.

💡 Suggested fail-safe patch
-            try
+            bool localReviveApplied = false;
+            try
             {
                 // Local-only revive: set state flags via reflection (read-only properties)
-                Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsDead", false);
-                Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsKnockedOut", false);
+                bool isDeadSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsDead", false);
+                bool isKoSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsKnockedOut", false);

                 // Set health backing field directly to bypass SyncVar setter
-                Utils.ReflectionUtils.TrySetFieldOrProperty(
+                bool healthSet = Utils.ReflectionUtils.TrySetFieldOrProperty(
                     __instance, "<Health>k__BackingField", __instance.MaxHealth);

+                if (!(isDeadSet && isKoSet && healthSet))
+                {
+                    MelonLogger.Warning("[S1API] Revive guard could not apply all local state updates; falling back to original Revive().");
+                    return true;
+                }
+
                 // Disable behaviours locally (non-networked equivalent of Disable_Server)
-                baseNpc.Behaviour.DeadBehaviour?.Disable();
-                baseNpc.Behaviour.UnconsciousBehaviour?.Disable();
+                baseNpc?.Behaviour?.DeadBehaviour?.Disable();
+                baseNpc?.Behaviour?.UnconsciousBehaviour?.Disable();

                 // Fire revive event so downstream listeners still react
                 __instance.onRevive?.Invoke();
+                localReviveApplied = true;
             }
             catch (Exception ex)
             {
                 MelonLogger.Warning($"[S1API] Revive guard failed for custom NPC: {ex.Message}");
+                return true;
             }

-            return false; // skip original to avoid SyncVar/networking calls
+            return !localReviveApplied ? true : false; // skip original only on successful local revive
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@S1API/Internal/Patches/NPCPatches.cs` around lines 1582 - 1605, The patch
currently always returns false (skipping original revive) even when the local
revive operations fail; change the method so it only returns false when all
local operations succeed. Capture the boolean results from
Utils.ReflectionUtils.TrySetFieldOrProperty calls for "<Health>k__BackingField",
"IsDead", and "IsKnockedOut", and check that
baseNpc.Behaviour.DeadBehaviour?.Disable(),
baseNpc.Behaviour.UnconsciousBehaviour?.Disable(), and
__instance.onRevive?.Invoke() execute without error; if any reflection call
returns false or an exception is thrown, log the failure and return true to
allow the original revive path to run, otherwise return false to skip the
original. Use the existing symbols (__instance,
Utils.ReflectionUtils.TrySetFieldOrProperty, baseNpc.Behaviour,
__instance.onRevive) when implementing the checks.


/// <summary>
Expand Down
31 changes: 6 additions & 25 deletions S1API/Items/AdditiveDefinitionBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
using S1Registry = Il2CppScheduleOne.Registry;
using S1Storage = Il2CppScheduleOne.Storage;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
using S1Registry = ScheduleOne.Registry;
using S1Storage = ScheduleOne.Storage;
#endif
Expand Down Expand Up @@ -39,11 +41,10 @@ internal AdditiveDefinitionBuilder()
_definition.StackLimit = 10;
_definition.BasePurchasePrice = 10f;
_definition.ResellMultiplier = 0.5f;
_definition.Category = S1ItemFramework.EItemCategory.Agriculture;
_definition.legalStatus = S1ItemFramework.ELegalStatus.Legal;
_definition.Category = S1CoreItemFramework.EItemCategory.Agriculture;
_definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal;
_definition.AvailableInDemo = true;
_definition.UsableInFilters = true;
_definition.LabelDisplayColor = Color.white;

// Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors.
_storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem");
Expand Down Expand Up @@ -82,10 +83,8 @@ private void CopyPropertiesFrom(S1ItemFramework.AdditiveDefinition source)
_definition.Description = source.Description;
_definition.Category = source.Category;
_definition.StackLimit = source.StackLimit;
_definition.Keywords = source.Keywords;
_definition.AvailableInDemo = source.AvailableInDemo;
_definition.UsableInFilters = source.UsableInFilters;
_definition.LabelDisplayColor = source.LabelDisplayColor;
_definition.Icon = source.Icon;
_definition.legalStatus = source.legalStatus;
_definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier;
Expand Down Expand Up @@ -116,7 +115,7 @@ public AdditiveDefinitionBuilder WithBasicInfo(string id, string name, string de
_definition.ID = id;
_definition.Name = name;
_definition.Description = description;
_definition.Category = (S1ItemFramework.EItemCategory)category;
_definition.Category = (S1CoreItemFramework.EItemCategory)category;

var displayName = string.IsNullOrEmpty(name) ? id : name;
if (!string.IsNullOrEmpty(displayName))
Expand Down Expand Up @@ -164,25 +163,7 @@ public AdditiveDefinitionBuilder WithPricing(float basePurchasePrice, float rese
/// </summary>
public AdditiveDefinitionBuilder WithLegalStatus(LegalStatus status)
{
_definition.legalStatus = (S1ItemFramework.ELegalStatus)status;
return this;
}

/// <summary>
/// Sets the color of the label displayed in UI.
/// </summary>
public AdditiveDefinitionBuilder WithLabelColor(Color color)
{
_definition.LabelDisplayColor = color;
return this;
}

/// <summary>
/// Sets keywords used for filtering and searching this additive.
/// </summary>
public AdditiveDefinitionBuilder WithKeywords(params string[] keywords)
{
_definition.Keywords = keywords;
_definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status;
return this;
}

Expand Down
8 changes: 0 additions & 8 deletions S1API/Items/BuildableItemDefinition.cs
Original file line number Diff line number Diff line change
Expand Up @@ -41,14 +41,6 @@ public BuildSoundType BuildSoundType
set => S1BuildableItemDefinition.BuildSoundType = (S1ItemFramework.BuildableItemDefinition.EBuildSoundType)value;
}

/// <summary>
/// The color displayed on the item's label in the UI.
/// </summary>
public new Color LabelDisplayColor
{
get => S1BuildableItemDefinition.LabelDisplayColor;
set => S1BuildableItemDefinition.LabelDisplayColor = value;
}
}

/// <summary>
Expand Down
35 changes: 6 additions & 29 deletions S1API/Items/BuildableItemDefinitionBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
using S1Registry = Il2CppScheduleOne.Registry;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
using S1Registry = ScheduleOne.Registry;
#endif

Expand Down Expand Up @@ -30,11 +32,10 @@ internal BuildableItemDefinitionBuilder()
_definition.StackLimit = 10;
_definition.BasePurchasePrice = 10f;
_definition.ResellMultiplier = 0.5f;
_definition.Category = S1ItemFramework.EItemCategory.Furniture;
_definition.legalStatus = S1ItemFramework.ELegalStatus.Legal;
_definition.Category = S1CoreItemFramework.EItemCategory.Furniture;
_definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal;
_definition.AvailableInDemo = true;
_definition.UsableInFilters = true;
_definition.LabelDisplayColor = Color.white;
_definition.BuildSoundType = S1ItemFramework.BuildableItemDefinition.EBuildSoundType.Wood;
}

Expand All @@ -57,10 +58,8 @@ private void CopyPropertiesFrom(S1ItemFramework.BuildableItemDefinition source)
_definition.Description = source.Description;
_definition.Category = source.Category;
_definition.StackLimit = source.StackLimit;
_definition.Keywords = source.Keywords;
_definition.AvailableInDemo = source.AvailableInDemo;
_definition.UsableInFilters = source.UsableInFilters;
_definition.LabelDisplayColor = source.LabelDisplayColor;
_definition.Icon = source.Icon;
_definition.legalStatus = source.legalStatus;
_definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier;
Expand Down Expand Up @@ -144,7 +143,7 @@ public BuildableItemDefinitionBuilder WithPricing(float basePurchasePrice, float
/// <returns>The builder instance for fluent chaining.</returns>
public BuildableItemDefinitionBuilder WithCategory(ItemCategory category)
{
_definition.Category = (S1ItemFramework.EItemCategory)category;
_definition.Category = (S1CoreItemFramework.EItemCategory)category;
return this;
}

Expand All @@ -159,36 +158,14 @@ public BuildableItemDefinitionBuilder WithStackLimit(int limit)
return this;
}

/// <summary>
/// Sets keywords used for filtering and searching this item.
/// </summary>
/// <param name="keywords">Array of keywords.</param>
/// <returns>The builder instance for fluent chaining.</returns>
public BuildableItemDefinitionBuilder WithKeywords(params string[] keywords)
{
_definition.Keywords = keywords;
return this;
}

/// <summary>
/// Sets the color of the label displayed in UI.
/// </summary>
/// <param name="color">The color to use for the item label.</param>
/// <returns>The builder instance for fluent chaining.</returns>
public BuildableItemDefinitionBuilder WithLabelColor(Color color)
{
_definition.LabelDisplayColor = color;
return this;
}

/// <summary>
/// Sets the legal status of the item.
/// </summary>
/// <param name="status">Whether the item is legal or illegal.</param>
/// <returns>The builder instance for fluent chaining.</returns>
public BuildableItemDefinitionBuilder WithLegalStatus(LegalStatus status)
{
_definition.legalStatus = (S1ItemFramework.ELegalStatus)status;
_definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status;
return this;
}

Expand Down
33 changes: 5 additions & 28 deletions S1API/Items/ClothingItemDefinitionBuilder.cs
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
#if (IL2CPPMELON)
using S1Clothing = Il2CppScheduleOne.Clothing;
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
using S1Registry = Il2CppScheduleOne.Registry;
using Il2CppCollections = Il2CppSystem.Collections.Generic;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1Clothing = ScheduleOne.Clothing;
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
using S1Registry = ScheduleOne.Registry;
using Il2CppCollections = System.Collections.Generic;
#endif
Expand Down Expand Up @@ -34,12 +36,11 @@ internal ClothingItemDefinitionBuilder()
_definition.StackLimit = 10;
_definition.BasePurchasePrice = 10f;
_definition.ResellMultiplier = 0.5f;
_definition.Category = S1ItemFramework.EItemCategory.Clothing;
_definition.legalStatus = S1ItemFramework.ELegalStatus.Legal;
_definition.Category = S1CoreItemFramework.EItemCategory.Clothing;
_definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal;
_definition.AvailableInDemo = true;
_definition.UsableInFilters = true;
_definition.LabelDisplayColor = Color.white;


// Clothing-specific defaults
_definition.Slot = S1Clothing.EClothingSlot.Head;
_definition.ApplicationType = S1Clothing.EClothingApplicationType.Accessory;
Expand Down Expand Up @@ -72,8 +73,6 @@ internal ClothingItemDefinitionBuilder(S1Clothing.ClothingDefinition source)
_definition.legalStatus = source.legalStatus;
_definition.AvailableInDemo = source.AvailableInDemo;
_definition.UsableInFilters = source.UsableInFilters;
_definition.LabelDisplayColor = source.LabelDisplayColor;
_definition.Keywords = source.Keywords;
_definition.StoredItem = source.StoredItem;
_definition.Equippable = source.Equippable;

Expand Down Expand Up @@ -215,28 +214,6 @@ public ClothingItemDefinitionBuilder WithPricing(float basePurchasePrice, float
return this;
}

/// <summary>
/// Sets keywords used for filtering and searching this item.
/// </summary>
/// <param name="keywords">Array of keywords.</param>
/// <returns>The builder instance for fluent chaining.</returns>
public ClothingItemDefinitionBuilder WithKeywords(params string[] keywords)
{
_definition.Keywords = keywords;
return this;
}

/// <summary>
/// Sets the color of the label displayed in UI.
/// </summary>
/// <param name="color">The color to use for the item label.</param>
/// <returns>The builder instance for fluent chaining.</returns>
public ClothingItemDefinitionBuilder WithLabelColor(Color color)
{
_definition.LabelDisplayColor = color;
return this;
}

/// <summary>
/// Builds the clothing item definition, registers it with the game's registry, and returns a wrapper.
/// </summary>
Expand Down
24 changes: 4 additions & 20 deletions S1API/Items/ItemDefinition.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
#if (IL2CPPMELON)
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
using S1ItemFramework = ScheduleOne.ItemFramework;
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
#endif

using UnityEngine;
Expand Down Expand Up @@ -74,7 +76,7 @@ public int StackLimit
public ItemCategory Category
{
get => (ItemCategory)S1ItemDefinition.Category;
set => S1ItemDefinition.Category = (S1ItemFramework.EItemCategory)value;
set => S1ItemDefinition.Category = (S1CoreItemFramework.EItemCategory)value;
}

/// <summary>
Expand All @@ -101,28 +103,10 @@ public bool AvailableInDemo
public LegalStatus LegalStatus
{
get => (LegalStatus)S1ItemDefinition.legalStatus;
set => S1ItemDefinition.legalStatus = (S1ItemFramework.ELegalStatus)value;
set => S1ItemDefinition.legalStatus = (S1CoreItemFramework.ELegalStatus)value;
}


/// <summary>
/// The color of the label shown in UI.
/// </summary>
public Color LabelDisplayColor
{
get => S1ItemDefinition.LabelDisplayColor;
set => S1ItemDefinition.LabelDisplayColor = value;
}

/// <summary>
/// Any keywords used to filter/search this item.
/// </summary>
public string[] Keywords
{
get => S1ItemDefinition.Keywords;
set => S1ItemDefinition.Keywords = value;
}

/// <summary>
/// Creates a new item instance with the specified quantity.
/// </summary>
Expand Down
Loading
Loading