-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
8 changed files
with
233 additions
and
45 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,3 @@ | ||
*.exe | ||
saves | ||
*.json | ||
*.hg |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
{ | ||
"deno.enable": true | ||
} | ||
"deno.enable": true | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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% |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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% |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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() |