Skip to content

Fix batteryless tool battery duplication#2752

Draft
mindless2831 wants to merge 1 commit into
SubnauticaNitrox:masterfrom
mindless2831:fix-batteryless-tool-duplication
Draft

Fix batteryless tool battery duplication#2752
mindless2831 wants to merge 1 commit into
SubnauticaNitrox:masterfrom
mindless2831:fix-batteryless-tool-duplication

Conversation

@mindless2831
Copy link
Copy Markdown
Contributor

@mindless2831 mindless2831 commented May 19, 2026

Summary

Fixes an exploit where battery-powered tools regain a new fully charged battery after being dropped and picked back up with no battery installed.

Closes #2668.

Background

Issue #2668 reports that battery-powered tools can duplicate batteries when dropped without an installed battery.

The repro is:

  1. Use a battery-powered tool, such as a Scanner, Flashlight, Seaglide, etc.
  2. Remove its battery.
  3. Drop the batteryless tool.
  4. Pick the tool back up.
  5. The tool incorrectly has a new fully charged battery installed.

This allows players to repeatedly remove the newly created battery, drop the empty tool again, and generate unlimited fully charged batteries.

Cause

Nitrox already has a dedicated installed-battery sync path. When a battery is installed into an EnergyMixin, Nitrox represents that installed battery as an InstalledBatteryEntity.

The broken behavior came from treating all battery-powered inventory conversion the same way.

There are actually three different cases that need different behavior:

  • newly crafted/generated battery-powered tools should receive their intended default battery;
  • existing dropped tools with no installed battery should preserve the empty battery slot;
  • existing dropped tools with an installed battery should preserve that installed battery, including when a different player picks the tool up.

The original exploit happened because an existing dropped batteryless tool could be converted back into an inventory item and incorrectly receive a default installed battery.

A later multiplayer edge case also needed to be handled: when Player A drops a tool with a battery and Player B picks it up, Player B's client may not have that battery represented in the local EnergyMixin.batterySlot.storedItem at the exact moment the pickup conversion runs. However, the server-side entity state can still contain the correct InstalledBatteryEntity.

What changed

This PR now handles battery state in both the client conversion path and the server pickup path.

Client-side inventory conversion

BatteryChildEntityHelper.TryPopulateInstalledBattery(...) now accepts an allowDefaultBattery flag.

When converting newly created/generated items, default battery population is still allowed. This preserves normal crafting behavior.

When converting an already-known existing entity back into inventory, default battery creation is not blindly applied. In that case, the helper only creates a battery child from local slot state when an actual battery is present.

This prevents an existing batteryless dropped tool from being converted into an inventory item with a new default battery.

Server-side pickup preservation

PickupItemPacketProcessor now preserves existing InstalledBatteryEntity children from the server's current entity state before replacing the dropped world entity with the picked-up InventoryItemEntity.

This protects the multiplayer case where one player drops a tool with an installed battery and another player picks it up. Even if the picking client does not locally serialize that installed battery child, the server carries forward the existing synced installed-battery child state.

Why this approach

This keeps the fix narrow while preserving intended behavior.

The bug was not general battery charge serialization. Tools with installed batteries already had a sync representation through InstalledBatteryEntity.

The missing behavior was distinguishing between:

  • a newly generated tool that should receive a default battery;
  • an existing dropped tool that was intentionally batteryless;
  • an existing dropped tool that already had a synced installed battery child.

By allowing default battery creation only where appropriate and preserving existing installed-battery children server-side during pickup, this avoids both the original battery-duplication exploit and the multiplayer pickup regression.

Tested

Tested the original exploit path:

  • Removed the battery from a battery-powered tool.
  • Dropped the batteryless tool.
  • Picked the tool back up.
  • Confirmed the tool remained batteryless instead of receiving a new fully charged battery.

Regression tested crafting:

  • Crafted a new battery-powered tool.
  • Confirmed the newly crafted tool still receives its intended default battery.

Regression tested installed batteries:

  • Dropped and picked up a tool with a partially charged battery installed.
  • Confirmed the installed battery and charge were preserved.
  • Dropped and picked up a tool with a full battery installed.
  • Confirmed the installed battery remained installed and was not duplicated or replaced.

Regression tested multiplayer pickup:

  • Player A dropped a tool with an installed battery.
  • Player B picked up the dropped tool.
  • Confirmed Player B received the tool with the installed battery preserved.
  • Player A dropped a batteryless tool.
  • Player B picked up the dropped tool.
  • Confirmed Player B received the tool batteryless.

