Valheim
Install with App

Details

Latest version
1.0.4
Last Updated
First Uploaded
Downloads
29
Likes
0
Size
341KB
Dependants

Categories

ADDatHost Valheim hosting
30% off

Changelog

1.0.4 — 2026-11-01

Updated due to the Valheim 1.0.21 patch. Rebuilt and checked against Valheim 1.0.21 (network version 41); no code changes beyond the version string, and the wire protocol stays 1, so 1.0.3 and 1.0.4 play together. The store page's website link is https://ravenirongames.com/. The BepInExPack dependency is now 5.4.2360.

1.0.3 — 2026-09-25

Updated due to 1.0.16 Patch.

1.0.2

Fixes from a code review of 1.0.1. The wire protocol stays 1, so 1.0.1 and 1.0.2 play together.

  • Changing worlds no longer carries fire into the next one. In single player or when hosting, logging out of a world with fire burning and loading another (or the same one again) kept the old world's fire running in the new one. Its burning objects pointed at unrelated objects in the new world, so pieces there could be destroyed, the old ground fire burned at its old coordinates, trees waiting to regrow were planted into the new world, and the new world's fire save was overwritten. Now the fire is saved to its own world when that world closes and then forgotten, and the next world loads its own. Dedicated servers were not affected.
  • A new fire's first trees no longer char almost at once. The tree-fire clock stood still while nothing burned, so the first tick of the next fire charged the whole quiet spell as burn time: after a few quiet minutes a tree lit by a fire arrow went to its last health in one tick and charred before it was old enough to spread. The clock now stops with the fire, so a new fire's trees burn at the configured pace from the first tick.
  • The server checks what a client asks it to light or put out. An ignite or extinguish request names an object, and the server used to build that object and take it over whatever it was, however far away. A modified client could name any object, another player's character included, and the server would take it over. The server now lights only trees, logs and burnable pieces, refuses a request for something more than 320 m from where the player's game last reported being, and puts out only an object that is already burning, without building anything. Pressing the extinguish key while aiming at a piece that is not burning no longer takes that piece from the player standing next to it.
  • Ground fire is drawn where it burns, whatever each player's GroundCellSize. The server now sends its own cell size with the ground fire, and each client places the flames, the warmth and the scorch marks with it. A player who had set a different size saw the whole fire at scaled coordinates, far from where it really burned, and was not warmed by it. Against a server older than 1.0.2 a client uses its own size, as before.
  • Soaked trees are left alone. Spread skipped a doused tree (a Dousing Bomb line) or a tree past the Ashlands edge only after building it and taking it from the nearby player, every 0.75 s. It now skips them before building anything.
  • Each fire remembers who lit it. Until now the igniter FireFront saved and offered to companion mods was one for the whole map, so Ragnarok's Wrath, for one, could only blame whoever lit the first fire since the map was last clear of fire, even after that fire had gone out. Now every burning object and ground cell records the player who lit it, fire that spreads keeps the igniter of the fire it came from, and the igniter is saved and restored with the fire. A save from an older version loads with the igniter unknown. The old map-wide value is unchanged, so older companion builds keep working. Newer ones can ask for each fire's igniter through FireManager.CollectActiveFiresWithIgniters and FireManager.FireIgniterNear. Nothing new goes over the network for it (a client's ignite request already carried the attacker's id), so it changes nothing for 1.0.1 clients.
  • Rain no longer puts out every fire at once after a pause. Turning fire back on with fireset enabled true charged the whole pause as rain time, so every fire in the rain went out on resume. Rain now ages fire by at most two spread cycles at a time.
  • The store page's website link now goes to the Raven Iron website.

This DLL was built from the commit tagged v1.0.2; the GitHub release names that commit and gives the DLL's md5.

Rig evidence (2026-09-24, on the dedicated test rig, from a build of e720f2b — on the same code as this release, since only documents and the store page's website link changed after it). A dedicated server on Valheim 1.0.15 (crossplay), plus single-player worlds on the same build; one client throughout. A fire stayed in its own world across a world change in single player: the next world loaded with nothing burning and no fire lines, and re-entering the first world restored its one burning tree. The same held joining the dedicated server: no local fire carried over, and no restore or burn lines appeared at join. Five arrow hits on the dedicated server all carried the same igniter id, and the fire save file's meta line and its ground lines, read while the fire burned, matched it; a second fire was also lit by arrow (the plan was startfire), so it carried the same id and a fire with no igniter was not shown there — the only such fire seen was the rain check's startfire fire below, whose fire event logged igniter 0 (its per-fire igniter was not read back). A client running a different GroundCellSize than the server (2 against the server's 1) drew scorch marks sized and placed for the server's cells once it joined, not its own. Extinguish by id put a fire out with no forced re-creation and no re-light, and after clearfires, pressing the extinguish key (G) at an unlit tree built nothing. After about two minutes with nothing burning, a tree lit by a fire arrow took a first tick of under 1 % of its health and burned at the configured pace from there (1.0.1 would have charged the whole two minutes in that first tick, more than half the tree's health), and was still burning two minutes later. With rain forced on (fireweather force Rain), one tree lit with startfire, and fire paused for 40 s with fireset enabled false, the tree kept burning after the resume and went out 15–18 s later on its rain-shortened timer, not at the resume itself — one tree and a 40 s pause is a smaller check than the planned two to four trees and 60 s. BurnPlayerBuildings false held throughout: ignite aimed at a player's wall answered Not burnable, and the only piece that caught was part of an unowned world ruin. Settings were confirmed back to their pre-test values afterward. Zero exceptions on either side.

Not tried in game: soaked or Ashlands-barred trees being skipped rather than re-created mid-spread (no burning neighbour was near the doused tree in this run); the late-ZDO ignite retry (every request in this run resolved directly, so the retry path was never used); the tree tick under a slower spread cycle (spreadinterval 10); being warmed by ground fire under a mismatched GroundCellSize (only where the flames and scorch marks sat was checked); a 1.0.1 client against a 1.0.2 server and the reverse (checked by code review, not restaged here); a queued fire keeping its igniter; a fire started by lightning or a creature recording no igniter; a save from before 1.0.2 restoring with the igniter unknown (covered off-game); a companion build that still reads the old map-wide igniter; a dedicated-server restart with fire still burning; a world change while hosting with another player connected; the server ignoring an ignite request for something that cannot burn, which any fire hit on a stone wall sends; the server refusing a request for a target out of reach (320 m to light, 200 m to put out), which only a modified client would send; and a second player: two players lighting separate fires at once, which would test igniter attribution between two real attackers, and the extinguish key pressed at an unlit piece beside another player. Off-game: 1815 checks, 0 failed.

1.0.1

  • FireFront no longer sets the Ashlands alight. The Ashlands' own fire (cinders, lava, burning ground) touches wood there all the time, and FireFront caught from every touch, so the whole biome burned and could not be played. A new setting, FireInAshlands (fireset ashlands), is off by default. With it off, FireFront starts no fire in the Ashlands by any path: vanilla fire, spread from outside, a client's ignite request, console commands, or a fire save written by 1.0.0, whose Ashlands fires are dropped at load. The Ashlands' own fire works as it does without FireFront. It is a new key, so the default reaches installs that already have a config file; burntheworld does not change it; the server's copy is the one read. While the server refuses fire there it logs at most one [ASHLANDS] line a minute, and ignite, startfire and firegroundignite say why they did nothing.
  • The DLL no longer carries the build machine's folder. Every DLL through 1.0.0 carried an absolute build path that included the build machine's user name. The build now maps source paths to a neutral root, so the DLL's checksum follows the commit it was built from, not the folder it was built in. This DLL was built from the commit tagged v1.0.1.

Rig evidence (2026-09-23, on the dedicated test rig, this source built just before the commit). In the Ashlands at about (51, -9126): startfire lit nothing, and the server refused 13 ignite requests the client forwarded for four Ashlands objects, logging its [ASHLANDS] line. fireset ashlands true, typed on the client, set the server's value and startfire then lit an Ashlands tree; fireset ashlands false turned it off again and the next startfire lit nothing. At spawn, startfire lit a beech as before. No errors from the mod in the session.

1.0.0

The first release. The code is 0.24.0's with the version string changed; the wire protocol stays 1, so 0.24 and 1.0 play together (each side shows the "different version, works together" notice).

  • What 1.0 means is in docs/ROADMAP.md, "The shorter road": the server is the authority for everything a client can ask of it (0.23), client and server compare versions at connect and numbers read the same on every language setting (0.24), the MIT licence, and a README that describes what happens on a dedicated server.
  • The README pass: every sentence checked against the code with a dedicated server as the reference, by five section auditors and a missing-feature finder with two refuters per finding, then rechecked by two independent readers. Nineteen corrections, among them: scorch marks fade after five minutes and lasting dirt is the opt-in dirtpaint; firebreak ground stops only fire crossing the ground; which commands run on the server and need its admin list; three fireset keys the table was missing; and the arson disclosure, stated as the code behaves: one igniter id for the whole server, the first player to light a fire while nothing burns, saved in the fire save file and read by a companion mod.

