<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: Foolgry Wang</title>
    <description>The latest articles on DEV Community by Foolgry Wang (@foolgry).</description>
    <link>https://dev.to/foolgry</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.us-east-2.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F4219382%2F8ef7dc30-6553-4380-bf76-565d908686b6.png</url>
      <title>DEV Community: Foolgry Wang</title>
      <link>https://dev.to/foolgry</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/foolgry"/>
    <language>en</language>
    <item>
      <title>What's inside an Enshrouded .ksd save file (and how I parse it in the browser)</title>
      <dc:creator>Foolgry Wang</dc:creator>
      <pubDate>Sat, 31 Oct 2026 16:59:59 +0000</pubDate>
      <link>https://dev.to/foolgry/whats-inside-an-enshrouded-ksd-save-file-and-how-i-parse-it-in-the-browser-3l07</link>
      <guid>https://dev.to/foolgry/whats-inside-an-enshrouded-ksd-save-file-and-how-i-parse-it-in-the-browser-3l07</guid>
      <description>&lt;p&gt;If you've put serious hours into &lt;em&gt;Enshrouded&lt;/em&gt;, at some point the save file starts to feel personal. You want to re-plan a skill build without redoing the grind, an item vanished after a crash, you're moving a character between a local world and a dedicated server, or a big patch is coming and you want a known-good backup first.&lt;/p&gt;

&lt;p&gt;All of that lives in one place: a &lt;code&gt;.ksd&lt;/code&gt; file on disk. It has a reputation for being opaque, mostly because opening it in a hex editor shows you compressed noise. But it's a small, layered container, and it's quite parseable. This post is a walk through what's inside and how I built a parser for it that runs entirely in the browser.&lt;/p&gt;

&lt;h2&gt;
  
  
  Why open the file at all
&lt;/h2&gt;

&lt;p&gt;Games usually give you sanctioned ways to fix things. Enshrouded gives you some, but not everything:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Respec options are limited.&lt;/strong&gt; Sometimes you want to re-plan a long-running character from scratch, not incrementally.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Bugs eat items.&lt;/strong&gt; Crashes and patch migrations have been known to take inventories with them.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Local vs. dedicated server.&lt;/strong&gt; Character data lives in a file you own, and moving it between setups is a common desire.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Backups before patches.&lt;/strong&gt; The most boring and most valuable use case: keep a copy you can roll back to.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;A hex editor technically works for peeking, but since most of the payload is compressed, it tells you almost nothing. To do anything useful you have to unpack the container first. And once you have a working parser, the friendliest place to run it is the browser: no installer, no platform questions, and nothing has to be trusted with your save.&lt;/p&gt;

&lt;h2&gt;
  
  
  The container, in layers
&lt;/h2&gt;

&lt;p&gt;A &lt;code&gt;.ksd&lt;/code&gt; file is not a flat dump of game objects. It's a small archive, and it helps to think of it in three layers.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 1: a container header.&lt;/strong&gt; The first bytes describe the file itself: format markers, version-ish fields, and how the rest of the file is laid out. I'm deliberately not going to quote byte offsets or magic constants here. They're exactly the kind of detail that rots between patches, and hardcoding them is the mistake that killed my first parser.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 2: length-prefixed, compressed sections.&lt;/strong&gt; After the header, the file divides into sections. Each section starts with its length, and the payload is compressed. The pragmatic move is to identify the compression empirically — by sniffing the payload — rather than trusting a single constant from an old forum post. Once a section is inflated, you're holding actual game data.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Layer 3: the object tree.&lt;/strong&gt; The decompressed payload is a serialized tree of game objects: entities with property lists, type tags, and nested structures. IDs cross-reference each other. Your character is one such object; inventory, equipment and skill data hang off it, sometimes several levels deep, sometimes referenced from more than one place.&lt;/p&gt;

&lt;p&gt;Two consequences follow from this shape. First, nearly everything interesting is unreadable until you inflate the right section — which is why hex-editing a &lt;code&gt;.ksd&lt;/code&gt; in place rarely works. Second, a parser should &lt;em&gt;walk&lt;/em&gt; the structure instead of seeking to fixed offsets. Patches reshuffle the layout; a walker survives that far better than a seek table.&lt;/p&gt;