@github-actions
Copy link
Copy Markdown

github-actions Bot commented May 19, 2026

Test Results

255 tests  ±0   252 ✅ ±0   14s ⏱️ -9s
  1 suites ±0     3 💤 ±0 
  1 files   ±0     0 ❌ ±0 

Results for commit 5bc3443. ± Comparison against base commit a89227d.

♻️ This comment has been updated with latest results.

@mindless2831
Copy link
Copy Markdown
Contributor Author

Holding on this briefly. Testing found that the first version fixes the dropped batteryless-tool exploit, but it also affects first-time crafting of battery-powered tools. I am updating the fix so crafted tools still receive their intended default battery while picked-up dropped batteryless tools remain batteryless.

@mindless2831 mindless2831 force-pushed the fix-batteryless-tool-duplication branch from 3a14d7f to 4c428c9 Compare May 19, 2026 09:41
@mindless2831
Copy link
Copy Markdown
Contributor Author

Updated this PR after additional testing found the first version was too broad.

The original one-line guard fixed the dropped batteryless-tool exploit, but it also affected newly crafted battery-powered tools. This has now been corrected.

The updated version distinguishes between newly created/generated items and existing known world entities being converted back into inventory:

  • newly crafted/generated tools can still receive their intended default battery;
  • existing dropped tools preserve their actual battery-slot state;
  • batteryless dropped tools stay batteryless;
  • tools with installed batteries still preserve their battery and charge.

I retested the exploit path and the crafted-tool regression case, and this should now be good for review again.

Copy link
Copy Markdown
Collaborator

@Measurity Measurity left a comment

Choose a reason for hiding this comment

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

With 2 players, if one drops an item with a battery and the other picks it up. Then no battery is given to the other player.

@mindless2831 mindless2831 force-pushed the fix-batteryless-tool-duplication branch from 4c428c9 to 5bc3443 Compare May 20, 2026 03:02
@mindless2831
Copy link
Copy Markdown
Contributor Author

Updated the PR body and implementation to reflect the final server-side preservation change.

The PR now handles the multiplayer pickup case by preserving existing InstalledBatteryEntity children from the server's current entity state before replacing the dropped world entity with the picked-up inventory entity.

That means the fix now covers:

  • crafted/generated tools still receiving their intended default battery;
  • batteryless dropped tools staying batteryless;
  • dropped tools with installed batteries preserving those batteries;
  • another player picking up a dropped tool with a battery and still receiving that battery.

This should address the prior concern about Player B picking up Player A's dropped battery-powered tool.

@mindless2831
Copy link
Copy Markdown
Contributor Author

With 2 players, if one drops an item with a battery and the other picks it up. Then no battery is given to the other player.

Good catch! Fixed and pushed!

@mindless2831 mindless2831 requested a review from Measurity May 20, 2026 03:23
@Measurity Measurity added Area: spawning Related to spawning and/or terrain Area: items Related to items and inventories Status: Waiting for reviews Pull Request is waiting for code review labels Jun 1, 2026
Comment on lines +38 to +59
private void PreserveExistingInstalledBatteries(NitroxId id, InventoryItemEntity pickedUpItem)
{
Optional<Entity> existingEntity = entityRegistry.GetEntityById(id);
if (existingEntity is not { HasValue: true })
{
return;
}

foreach (InstalledBatteryEntity existingBattery in existingEntity.Value.ChildEntities.OfType<InstalledBatteryEntity>())
{
bool alreadyIncluded = pickedUpItem.ChildEntities
.OfType<InstalledBatteryEntity>()
.Any(incomingBattery =>
incomingBattery.Id == existingBattery.Id ||
incomingBattery.ComponentIndex == existingBattery.ComponentIndex);

if (!alreadyIncluded)
{
pickedUpItem.ChildEntities.Add(existingBattery);
}
}
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Unfortunately it's still possible to have buggy behavior.

The client doesn't update the server on the battery of an item properly. Meaning the server is unable to correct it in the highlighted code.

For example:

  1. Player 1: Fab repair tool
  2. Player 1: Drop tool
  3. Player 2: Pickup
  4. Player 2: Notice no battery in tool

@Measurity Measurity removed the Status: Waiting for reviews Pull Request is waiting for code review label Jun 1, 2026
@Measurity Measurity marked this pull request as draft June 1, 2026 07:42
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Area: items Related to items and inventories Area: spawning Related to spawning and/or terrain

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Battery duplication issue

2 participants