Rig evidence (2026-09-23, on the dedicated test rig). A 0.23 client on the 1.0 server: the server logged [VERSION] peer ... has not answered FireFront's version check in 60 s ... This server runs 1.0.0 (protocol 1). and the tester saw the centre-screen message. A 1.0 client on a 0.23 server: the client logged [VERSION] FireFront: the server did not answer the version check ... about 30 s after joining and the tester saw the centre-screen message. German number format: the 1.0 server ran as de-DE under Mono (the probe logged 1.5 formats as '1,5', '1.5' parses as 15, the trap 0.24 removed) and booted clean; the save file's cross-culture reading is covered by the harness. Admin refusal (0.23), matching versions and the decimal comma (0.24) were played on code 1.0 does not change.

0.24.0

  • Client and server compare versions at connect. Nothing put FireFront's version on the wire before, so a mixed pair failed silently: ignition doing nothing, fire the server simulated that the client never drew. A new routed RPC, FireFront_Version, carries the plugin version and a wire protocol number both ways once the client can reach the server (Fire/VersionCheck.cs). Same version: one Info line on each side. Same protocol, different version: a Warn and a top-left message, since they work together. Different protocol: a Warn and a centre-screen message saying fire will not show or sync correctly. The protocol number (Plugin.WireProtocol, 1 as of this release) changes only when an RPC's name, arguments or payload layout change.
  • Older builds on either side are caught too. A client whose server never answers (a server on 0.23 or older, or none) tells the player after three tries 10 s apart. A server gives every connected peer 60 s; one that never sent its version gets a Warn in the server log and, once that player's character has spawned, a console line plus a centre-screen message on the client, through two channels vanilla registers on every client (RemotePrint and the routed ShowMessage), so it works with no FireFront or an old one on the far side. The new RPC is in the 0.23 sender guard ([AUTH] routed-sender guard armed: 14 methods); the boot line reads All 13 FireFront RPCs.
  • On-screen version notices wait for the player's character to spawn, plus 3 s; the log and console lines are written at once. Found by the review before release: the reply arrives within one round trip, several seconds before the character exists, and a top-left message needs the character while a centre message under the loading screen is never seen.
  • Numbers mean the same thing on every language setting. There was not one invariant culture in the tree. The fire save file was written and read with the machine's culture, so a store written on an English machine and read on a German one put 1.5 back as 15, and fireset spreadradius 1,5 typed on a German client reached the server's BepInEx parser, which is invariant, as 15. Every number FireFront writes as text or reads from text now goes through Utils/InvariantNumbers.cs: written in the invariant culture at round-trip precision, read in the invariant culture with two allowances for old files and for typing (a single comma with no dot is a decimal comma; the Unicode minus reads as -), and thousands separators, NaN and the infinities refused rather than guessed. The save file keeps format version 1: a store written by any earlier build, on any culture, reads as its writer meant it, so an upgrade loses nothing. The server applies the same rewrite to a forwarded fireset, for clients older than 0.24. Vanilla's own console argument parser (startfire 12,5) was already invariant and is untouched.
  • The off-game harness covers it: 1,680 new checks run the reader and writer under ten cultures (invariant, en-US, de-DE, fr-FR, ru-RU, sv-SE, tr-TR, pt-BR, es-ES, fi-FI), including every culture reading every other culture's pre-0.24 save file. The harness runs on .NET with ICU culture data, which formats sv-SE and fi-FI negatives with U+2212: stricter than the Mono the game runs on.
  • A texture-generator name match lowercased with the machine's culture, which on a Turkish machine turns I into dotless ı; it lowercases invariantly now.
  • MIT licence, in the repository and in the zip (LICENSE). Copyright Raven Iron Games and FireFront contributors.
  • docs/ROADMAP.md is rewritten to the shorter road agreed on 2026-09-23: 1.0 is 0.23, this release, the licence and a README pass; the rest moves after 1.0.

Rig evidence (2026-09-23, on the dedicated test rig, both sides on 0.24.0, en-US). Server boot: All 13 FireFront RPCs registered, [AUTH] routed-sender guard armed: 14 methods (13 FireFront + RPC_Damage). On join, server: [VERSION] peer <id> (Steam_...) runs FireFront 0.24.0, same as this server.; client: [VERSION] the server runs FireFront 0.24.0, same as this game.; nothing on screen. fireset burnduration 240,5 typed with a decimal comma read fireset burnduration = 240.5 on this machine and [server] FireFront: fireset burnduration = 240.5 on the server. (before 0.24 an en-US machine read it as 2405, clamped to 600); fireset spreadradius 1,5 came back as 2, the key's minimum, where before 0.24 it would have been 15. Zero exceptions on either side. Not played, left out on purpose: a 0.23 client against a 0.24 server and the reverse (the two mismatch warnings), and a de-DE process reading the fire save file; the last is covered by the harness's 1,680 checks, which run the same code. The shipped build adds the notice-timing fix above, found by review after this session; it changes only what a player sees when versions differ.

