Skip to content

Commit c4c6016

Browse files
authored
Merge pull request #58 from ifBars/beta
2 parents 1d83d73 + 8d6169a commit c4c6016

19 files changed

Lines changed: 292 additions & 159 deletions

.github/workflows/docs.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -256,7 +256,7 @@ jobs:
256256
uses: actions/upload-pages-artifact@v3
257257
with:
258258
path: ./S1API/_site
259-
if: github.event_name != 'pull_request'
259+
if: github.event_name != 'pull_request' && github.ref != 'refs/heads/beta'
260260

261261
# Save cache even if build fails (to enable beta cache priming)
262262
# Save on PR events AND on pushes to main branches (to update cache after assembly updates)
@@ -273,7 +273,7 @@ jobs:
273273
url: ${{ steps.deployment.outputs.page_url }}
274274
runs-on: ubuntu-latest
275275
needs: build
276-
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/beta' || github.ref == 'refs/heads/npc-prefabs'
276+
if: github.ref == 'refs/heads/master' || github.ref == 'refs/heads/main' || github.ref == 'refs/heads/stable' || github.ref == 'refs/heads/npc-prefabs'
277277
steps:
278278
- name: Deploy to GitHub Pages
279279
id: deployment

S1API/Entities/NPC.cs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1664,6 +1664,8 @@ public bool IsInvincible
16641664

16651665
/// <summary>
16661666
/// Revives the NPC.
1667+
/// Note: custom NPC revive currently routes through a temporary NPCHealth patch with a reflective fallback meant
1668+
/// for pre-network-init safety; live multiplayer calls should eventually stay on the authoritative network path.
16671669
/// </summary>
16681670
public void Revive() =>
16691671
S1NPC.Health.Revive();
@@ -1718,9 +1720,18 @@ public void LerpScale(float scale, float lerpTime) =>
17181720

17191721
/// <summary>
17201722
/// Causes the NPC to become panicked.
1723+
/// Currently host/server-only: non-host clients hit the SafeIsServer guard below and this becomes a silent no-op,
1724+
/// which is inconsistent with other server-RPC-style wrappers in the API and with the runtime docs.
17211725
/// </summary>
1722-
public void Panic() =>
1723-
S1NPC.SetPanicked();
1726+
public void Panic()
1727+
{
1728+
// TODO: Revisit this guard. Unlike wrappers such as Cartel.SetStatus and CombatBehaviour.SetAndAttackTarget,
1729+
// Panic() can no longer be invoked meaningfully from multiplayer clients because non-host callers return here.
1730+
if (!SafeIsServer())
1731+
return;
1732+
1733+
S1NPC.SetPanicked_Server();
1734+
}
17241735

17251736
/// <summary>
17261737
/// Causes the NPC to stop panicking, if they are currently.

S1API/Growing/SeedCreator.cs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
#if (IL2CPPMELON)
22
using S1Growing = Il2CppScheduleOne.Growing;
33
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
4+
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
45
using S1Registry = Il2CppScheduleOne.Registry;
56
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
67
using S1Growing = ScheduleOne.Growing;
78
using S1ItemFramework = ScheduleOne.ItemFramework;
9+
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
810
using S1Registry = ScheduleOne.Registry;
911
#endif
1012

@@ -32,7 +34,7 @@ public static SeedDefinition CreateSeed(
3234
seed.Name = name;
3335
seed.Description = description;
3436
seed.StackLimit = stackLimit;
35-
seed.Category = S1ItemFramework.EItemCategory.Agriculture;
37+
seed.Category = S1CoreItemFramework.EItemCategory.Agriculture;
3638

3739
// if (icon != null)
3840
// {
Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,113 @@
1+
using System;
2+
using S1API.Logging;
3+
using UnityEngine;
4+
#if IL2CPPMELON
5+
using Il2CppInterop.Runtime;
6+
#endif
7+
8+
namespace S1API.Internal.Diagnostics
9+
{
10+
/// <summary>
11+
/// INTERNAL: Hooks Unity's threaded log callback to emit detailed stack traces for NullReferenceException logs.
12+
/// </summary>
13+
internal static class UnityExceptionTraceHook
14+
{
15+
private static readonly Log Logger = new Log("UnityExceptionTrace");
16+
private static readonly object Sync = new object();
17+
#if IL2CPPMELON
18+
private static readonly System.Action<string, string, LogType> ManagedCallback = OnUnityLogMessageReceived;
19+
private static readonly Application.LogCallback Callback = DelegateSupport.ConvertDelegate<Application.LogCallback>(ManagedCallback);
20+
#endif
21+
22+
private static string _lastExceptionSignature;
23+
private static DateTime _lastExceptionAtUtc;
24+
private static bool _installed;
25+
26+
internal static void Install()
27+
{
28+
if (_installed)
29+
{
30+
return;
31+
}
32+
33+
#if IL2CPPMELON
34+
Application.add_logMessageReceivedThreaded(Callback);
35+
#else
36+
Application.logMessageReceivedThreaded += OnUnityLogMessageReceived;
37+
#endif
38+
_installed = true;
39+
}
40+
41+
internal static void Remove()
42+
{
43+
if (!_installed)
44+
{
45+
return;
46+
}
47+
48+
#if IL2CPPMELON
49+
_installed = false;
50+
return;
51+
#else
52+
try
53+
{
54+
Application.logMessageReceivedThreaded -= OnUnityLogMessageReceived;
55+
}
56+
catch (Exception ex)
57+
{
58+
Logger.Warning($"[Unity] Failed to remove threaded log callback during shutdown: {ex.Message}");
59+
}
60+
61+
_installed = false;
62+
#endif
63+
}
64+
65+
private static void OnUnityLogMessageReceived(string condition, string stackTrace, LogType type)
66+
{
67+
if (type != LogType.Exception)
68+
{
69+
return;
70+
}
71+
72+
if (!LooksLikeNullReference(condition, stackTrace))
73+
{
74+
return;
75+
}
76+
77+
string signature = string.Concat(condition ?? string.Empty, "\n", stackTrace ?? string.Empty);
78+
if (ShouldSuppressDuplicate(signature))
79+
{
80+
return;
81+
}
82+
83+
Logger.Error($"[Unity] {condition}");
84+
85+
if (!string.IsNullOrWhiteSpace(stackTrace))
86+
{
87+
Logger.Error($"[Unity] stack trace:\n{stackTrace}");
88+
}
89+
}
90+
91+
private static bool LooksLikeNullReference(string condition, string stackTrace)
92+
{
93+
string haystack = string.Concat(condition ?? string.Empty, "\n", stackTrace ?? string.Empty);
94+
return haystack.Contains("NullReferenceException", StringComparison.Ordinal);
95+
}
96+
97+
private static bool ShouldSuppressDuplicate(string signature)
98+
{
99+
lock (Sync)
100+
{
101+
DateTime now = DateTime.UtcNow;
102+
if (signature == _lastExceptionSignature && (now - _lastExceptionAtUtc).TotalSeconds < 1)
103+
{
104+
return true;
105+
}
106+
107+
_lastExceptionSignature = signature;
108+
_lastExceptionAtUtc = now;
109+
return false;
110+
}
111+
}
112+
}
113+
}

S1API/Internal/Patches/LoadingScreenPatches.cs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -211,7 +211,7 @@ private static void CloseLoadingScreenDirectly(S1UI.LoadingScreen loadingScreen)
211211
{
212212
ReflectionUtils.TrySetFieldOrProperty(loadingScreen, "IsOpen", false);
213213

214-
var musicPlayer = S1DevUtilities.Singleton<S1Audio.MusicPlayer>.Instance;
214+
var musicPlayer = S1DevUtilities.Singleton<S1Audio.MusicManager>.Instance;
215215
if (musicPlayer != null)
216216
{
217217
musicPlayer.SetTrackEnabled("Loading Screen", enabled: false);

S1API/Internal/Patches/NPCPatches.cs

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1567,6 +1567,9 @@ private static bool NPCInventory_OnSleepStart_Prefix(S1NPCs.NPCInventory __insta
15671567
/// Temporary patch while S1API NPCs are not networked
15681568
/// Guard Revive() for custom S1API NPCs to avoid Health SyncVar access before FishNet init.
15691569
/// Applies equivalent revive effects without touching the SyncVar setter path.
1570+
/// TODO: Restrict this reflective fallback to the pre-network-init window only.
1571+
/// Once a custom NPC is live/networked, revive should stay on the authoritative server/original path instead of
1572+
/// mutating local health/death state and skipping the replicated revive flow.
15701573
/// </summary>
15711574
[HarmonyPatch(typeof(S1NPCs.NPCHealth), nameof(S1NPCs.NPCHealth.Revive))]
15721575
[HarmonyPrefix]
@@ -1579,8 +1582,39 @@ private static bool NPCHealth_Revive_Prefix(S1NPCs.NPCHealth __instance)
15791582
if (apiNpc == null || !apiNpc.IsCustomNPC)
15801583
return true; // use original for base NPCs
15811584

1582-
// Skip S1API NPCs for now
1583-
return false;
1585+
try
1586+
{
1587+
// NOTE: This currently mutates revive state locally and then always skips the original revive call below.
1588+
// That is only safe before networking is initialized; on live/networked NPCs, client-side calls can revive
1589+
// only the local wrapper copy while server-side calls bypass the authoritative replicated revive path.
1590+
bool healthSet = Utils.ReflectionUtils.TrySetFieldOrProperty(
1591+
__instance, "<Health>k__BackingField", __instance.MaxHealth);
1592+
bool isDeadSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsDead", false);
1593+
bool isKnockedOutSet = Utils.ReflectionUtils.TrySetFieldOrProperty(__instance, "IsKnockedOut", false);
1594+
1595+
if (!healthSet || !isDeadSet || !isKnockedOutSet)
1596+
{
1597+
MelonLogger.Warning(
1598+
$"[S1API] Revive guard reflection failed for custom NPC '{baseNpc?.ID ?? "<unknown>"}' " +
1599+
$"(Health={healthSet}, IsDead={isDeadSet}, IsKnockedOut={isKnockedOutSet}); falling back to original revive.");
1600+
return true;
1601+
}
1602+
1603+
// Disable behaviours locally (non-networked equivalent of Disable_Server)
1604+
baseNpc.Behaviour.DeadBehaviour?.Disable();
1605+
baseNpc.Behaviour.UnconsciousBehaviour?.Disable();
1606+
1607+
// Fire revive event so downstream listeners still react
1608+
__instance.onRevive?.Invoke();
1609+
1610+
return false; // skip original to avoid SyncVar/networking calls; revisit for post-init/live NPC revives.
1611+
}
1612+
catch (Exception ex)
1613+
{
1614+
MelonLogger.Warning(
1615+
$"[S1API] Revive guard failed for custom NPC '{baseNpc?.ID ?? "<unknown>"}': {ex.Message}. Falling back to original revive.");
1616+
return true;
1617+
}
15841618
}
15851619

15861620
/// <summary>

S1API/Internal/S1APIPreferences.cs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ internal static class S1APIPreferences
99
{
1010
private static MelonPreferences_Category _category;
1111
internal static MelonPreferences_Entry<bool> EnableMugshotLoadingScreen;
12+
internal static MelonPreferences_Entry<bool> EnableUnityNullReferenceTraceLogging;
1213

1314
/// <summary>
1415
/// Initializes the S1API preferences category and entries. Call from OnInitializeMelon.
@@ -20,6 +21,11 @@ internal static void Initialize()
2021
"EnableMugshotLoadingScreen",
2122
true,
2223
"When true, the loading screen stays open until custom NPC mugshots finish generating. Set to false to let the base game close the loading screen immediately.");
24+
25+
EnableUnityNullReferenceTraceLogging = _category.CreateEntry<bool>(
26+
"EnableUnityNullReferenceTraceLogging",
27+
true,
28+
"When true, S1API subscribes to Unity's threaded log callback and emits stack traces for NullReferenceException logs to help diagnose runtime issues.");
2329
}
2430
}
2531
}