&lt;h2&gt;
  
  
  Parsing it in the browser: the decisions that mattered
&lt;/h2&gt;

&lt;h3&gt;
  
  
  1. Everything heavy lives in a Web Worker
&lt;/h3&gt;

&lt;p&gt;Decompressing sections and walking a binary tree is exactly the kind of work that freezes a main thread: the tab goes grey, the spinner stops spinning, and users assume the site died. So the whole pipeline — reading the &lt;code&gt;File&lt;/code&gt; into an &lt;code&gt;ArrayBuffer&lt;/code&gt;, walking the container, inflating sections, building the object model — runs in a worker. The main thread only receives small progress messages and the final structured result. The UI stays responsive mid-parse, which for a tool whose entire pitch is "trust us with your save file" turns out to matter more than you'd expect.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. TypedArray and DataView, with a cursor
&lt;/h3&gt;

&lt;p&gt;The binary layer is deliberately boring: a &lt;code&gt;DataView&lt;/code&gt; over the buffer, an explicit byte order, and a cursor exposing helpers like &lt;code&gt;readUInt32()&lt;/code&gt; and &lt;code&gt;readLengthPrefixedString()&lt;/code&gt;. Sections are handled lazily — decompress one, wrap its bytes in a new view, keep walking. Nothing becomes real JavaScript objects until the UI actually asks for it, which keeps memory sane on larger saves.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Fail loudly, never guess
&lt;/h3&gt;

&lt;p&gt;This is the principle I care about most. When the parser meets something it doesn't understand — an unfamiliar version, an unknown property tag, a structure that doesn't line up — it stops and reports where it stopped, instead of improvising. In this domain, the failure mode of "close enough" is a silently corrupted sixty-hour character, and that cost lands on the player, not on us. Writing follows the same asymmetry: the editor only re-serializes structures it fully understands and preserves everything else verbatim. Unknown means untouched.&lt;/p&gt;

&lt;h2&gt;
  
  
  Things that bit me
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Fixed offsets.&lt;/strong&gt; My first attempt seeked to hardcoded positions. The next patch reshuffled the layout, and everything past that point turned to noise. Walk, don't seek.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cursor drift.&lt;/strong&gt; One off-by-one in a length-prefixed string made every later field in the section parse as &lt;em&gt;plausible&lt;/em&gt; garbage — worse than obviously broken data. Now every read validates its length against the bytes actually remaining.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Trusting labels over bytes.&lt;/strong&gt; Assuming every compressed block uses whatever its header claims was too optimistic. Dispatching on the payload's own signature is far more robust.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Cross-referenced values.&lt;/strong&gt; Some values appear in more than one node — the live one plus a cached or derived copy. Editing a single field and calling it done produced half-applied changes until I traced the references properly.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  The tool
&lt;/h2&gt;

&lt;p&gt;I ended up wrapping all of this into a free, browser-based save editor: &lt;a href="https://enshroudedsaveeditor.com" rel="noopener noreferrer"&gt;enshroudedsaveeditor.com&lt;/a&gt;. No login, nothing uploaded — your file is parsed in a Web Worker on your machine and never leaves the browser, and any structure the editor doesn't understand is reported as an error rather than rewritten. It's in Beta, so the one rule that actually matters: &lt;strong&gt;copy your save file first, and edit the copy.&lt;/strong&gt; Backups are free; characters are not.&lt;/p&gt;

&lt;p&gt;If you've poked at &lt;code&gt;.ksd&lt;/code&gt; files yourself — or you try the editor and it refuses a file you believe is valid — I'd genuinely like to hear about it in the comments. Details that survive patches come from real saves, and error reports are the most useful input there is.&lt;/p&gt;

</description>
      <category>gamedev</category>
      <category>webdev</category>
      <category>javascript</category>
      <category>reverseengineering</category>
    </item>
  </channel>
</rss>