0.23.0

  • A non-admin can no longer change the server's settings. A fireset forwarded from a client, and the ConfigurationManager sync that lands on the same server-side setter, are checked against the server's own adminlist before anything is applied: the vanilla kick/ban check (ZNet.ListContainsId on the socket's host name) that the relayed commands already use. A refused change answers [server] FireFront: fireset <key> refused — you are not in the server's adminlist. in the sender's console and is logged on the server under [AUTH]. The check is server-side only, on purpose: a client-side gate is false on every server without an adminlist file, which is how 0.21.6 made the sync a silent no-op.
  • The server checks who a routed RPC came from. Valheim reads a routed RPC's sender id straight off the wire and relays it verbatim, so a modified client could write the server's id as the sender, address the package to everybody, and every other client's "only from the server" filter would pass it. A Harmony prefix on ZRoutedRpc.RPC_RoutedRPC, server side, now drops a package for any of FireFront's twelve RPCs, or for vanilla's RPC_Damage (the tree, log and wear-and-tear prefixes act on its sender), whose claimed sender is not the peer the connection belongs to. Scoped to those thirteen methods so no other traffic is touched. A client cannot register with the server's own id (vanilla refuses it as already connected), so a sender that matches its connection is never the server either. Drops are logged at Warn, one line per peer per kind of refusal per ten seconds, with a count of what was not logged. The guard reports [AUTH] routed-sender guard armed: 13 methods at RPC registration; if ZNet.m_peers cannot be reflected it says so at Warn and every handler's own clamps stand alone, as in 0.22.
  • fireset says where a value landed, and the server always answers. A typed fireset writes the copy on the typist's machine first (several keys are read on both sides) and then forwards it. Before 0.23 the server never refused, so the local reply stood in for both. Found in play on the rig: a refused non-admin saw burntheworld = True (in force: True) and read it as the server's state. On a client connected to a server the reply now says on this machine, a second line says the server answers separately, and the server replies to every forwarded fireset as a [server] line: = <value> on the server, refused (the server's value is unchanged, the local copy keeps what was set), no such key, or could not read the value. The ConfigurationManager route gets the same replies.
  • Extinguish requests are bounded by the server's settings. The radius a client sends with the key or a Dousing Bomb was applied as sent. It is now capped at the larger of the server's ExtinguishGroundRadius and DousingBombRadius, and the position must be within 200 m of where the server last saw the player (its reference position, refreshed every 2 s). Out of reach or not finite is refused and logged; over the cap is clamped and logged. docs/CONFIG-KEY-MAP.md no longer calls the server's copies of those two keys dead.
  • Paint assignments are rate-limited on the client. The server sends at most one per second per peer; more than ten in five seconds are dropped and logged. The 4096-cell cap and the radius clamp per message are unchanged.
  • The README no longer says creatures burn on a dedicated server. Players do (the server tells the owning client, since 0.21.8); creatures burn in a hosted game only, because a dedicated server has no physical world where they stand.
  • Struck from the roadmap as already done: restored ground-fire cells do get a FireBurnZone (0.21.9, 9900edf; the restore path queues them for UpgradeDarkGroundCells).

Rig evidence (2026-09-23, on the dedicated test rig, both sides on the same build). Server boot: [AUTH] routed-sender guard armed: 13 methods (12 FireFront + RPC_Damage), no FireFront warning or error. As admin: fireset burntheworld true then false applied on the server (fireset (remote from <peer>): burntheworld = True, then = False); clearfires relayed and ran. Off the adminlist (vanilla re-read the file within 10 s, no restart): [AUTH] refused fireset 'burntheworld = false' from a peer not in the adminlist; a ConfigurationManager slider, [AUTH] refused fireset 'dirtpaintradius = 8'; G and the Dousing Bomb still put fires out. The forged packets, from a rig-only test plugin that is not part of FireFront: a PaintAssign claiming the server's id, [AUTH] dropped a routed RPC (method -677775963) ... it claims sender <server>, the connection is peer <owner>; thirty at once, one line and (29 more of these from this peer in the last 10 s not logged) on the next; an RPC_Damage with FireFront's tick marker (method 1130726949), dropped; a FireDamage of 1e9 (method -1026555515), dropped; an honest request with a 100 km radius, extinguish radius 100000.0 m clamped to the server's 15.0 m; one 5 km away, refused an extinguish request at (4945,-31): 5000 m from where the server last saw the player, limit 200 m. Found in play and fixed the same evening: the fireset wording (the entry above). Retested on the fixed build: as admin the console read burntheworld = True on this machine, the sent-to-the-server line, then [server] FireFront: fireset burntheworld = True on the server.; as non-admin the [server] line read refused — you are not in the server's adminlist. The server's value is unchanged; the copy on your machine keeps what you set. Zero exceptions either side.

0.22.1

  • A player who joins mid-fire now sees the fire. Object fires (structures, trees, logs) were announced only by a FireEvent broadcast at ignition; a client that connected after it saw the tree taking damage, charring and falling with no flames on it. The join-time snapshot request the ground fire already had (FireFront_GroundSyncRequest, sent once the routed channel can reach the server) is now answered with a second package, FireFront_ObjectFireSync: every burning ZDOID with its burn age and whether it has dropped to embers. The client books each one exactly as a FireEvent would (it is idempotent against fires it already heard about), backdates its own smoulder clock by the server's age so a fire that burned ten minutes elsewhere does not start fresh, and asks again after 10 s if no object snapshot arrived (three tries; the server rate-limits per sender, so a repeat that was not needed costs nothing). Only the connected server's reply is accepted. A burner that is not loaded on the client yet is drawn when it instantiates, as before.
  • Charred wood looks like charred wood, and the embers are dialled back. The 0.22.0 char was a dark tint over the vanilla bark with a uniform ember lattice glowing at the game's Ashlands HDR value (4.78) through every crack of the whole trunk: it consumed the tree. Now the client builds, once per species, a charred albedo FROM the species' own bark texture (read back through a RenderTexture; the bundle textures are not CPU-readable) - domain-warped "alligator" plates cut by black fissures, the grain kept as tone, per-plate variation, pale ash flecks on the plate tops - and a normal map that carries the char relief over the vanilla bark grain, AG-packed like every map the game ships. The ember glow lives only in scattered pockets: CharredEmberCoverage (0.2) is the fraction of the trunk carrying live embers, thresholded at an honest quantile of a low-frequency field so the number means what it says; inside a pocket the glow bleeds along the fissures and adds pin-point embers on the plates, the way vanilla's own Ashlands tree mask is a field of points, and nothing glows outside. CharredEmberIntensity (0.4; 1.0 ≈ the Ashlands value) sets the heat, so at the default only the cores cross the bloom threshold. Four mask variants, picked per object from its ZDOID, so neighbouring snags glow in different places; the same masks and dial drive the live-burn bark char below the fire front. When the read- back fails (no graphics device, unexpected format) the tint look of 0.22.0 stays. The generator is SharedMedia.ProceduralTextures, vendored from libs-Tools and previewable offline with libs-Tools/CSharp/TexturePreview against the extracted vanilla bark - the contact sheet there is what shipped. firedumptex writes what this client generated as PNG under BepInEx/config/FireFront-textures/.
  • Charred wood smokes after the fire is out. A charred trunk (two or three points on its lower two thirds) and a fallen charred log (three points along it) give off thin grey wisps for CharredSmokeSeconds (90 s, world time, so every peer and every late joiner sees the same stage), full for two thirds of it and tapering to nothing; the same Lux-lit vanilla smoke material the fire uses, at a hundredth of the rate, drifting with the real wind. Off under LowSpec, CharredSmokeEnabled otherwise, capped at 32 smoking objects world-wide.
  • The whole-tree glow from the first in-game run (NomadicWar, PR #3, 2026-09-20) is closed at the root. Trunks rendered as pink-white rods root to crown, canopies flat pink, distant LOD trunks neon. The mask kept bright orange in RGB everywhere with the pattern only in alpha; Custom/Vegetation does multiply by alpha (checked in BOTH the forward and the deferred GLSL: u_xlat2.xyz = u_xlat2.www * u_xlat2.xyz), but Standard's _EmissionMap does not, and mip levels average RGB and alpha separately, so at LOD distance a sparse mask became "mean alpha × bright orange" over the whole surface. The mask is now PREMULTIPLIED with alpha 255: black between embers in every pass, every shader and every mip. Also: a mask is always bound (Texture2D.blackTexture when the generator could not run - an unbound slot is Unity's default WHITE, and 0.22.0 drove _EmissionColor into exactly that); atlas species (Pine, Fir) get masks confined to the bark block during a live burn, found by eroding the atlas alpha and keeping the largest solid component, so needle cards never light; the charred glow fades to a third past 30-90 m and the live-burn embers drop to 30 % when the rig is far, so a distant snag is black, not a lamp.
  • The scorch grid is gone. Rows of dark squares across burnt hillsides: one near-opaque 1.5 m umber disc per 1 m cell, feathered rims overlapping LIGHTER than the discs (a lattice), horizontal quads cutting into slopes, a palette lighter than a Black Forest floor so the tiles read both ways, on Sprites/Default. Now a multiply-blended soot blot (vanilla's Custom/Particle (Unlit) from the ember donor, Blend OneMinusSrcAlpha SrcAlpha on black vertices with the shader's fog on - see the last entry below for why not DstColor Zero - every other fade in the shader off, queue 2950) that darkens whatever is lit - grass, sun, shadow and the fire's own light survive, and two blots overlapping only get darker - laid on the terrain's own normal by raycast, about two blots in five cells at 1.6-2.2x the cell, jittered, with a noise-warped outline and pale ash flecks. The first five placements log [SCORCH] (position, terrain hit, shader, blend) so "no marks" is diagnosable as absent vs invisible. The 0.21.x alpha disc remains the fallback when no donor material exists.
  • Only the server may paint fire on a client. ZRoutedRpc relays the sender id verbatim, and the two cosmetic handlers (FireEvent, GroundFireSync) accepted it from anyone, so a modded client could address a forged event to Everybody and put phantom fires on every screen. All three client-side handlers now apply the IsFromServer check the fire-damage handler already had. (Found by the 2026-09-20 decompile pass; ZRoutedRpc.Register is also a Dictionary.Add that throws on a duplicate name, not the overwrite the old comment claimed — the per-instance registration guard is what keeps it safe, and the comment now says so.)
  • New fireset keys: charredember, charredembercover, charredsmoke, charredsmokeseconds. firestatus reports all four. No ledger rung: nothing renamed or re-defaulted.
  • Verified headless: 0.22.0 booted the real Valheim l-1.0.15 Linux dedicated server with every patch applied and 0 exceptions (~/valheim-testbed/boot-check.sh, see HANDOFF).
  • Five things fixed on review and on the first run (NomadicWar, 2026-09-20): the charred textures are generated on worker threads, because the first version built a 512² field and four 512² masks on the main thread the frame the first tree caught, a freeze of several hundred milliseconds (the field build starts at plugin load on a client, so a tree that streams in already charred does not wait for it either); the join snapshot's burn age is clamped and turned into an ignition instant on arrival, so a burner that instantiates a minute after the packet is not a minute behind; the scorch thinning is keyed on the ground cell rather than the metre, which at a GroundCellSize below 1 folded neighbouring cells onto one pick; and every velocity and force module now writes x, y and z in one curve mode, because Unity logged Particle Velocity curves must all be in the same mode once per frame per live flame (80,000 lines in the first eight minutes of play, each a BepInEx disk write) - BuildFlames set y to two constants and left x and z as constants, and the wind update assigned bare floats. The same shape sat in BuildCrownFlames since 0.21.2, so 0.21.x logged it too whenever a crown burned. And a scorch decal now waits for its zone: the sync stream reports a cell going out anywhere on the map, during the loading screen included, and the mark was spawned on the spot - five of five logged marks had no terrain under them, 500 m from the player, floating at the synced height with no tilt. They queue and spawn, with the lifetime they have left, once the zone is loaded on that client (since 0.21.15 the remote-mirror path had this hole on main too).
  • The six look items from that review, and one thing found under them. The scorch blot is the size the entry above says: both callers passed the ground cell x 1.5 on top of the spawner's own 1.6-2.2x, so a blot was 2.4-3.3 m on a 1 m cell and burnt ground sat under 2.7 multiply blots on average; the callers now pass the cell, which is 1.2 blot outlines (about half a blot of full soot tone) per burnt square metre. The quad is fitted to five terrain rays instead of one - centre and its four corners - so its plane is the mean of what it spans and it is lifted along that normal by whatever its highest corner needs (4 cm minimum; a lift past 35 cm - a corner more than 31 cm above the centre plane - is a hollow no flat quad can lie in, and that mark is dropped and says so in the log; a heightfield bump BETWEEN the five samples can still poke through, the slope case is exact). The [SCORCH] mark N line now prints terrain-hits=k/5 lift=Lm. Under the tint question sat the real defect: the 0.22.1 decal drew nothing. Custom/Particle (Unlit)'s compiled fragment never outputs the texture's RGB - its colour is the VERTEX colour, and the texture contributes one channel, picked by _AlphaChannel, into alpha - so Blend DstColor Zero multiplied the ground by white. The multiply now rides on alpha (_DstBlend = SrcAlpha, _AlphaChannel = red, the blot's tone: 0.30 as stored, which displays as the ground at about 30 % of its brightness at the heart), and it is fogged the way vanilla's own particles are rather than darkening ground that is already fog-grey: the quad's vertex colour is black, the shader's _FejdFog is ON so the fragment's RGB is exactly the fog term, and Blend OneMinusSrcAlpha SrcAlpha works out to a * (1 - f) * lit + f * fog - the multiply under the fog (review of the fix found the bare framebuffer * a scaling the fog term too, a black blob on grey mist at 50 m). Every other fade the GLSL applies to that alpha is forced to 1 with the reason for each in the code: _SkyMask (a top-down depth test that hides anything under a canopy - the ground under a burnt tree is exactly that; the donor has it ON), the soft-particle depth fade (compiled under the GLOBAL keyword, not the material toggle; _SoftNearFade -1000 saturates it whatever the gap), and the near-camera fade (_CameraFadeFactor 0.2 on the donor faded a mark out as the player walked up to it). The [SHADER-DIAG] scorch decal material line reads every value back. The charred twin of a pine or fir now takes the bark-confined ember mask, not the plain one that lit the dead atlas' branch cards, and the post-burn glow is driven per material slot exactly as the live burn is: leaf and non-emissive slots get no block, a slot whose mask is not ready gets a black mask AND a black colour in the same block, and atlas clones keep a black mask at the material level for good (a coverage change no longer points them at the plain mask). The live-burn glow fades over 30-90 m on the very ramp the charred twin uses (FireVFXController.EmberDistanceFactor), instead of stepping to 30 % at 70 m, so a tree that turns from burning to charred at 50 m keeps its brightness; the 70 m step stays as the emitter cost LOD only. And retired ember-mask sets (a coverage change parks the old set, about 5.5 MB, because a destroyed texture samples white) are now freed: once every mask of the new generation exists - all four plain variants and all four of every parked atlas - and ten seconds of Time.time have passed (forty of the longest consumer tick), so every block has been re-bound; the argument that no block can then hold a colour against a dead mask is written above CharredTextures.ReapRetired, and the charred controller now writes one explicit black block when a tree's glow ends or is switched off, which closes the two holes that argument had. firestatus reports retired masks N.

0.22.0

  • Fire climbs the tree, and the height of the flames is the tree's health. A burning tree or log now takes real, unseen fire damage every TreeFireTickInterval seconds (2 s), sized so a healthy tree dies at TreeFireKillFraction (90 %) of BurnDurationSeconds; the fire front starts at the foot and climbs the trunk as the health drops, so flames in the crown mean a tree about to go. Every tick prints as [TREE-HP] under firedebug. The damage cannot go through vanilla: every tree and log in 1.0.15 has m_damageModifiers.m_fire = Immune, so TreeBase.RPC_Damage zeroes a fire hit before it touches health, and its DamageText.ShowText is unconditional and routed to every peer within 30 m, so no vanilla hit is ever silent (TreeBase.cs:113-122, DamageText.cs:184-192). The server therefore sends a HitData carrying a marker in m_statusEffectHash (serialized, unread by trees) to whichever peer OWNS the tree, and the mod's own RPC_Damage prefix applies it there straight to the ZDO health: no text, no shake, no hit effect. On a dedicated server that owner is the nearest client, not the server, which has no GameObject for the tree at all and whose claims ReleaseNearbyZDOS hands back within seconds - so nothing here claims ownership. Health is a ZDO float, replicated to every peer, and is what the flames on every client climb by; no new sync exists for it. The burn timer stays as the fallback authority (rain shortens the timer, not the ticks). TreeFireDamageEnabled false restores the old clock-driven climb.
  • A tree the fire kills is charred in place, then falls or stands. There is no burnt tree in the game (all 115 Charred* prefabs are Ashlands enemies and gear; verified against the 7921-prefab dump), so the dead tree is replaced by a clone of ITS OWN species, at the same transform and scale, re-skinned: Custom/Vegetation bark tinted to charcoal with the ridges pushed into fissures, an ember-crack mask glowing in HDR through the shader's own emissive path and fading over CharredEmberGlowSeconds, vanilla's Ashlands Ash_d dusting the upward faces through the moss layer, and the leaf cards deleted in both the forward and the shadow pass (_Color.a 0, _Cutoff 2). Pine and fir share one atlas for trunk and needles, so they get vanilla's own dead atlases instead (PineTree_01_dead, Pine_tree_small_dead). One cached material clone per vanilla material, shared by every charred tree of that species; the first draft's renderer.materials allocated and leaked a Material per slot per renderer per tree. The hover text reads "Charred Beech". CharredCollapseDelaySeconds (3 s) after the swap, the TreeDestructionRate roll (65 %) decides: COLLAPSE, and the charred trunk falls with vanilla's own felling - the species' log prefab thrown from log_spawnp with TreeBase.SpawnLog's exact impulse, so it tips, hits the ground with the real crash (ImpactEffect: sfx_tree_fall_hit, dust, camera shake) and replicates through ZSyncTransform - born charred, no wood, no stub, CharredCoalMin-CharredCoalMax coal at the foot, and it crumbles to ash CharredLogCrumbleSeconds (20 s) after landing; or STAND, a blackened snag at CharredTreeHealthFraction health that a player can chop later for the same coal and nothing else. The fate, the times and the fall direction (downwind, scattered) are baked into the charred tree's ZDO, so they replicate, survive a save and a restart, and execute on whichever peer owns the tree when its time comes. Charred wood is spent fuel: it never ignites again, and spread never picks it. Regrowth is queued only for a collapse.
  • The 0.21.x fire visuals are gone, and why they looked like streaks. The LineRenderer ribbons read a "flame" texture off fire_pit by substring, which landed on low_flames - an INACTIVE low-LOD emitter whose texture is an 8x8 point-filtered grey blob - and tiled it once per metre along camera-facing strips up to 30 m long, scrolled on the wrong UV axis, twelve of them additive on a 0.4 m trunk, with a gradient that never left 0..1 and reached the bloom only by stacking: white lasers. Read out of the 1.0.15 bundles with UnityPy, 2026-09-20. Vanilla flame is not a flame picture: it is a greyscale FLIPBOOK sheet on Custom/Gradient Mapped Particle (Unlit), recoloured per particle by two HDR colours delivered through custom vertex streams. The fire is now built that way, from clones of the game's own materials found by exact child name (fire_pit/flames (1), sparcs (1), bonfire/Smoke, flare, BlobLava/HeatDistort), never by Shader.Find, which misses bundle-loaded shaders even after world load (the mod's own log proved it). Per burning object: vertical-billboard flipbook tongues at the fire front and a band of licks from the foot to the front; embers that are motes (0.02-0.06 m) with buoyancy, turbulence and drag, never stretched; smoke on Lux Lit Particles/Bumped, lit by the fire and shadowed by the sun; a refraction heat shimmer (HeatHazeEnabled); vanilla's glow-halo trick; the bark itself blackening and glowing with ember cracks below the front (BarkCharEnabled, a property block, restored exactly when the fire goes out); and one point light at vanilla's campfire values with SOFT SHADOWS (FireShadowsEnabled), managed by the game's own LightLod (fade at 40 m, shadows at 20 m, inside the player's point-light-shadow budget) and LightFlicker. Wind is read off EnvMan directly; the per-frame reflection is gone. Textures the mod still bakes are sRGB or linear as appropriate, with full mip chains, trilinear and 8x anisotropic; 0.21.x built them at 128x128 with mips OFF.
  • Every existing Visuals key works again. FireSmokeEnabled, TreeFlameScaling, CrownSparksEnabled, MaxFlameHeight, TallFireMaxConcurrent and their low-spec caps, and SmoulderingVfxEnabled, were all read only by the dead 0.20.x builder; the 0.21.x controller ignored them and firestatus reported settings that were not in force. The low-spec preset now also drops fire shadows and heat haze first, since a shadow-casting point light is the single most expensive thing a fire draws.
  • Nothing is drawn on a headless server any more. 0.21.x built and updated a full LineRenderer rig for every burning object on the dedicated server, every frame, into a graphics device that does not exist.
  • Remote (client) fire is keyed by ZDOID, like the server's. It was keyed by Component, so a tree the client de-instantiated and rebuilt was a different key: the fire could neither be found again nor told to stop, and a burner destroyed by the charring before its stop event arrived left a phantom fire. A burner that instantiates after its fire started now gets its fire on arrival; a fire whose object no longer exists anywhere is swept every 2 s. The client's event handler also no longer resolves the burner through ComponentFromZdoid, which force-creates the object and CLAIMS OWNERSHIP of it - on a client that would have pulled a far-away tree's ownership, and the damage ticks with it, to whichever client heard about the fire first.
  • What is NOT possible, so nobody looks for it again: hardware ray tracing. Valheim 1.0.15 is Unity 6000.0.75f1 on the Built-in Render Pipeline (deferred, HDR), with zero RayTracingShader assets and the Vulkan device created with every VK_KHR_ray_tracing_* extension disabled. Shadow-casting lights, HDR emissive into the bloom, normal maps, soft particles, lit smoke and refraction are the ceiling, and vanilla already uses all of them.
  • New keys: TreeFireDamageEnabled, TreeFireTickInterval, TreeFireKillFraction, CharredCollapseDelaySeconds, CharredCoalMin, CharredCoalMax, CharredTreeHealthFraction, CharredLogCrumbleSeconds, CharredEmberGlowSeconds (Trees); FireShadowsEnabled, HeatHazeEnabled, BarkCharEnabled (Visuals). TreeDestructionRate is now the COLLAPSE chance, ranged 0-100, and reachable from fireset at last (treedestruction); all of the above are fireset keys and appear in firestatus.
  • Fixed: TreeLog fire measured the log along transform.forward; a log's long axis is its local +Y. The charred clone is scaled like the original (scaleScalar, wild firs run 1.5-3x; the first draft came up at 1.0). The charred flag is pre-baked into the ZDO before Instantiate (ZNetView execution order -20), so the Awake postfix sees it on the spawning peer too; set after Instantiate it was only ever seen by other peers.
  • Builds on Linux now: libs/ from libs-Tools plus a small Cecil publicizer for the two game assemblies (none existed on disk and assembly_publicizer.dll has no runtimeconfig); ~/.dotnet/dotnet build, 0 errors.