S1API/Items/AdditiveDefinitionBuilder.cs

Lines changed: 6 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
#if (IL2CPPMELON)
22
using S1ItemFramework = Il2CppScheduleOne.ItemFramework;
3+
using S1CoreItemFramework = Il2CppScheduleOne.Core.Items.Framework;
34
using S1Registry = Il2CppScheduleOne.Registry;
45
using S1Storage = Il2CppScheduleOne.Storage;
56
#elif (MONOMELON || MONOBEPINEX || IL2CPPBEPINEX)
67
using S1ItemFramework = ScheduleOne.ItemFramework;
8+
using S1CoreItemFramework = ScheduleOne.Core.Items.Framework;
79
using S1Registry = ScheduleOne.Registry;
810
using S1Storage = ScheduleOne.Storage;
911
#endif
@@ -39,11 +41,10 @@ internal AdditiveDefinitionBuilder()
3941
_definition.StackLimit = 10;
4042
_definition.BasePurchasePrice = 10f;
4143
_definition.ResellMultiplier = 0.5f;
42-
_definition.Category = S1ItemFramework.EItemCategory.Agriculture;
43-
_definition.legalStatus = S1ItemFramework.ELegalStatus.Legal;
44+
_definition.Category = S1CoreItemFramework.EItemCategory.Agriculture;
45+
_definition.legalStatus = S1CoreItemFramework.ELegalStatus.Legal;
4446
_definition.AvailableInDemo = true;
4547
_definition.UsableInFilters = true;
46-
_definition.LabelDisplayColor = Color.white;
4748

