Skip to content

Commit

Permalink
proper fix for loot drop bug now that i know the cause
Browse files Browse the repository at this point in the history
  • Loading branch information
jakzo committed Jul 19, 2024
1 parent 408ca3c commit ace7964
Show file tree
Hide file tree
Showing 12 changed files with 211 additions and 332 deletions.
3 changes: 2 additions & 1 deletion .vscode/settings.json
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@
"xd:pre",
"HintPath",
"BonelabPath",
"BoneworksPath"
"BoneworksPath",
"PostBuildEvent"
]
}
2 changes: 1 addition & 1 deletion SlzSpeedrunTools.sln
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Project("{D4EFCE32-FB9C-666F-CB8A-DA081F9BDB08}") = "BoneworksHundredStatus", "p
EndProject
Project("{81FCC4E8-8A09-AD74-7CAB-31A3DCAD4264}") = "BonelabAmmoBugFix", "projects\Bonelab\AmmoBugFix\AmmoBugFix.csproj", "{59614574-AE6C-55D4-024A-21B97605D6F1}"
EndProject
Project("{C8D343BD-EF6E-9631-972E-B4219D723E51}") = "BoneworksLootDropBugfix", "projects\Boneworks\LootDropBugfix\LootDropBugfix.csproj", "{C6766168-937D-D108-4DB1-5BCAF3D7376F}"
Project("{C8D343BD-EF6E-9631-972E-B4219D723E51}") = "BoneworksLootDropBugfix", "projects\Boneworks\LootDropBugfix\Project.csproj", "{C6766168-937D-D108-4DB1-5BCAF3D7376F}"
EndProject
Project("{F24D5D50-5E53-31D8-AF5C-2040F7BE2926}") = "BonelabLootDropBugfix", "projects\Bonelab\LootDropBugfix\LootDropBugfix.csproj", "{792FC5C9-C984-EBB2-E2C2-86AB4D227675}"
EndProject
Expand Down
1 change: 1 addition & 0 deletions changesets/Boneworks_LootDropBugfix_Major.md
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Found the actual source of the bug and rewrote the mod to patch the broken method. Experimentally verified by breaking 8k+ ammo boxes with a script (included in this mod under the `test` configuration option).
6 changes: 4 additions & 2 deletions projects/Boneworks/LootDropBugfix/AppVersion.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
namespace Sst.LootDropBugfix {
static class AppVersion { public const string Value = "1.3.0"; }
namespace Sst.LootDropBugfix;

static class AppVersion {
public const string Value = "1.3.0";
}
87 changes: 0 additions & 87 deletions projects/Boneworks/LootDropBugfix/LootDropBugfix.csproj

This file was deleted.

23 changes: 23 additions & 0 deletions projects/Boneworks/LootDropBugfix/Project.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<Project Sdk="Microsoft.NET.Sdk" DefaultTargets="BuildAll">
<Target Name="BuildAll">
<!-- All game/Melon Loader version combinations to build -->
<MSBuild Projects="$(MSBuildProjectFile)" Targets="Build"
Properties="Configuration=$(Configuration);Patch=3;MelonLoader=5" />
</Target>

<PropertyGroup>
<CopyIntoGameAfterBuild>true</CopyIntoGameAfterBuild>

<ProjectGuid>{EAE1410F-B5CF-47D6-8764-2FCAEE822C9D}</ProjectGuid>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

<ItemGroup>
<Compile Include="AppVersion.cs" />
<Compile Include="src/**/*.cs" />
<Compile Include="../../../common/Utilities/Metadata.cs" />
<Compile Include="../../../common/Utilities/Dbg.cs" />
<Compile Include="../../../common/Boneworks/Il2CppNullable.cs" />
</ItemGroup>

</Project>
16 changes: 15 additions & 1 deletion projects/Boneworks/LootDropBugfix/README.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,22 @@
Fixes bug where dropped loot sometimes does not spawn.

# Why does the bug happen?

`LootTableData.GetLootItem` returns uses `UnityEngine.Random.RandomRange(0, 100)` to get a random percentage and returns the loot item at that percentage point (eg. if there are 10 items with 10% chance each, a random number of 42 would return the 4th item). They do this by iterating through each item then doing `if (lower < n && n <= upper) return thisItem;` however this logic will never return any item if the randomly generated number is 0 because `lower` starts at 0 and 0 is not less than 0.

There are a couple of issues and likely misunderstandings of `RandomRange` by whoever wrote the code because:

- `RandomRange(0, 100)` returns an _integer_ not a float
- The loot table uses floats for item chances in the loot table so a non-integer chance for an item will not make a difference
- `RandomRange(0, 100)` can return 0 but not 100
- The logic uses `n <= 100` for the last item in the loot table but n will never be 100, meaning the last item has a slightly lower chance than the others
- Eg. for 10 items with 10% chance each the first item would be returned for n = 1 to 10, second for n = 11 to 20, etc. so 10 values of n each until the last item which only has n = 91 to 99

SLZ finally fixed the loot drop bug in patch 4 of Bonelab but they did this by changing the lower bound of the random number from 0 to 1, so the issues listed above still remain.

# Installation

- Make sure [Melon Loader](https://melonwiki.xyz/#/?id=what-is-melonloader) version 0.5.4 or newer is installed in Boneworks
- Make sure [Melon Loader](https://melonwiki.xyz/#/?id=what-is-melonloader) version 0.5.4 is installed in Boneworks
- Download [the mod from Thunderstore](https://boneworks.thunderstore.io/package/jakzo/LootDropBugfix/) (click on "Manual Download")
- Open the downloaded `.zip` file and extract `Mods/LootDropBugfix.dll` into `BONEWORKS/Mods/LootDropBugfix.dll` which is usually at:
- Steam: `C:\Program Files (x86)\Steam\steamapps\common\BONEWORKS\BONEWORKS`
Expand Down
143 changes: 101 additions & 42 deletions projects/Boneworks/LootDropBugfix/src/AmmoDebugger.cs
Original file line number Diff line number Diff line change
@@ -1,53 +1,112 @@
using System.Linq;
using System.Collections;
using System.Collections.Generic;
using MelonLoader;
using UnityEngine;
using StressLevelZero.Props;
using StressLevelZero.Pool;
using StressLevelZero.Data;

namespace Sst.LootDropBugfix {
public static class AmmoDebugger {
private static MelonPreferences_Entry<bool> _prefDebugAmmo;
private static float _lastUpdate = 0;
private static ObjectDestructable _toBreak;
private static bool _replacedAmmo = false;

public static void Initialize() {
var category = MelonPreferences.CreateCategory(BuildInfo.NAME);
_prefDebugAmmo = category.CreateEntry(
"debug_ammo", false,
"Teleports and breaks ammo boxes to test item replacement");
namespace Sst.LootDropBugfix;

public class AmmoDebugger {
private static Il2CppSystem.Nullable<bool> _emptyNullableBool =
new Utilities.Il2CppNullable<bool>(null);
private static Il2CppSystem.Nullable<Color> _emptyNullableColor =
new Utilities.Il2CppNullable<Color>(null);

private MelonPreferences_Entry<bool> _prefTest;
private MelonPreferences_Entry<float> _prefTestSpeed;

public AmmoDebugger(MelonPreferences_Category prefCategory) {
_prefTest = prefCategory.CreateEntry(
"test", false, "Test", "Spawns and breaks ammo boxes to test the fix");
_prefTestSpeed = prefCategory.CreateEntry(
"test_speed", 1f, "Test speed",
"Rate at which the test ammo boxes spawn and break");
_prefTest.OnValueChanged += (a, b) => OnLevelStart();
}

public static void OnAmmoReplaced() { _replacedAmmo = true; }

public static void OnUpdate() {
if (!_prefDebugAmmo.Value || Time.time - _lastUpdate <= 0.5f ||
_replacedAmmo)
return;

_lastUpdate = Time.time;
if (_toBreak) {
_toBreak.TakeDamage(Vector3.back, 100, false,
StressLevelZero.Combat.AttackType.Piercing);
_toBreak = null;
} else {
foreach (var ammo in GameObject
.FindObjectsOfType<StressLevelZero.AmmoPickup>())
GameObject.Destroy(ammo.transform.parent.gameObject);
var head = GameObject.FindObjectOfType<StressLevelZero.Rig.RigManager>()
.physicsRig.m_head;
var ammoCrates =
GameObject.FindObjectsOfType<ObjectDestructable>()
.Where(obj =>
obj.lootTable != null && Mod.IsAmmoCrate(obj) &&
(obj.transform.position - head.position).sqrMagnitude >
25)
.ToArray();
if (ammoCrates.Length > 0) {
_toBreak = ammoCrates[0];
_toBreak.transform.position =
head.position + head.rotation * new Vector3(0, 0, 2);
public void OnLevelStart() {
if (_prefTest.Value)
MelonCoroutines.Start(SpawnAndBreakAmmoCrates());
}

public IEnumerator SpawnAndBreakAmmoCrates() {
var head = Object.FindObjectOfType<StressLevelZero.Rig.RigManager>()
?.physicsRig?.m_head;
// TODO: Could use the hover junkers ship ammo box as a prefab because it
// is always loaded
var cratePrefab = Object.FindObjectsOfType<ObjectDestructable>()
.FirstOrDefault(obj => obj.lootTable?.name.StartsWith(
"AmmoCrateTable_") ??
false)
?.gameObject;

var numDestroyed = 0;
var numMissingLootItem = 0;
while (head != null && cratePrefab != null && _prefTest.Value) {

var crate = Object.Instantiate(cratePrefab.gameObject,
head.position +
head.rotation * new Vector3(0, 0, 2),
Quaternion.identity);
yield return new WaitForSeconds(0.2f / _prefTestSpeed.Value);

if (crate == null) {
yield return new WaitForSeconds(0.5f / _prefTestSpeed.Value);
continue;
}

var obj = crate.GetComponent<ObjectDestructable>();
var prefabNames =
obj.lootTable.items.Select(item => item.spawnable.prefab.name);
var spawnPosition = obj.spawnTarget.position;
obj.TakeDamage(Vector3.back, 100, false,
StressLevelZero.Combat.AttackType.Piercing);

numDestroyed++;
var spawnedLootItem =
FindSpawnedLootItem(prefabNames, spawnPosition, false);
if (spawnedLootItem == null)
numMissingLootItem++;
MelonLogger.Msg($"Loot bugs: {numMissingLootItem} / {numDestroyed}");
yield return new WaitForSeconds(0.5f / _prefTestSpeed.Value);

spawnedLootItem?.GetComponent<Poolee>().Despawn(_emptyNullableBool,
_emptyNullableColor);
yield return new WaitForSeconds(0.2f / _prefTestSpeed.Value);
}
}

private GameObject FindSpawnedLootItem(IEnumerable<string> prefabNames,
Vector3 spawnPosition,
bool checkExactPosition) {
foreach (var collider in Physics.OverlapSphere(spawnPosition, 0.5f)) {
var topLevelObject = collider.transform;
while (topLevelObject.parent)
topLevelObject = topLevelObject.parent;
var isCorrectPosition =
!checkExactPosition ||
(topLevelObject.position - spawnPosition).sqrMagnitude < 1e-9;
if (isCorrectPosition &&
prefabNames.Any(topLevelObject.name.StartsWith)) {
return topLevelObject.gameObject;
}
}
return null;
}

public static bool IsLootGuaranteed(LootTableData lootTable) {
var totalPercentage =
lootTable.items.Aggregate(0f, (total, item) => total + item.percentage);
return totalPercentage >= 100f;
}

public void OnGetLootItem(LootTableData lootTable, SpawnableObject result) {
if (_prefTest.Value && IsLootGuaranteed(lootTable) && result == null) {
MelonLogger.Warning(
"LootTableData.GetLootItem just returned null when loot chances add up to 100%!");
}
}
}
}
4 changes: 2 additions & 2 deletions projects/Boneworks/LootDropBugfix/src/AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,10 @@
"https://boneworks.thunderstore.io/package/jakzo/LootDropBugfix/")]
[assembly:MelonGame(Sst.Metadata.DEVELOPER, Sst.Metadata.GAME_BONEWORKS)]

namespace Sst.LootDropBugfix {
namespace Sst.LootDropBugfix;

public static class BuildInfo {
public const string NAME = "LootDropBugfix";
public const string DESCRIPTION =
"Fixes bug where dropped loot sometimes does not spawn.";
}
}
Loading

0 comments on commit ace7964

Please sign in to comment.