0.21.16

  • Real dirt where the ground burned, if you want it. The UseVanillaDirtPaint option has been in the config for a long time and has never once worked on a dedicated server: the server tried to paint terrain it does not have, found nothing, and threw every patch away. The server now hands each burnt patch to one player's game - whoever holds that piece of ground, or the nearest player to it - and that game lays the dirt through the same terrain paint the Hoe uses: it follows the slope, joins up across cells, keeps the grass off, is seen by everyone and is saved with the world. It is permanent, like a hoe mark, which is why it stays off by default. Turn it on with fireset dirtpaint true (a server setting; the command forwards), and fireset scorchmarks false to see it without the decal on top. Where it had worked before, in a hosted game, the painter never refreshed the terrain either, so the host saw its own dirt only after a reload, and it could not paint any zone nobody had ever hoed - which is most forest. Both fixed.
  • The README stopped promising something a dedicated server mostly cannot do. Dirt paths and tilled ground are firebreaks in a hosted game; on a dedicated server they are only near the world's centre, because that is the only ground the server itself keeps loaded, and everywhere else only water is. The wording now says so. The fix is its own release.

0.21.15

  • Burnt ground leaves a scar you can actually see. On a dedicated server it never did. The scorch mark was drawn only on the machine running the simulation, and that machine has no screen - so the server quietly made about two thousand invisible ones every five minutes and no player ever saw a single mark. Your game now draws its own as each patch of ground burns out.
  • A new fire no longer starts at full strength. Fire is supposed to build for its first few minutes rather than exploding outward. That ramp was measured from one clock shared by the whole world, which only restarted once every fire everywhere had gone out - so for as long as anything was still burning, a fresh torch fire began at full reach with no ramp at all. Every fire now has its own clock, which is what the concurrency limits had always used.
  • The ground-fire visual limit finally applies to the machine doing the drawing. It governed the server, which on a dedicated server draws nothing, while your game drew every burning patch with no ceiling whatsoever. If you had turned the limit down, or switched on the low-spec preset to survive a big fire, neither reached you. Now they do.
  • Dedicated servers stop doing a large amount of work that could never produce anything: a damage probe was attached to every burning object and every burning patch of ground, sweeping for victims four times a second, in a place where the things it sweeps for do not exist.
  • Honest about a limit rather than pretending: creatures do not take fire damage on a dedicated server, and never have. Finding them needs a physical world, which a dedicated server does not have. Players burn everywhere, on their own machine, and are unaffected. The setting now says so.
  • An object that burned down could, if a piece of the game had moved under us, quietly survive while everyone was told the fire went out. It now removes it anyway and says what happened.
  • fireset gains waterblocks, maxkills and firesmoke. All three drive real behaviour and could not be reached from anywhere, despite the promise that every setting is live-tunable.
  • Corrected three settings' descriptions and the README's line about rain, all of which described behaviour the mod no longer has. Rain now puts out buildings and trees too, not just grass.