4849
// Provide a minimal StoredItem placeholder so the field is never null in tooling/inspectors.
4950
_storedItemPlaceholder = new GameObject("S1API_DefaultStoredItem");
@@ -82,10 +83,8 @@ private void CopyPropertiesFrom(S1ItemFramework.AdditiveDefinition source)
8283
_definition.Description = source.Description;
8384
_definition.Category = source.Category;
8485
_definition.StackLimit = source.StackLimit;
85-
_definition.Keywords = source.Keywords;
8686
_definition.AvailableInDemo = source.AvailableInDemo;
8787
_definition.UsableInFilters = source.UsableInFilters;
88-
_definition.LabelDisplayColor = source.LabelDisplayColor;
8988
_definition.Icon = source.Icon;
9089
_definition.legalStatus = source.legalStatus;
9190
_definition.PickpocketDifficultyMultiplier = source.PickpocketDifficultyMultiplier;
@@ -116,7 +115,7 @@ public AdditiveDefinitionBuilder WithBasicInfo(string id, string name, string de
116115
_definition.ID = id;
117116
_definition.Name = name;
118117
_definition.Description = description;
119-
_definition.Category = (S1ItemFramework.EItemCategory)category;
118+
_definition.Category = (S1CoreItemFramework.EItemCategory)category;
120119

121120
var displayName = string.IsNullOrEmpty(name) ? id : name;
122121
if (!string.IsNullOrEmpty(displayName))
@@ -164,25 +163,7 @@ public AdditiveDefinitionBuilder WithPricing(float basePurchasePrice, float rese
164163
/// </summary>
165164
public AdditiveDefinitionBuilder WithLegalStatus(LegalStatus status)
166165
{
167-
_definition.legalStatus = (S1ItemFramework.ELegalStatus)status;
168-
return this;
169-
}
170-
171-
/// <summary>
172-
/// Sets the color of the label displayed in UI.
173-
/// </summary>
174-
public AdditiveDefinitionBuilder WithLabelColor(Color color)
175-
{
176-
_definition.LabelDisplayColor = color;
177-
return this;
178-
}
179-
180-
/// <summary>
181-
/// Sets keywords used for filtering and searching this additive.
182-
/// </summary>
183-
public AdditiveDefinitionBuilder WithKeywords(params string[] keywords)
184-
{
185-
_definition.Keywords = keywords;
166+
_definition.legalStatus = (S1CoreItemFramework.ELegalStatus)status;
186167
return this;
187168
}
188169

S1API/Items/BuildableItemDefinition.cs

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -41,14 +41,6 @@ public BuildSoundType BuildSoundType
4141
set => S1BuildableItemDefinition.BuildSoundType = (S1ItemFramework.BuildableItemDefinition.EBuildSoundType)value;
4242
}
4343

44-
/// <summary>
45-
/// The color displayed on the item's label in the UI.
46-
/// </summary>
47-
public new Color LabelDisplayColor
48-
{
49-
get => S1BuildableItemDefinition.LabelDisplayColor;
50-
set => S1BuildableItemDefinition.LabelDisplayColor = value;
51-
}
5244
}
5345

5446
/// <summary>

0 commit comments

Comments
 (0)