diff --git a/.gitignore b/.gitignore
index f31375e..858c330 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,3 @@
*.exe
-saves
\ No newline at end of file
+*.json
+*.hg
\ No newline at end of file
diff --git a/.vscode/settings.json b/.vscode/settings.json
index 3c2e53b..cbac569 100644
--- a/.vscode/settings.json
+++ b/.vscode/settings.json
@@ -1,3 +1,3 @@
{
- "deno.enable": true
-}
\ No newline at end of file
+ "deno.enable": true
+}
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..92cec05
--- /dev/null
+++ b/README.md
@@ -0,0 +1,28 @@
+# NMS-Save-Decoder
+
+I needed something to decode a .hg file, in case save editors aren't updated, or I have some other weirdly specific issue with a save.
+
+So this is the solution :shrug:
+
+This is all very hacky and barely works. It uses a [Python file by Robert Maupin](https://gist.github.com/Chase-san/16076aaa90429ea6170550926b70f48b) to compress and decompress the save, and my own Typescript Deno code to apply the mapping and write it to the disc.
+
+The two parts are glued together by a Windows Batch file.
+
+## Usage
+
+### Decoding a Save
+
+Drag'n'drop your save.hg file on the `decode.bat` file.
+
+### Encoding a Save
+
+Drag'n'drop your save.json file on the `encode.bat` file.
+
+## Build the TS Part from Source
+
+1. [Install Deno](https://docs.deno.com/runtime/manual/getting_started/installation)
+2. Execute this command:
+
+```sh
+deno compile --allow-read --allow-write --allow-net applyMapping.ts
+```
diff --git a/applyMapping.ts b/applyMapping.ts
new file mode 100644
index 0000000..494e956
--- /dev/null
+++ b/applyMapping.ts
@@ -0,0 +1,90 @@
+// deno-lint-ignore-file no-explicit-any
+type Mapping = { Key: string; Value: string };
+
+interface MappingObject {
+ libMBIN_version: string;
+ Mapping: Mapping[];
+}
+
+const [file, mappingFileName] = Deno.args;
+
+let mapping;
+
+if (mappingFileName) {
+ const mappingFileData = Deno.readTextFileSync(mappingFileName);
+ mapping = JSON.parse(mappingFileData);
+} else {
+ mapping = await fetchMapping();
+}
+
+const fileData = Deno.readTextFileSync(file);
+
+let fileJson;
+console.log("parsing file...");
+try {
+ fileJson = JSON.parse(fileData);
+} catch {
+ fileJson = JSON.parse(fileData.slice(0, -1));
+}
+
+const isMapped = Boolean(fileJson.Version);
+
+const mappingFunction = isMapped ? reverseMapKeys : mapKeys;
+
+console.log("mapping...");
+const mappedSave = mappingFunction(fileJson, mapping);
+
+Deno.writeTextFileSync(
+ file,
+ JSON.stringify(mappedSave, null, isMapped ? undefined : 2), // minify when compressing
+);
+console.log("done!");
+
+function mapKeys(json: any, mapping: Mapping[]): any {
+ if (Array.isArray(json)) {
+ return json.map((item) => mapKeys(item, mapping));
+ } else if (typeof json === "object" && json !== null) {
+ const newJson: any = {};
+ for (const key in json) {
+ const mappedKey = mapping.find((m) => m.Key === key)?.Value;
+ if (mappedKey) {
+ newJson[mappedKey] = mapKeys(json[key], mapping);
+ } else {
+ newJson[key] = mapKeys(json[key], mapping);
+ }
+ }
+ return newJson;
+ } else {
+ return json;
+ }
+}
+
+function reverseMapKeys(json: any, mapping: Mapping[]): any {
+ if (Array.isArray(json)) {
+ return json.map((item) => reverseMapKeys(item, mapping));
+ } else if (typeof json === "object" && json !== null) {
+ const newJson: any = {};
+ for (const key in json) {
+ const originalKey = mapping.find((m) => m.Value === key)?.Key;
+ if (originalKey) {
+ newJson[originalKey] = reverseMapKeys(json[key], mapping);
+ } else {
+ newJson[key] = reverseMapKeys(json[key], mapping);
+ }
+ }
+ return newJson;
+ } else {
+ return json;
+ }
+}
+
+async function fetchMapping() {
+ const mappingUrl =
+ "https://github.com/monkeyman192/MBINCompiler/releases/latest/download/mapping.json";
+ console.log("downloading mapping...");
+ const fetchedFile = await fetch(mappingUrl);
+ const fetchedJson: MappingObject = await fetchedFile.json();
+ console.log("success!");
+
+ return fetchedJson.Mapping;
+}
diff --git a/decode.bat b/decode.bat
new file mode 100644
index 0000000..d4f1279
--- /dev/null
+++ b/decode.bat
@@ -0,0 +1,4 @@
+@echo off
+set "jsonfilename=%~n1.json"
+python3 nmssavetool.py decompress %1 %jsonfilename%
+applyMapping.exe %jsonfilename%
diff --git a/encode.bat b/encode.bat
new file mode 100644
index 0000000..4efba39
--- /dev/null
+++ b/encode.bat
@@ -0,0 +1,4 @@
+@echo off
+set "compressedfilename=%~n1.hg"
+applyMapping.exe %1
+python3 nmssavetool.py compress %1 %compressedfilename%
diff --git a/main.ts b/main.ts
deleted file mode 100644
index 54e90af..0000000
--- a/main.ts
+++ /dev/null
@@ -1,42 +0,0 @@
-import { Buffer } from "https://deno.land/std@0.141.0/node/buffer.ts";
-import lz4 from "npm:lz4";
-
-function decompressSave(file: Uint8Array) {
- const buf = Buffer.from(file);
-
- let index = 0;
- const chunks = [];
-
- while (index < buf.length) {
- const magic = buf.readUIntLE(index, 4);
- index += 4;
-
- if (magic !== 0xfeeda1e5) {
- console.error("Invalid Block assuming already decompressed");
- return buf.toString("binary");
- }
-
- const compressedSize = buf.readUIntLE(index, 4);
- index += 4;
- const uncompressedSize = buf.readUIntLE(index, 4);
- index += 4;
-
- index += 4; // skip 4 bytes
-
- const output = Buffer.alloc(uncompressedSize);
- lz4.decodeBlock(buf, output, index, index + compressedSize);
- index += compressedSize;
-
- chunks.push(output);
- }
-
- return Buffer.concat(chunks).toString("binary").slice(0, -1);
-}
-
-const filePath = Deno.args[0];
-
-const file = Deno.readFileSync(filePath);
-
-const decompressedFile = decompressSave(file);
-
-Deno.writeTextFileSync('result.json', decompressedFile);
\ No newline at end of file
diff --git a/nmssavetool.py b/nmssavetool.py
new file mode 100644
index 0000000..ce824bc
--- /dev/null
+++ b/nmssavetool.py
@@ -0,0 +1,103 @@
+#!/usr/bin/python3
+
+# Copyright 2021, Robert Maupin
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty, provided the copyright notice and
+# this notice are preserved. This file is offered as-is, without any warranty.
+
+import io
+import os
+import lz4.block
+import math
+import sys
+
+from glob import glob
+
+FILE_PATH = os.path.dirname(os.path.realpath(__file__))
+
+def uint32(data: bytes) -> int:
+ """Convert 4 bytes to a little endian unsigned integer."""
+ return int.from_bytes(data, byteorder='little', signed=False) & 0xffffffff
+
+def byte4(data: int):
+ """Convert unsigned 32 bit integer to 4 bytes."""
+ return data.to_bytes(4, byteorder='little', signed=False)
+
+# The important part
+def decompress(data):
+ """Decompresses the given save bytes."""
+ size = len(data)
+ din = io.BytesIO(data)
+ out = bytearray()
+ while din.tell() < size:
+ magic = uint32(din.read(4))
+ if magic != 0xfeeda1e5:
+ print("Invalid Block, bad file")
+ return bytes() # some unsupported format
+ compressedSize = uint32(din.read(4))
+ uncompressedSize = uint32(din.read(4))
+ din.seek(4, 1) # skip 4 bytes
+ out += lz4.block.decompress(din.read(compressedSize), uncompressed_size=uncompressedSize)
+ return out
+
+# The important part, part 2
+def compress(data):
+ """Compresses the given save bytes."""
+ size = len(data)
+ din = io.BytesIO(data)
+ out = bytearray()
+ while din.tell() < size:
+ uncompressedSize = min([0x80000, size - din.tell()])
+ block = lz4.block.compress(din.read(uncompressedSize), store_size=False)
+ out += byte4(0xfeeda1e5)
+ out += byte4(len(block))
+ out += byte4(uncompressedSize)
+ out += byte4(0)
+ out += block
+ return out
+
+def readFile(path):
+ fin = open(path, "rb")
+ data = fin.read()
+ fin.close()
+ return data
+
+def writeFile(path, data):
+ fout = open(path, "wb")
+ fout.write(data)
+ fout.close()
+
+
+def findSaveGames():
+ """Finds all saves on windows."""
+ return glob("C:\\Users\\*\\AppData\\Roaming\\HelloGames\\NMS\\*\\save*.hg")
+
+def usage(name):
+ print("No Man's Sky Save Tool v0.1")
+ print("Copyright (c) 2021 Robert Maupin. Permissive License.")
+ print("Usage: %s compress