0.21.14

  • Requires BepInExPack Valheim 5.4.2350, which is what this is actually built and tested against. The declared minimum had been left at 5.4.2333 while everything moved on around it - harmless so far, since it reads as a floor rather than a pin, but it was drift rather than a decision.

0.21.13

  • Fire that would not go out. A tree could finish burning, or you could clear every fire on the server, and the flames stayed on screen - burning away on something that was no longer there, forever, while the fire count read zero. The message that says a fire has stopped identifies it by the object that was burning, and the most common way a fire stops is that the thing burned down, which takes that identity with it. Your game then had no way left to work out which flames the message meant, so it did nothing. It now remembers, when a fire starts, which flames belong to it, and sweeps up anything left burning on something that has gone.

0.21.12

  • Whether fire keeps you warm, and how far that reaches, can now be changed from the console like every other setting: fireset firewarmth and fireset firewarmthradius. They were config-file only when the feature landed, which made them the odd ones out.
  • The README is a README again rather than an accumulated list of release announcements, and it now documents all 59 tunable settings - eight of them had never been written down anywhere.

0.21.11

  • A wildfire keeps you warm. You could stand in the middle of a burning forest and freeze to death. Fire now holds off Cold and Freezing while you are near it, exactly as a campfire does - it goes through vanilla's own near-a-fire check, so shelter, frost resistance and getting wet all still behave the way you expect. Reaches a little further than the fire can burn you, because a wildfire should be felt from further away than it hurts. FireKeepsYouWarm, FireWarmthRadius.
  • Fire damage worked only near where the fire started. 0.21.9 added a height check so burning ground could not hurt someone on the floor above it, and measured it against a height the server had no way to know - so a player even a little downhill of the fire's origin took nothing at all. The server can now work out the real ground height for itself, which fixes the damage, and also puts the flames on the ground instead of floating above or sunk into a slope.
  • A joining player can no longer be impersonated to make the server shout. The request a client sends on connect is now checked against who is actually connected before the server acts on it.
  • Fire effects a client had drawn for a world it has left are cleared properly - the object half of that was still leaking.
  • The Apocalypse preset no longer recreates the very problem 0.21.9 fixed: it raises how many ground cells burn, so it now raises how many can be drawn to match.

0.21.10

  • Join a server mid-fire and you now see the fire. Ground fire was sent to players purely as a running list of changes, so anything already alight when you connected was never mentioned to you at all - it burned invisibly beside you for the rest of its life, and it hurt. Your game now asks for the current state as soon as it connects. This was the last place a player's screen was never reconciled against the server.
  • A dedicated server no longer builds fire effects for a machine with no screen. It was creating and simulating a particle system for every burning cell and every burning object, none of which anything could ever render - players draw their own from the sync. A server hosting from inside your own game is unaffected and still shows you its fire.
  • Fire that survives a server restart can hurt things again. Restored cells were written straight into the simulation and skipped the step that gives a cell its damage zone, so after a restart creatures wandered through a burning forest untouched. They now join the queue and are set up a few at a time, so a restart does not stutter.

