Compare commits

...
10 Commits
15 changed files with 969 additions and 425 deletions
+6 -18
View File
@@ -6,7 +6,7 @@
<RootNamespace>AutoRefillFires</RootNamespace> <RootNamespace>AutoRefillFires</RootNamespace>
<LangVersion>latest</LangVersion> <LangVersion>latest</LangVersion>
<PluginVersion>1.0.0</PluginVersion> <PluginVersion>1.2.0</PluginVersion>
<ValheimPath>D:\SteamLibrary\steamapps\common\Valheim</ValheimPath> <ValheimPath>D:\SteamLibrary\steamapps\common\Valheim</ValheimPath>
<R2ProfileName>Default</R2ProfileName> <R2ProfileName>Default</R2ProfileName>
@@ -60,11 +60,7 @@
</Target> </Target>
<Target Name="GenerateVersionInfo" BeforeTargets="BeforeCompile"> <Target Name="GenerateVersionInfo" BeforeTargets="BeforeCompile">
<WriteLinesToFile <WriteLinesToFile File="$(IntermediateOutputPath)VersionInfo.cs" Overwrite="true" Lines="namespace AutoRefillFires { internal static class VersionInfo { public const string Version = &quot;$(PluginVersion)&quot;%3B } }" />
File="$(IntermediateOutputPath)VersionInfo.cs"
Overwrite="true"
Lines="namespace AutoRefillFires { internal static class VersionInfo { public const string Version = &quot;$(PluginVersion)&quot;%3B } }"
/>
<ItemGroup> <ItemGroup>
<Compile Include="$(IntermediateOutputPath)VersionInfo.cs" /> <Compile Include="$(IntermediateOutputPath)VersionInfo.cs" />
@@ -84,21 +80,13 @@
<MakeDir Directories="$(PackageRoot)" /> <MakeDir Directories="$(PackageRoot)" />
<MakeDir Directories="$(PackagePlugins)" /> <MakeDir Directories="$(PackagePlugins)" />
<Copy <Copy SourceFiles="$(TargetPath)" DestinationFolder="$(PackagePlugins)" />
SourceFiles="$(TargetPath)"
DestinationFolder="$(PackagePlugins)" />
<Copy <Copy SourceFiles="$(SolutionDir)Thunderstore\README.md" DestinationFolder="$(PackageRoot)" />
SourceFiles="$(SolutionDir)Thunderstore\README.md"
DestinationFolder="$(PackageRoot)" />
<Copy <Copy SourceFiles="$(SolutionDir)Thunderstore\CHANGELOG.md" DestinationFolder="$(PackageRoot)" />
SourceFiles="$(SolutionDir)Thunderstore\CHANGELOG.md"
DestinationFolder="$(PackageRoot)" />
<Copy <Copy SourceFiles="$(SolutionDir)Thunderstore\icon.png" DestinationFolder="$(PackageRoot)" />
SourceFiles="$(SolutionDir)Thunderstore\icon.png"
DestinationFolder="$(PackageRoot)" />
<Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -Command &quot;(Get-Content '$(SolutionDir)Thunderstore\manifest.json' -Raw).Replace('__VERSION__', '$(PluginVersion)') | Set-Content '$(PackageRoot)\manifest.json' -Encoding UTF8&quot;" /> <Exec Command="powershell -NoProfile -ExecutionPolicy Bypass -Command &quot;(Get-Content '$(SolutionDir)Thunderstore\manifest.json' -Raw).Replace('__VERSION__', '$(PluginVersion)') | Set-Content '$(PackageRoot)\manifest.json' -Encoding UTF8&quot;" />
+8
View File
@@ -0,0 +1,8 @@
namespace AutoRefillFires
{
public enum FuelSourcePriority
{
PlayerFirst,
ContainersFirst
}
}
+61
View File
@@ -0,0 +1,61 @@
using BepInEx.Configuration;
using UnityEngine;
namespace AutoRefillFires
{
internal class ModConfig
{
public ConfigEntry<float> Radius { get; }
public ConfigEntry<float> CheckInterval { get; }
public ConfigEntry<float> RefillBelowPercent { get; }
public ConfigEntry<bool> UseNearbyContainers { get; }
public ConfigEntry<float> ContainerRadius { get; }
public ConfigEntry<bool> RefillToMax { get; }
public ConfigEntry<int> RefillAmount { get; }
public ConfigEntry<bool> FillCampfires { get; }
public ConfigEntry<bool> FillHearths { get; }
public ConfigEntry<bool> FillStandingTorches { get; }
public ConfigEntry<bool> FillWallTorches { get; }
public ConfigEntry<bool> FillBraziers { get; }
public ConfigEntry<bool> FillBonfires { get; }
public ConfigEntry<bool> FillHotTubs { get; }
public ConfigEntry<bool> FillOtherFireplaces { get; }
public ConfigEntry<int> KeepFuelReserve { get; }
public ConfigEntry<FuelSourcePriority> FuelSourcePriority { get; }
public ConfigEntry<bool> OnlyRefillOwnPieces { get; }
public ConfigEntry<KeyboardShortcut> ToggleHotkey { get; }
public ConfigEntry<ModLogLevel> LogLevel { get; }
public ModConfig(ConfigFile config)
{
Radius = config.Bind("General", "Radius", 20f, "Refill fireplaces within this radius around the player.");
CheckInterval = config.Bind("General", "CheckInterval", 3f, "How often fireplaces are checked, in seconds.");
RefillBelowPercent = config.Bind("General", "RefillBelowPercent", 0.75f, "Refill when fuel drops below this percentage.");
RefillToMax = config.Bind("General", "RefillToMax", true, "If true, refill fireplaces to maximum fuel. If false, add only RefillAmount fuel.");
RefillAmount = config.Bind("General", "RefillAmount", 5, "Amount of fuel to add when RefillToMax is false.");
UseNearbyContainers = config.Bind("Containers", "UseNearbyContainers", false, "Allow plugin to take fuel from nearby containers.");
ContainerRadius = config.Bind("Containers", "ContainerRadius", 10f, "Maximum distance from the fireplace to search for containers.");
FillCampfires = config.Bind("Fireplace Types", "FillCampfires", true, "Automatically refill campfires.");
FillHearths = config.Bind("Fireplace Types", "FillHearths", true, "Automatically refill hearths.");
FillStandingTorches = config.Bind("Fireplace Types", "FillStandingTorches", true, "Automatically refill all standing torches, including colored variants.");
FillWallTorches = config.Bind("Fireplace Types", "FillWallTorches", true, "Automatically refill all wall torches, including colored variants.");
FillBraziers = config.Bind("Fireplace Types", "FillBraziers", true, "Automatically refill braziers.");
FillBonfires = config.Bind("Fireplace Types", "FillBonfires", true, "Automatically refill bonfires.");
FillHotTubs = config.Bind("Fireplace Types", "FillHotTubs", true, "Automatically refill hot tubs.");
FillOtherFireplaces = config.Bind("Fireplace Types", "FillOtherFireplaces", true, "Automatically refill unknown or modded Fireplace-based objects.");
ToggleHotkey = config.Bind("General", "ToggleHotkey", new KeyboardShortcut(KeyCode.F7), "Hotkey used to enable or disable automatic refilling.");
KeepFuelReserve = config.Bind("Fuel", "KeepFuelReserve", 0, "Minimum amount of fuel to keep in the source inventory. 10 means the last 10 Wood/Resin will not be used.");
FuelSourcePriority = config.Bind("Fuel", "FuelSourcePriority", AutoRefillFires.FuelSourcePriority.PlayerFirst, "Select whether player inventory or nearby containers should be used first.");
OnlyRefillOwnPieces = config.Bind("General", "OnlyRefillOwnPieces", false, "If enabled, only refill fireplaces and torches built by the local player.");
LogLevel = config.Bind("Logging", "LogLevel", ModLogLevel.Info, "Logging verbosity. Available values: None, Error, Warning, Info, Debug.");
}
}
}
+11
View File
@@ -0,0 +1,11 @@
namespace AutoRefillFires
{
public enum ModLogLevel
{
None = 0,
Error = 1,
Warning = 2,
Info = 3,
Debug = 4
}
}
+169
View File
@@ -0,0 +1,169 @@
using System.Collections.Generic;
using UnityEngine;
namespace AutoRefillFires
{
internal class FuelManager
{
private readonly ModConfig _config;
private readonly ModLogger _log;
public FuelManager(ModConfig config, ModLogger log)
{
_config = config;
_log = log;
}
public int ConsumeFuel(Player player, Transform target, string fuelName, int requestedAmount, Container[] containers)
{
if (requestedAmount <= 0)
return 0;
_log.Debug(
$"Fuel request: fuel='{fuelName}', " +
$"requested={requestedAmount}, " +
$"priority={_config.FuelSourcePriority.Value}, " +
$"containers={_config.UseNearbyContainers.Value}, " +
$"reserve={_config.KeepFuelReserve.Value}"
);
int consumed = 0;
if (_config.FuelSourcePriority.Value == FuelSourcePriority.ContainersFirst)
{
if (_config.UseNearbyContainers.Value && containers != null)
consumed += ConsumeFuelFromNearbyContainers(target, fuelName, requestedAmount - consumed, containers);
if (consumed < requestedAmount)
consumed += ConsumeFuelFromPlayer(player, fuelName, requestedAmount - consumed);
}
else
{
consumed += ConsumeFuelFromPlayer(player, fuelName, requestedAmount);
if (_config.UseNearbyContainers.Value && containers != null && consumed < requestedAmount)
consumed += ConsumeFuelFromNearbyContainers(target, fuelName, requestedAmount - consumed, containers);
}
_log.Debug(
$"Fuel request result: fuel='{fuelName}', " +
$"requested={requestedAmount}, " +
$"consumed={consumed}"
);
return consumed;
}
private int ConsumeFuelFromPlayer(Player player, string fuelName, int requestedAmount)
{
if (requestedAmount <= 0)
return 0;
Inventory inventory = player.GetInventory();
if (inventory == null)
{
_log.Debug("Player inventory is null.");
return 0;
}
int availableFuel = inventory.CountItems(fuelName);
int usableFuel = Mathf.Max(availableFuel - _config.KeepFuelReserve.Value, 0);
int amountToTake = Mathf.Min(requestedAmount, usableFuel);
if (amountToTake <= 0)
{
_log.Debug(
$"Player inventory: fuel='{fuelName}', " +
$"available={availableFuel}, " +
$"reserve={_config.KeepFuelReserve.Value}, " +
$"usable=0"
);
return 0;
}
inventory.RemoveItem(fuelName, amountToTake);
_log.Debug(
$"Player inventory: consumed={amountToTake}x {fuelName}, " +
$"before={availableFuel}, " +
$"remaining={availableFuel - amountToTake}"
);
return amountToTake;
}
private int ConsumeFuelFromNearbyContainers(Transform target, string fuelName, int requestedAmount, Container[] containers)
{
if (requestedAmount <= 0)
return 0;
if (containers == null || containers.Length == 0)
return 0;
List<ContainerCandidate> candidates = new List<ContainerCandidate>();
foreach (Container container in containers)
{
if (container == null)
continue;
float distance = Vector3.Distance(target.position, container.transform.position);
if (distance > _config.ContainerRadius.Value)
continue;
Inventory inventory = container.GetInventory();
if (inventory == null)
continue;
int availableFuel = inventory.CountItems(fuelName);
int usableFuel = Mathf.Max(availableFuel - _config.KeepFuelReserve.Value, 0);
if (usableFuel <= 0)
continue;
candidates.Add(new ContainerCandidate
{
Container = container,
Distance = distance
});
}
candidates.Sort((a, b) => a.Distance.CompareTo(b.Distance));
int consumed = 0;
foreach (ContainerCandidate candidate in candidates)
{
if (consumed >= requestedAmount)
break;
Inventory inventory = candidate.Container.GetInventory();
if (inventory == null)
continue;
int availableFuel = inventory.CountItems(fuelName);
int usableFuel = Mathf.Max(availableFuel - _config.KeepFuelReserve.Value, 0);
if (usableFuel <= 0)
continue;
int amountToTake = Mathf.Min(requestedAmount - consumed, usableFuel);
inventory.RemoveItem(fuelName, amountToTake);
consumed += amountToTake;
_log.Debug(
$"Container '{candidate.Container.name}' ({candidate.Distance:0.0}m): " +
$"consumed={amountToTake}x {fuelName}"
);
}
return consumed;
}
}
}
+40
View File
@@ -0,0 +1,40 @@
using BepInEx.Logging;
namespace AutoRefillFires
{
internal class ModLogger
{
private readonly ModConfig _config;
private readonly ManualLogSource _logger;
public ModLogger(ModConfig config, ManualLogSource logger)
{
_config = config;
_logger = logger;
}
public void Debug(string message)
{
if (_config.LogLevel.Value >= ModLogLevel.Debug)
_logger.LogInfo($"[DEBUG] {message}");
}
public void Info(string message)
{
if (_config.LogLevel.Value >= ModLogLevel.Info)
_logger.LogInfo(message);
}
public void Warning(string message)
{
if (_config.LogLevel.Value >= ModLogLevel.Warning)
_logger.LogWarning(message);
}
public void Error(string message)
{
if (_config.LogLevel.Value >= ModLogLevel.Error)
_logger.LogError(message);
}
}
}
+8
View File
@@ -0,0 +1,8 @@
namespace AutoRefillFires
{
internal class ContainerCandidate
{
public Container Container;
public float Distance;
}
}
+9
View File
@@ -0,0 +1,9 @@
namespace AutoRefillFires
{
internal class FireplaceCandidate
{
public Fireplace Fireplace;
public float Distance;
public float FuelPercent;
}
}
+27 -367
View File
@@ -1,150 +1,51 @@
using BepInEx; using BepInEx;
using BepInEx.Configuration;
using UnityEngine; using UnityEngine;
namespace AutoRefillFires namespace AutoRefillFires
{ {
[BepInPlugin(Plugin.PluginGuid, Plugin.PluginName, Plugin.PluginVersion)]
[BepInPlugin(
Plugin.PluginGuid,
Plugin.PluginName,
Plugin.PluginVersion
)]
public class Plugin : BaseUnityPlugin public class Plugin : BaseUnityPlugin
{ {
public const string PluginGuid = "simplifydave.autorefillfires"; public const string PluginGuid = "simplifydave.autorefillfires";
public const string PluginName = "Auto Refill Fires"; public const string PluginName = "Auto Refill Fires";
public const string PluginVersion = VersionInfo.Version; public const string PluginVersion = VersionInfo.Version;
private ConfigEntry<float> _radius; private ModConfig _config;
private ConfigEntry<float> _checkInterval; private ModLogger _log;
private ConfigEntry<float> _refillBelowPercent; private RefillManager _refillManager;
private ConfigEntry<bool> _useNearbyContainers;
private ConfigEntry<float> _containerRadius;
private ConfigEntry<bool> _refillToMax;
private ConfigEntry<int> _refillAmount;
private ConfigEntry<bool> _fillCampfires;
private ConfigEntry<bool> _fillHearths;
private ConfigEntry<bool> _fillStandingTorches;
private ConfigEntry<bool> _fillWallTorches;
private ConfigEntry<bool> _fillBraziers;
private ConfigEntry<bool> _fillOtherFireplaces;
private ConfigEntry<KeyboardShortcut> _toggleHotkey;
private bool _modEnabled = true; private bool _modEnabled = true;
private float _nextCheckTime; private float _nextCheckTime;
private void Awake() private void Awake()
{ {
_radius = Config.Bind( _config = new ModConfig(Config);
"General", _log = new ModLogger(_config, Logger);
"Radius", _refillManager = new RefillManager(_config, _log);
20f,
"Refill fireplaces within this radius around the player."
);
_checkInterval = Config.Bind(
"General",
"CheckInterval",
3f,
"How often fireplaces are checked, in seconds."
);
_refillBelowPercent = Config.Bind(
"General",
"RefillBelowPercent",
0.75f,
"Refill when fuel drops below this percentage."
);
_refillToMax = Config.Bind(
"General",
"RefillToMax",
true,
"If true, refill fireplaces to maximum fuel. If false, add only RefillAmount fuel."
);
_refillAmount = Config.Bind(
"General",
"RefillAmount",
5,
"Amount of fuel to add when RefillToMax is false."
);
_useNearbyContainers = Config.Bind(
"Containers",
"UseNearbyContainers",
false,
"Allow plugin to take fuel from nearby containers."
);
_containerRadius = Config.Bind(
"Containers",
"ContainerRadius",
10f,
"Maximum distance from the fireplace to search for containers."
);
_fillCampfires = Config.Bind(
"Fireplace Types",
"FillCampfires",
true,
"Automatically refill campfires."
);
_fillHearths = Config.Bind(
"Fireplace Types",
"FillHearths",
true,
"Automatically refill hearths."
);
_fillStandingTorches = Config.Bind(
"Fireplace Types",
"FillStandingTorches",
true,
"Automatically refill all standing torches, including colored variants."
);
_fillWallTorches = Config.Bind(
"Fireplace Types",
"FillWallTorches",
true,
"Automatically refill all wall torches, including colored variants."
);
_fillBraziers = Config.Bind(
"Fireplace Types",
"FillBraziers",
true,
"Automatically refill braziers."
);
_fillOtherFireplaces = Config.Bind(
"Fireplace Types",
"FillOtherFireplaces",
true,
"Automatically refill unknown or modded Fireplace-based objects."
);
_toggleHotkey = Config.Bind(
"General",
"ToggleHotkey",
new KeyboardShortcut(KeyCode.F7),
"Hotkey used to enable or disable automatic refilling."
);
Logger.LogInfo("Auto Refill Fires loaded!"); Logger.LogInfo("Auto Refill Fires loaded!");
} }
private void Update() private void Update()
{ {
if (_toggleHotkey.Value.IsDown()) if (_config.ToggleHotkey.Value.IsDown())
{ {
_modEnabled = !_modEnabled; _modEnabled = !_modEnabled;
// Reload config when turning the mod OFF
if (!_modEnabled)
{
try
{
Config.Reload();
_log.Info("Configuration reloaded.");
}
catch (System.Exception ex)
{
_log.Error($"Failed to reload configuration: {ex}");
}
}
string status = _modEnabled string status = _modEnabled
? "Auto Refill Fires: ENABLED" ? "Auto Refill Fires: ENABLED"
: "Auto Refill Fires: DISABLED"; : "Auto Refill Fires: DISABLED";
@@ -152,14 +53,9 @@ namespace AutoRefillFires
Player player = Player.m_localPlayer; Player player = Player.m_localPlayer;
if (player != null) if (player != null)
{ player.Message(MessageHud.MessageType.Center, status);
player.Message(
MessageHud.MessageType.Center,
status
);
}
Logger.LogInfo(status); _log.Info(status);
} }
if (!_modEnabled) if (!_modEnabled)
@@ -168,250 +64,14 @@ namespace AutoRefillFires
if (Time.time < _nextCheckTime) if (Time.time < _nextCheckTime)
return; return;
_nextCheckTime = Time.time + _checkInterval.Value; _nextCheckTime = Time.time + _config.CheckInterval.Value;
Player localPlayer = Player.m_localPlayer; Player localPlayer = Player.m_localPlayer;
if (localPlayer == null) if (localPlayer == null)
return; return;
RefillNearbyFireplaces(localPlayer); _refillManager.Run(localPlayer);
}
private bool ShouldRefillFireplace(Fireplace fireplace)
{
string objectName = fireplace.gameObject.name
.Replace("(Clone)", "")
.ToLowerInvariant();
// Campfire
if (objectName.Contains("firepit"))
return _fillCampfires.Value;
// Hearth
if (objectName.Contains("hearth"))
return _fillHearths.Value;
// All standing / ground torches, including colored variants
if (objectName.Contains("groundtorch"))
return _fillStandingTorches.Value;
// All wall torches, including colored variants
if (objectName.Contains("walltorch"))
return _fillWallTorches.Value;
// Braziers
if (objectName.Contains("brazier"))
return _fillBraziers.Value;
// Unknown / modded Fireplace-based objects
return _fillOtherFireplaces.Value;
}
private void RefillNearbyFireplaces(Player player)
{
Fireplace[] fireplaces =
Object.FindObjectsByType<Fireplace>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None
);
foreach (Fireplace fireplace in fireplaces)
{
if (fireplace == null)
continue;
float distance = Vector3.Distance(
player.transform.position,
fireplace.transform.position
);
if (distance > _radius.Value)
continue;
TryRefillFireplace(player, fireplace);
}
}
private void TryRefillFireplace(Player player, Fireplace fireplace)
{
if (!ShouldRefillFireplace(fireplace))
return;
ZNetView nview = fireplace.GetComponent<ZNetView>();
if (nview == null)
nview = fireplace.GetComponentInParent<ZNetView>();
if (nview == null)
return;
if (!nview.IsValid())
return;
ZDO zdo = nview.GetZDO();
if (zdo == null)
return;
if (fireplace.m_fuelItem == null)
return;
float currentFuel = zdo.GetFloat(ZDOVars.s_fuel, 0f);
float maxFuel = fireplace.m_maxFuel;
if (maxFuel <= 0f)
return;
float fuelPercent = currentFuel / maxFuel;
if (fuelPercent >= _refillBelowPercent.Value)
return;
string fuelName =
fireplace.m_fuelItem.m_itemData.m_shared.m_name;
int missingFuel =
Mathf.CeilToInt(maxFuel - currentFuel);
if (missingFuel <= 0)
return;
int requestedFuel;
if (_refillToMax.Value)
{
requestedFuel = missingFuel;
}
else
{
requestedFuel = Mathf.Min(
Mathf.Max(_refillAmount.Value, 0),
missingFuel
);
}
if (requestedFuel <= 0)
return;
int fuelAdded = 0;
for (int i = 0; i < requestedFuel; i++)
{
if (!TryConsumeFuel(
player,
fireplace,
fuelName
))
{
break;
}
nview.InvokeRPC(
"RPC_AddFuel",
new object[0]
);
fuelAdded++;
}
if (fuelAdded > 0)
{
Logger.LogInfo(
$"Refilled {fireplace.name}: " +
$"{currentFuel:0.0}/{maxFuel:0.0}, " +
$"fuel={fuelName}, " +
$"added={fuelAdded}, " +
$"mode={(_refillToMax.Value ? "max" : "fixed")}"
);
}
}
private bool TryConsumeFuel(Player player, Fireplace fireplace, string fuelName)
{
Inventory playerInventory = player.GetInventory();
if (playerInventory.CountItems(fuelName) > 0)
{
playerInventory.RemoveItem(fuelName, 1);
Logger.LogDebug(
$"Fuel {fuelName} taken from player inventory."
);
return true;
}
if (!_useNearbyContainers.Value)
return false;
return TryConsumeFuelFromNearbyContainer(
fireplace,
fuelName
);
}
private bool TryConsumeFuelFromNearbyContainer(
Fireplace fireplace,
string fuelName)
{
Container[] containers =
Object.FindObjectsByType<Container>(
FindObjectsInactive.Exclude,
FindObjectsSortMode.None
);
Container closestContainer = null;
float closestDistance = float.MaxValue;
foreach (Container container in containers)
{
if (container == null)
continue;
float distance = Vector3.Distance(
fireplace.transform.position,
container.transform.position
);
if (distance > _containerRadius.Value)
continue;
Inventory inventory = container.GetInventory();
if (inventory == null)
continue;
if (inventory.CountItems(fuelName) <= 0)
continue;
if (distance < closestDistance)
{
closestDistance = distance;
closestContainer = container;
}
}
if (closestContainer == null)
return false;
Inventory chestInventory =
closestContainer.GetInventory();
if (chestInventory == null)
return false;
if (chestInventory.CountItems(fuelName) <= 0)
return false;
chestInventory.RemoveItem(fuelName, 1);
Logger.LogInfo(
$"Fuel {fuelName} taken from container " +
$"{closestContainer.name} " +
$"({closestDistance:0.0}m)"
);
return true;
} }
} }
} }
+51 -15
View File
@@ -1,33 +1,69 @@
# Auto Refill Fires # Auto Refill Fires
A lightweight BepInEx mod for Valheim that automatically refills nearby fireplaces and torches. A lightweight BepInEx mod for Valheim that automatically refills nearby fireplaces and torches.
## What's New in 1.2.0
##### [Full CHANGELOG](https://github.com/LabodiDavid/AutoRefillFires/blob/main/Thunderstore/CHANGELOG.md)
- Added automatic Hot Tub refilling with a separate `FillHotTubs` toggle
- Fixed Resin candles being incorrectly treated as refillable objects
- Major internal refactor and code cleanup
## Features ## Features
- Automatically refills nearby campfires - Automatically refills campfires, hearths, torches, braziers, bonfires, hot tubs and other compatible fireplaces
- Automatically refills hearths - Can use fuel from the player inventory and nearby containers
- Automatically refills standing torches, including colored variants - Configurable refill radius and threshold
- Automatically refills wall torches
- Automatically refills braziers
- Uses the correct fuel type automatically
- Can consume fuel from the player's inventory
- Optional support for nearby containers
- Configurable refill radius
- Configurable refill threshold
- Refill to maximum or by a fixed amount - Refill to maximum or by a fixed amount
- Individual enable/disable options for fireplace types - Individual enable/disable options for fireplace types
## Requirements
- Valheim
- BepInExPack Valheim
## Configuration ## Configuration
The configuration file is generated after launching the game once with the mod installed. The configuration file is generated after launching the game once with the mod installed.
`BepInEx/config/simplifydave.autorefillfires.cfg` `BepInEx/config/simplifydave.autorefillfires.cfg`
### Hotkey
Default:
`F7`
Pressing the hotkey toggles Auto Refill Fires on or off.
When disabled, the configuration file is automatically reloaded, allowing most settings to be changed without restarting Valheim.
### Fuel Reserve
Example:
`KeepFuelReserve = 10`
The mod will leave the last 10 matching fuel items untouched in each fuel source.
### Fuel Source Priority
Available values:
`PlayerFirst`
`ContainersFirst`
If the preferred source does not contain enough usable fuel, the other source is used automatically.
### Logging
Available levels:
`None`, `Error`, `Warning`, `Info`, `Debug`
Use `Debug` to see fireplace detection, fuel levels, refill decisions and fuel source selection.
## Requirements
- Valheim
- BepInExPack Valheim
## Installation ## Installation
Install using r2modman / Thunderstore Mod Manager, or manually place `AutoRefillFires.dll` inside: Install using r2modman / Thunderstore Mod Manager, or manually place `AutoRefillFires.dll` inside:
+200
View File
@@ -0,0 +1,200 @@
using UnityEngine;
namespace AutoRefillFires
{
internal class FireplaceRefiller
{
private readonly ModConfig _config;
private readonly FuelManager _fuelManager;
private readonly ModLogger _log;
public FireplaceRefiller(ModConfig config, FuelManager fuelManager, ModLogger log)
{
_config = config;
_fuelManager = fuelManager;
_log = log;
}
public void TryRefill(Player player, Fireplace fireplace, Container[] containers)
{
_log.Debug($"TryRefillFireplace START: '{fireplace.gameObject.name}'");
if (!ShouldRefillFireplace(fireplace))
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - type/config filter returned false.");
return;
}
if (_config.OnlyRefillOwnPieces.Value && !IsOwnPiece(player, fireplace))
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - not owned by local player.");
return;
}
ZNetView nview = fireplace.GetComponent<ZNetView>();
if (nview == null)
nview = fireplace.GetComponentInParent<ZNetView>();
if (nview == null)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - no ZNetView found.");
return;
}
if (!nview.IsValid())
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - ZNetView is not valid.");
return;
}
ZDO zdo = nview.GetZDO();
if (zdo == null)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - ZDO is null.");
return;
}
if (fireplace.m_fuelItem == null || fireplace.m_fuelItem.m_itemData == null || fireplace.m_fuelItem.m_itemData.m_shared == null)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - fuel item is null.");
return;
}
float currentFuel = zdo.GetFloat(ZDOVars.s_fuel, 0f);
float maxFuel = fireplace.m_maxFuel;
if (maxFuel <= 0f)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - maxFuel <= 0 ({maxFuel}).");
return;
}
float fuelPercent = currentFuel / maxFuel;
_log.Debug(
$"Fuel state for '{fireplace.gameObject.name}': " +
$"currentFuel={currentFuel:0.0}, maxFuel={maxFuel:0.0}, " +
$"fuelPercent={fuelPercent:0.00}, threshold={_config.RefillBelowPercent.Value:0.00}"
);
if (fuelPercent >= _config.RefillBelowPercent.Value)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - fuelPercent >= threshold.");
return;
}
string fuelName = fireplace.m_fuelItem.m_itemData.m_shared.m_name;
int missingFuel = Mathf.CeilToInt(maxFuel - currentFuel);
if (missingFuel <= 0)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - missingFuel <= 0 ({missingFuel}).");
return;
}
int requestedFuel = _config.RefillToMax.Value
? missingFuel
: Mathf.Min(Mathf.Max(_config.RefillAmount.Value, 0), missingFuel);
_log.Debug(
$"Refill request for '{fireplace.gameObject.name}': " +
$"fuel='{fuelName}', missingFuel={missingFuel}, requestedFuel={requestedFuel}, " +
$"refillMode={(_config.RefillToMax.Value ? "max" : "fixed")}"
);
if (requestedFuel <= 0)
{
_log.Debug($"Skipping '{fireplace.gameObject.name}' - requestedFuel <= 0.");
return;
}
int fuelConsumed = _fuelManager.ConsumeFuel(player, fireplace.transform, fuelName, requestedFuel, containers);
if (fuelConsumed <= 0)
{
_log.Debug($"No usable fuel found for '{fireplace.gameObject.name}'.");
return;
}
for (int i = 0; i < fuelConsumed; i++)
nview.InvokeRPC("RPC_AddFuel", new object[0]);
_log.Info(
$"Refilled {fireplace.name}: {currentFuel:0.0}/{maxFuel:0.0}, " +
$"fuel={fuelName}, added={fuelConsumed}, " +
$"mode={(_config.RefillToMax.Value ? "max" : "fixed")}"
);
}
private bool ShouldRefillFireplace(Fireplace fireplace)
{
string objectName = fireplace.gameObject.name.Replace("(Clone)", "").ToLowerInvariant();
bool result;
string matchedRule;
if (objectName.Contains("candle"))
{
result = false;
matchedRule = "IgnoredCandle";
}
if (objectName.Contains("fire_pit") || objectName.Contains("firepit"))
{
result = _config.FillCampfires.Value;
matchedRule = "FillCampfires";
}
else if (objectName.Contains("hearth"))
{
result = _config.FillHearths.Value;
matchedRule = "FillHearths";
}
else if (objectName.Contains("groundtorch"))
{
result = _config.FillStandingTorches.Value;
matchedRule = "FillStandingTorches";
}
else if (objectName.Contains("walltorch"))
{
result = _config.FillWallTorches.Value;
matchedRule = "FillWallTorches";
}
else if (objectName.Contains("brazier"))
{
result = _config.FillBraziers.Value;
matchedRule = "FillBraziers";
}
else if (objectName.Contains("bonfire"))
{
result = _config.FillBonfires.Value;
matchedRule = "FillBonfires";
}
else
{
result = _config.FillOtherFireplaces.Value;
matchedRule = "FillOtherFireplaces";
}
_log.Debug(
$"ShouldRefillFireplace: name='{fireplace.gameObject.name}', " +
$"normalized='{objectName}', matchedRule={matchedRule}, result={result}"
);
return result;
}
private bool IsOwnPiece(Player player, Fireplace fireplace)
{
Piece piece = fireplace.GetComponent<Piece>();
if (piece == null)
piece = fireplace.GetComponentInParent<Piece>();
if (piece == null)
return true;
return piece.GetCreator() == player.GetPlayerID();
}
}
}
+127
View File
@@ -0,0 +1,127 @@
using UnityEngine;
namespace AutoRefillFires
{
internal class HotTubRefiller
{
private readonly ModConfig _config;
private readonly FuelManager _fuelManager;
private readonly ModLogger _log;
public HotTubRefiller(ModConfig config, FuelManager fuelManager, ModLogger log)
{
_config = config;
_fuelManager = fuelManager;
_log = log;
}
public void RefillNearby(Player player, Smelter[] smelters, Container[] containers)
{
int hotTubsInRange = 0;
foreach (Smelter smelter in smelters)
{
if (smelter == null)
continue;
string objectName = smelter.gameObject.name.Replace("(Clone)", "").ToLowerInvariant();
if (objectName != "piece_bathtub")
continue;
float distance = Vector3.Distance(player.transform.position, smelter.transform.position);
if (distance > _config.Radius.Value)
continue;
hotTubsInRange++;
_log.Debug(
$"Hot tub in range: name='{smelter.gameObject.name}', " +
$"distance={distance:0.0}m"
);
TryRefillHotTub(player, smelter, containers);
}
_log.Debug($"Hot tub scan end: inRange={hotTubsInRange}");
}
private void TryRefillHotTub(Player player, Smelter smelter, Container[] containers)
{
if (smelter == null)
return;
ZNetView nview = smelter.GetComponent<ZNetView>();
if (nview == null)
nview = smelter.GetComponentInParent<ZNetView>();
if (nview == null || !nview.IsValid())
{
_log.Debug("Hot tub: invalid ZNetView.");
return;
}
ZDO zdo = nview.GetZDO();
if (zdo == null)
{
_log.Debug("Hot tub: ZDO is null.");
return;
}
if (smelter.m_fuelItem == null || smelter.m_fuelItem.m_itemData == null || smelter.m_fuelItem.m_itemData.m_shared == null)
{
_log.Debug("Hot tub: fuel item is null.");
return;
}
string fuelName = smelter.m_fuelItem.m_itemData.m_shared.m_name;
float currentFuel = zdo.GetFloat(ZDOVars.s_fuel, 0f);
float maxFuel = smelter.m_maxFuel;
if (maxFuel <= 0f)
return;
float fuelPercent = currentFuel / maxFuel;
_log.Debug(
$"Hot tub fuel state: fuel='{fuelName}', " +
$"current={currentFuel:0.00}, max={maxFuel:0.00}, " +
$"percent={fuelPercent * 100f:0.0}%, threshold={_config.RefillBelowPercent.Value * 100f:0.0}%"
);
if (fuelPercent >= _config.RefillBelowPercent.Value)
{
_log.Debug("Skipping hot tub - fuelPercent >= threshold.");
return;
}
int missingFuel = Mathf.CeilToInt(maxFuel - currentFuel);
int requestedFuel = _config.RefillToMax.Value ? missingFuel : Mathf.Min(_config.RefillAmount.Value, missingFuel);
if (requestedFuel <= 0)
return;
_log.Debug($"Hot tub refill requested: missing={missingFuel}, requested={requestedFuel}");
int fuelConsumed = _fuelManager.ConsumeFuel(player, smelter.transform, fuelName, requestedFuel, containers);
if (fuelConsumed <= 0)
{
_log.Debug("Hot tub refill stopped: no usable fuel.");
return;
}
for (int i = 0; i < fuelConsumed; i++)
nview.InvokeRPC("RPC_AddFuel", new object[0]);
_log.Info(
$"Refilled hot tub: {currentFuel:0.0}/{maxFuel:0.0}, " +
$"fuel={fuelName}, added={fuelConsumed}, " +
$"mode={(_config.RefillToMax.Value ? "max" : "fixed")}"
);
}
}
}
+112
View File
@@ -0,0 +1,112 @@
using System.Collections.Generic;
using UnityEngine;
namespace AutoRefillFires
{
internal class RefillManager
{
private readonly ModConfig _config;
private readonly FireplaceRefiller _fireplaceRefiller;
private readonly HotTubRefiller _hotTubRefiller;
private readonly ModLogger _log;
public RefillManager(ModConfig config, ModLogger log)
{
_config = config;
_log = log;
FuelManager fuelManager = new FuelManager(_config, _log);
_fireplaceRefiller = new FireplaceRefiller(_config, fuelManager, _log);
_hotTubRefiller = new HotTubRefiller(_config, fuelManager, _log);
}
public void Run(Player player)
{
Fireplace[] fireplaces = Object.FindObjectsByType<Fireplace>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
Smelter[] smelters = null;
if (_config.FillHotTubs.Value)
smelters = Object.FindObjectsByType<Smelter>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
Container[] containers = null;
if (_config.UseNearbyContainers.Value)
containers = Object.FindObjectsByType<Container>(FindObjectsInactive.Exclude, FindObjectsSortMode.None);
List<FireplaceCandidate> candidates = new List<FireplaceCandidate>();
foreach (Fireplace fireplace in fireplaces)
{
if (fireplace == null)
continue;
float distance = Vector3.Distance(player.transform.position, fireplace.transform.position);
if (distance > _config.Radius.Value)
continue;
ZNetView nview = fireplace.GetComponent<ZNetView>();
if (nview == null)
nview = fireplace.GetComponentInParent<ZNetView>();
if (nview == null || !nview.IsValid())
continue;
ZDO zdo = nview.GetZDO();
if (zdo == null)
continue;
float maxFuel = fireplace.m_maxFuel;
if (maxFuel <= 0f)
continue;
float currentFuel = zdo.GetFloat(ZDOVars.s_fuel, 0f);
float fuelPercent = currentFuel / maxFuel;
candidates.Add(new FireplaceCandidate
{
Fireplace = fireplace,
Distance = distance,
FuelPercent = fuelPercent
});
}
// Priority: lowest fuel percentage first, then nearest fireplace.
candidates.Sort((a, b) =>
{
int fuelCompare = a.FuelPercent.CompareTo(b.FuelPercent);
if (fuelCompare != 0)
return fuelCompare;
return a.Distance.CompareTo(b.Distance);
});
_log.Debug(
$"Scan start: loadedFireplaces={fireplaces.Length}, " +
$"inRange={candidates.Count}, " +
$"loadedContainers={(containers != null ? containers.Length : 0)}, " +
$"radius={_config.Radius.Value:0.0}m"
);
foreach (FireplaceCandidate candidate in candidates)
{
_log.Debug(
$"Priority: name='{candidate.Fireplace.gameObject.name}', " +
$"fuel={candidate.FuelPercent * 100f:0.0}%, " +
$"distance={candidate.Distance:0.0}m"
);
_fireplaceRefiller.TryRefill(player, candidate.Fireplace, containers);
}
if (_config.FillHotTubs.Value && smelters != null)
_hotTubRefiller.RefillNearby(player, smelters, containers);
_log.Debug("Scan end.");
}
}
}
+89 -10
View File
@@ -1,20 +1,99 @@
# Changelog # Changelog
## 1.0.0 All notable changes to Auto Refill Fires will be documented in this file.
## [1.2.0] - 2026-09-06
### Added
- Added automatic Hot Tub refilling support.
- Added a separate `FillHotTubs` configuration option.
- Hot Tubs use the same fuel source, reserve, priority and refill settings as other supported objects.
### Changed
- Refactored the plugin into separate components for configuration, fuel handling, fireplace refilling, hot tub refilling, logging and scan coordination.
- Simplified `Plugin.cs` so it mainly handles plugin startup, toggle state and scan timing.
- Added shared logging through `ModLogger`.
- Cleaned up unused code and imports.
- Standardized and simplified code formatting across the project.
- Improved internal code organization and maintainability.
### Fixed
- Fixed Resin candles being incorrectly detected as refillable fireplaces.
- Removed leftover dead code from the previous fuel refill implementation.
## [1.1.0] - 2026-08-29
### Added
- Added a configurable toggle hotkey.
- Default: `F7`
- Allows automatic refilling to be enabled or disabled while in-game.
- The configuration file is automatically reloaded when Auto Refill Fires is disabled, allowing settings to be changed without restarting the game.
- Added configurable logging levels:
- `None`
- `Error`
- `Warning`
- `Info`
- `Debug`
- Added `KeepFuelReserve`.
- Prevents Auto Refill Fires from consuming the last configured amount of fuel from player inventories and containers.
- Example: a reserve of `10` means the last 10 Wood, Resin, or other fuel items will be kept.
- Added configurable fuel source priority:
- `PlayerFirst`
- `ContainersFirst`
- Added support for using multiple nearby containers during a single refill.
- Fuel is taken from the nearest usable containers first.
- If the selected source cannot provide enough fuel, the plugin automatically falls back to the other configured source.
- Added `OnlyRefillOwnPieces`.
- Optionally restricts automatic refilling to fireplaces and torches built by the local player.
- Added more detailed Debug logging for:
- Fireplace detection
- Range checks
- Fuel levels and refill decisions
- Fuel source selection
- Nearby container availability
- Fuel consumption
### Changed
- Significantly reduced unnecessary container scanning.
- Containers are now discovered once per refill scan cycle and reused for all fireplaces during that cycle.
- A container is no longer rediscovered once for every individual fuel item added.
- Fuel is now removed in batches instead of one item at a time where possible.
- Nearby containers are sorted by distance and the closest usable container is preferred.
- Improved fireplace type detection.
- Vanilla campfires using the `fire_pit` prefab are correctly recognized as campfires.
- Improved handling of unknown and modded objects based on Valheim's `Fireplace` component.
### Fixed
- Fixed excessive scene-wide container searches when refilling fireplaces to maximum fuel.
- Fixed campfires potentially being classified as `OtherFireplaces` because of their vanilla prefab name.
- Fixed Debug messages being hidden by the default BepInEx Debug log filter.
---
## [1.0.0] - 2026-08-23
Initial release. Initial release.
### Features ### Features
- Automatic campfire refilling - Automatic campfire, hearth, standing/wall torch, brazier refilling
- Automatic hearth refilling
- Automatic standing torch refilling
- Automatic wall torch refilling
- Automatic brazier refilling
- Supports colored torch variants
- Automatically detects the required fuel type
- Optional nearby container fuel usage - Optional nearby container fuel usage
- Configurable refill radius - Configurable refill radius, treshold
- Configurable refill threshold
- Refill-to-max or fixed refill amount - Refill-to-max or fixed refill amount
- Individual fireplace type toggles - Individual fireplace type toggles
+51 -15
View File
@@ -1,33 +1,69 @@
# Auto Refill Fires # Auto Refill Fires
A lightweight BepInEx mod for Valheim that automatically refills nearby fireplaces and torches. A lightweight BepInEx mod for Valheim that automatically refills nearby fireplaces and torches.
## What's New in 1.2.0
##### [Full CHANGELOG](https://github.com/LabodiDavid/AutoRefillFires/blob/main/Thunderstore/CHANGELOG.md)
- Added automatic Hot Tub refilling with a separate `FillHotTubs` toggle
- Fixed Resin candles being incorrectly treated as refillable objects
- Major internal refactor and code cleanup
## Features ## Features
- Automatically refills nearby campfires - Automatically refills campfires, hearths, torches, braziers, bonfires, hot tubs and other compatible fireplaces
- Automatically refills hearths - Can use fuel from the player inventory and nearby containers
- Automatically refills standing torches, including colored variants - Configurable refill radius and threshold
- Automatically refills wall torches
- Automatically refills braziers
- Uses the correct fuel type automatically
- Can consume fuel from the player's inventory
- Optional support for nearby containers
- Configurable refill radius
- Configurable refill threshold
- Refill to maximum or by a fixed amount - Refill to maximum or by a fixed amount
- Individual enable/disable options for fireplace types - Individual enable/disable options for fireplace types
## Requirements
- Valheim
- BepInExPack Valheim
## Configuration ## Configuration
The configuration file is generated after launching the game once with the mod installed. The configuration file is generated after launching the game once with the mod installed.
`BepInEx/config/simplifydave.autorefillfires.cfg` `BepInEx/config/simplifydave.autorefillfires.cfg`
### Hotkey
Default:
`F7`
Pressing the hotkey toggles Auto Refill Fires on or off.
When disabled, the configuration file is automatically reloaded, allowing most settings to be changed without restarting Valheim.
### Fuel Reserve
Example:
`KeepFuelReserve = 10`
The mod will leave the last 10 matching fuel items untouched in each fuel source.
### Fuel Source Priority
Available values:
`PlayerFirst`
`ContainersFirst`
If the preferred source does not contain enough usable fuel, the other source is used automatically.
### Logging
Available levels:
`None`, `Error`, `Warning`, `Info`, `Debug`
Use `Debug` to see fireplace detection, fuel levels, refill decisions and fuel source selection.
## Requirements
- Valheim
- BepInExPack Valheim
## Installation ## Installation
Install using r2modman / Thunderstore Mod Manager, or manually place `AutoRefillFires.dll` inside: Install using r2modman / Thunderstore Mod Manager, or manually place `AutoRefillFires.dll` inside: