Skip to content

Commit

Permalink
working hacky thing
Browse files Browse the repository at this point in the history
  • Loading branch information
Lenni009 committed Mar 16, 2024
1 parent 53fd4c5 commit 1795e0f
Show file tree
Hide file tree
Showing 8 changed files with 233 additions and 45 deletions.
3 changes: 2 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
@@ -1,2 +1,3 @@
*.exe
saves
*.json
*.hg
4 changes: 2 additions & 2 deletions .vscode/settings.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
"deno.enable": true
}
"deno.enable": true
}
28 changes: 28 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -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
```
90 changes: 90 additions & 0 deletions applyMapping.ts
Original file line number Diff line number Diff line change
@@ -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;
}
4 changes: 4 additions & 0 deletions decode.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@echo off
set "jsonfilename=%~n1.json"
python3 nmssavetool.py decompress %1 %jsonfilename%
applyMapping.exe %jsonfilename%
4 changes: 4 additions & 0 deletions encode.bat
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
@echo off
set "compressedfilename=%~n1.hg"
applyMapping.exe %1
python3 nmssavetool.py compress %1 %compressedfilename%
42 changes: 0 additions & 42 deletions main.ts

This file was deleted.

103 changes: 103 additions & 0 deletions nmssavetool.py
Original file line number Diff line number Diff line change
@@ -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 <input> <output>" % os.path.basename(name))
print("Usage: %s decompress <input> <output>" % os.path.basename(name))
print("Usage: %s list" % os.path.basename(name))
print(" compress Decompress a given 'No Man's Sky' save file.")
print(" decompress Compress a given 'No Man's Sky' save file.")
print(" list Searchs for save files and outputs what files were found.")

def main():
args = sys.argv
if len(args) == 2:
if args[1] == "l" or args[1] == "list":
for file in findSaveGames():
print(file)
return
if len(args) == 4:
if args[1] == "c" or args[1] == "compress":
writeFile(args[3], compress(readFile(args[2])))
print("done")
return
if args[1] == "d" or args[1] == "decompress":
writeFile(args[3], decompress(readFile(args[2])))
print("done")
return
usage(args[0])

main()

0 comments on commit 1795e0f

Please sign in to comment.