0.21.9

  • Two ground-fire cells in five were burning invisibly, and had been for a long time. The cap on how many cells get a particle visual defaulted to 30 while the cap on how many can burn defaulted to 50, so at any real fire size the rest damaged you and showed nothing. It was sitting in plain sight in a tester's log from three weeks ago - ground 50/50 and vfxcap 30 on the same line, every heartbeat - and nobody had read the two numbers against each other. The visual cap now defaults to 200, and an existing config file still holding the old 30 is moved up with it, because a stored 30 was the mod's own default rather than anything anyone chose. A cap you set yourself is left alone.
  • A cell that cannot be drawn now waits its turn instead of staying dark forever. The cap was decided by a race at the moment each cell lit and never revisited: a cell that lost burned invisibly for its whole life even after half the fire had gone out and the visuals were free. That is why the flame front read as patchy and lagging rather than simply smaller - and why raising the cap on its own would have moved the threshold without fixing the shape. Cells now take a visual as soon as another finishes, a few per cycle so it cannot spike the frame. firestatus reports how many are lit and how many are waiting.
  • Checking those two caps no longer walks every live fire object twice per ignition, which was quadratic work across a spreading fire.
  • The flame front was trailing the real one by up to two spread steps. Ground fire is sent to players as a running list of changes, and that send was sitting inside the spread cycle's own timer, so its one-second interval was rounded up to the next cycle boundary - one and a half seconds in practice, and up to ten on a server with a slow spread cycle. About nine cells were alight at the front that nobody had been told about, and ten already dead that everyone was still drawing. It now keeps its own clock, and sends twice as often.
  • Fire is drawn on your ground, not the server's guess at it. A dedicated server has no terrain to measure against, so the height it sends for a burning cell is not a ground height at all - it is inherited from whatever first caught fire and then carried unchanged across the whole spread. On flat ground you would never notice; on a hillside the flames sank into the slope or floated above it, further out the further the fire had travelled. Your own game knows where the ground is and now asks itself.
  • Fire is visible again after you log out and come back. The effects a client was drawing are destroyed with the world when you leave, but the record of them was not, so on rejoining those cells were treated as already drawn and stayed dark for the rest of the session - and it got worse every time you logged back in.

0.21.8

  • Fire has never hurt a player on a dedicated server. It does now. Damage was applied by a zone that polls the physics engine for characters standing near a fire. A dedicated server has no terrain, no instances and no colliders where players actually are, only networked data, so that poll found trees and scenery near the flames and never once found the player standing in them. Silent the whole way: no error, no warning, and it worked fine on a world someone hosted themselves, where their own character is a real local object. Reported by Wu'barrk, who stood in a fire on a dedicated server and did not burn, and confirmed from two servers' logs, where the zone's own staged diagnostics recorded colliders found four times and a character resolved zero times.

    The server now reads player positions from the networked data, which is always available, and tells each player's own machine to set them alight, since vanilla's burning effect needs a live character and only that machine has one. The same division of labour the mod already uses for ignition. The physics zone keeps handling creatures wherever physics is real, and no longer touches players at all, so a host cannot be burned twice, and with FireHurtsPlayerOnly set it is no longer given a damage zone to attach at all - during a big fire that was up to two thousand objects a frame doing nothing.

    A client ignores a fire-damage message that did not come from the server, and clamps the one it accepts. The sender check alone is not enough and it is worth being precise about why: Valheim reads the sender id off the wire and relays it unchanged, so it can be forged. The clamp is the guard that actually holds - without it the message would have been a one-packet server-wide instakill, and a non-finite value would have left a character whose health could be neither healed nor restored, saved to disk that way.

    This is the third time the same trap has bitten: spread hit it in 0.17.4, tree regrowth in 0.21.5, and now player damage. Anything that reaches for physics or instances on the server is wrong by default.

  • Burning ground burns what is standing on it, not everything above and below it. A ground fire cell is tracked by its horizontal position, so testing whether a player is in one had to be given a height as well; without it a fire on the grass would have burned the player on the floor above it, or in the crypt below, with no flame in sight and nothing to say what was killing them.

  • A failure in the player-damage pass backs off and retries instead of switching itself off for the rest of the session. One unlucky frame would otherwise have restored the exact bug above on a server that still looked completely healthy.

  • Fire damage keeps its own clock. It was being delivered on the spread cycle's schedule, so a one-second damage interval actually landed every 1.5 seconds at stock settings - and a server with a slow spread cycle would have stretched it to ten.

0.21.7

  • The config-manager sync did not work at all, and said nothing about it. 0.21.6 gated it on a client-side admin check. That check asks Valheim whether the local player is on the server's admin list, and it answers no whenever the server has no admin list, which is every private test server, so a server's own admin was blocked on their own machine with no message anywhere: the slider moved, the client's file was rewritten, the server never heard. The gate bought nothing even where it worked, because fireset is the typed route to the same server-side setter and has never been gated either, so anyone who could abuse the manager could type the command instead. It is gone. The real check belongs on the server, against its own admin list, and is still the deliberate scope left for a public release; it now covers both routes rather than one.
  • A setting changed at the main menu is delivered when the handshake finishes, not thrown away a frame after the world scene loads. The held changes were flushed as soon as Valheim's networking object existed, which is several seconds before a connection or an admin list, so the flush concluded the player was not an admin and discarded everything. Every time, for the one workflow the feature exists to serve.
  • A fireset whose config file is momentarily locked no longer kills the sync for the session. Suppression while a command applies a value was a single flag cleared afterwards, and BepInEx writes the config file before it calls change handlers without guarding that write, so an editor or antivirus holding the file threw straight past the line that cleared it. It is now scoped to one key for one second and expires on its own.
  • A restored fire can no longer light a bystander when an earlier line has claimed its neighbour. Candidates already claimed still count towards "is this ambiguous", so two halves of one trunk cannot resolve into a confident wrong match once the first is taken.
  • Regrowth plants at most five trees per cycle. A burned forest comes due all at once, and every plant is an object creation that every client in range then receives.
  • The blaze age of a restored fire cannot leak into an unrelated later fire. Adoption now happens inside the restore rather than later in the same frame, which closes the window where an early return could strand it.
  • A setting changed in the config manager is sent once, when it settles. ConfigurationManager raises a change per keystroke and per drag frame, so typing "120" sent 1, then 12, then 120, each one applied immediately to a running server - for a moment every fire on it burned out in a single second, and a slider would do that tens of times a second. Changes now wait for a short quiet period, coalesced per setting, with a ceiling so a slider still takes effect while you watch it. A typed fireset is unaffected and still goes immediately.
  • A restore that cannot find an object now says whether nothing matched or too much did.

0.21.6

  • Fire persistence was restoring the wrong objects, and lighting fires that were never burning. A burning object was saved under its ZDOID, and a ZDOID does not survive a world reload: ZDO.Load opens with m_uid.SetID(++ZDOID.m_loadID), so every object read from disk is handed a fresh sequential id and the one it was saved under is discarded. The user half becomes ZDOID's "unknown former user" sentinel, which is why every persisted entry prints as 1:NNNNN. So the id in the store named a different object each boot. The existence check passed, because the id did exist, and that is how this survived from 0.18.0 to 0.21.5 with nothing in the log. Caught on the test server on 2026-09-18: one restart resolved 23 burning trees onto stumps, wood drops and rocks and restored none of them; the next resolved 12 onto live trees that had not been burning and set them alight, and the fire ran to 45 objects within a minute of a boot that was supposed to resume 23.

    A burning object is now stored with its prefab name and its LIVE position, and found again by both through the same sector scan the spread system uses. Live position matters more than it sounds: the position on a burner was captured once at ignition on the basis that "trees and pieces do not move", and a felled log has a Rigidbody, is thrown force and torque as it spawns, and is shoved again by every hit, while the server owns and simulates it. Logs are most of what a forest fire leaves burning, 18 of 21 entries in the store that caught this.

    The match must be unambiguous: exactly one object of that prefab within 0.75 m, with each object claimed once. Two candidates means there is no way to tell which was burning, so the line is dropped. That is deliberate. A refusal costs one fire, while a guess starts one, and a bystander picked by nearest-match would inherit the dead object's burn age at full spread maturity. Nothing moves while the server is down, so the tolerance only has to absorb the round trip through the text store. One sector scan serves a whole cluster of lines, since they are all one fire.

    The ZDOID is still written, as a diagnostic and so an older build can still read the file, and is no longer followed. Object lines written before 0.21.6 are dropped with a warning rather than guessed at, once, on the first boot; ground fire, scorched ground and tree regrowth were never affected because they were always keyed by position.

  • A restored fire with no surviving objects no longer ramps from cold. Ground cells come back without an event and are adopted into one a moment later, after the point where the restore had already released the blaze age it was supposed to hand over.

  • A repeated fireset reaches the server again. A cache meant to stop a double send recorded what the client had sent rather than what the server held, so once the two diverged the client could never re-assert that setting, while the console still printed success. The double send is prevented at its source instead.

  • A setting changed before joining a world is delivered on connect rather than dropped, which is the config manager's main-menu workflow and the reason the feature exists.

  • firetreeregrow with regrowth switched off says so, instead of silently discarding the queue's retry schedule and reporting the result as though fire were in the way.

  • firestatus keeps its REFUSED suffix even if writing the config file throws, which is the one failure the migration is built around.

0.21.5

  • Tree regrowth had never grown a tree on a dedicated server, and could not. Every entry in firetreeregrowlist waited behind ZNetScene.IsAreaReady, which (decompiled from the 1.0.15 server) first requires the zone to be in the zone system's loaded table. Headless, every zone a player stands in is a ghost zone and never enters that table, so the gate was false everywhere but world origin, and each entry burned its twenty 30-second retries and was dropped in silence. The test server's log holds 2,085 heartbeats over three weeks with regrowntrees 0; a forced attempt on 2026-09-18 answered "16 forced, 15 still pending" with the counter unmoved. Behind the gate it was worse: ZNetScene.SpawnObject is a void in 1.0.x that broadcasts a "SpawnObject" RPC to every peer, each of which instantiates its own copy, and its null return was read as failure - so on a hosting client it would have planted one tree per connected machine, up to twenty times. Regrowth now instantiates the prefab directly on the server, which is exactly what vanilla's own RPC handler does on each receiver: one persistent ZDO, every client in range receives it. The readiness gate is gone, an attempt counts only when a spawn actually fails, a spot with something player-built within 3 m is dropped with a log line instead of retried, and one [REGROW] line per cycle says how many came back. Every entry dropped before this release is gone for good.
  • Settings changed in ConfigurationManager now reach the server. Its UI edits only the machine it runs on, so on a client every server-side setting in it was a no-op: the slider moved, the client's own file was rewritten, and the simulation never changed. The same trap fireset was given a relay for in 0.18.3, with no console line to hint at it. Any runtime change to a setting the server owns is now forwarded exactly as the equivalent fireset, gated on the same admin check, deduplicated so a typed command does not go twice, and forgotten when the connection changes. Genuinely client-side settings stay local.
  • Regrowth no longer plants into a live fire. A tree that came back into ground that was still burning caught immediately: on 2026-09-18 five of the first eight trees ever regrown on the dedicated server reignited within seconds, because ground fire routinely outlives the 900 second regrowth timer, leaving a burn that looks as dead as before. Fire within 4 m now defers the entry 30 seconds at a time, and a deferral is not an attempt.
  • Turning tree regrowth off now stops trees already queued. The switch gated only the enqueue. That was invisible while nothing headless could spawn; with regrowth working, an admin who turned it off mid-fire would have watched the queue keep planting for another fifteen minutes, and survive a restart. Queued entries are kept, so turning it back on resumes.
  • A tree that grows is saved immediately instead of at the next 60 second tick. The entry left memory the moment it succeeded, so a hard kill inside that window read it back and planted a second tree inside the first.
  • firetreeregrow reports what actually happened. It returned only "still pending", which falls the same way whether an entry grew a tree or was dropped, so a deletion read as a success. It now names grown, dropped and pending separately.
  • A regrowth spot whose build check cannot run is left alone rather than planted. The check failed open, so a server where the sector scan was unavailable would have grown trees up through people's floors.
  • Attempt counts from an older store are no longer trusted. Before this release the counter incremented on every deferral, which headless meant every cycle, so a carried-over count sat near its cap for a reason that no longer exists and would drop the tree on its first real failure. They restore at zero.
  • One scan serves a cluster of regrowth entries. The build check walks a 192 m block of ZDOs to answer a 3 m question, and entries come due together; it is now done once per cluster instead of once per entry.
  • firestatus no longer reports a crashed migration as a clean boot. The state that knew is reset before the console can read it, so the outcome is folded into the line itself.
  • firestatus from a client now shows the server's config migration line. The status reply carried only the fire counts, so the one check 0.21.4's handoff asked for could not be made from a client. It arrives as a second [server] line.
  • A restored blaze no longer restarts its ramp cold. FireEvent.RestoredRampAge had been declared in 0.19.x and never assigned (the Ragnarok's Wrath session spotted the compiler warning), so after every server restart each fire fell to its ramp-start intensity and climbed again. The sidecar stores no per-event age, but every restored burner carries its own and an event is as old as its oldest burner; that age is applied and logged per event at restore.

0.21.4

  • Eight corrections to 0.21.3's config migration, and a harness that can reach it. 0.21.3 shipped the migration with a test project that compiled the pure ledger only — every engine-side rule was unmeasured, in the one mod of the three with a live rung of each kind. The harness now stubs BepInEx and compiles the real FireConfig, and went 35 → 75 assertions. Each fix below was proven by reverting it and watching a named test fail; thirteen such mutations, all caught.

    • The pre-bind snapshot matched keys case-INSENSITIVELY. BepInEx's own ConfigDefinition.Equals is ordinal and case-sensitive, so a mis-cased line is a different key to the game. An ignore-case snapshot answered "present" for a key BepInEx treats as absent, which cancels the one step whose entire safety is that absence test. Read out of BepInEx.dll rather than assumed; the test stub had it backwards too, so every apply assertion would have passed for a reason that does not hold on a real machine.
    • A retirement that failed was reported as harmless, and the version stamped anyway. Binding and removing a key both touch the file, so a transient lock — antivirus, cloud sync, a config manager, a second process in the same directory — takes the drop down through nobody's fault. The stamp then wrote "already migrated" and the retirement was never retried: a one-second lock made permanent. The drop now reports upward and the version is left unstamped, so the next boot tries again.
    • A rebase row naming a key this build does not bind was skipped in silence, and the file stamped as though it had moved. It warns now.
    • A retirement could be applied to a key this build STILL binds, deleting a live setting outright. Relying on BepInEx's cast to throw was not protection: Bind returns the existing entry for an already-bound definition, so the cast only fails when the type differs. Refused explicitly now, with a warning that names the row.
    • The version stamp only ever rises. It was assigned unconditionally, so opening a world with an older build dragged a newer file's stamp down; rolling forward then replayed rungs against values an admin had since chosen, and a rebase cannot tell a deliberate choice from the old default it happens to equal.
    • A negative stamp made the migration walk two billion rungs, freezing the boot thread on a hand-edited file. Clamped.
    • One key could be decided twice in one boot when two rungs named it, the later silently winning. First rung wins now, and the same key can no longer be rebased and retired in the same pass.
    • The status line reported the plan's INTENT, not what happened. firestatus printed "1 retired key dropped" for a key still sitting in the file, with the only contradiction a warning hundreds of log lines earlier. Refusals now correct the summary.

0.21.3

  • The config file migrates itself. BepInEx persists every bound value to disk, so a changed default never reaches an install that already wrote the key. This mod has hit that twice and answered by hand both times: 0.18.7 renamed VerboseLogging to DebugLogging so a stored true could not follow it, and 0.19.14 could only tell people to set the smoulder threshold themselves after 0.19.13 moved its default from 0.45 to 0.65. Now the family's machinery, Wu'barrk's from Wings of the Valkyrie by way of Valkyrie's Cargo, does it: the raw file is read before any bind, a [Meta] ConfigVersion stamps the layout, a value still equal to an OLD default moves to the new one while anything an admin set stays (and is named in the log), a key no current build binds is dropped instead of riding along as an orphan, a copy of the previous file lands beside it as .v0.bak first, and a migration that cannot back the file up changes nothing and retries next boot. A failed migration never stops the mod loading.

    Version 1, this release: SmoulderAfterFraction still at 0.45 becomes 0.65; the orphan Debug.VerboseLogging is removed. Every install seen in testing already carries 0.65, so on those the boot line reads "nothing to migrate" and stamps the version; the rung is for the testers 0.19.14 could only advise. firestatus prints this machine's own migration line. The decisions live in Config/ConfigLedger.cs, pure and off-game, with a harness under tests/ (tools\run-tests.ps1, 35 checks) - the mod's first.

0.21.2

  • Flames on the outside of the crown, where a leafy tree can show them. The first burning beech looked at on 0.21.1 showed sparks only and no flame. The column from 0.20.1 is a cone under a metre wide up the middle of the tree, and a beech's crown is solid foliage from about three metres up, so the leaves drew over every flame inside it; only the sparks, thrown on a 35 degree cone, escaped. A fir's bare trunk shows the column, which is why it was judged fine there and never on a broadleaf.

    Tall burners now also get CrownFlames: a hemispherical shell of flame sized to the tree's measured canopy (MeasureBurnerCrownRadius, the same renderer-bounds walk as the height, 4-6m on a wild beech), centred a little above mid-height, emitting from the outer third of the radius so the fire sits on the leaves rather than in them, in particles big enough to read at distance and licking upward. The trunk column stays. Cost scales with crown area, halves under the low-spec preset, and the existing tall-fire cap bounds how many burners get a shell at all. Judged by eye the same day on a beech and a fir in the Mountains: the beech shows flame over its crown, the fir keeps its column and reads fine - which also closes out the column from 0.20.1 and the additive fire from 0.20.5, both unjudged until now.

0.21.1

  • fireweather force <EnvName> / fireweather reset: a server-side weather override for testing. The first live fireweather on 0.21.0 showed the machinery working and exposed the gap: vanilla's env Rain writes a debug override on the client it is typed on and nowhere else, so a player standing in forced rain saw their client and vanilla agree on 'Rain' while the server, correctly, answered 'Clear' for that spot. This sets the same field on the server (relayed, admin-gated like every relayed command), where the resolver already honours it ahead of the roll. Names are vanilla's, case-sensitive. Not saved; a restart clears it.

0.21.0

  • Rain now reaches a dedicated server, and it douses fire. Two things, because the first one turned out to be broken: RainSuppressesGroundFire read EnvMan.s_isWet, and EnvMan.UpdateEnvironment returns before choosing an environment when there is no main camera. A dedicated server has no camera, so its environment never changes from the startup value and that flag is false forever - the test server logged raining False in every heartbeat of every run since August, including with the player standing in rain. The key only ever did anything in single-player or on a player-hosted world. (Wind updates on a separate path, which is why the wind reflection verified live and this never could.)

    The server now replays vanilla's own selection for the FIRE's position: weather is deterministic per environment period and biome sector, drawn from a Random seeded with the period number, so it computes the same answer every client does and caches it per 64m zone per period. The overrides vanilla applies ahead of the roll (forceenv, the env command, a random event whose area covers the fire, an alt-biome forced environment, a persistent event whose radius covers the fire) are honoured in order; the two event kinds are scoped to the fire's position, because vanilla scopes them to the local player's and headless that is the origin. Clients blend into a new environment over 2s, so the server can lead a player's view by about that much. Not replicated: EnvZone (a per-player trigger volume) and the Ashlands/Deepnorth edge fixup that needs a loaded heightmap.

    And rain now acts on object fire, which by design it never touched: while rain falls on a burning tree or building it passes fire to nothing - no neighbours, no ZDO candidates, no ground seeds - and its clock runs faster, at RainObjectBurnDurationMultiplier (0.3: about 3x, so a tree that catches in rain is out in ~72s at the 240s default). Ground fire gets the same clock treatment via the existing RainGroundBurnDurationMultiplier, which used to shorten only cells lit during rain and now also reaches the cells rain arrives on later. Direct ignitions still work in rain - a torch or a lightning strike lights the tree, the rain then puts it out - because rain stops spread, it does not forbid fire. Both new keys (RainSuppressesObjectFire, RainObjectBurnDurationMultiplier) are under [Weather], live as fireset rainobjects / rainobjectmultiplier, and off under burntheworld like every other restraint.

    fireweather prints the environment FireFront resolves at your position; on a client it also prints what vanilla is showing you, and the two must agree, then it relays so the server prints its answer for the same spot. That is the verification. The status line's raining is now wet/total burners.

0.20.7

  • The spread diagnostic only logs when debug logging is on. [SPREAD-DIAGNOSTIC] was added in 0.17.4 to prove that tree spread works on a dedicated server, where WearNTear.AllPieces is always empty. It did that, and then kept reporting the same counts every 5 seconds for as long as anything burned: 3,283 lines in one day on the test server, more than twice the heartbeat, none of them saying anything new. It is now gated behind the existing DebugLogging key (fireset debug true, which reaches the server like every other fireset key), so the tool is still there for the next "why won't it spread" report and silent otherwise. No new config key: the default-off flag that already exists is exactly the switch this line should have had.

0.20.6

  • Object fire VFX is budgeted per frame, like ground fire already was. Measured on a live client with 34 objects alight: CPU spikes of 3-4x the median arriving roughly every 1.4s, against a 0.75s spread interval. The cause is that HandleFireEventBroadcast built each burner's whole rig - two or three ParticleSystems plus a realtime Light - inline and synchronously, so an entire batch of ignitions from one spread pass was constructed in a single frame. Ground cells have gone through a queue with a per-frame budget since 0.18.6; object fire never did, and it is the more expensive of the two per instance.

    Ignitions now queue and drain at 2 per frame (lower than ground's 3, because a ground cell builds one cheap system and no light). An ignition that is extinguished or unloaded while queued is dropped rather than built and orphaned.

    This cost was latent before 0.20.1 and that release is what exposed it: a tall burner builds a taller particle column, an extra crown-spark system and a longer-range light, which is several times the construction work of the small flame every burner used to get.

0.20.5

  • The blocky fire is fixed, and the blend was never the cause. Custom/Particle (Unlit) exposes [Enum(Red,0,Green,1,Blue,2,Alpha,3)] _AlphaChannel and defaults to 0, Red - it takes alpha from whichever channel you nominate. FireFront's generated particle texture is white RGB with its falloff in the ALPHA channel, so red read 1.0 across the whole quad, every particle drew as a solid square, and no value of _SrcBlend could have helped. Two were tried (3 SrcColor, then 5 SrcAlpha) and both produced blocks.

    Fixed at both ends deliberately: the material now sets _AlphaChannel = 3 (Alpha), and the additive material gets its own texture with the falloff baked into RGB as well, so the edges go to black whichever channel the shader actually samples - and under additive blending black adds nothing. _Cull is set to Off so billboards are never wound away.

    Worth recording for next time: the .shader file in the AssetRipper export is a //DummyShaderTextExporter stub, because shader bytecode cannot be decompiled. Its PROPERTY LIST is real and is what solved this; its body is not and must not be read as the shader's behaviour.

0.20.4

  • Fire rendered as hard-edged squares at 0.20.3. Fixed. The additive material copied vanilla's _SrcBlend 3, which is SrcColor - a blend that ignores the alpha channel completely. FireFront's particle texture is white RGB that fades out THROUGH ALPHA, so every quad contributed at full strength right to its corners and the fire came out as a cloud of red squares. Vanilla can use SrcColor because its own textures bake the falloff into RGB; ours does not. Now _SrcBlend 5 (SrcAlpha) x _DstBlend 1 (One), the classic additive pairing for an alpha-faded texture. The lesson: copy vanilla's values only when you also have vanilla's texture.

  • Confirmed in play: Shader.Find("Custom/Particle (Unlit)") MISSES ON THE CLIENT TOO, not just on the headless server, so 0.20.2 would have silently fallen back to the old look and taught us nothing. The 0.20.3 borrow-from-a-vanilla-material route is what actually gets the shader, and the log says so: additive shader acquired via borrowed from a vanilla fire material: "Custom/Particle (Unlit)".

0.20.3

  • The additive flame material no longer depends on Shader.Find. 0.20.2 asked for Custom/Particle (Unlit) by name, and Shader.Find only sees shaders currently resident - so it can miss one the game definitely ships, and it misses every time on a headless server, which loads none at all. It now falls back to reading the shader straight off a vanilla fire material (fire_pit, then bonfire, then piece_groundtorch), preferring one that exposes _SrcBlend/_DstBlend so the additive blend can still be forced rather than inherited. A prefab registered in ZNetScene carries its materials, and a material always carries a live shader, so that route is not subject to load-order timing. Names are avoided on purpose: the only thing tying vanilla flame materials to shader names is AssetRipper's builtin fileID table, which is its own mapping and not the game's, and trusting it inverted the shader survey twice. The log now records which route won and what the shader actually turned out to be.

  • tools/stop-test-server.ps1 stopped crying wolf. It confirmed saves by grepping the log for World saved ( ...ms ), which Valheim 1.0.12 no longer emits - a clean shutdown now logs Saving and then Unload lines. The result was "world state is lost" after every clean stop: four in a row on 2026-09-12, all false, each disproved by looking at the world on disk. It now checks the world's own write time against the moment the stop began and treats the log line as a secondary signal, so the warning means something again. Takes -World and -SaveDir for servers that keep saves somewhere other than LocalLow. Note 1.0.12 writes a world as a DIRECTORY, so a loose <World>.db beside it is a stale pre-1.0 backup whose timestamp means nothing.

0.20.2

  • Fire is drawn additively, so it reads as fire instead of as dots. Photographed in play at 0.20.1: a mass of flame particles looked like a hundred separate orange discs hanging in the air, not a body of fire. The cause is that the only particle shader that resolves in Valheim's build is Sprites/Default, which is ALPHA BLENDED - overlapping particles occlude one another rather than accumulating light, so density never becomes brightness. Flames, crown sparks and ground fire now render through the game's own Custom/Particle (Unlit) with _SrcBlend 3 / _DstBlend 1 / _ZWrite 0, the exact values vanilla uses on ashrain_cinder.mat. Smoke deliberately stays alpha blended, because additive smoke glows instead of darkening.

    If that shader ever fails to resolve, flames fall back to the old alpha material and say so once in the log - a miss costs the old look rather than invisible fire.


Older entries are in the full changelog on GitHub: https://github.com/RavenIron-Games/FireFront/blob/main/CHANGELOG.md

Thunderstore development is made possible with ads. Please consider making an exception to your adblock.