goatfungus / NMSSaveEditor

No Man's Sky - Save Editor
1.91k stars 240 forks source link

Feature Request: CLI tool to export (Decrypt?) the save file to a more readable json #178

Open m-vandeneede opened 5 years ago

m-vandeneede commented 5 years ago

I'm currently working on a project that automatically unbricks savefiles (mainly for the BensAVirgin bug), but it would be nice if I could decrypt the savefile straight from a CLI command.. Something like https://github.com/matthew-humphrey/nmssavetool, but this one doesn't seem to properly work anymore

jeffswt commented 5 years ago

image

CODE BELOW

import json
import sys

json_map = {
    'F2P': 'Version', '8>q': 'Platform', '6f=': 'PlayerStateData',
    'yhJ': 'UniverseAddress', 'Iis': 'RealityIndex', 'oZw': 'GalacticAddress',
    'dZj': 'VoxelX', 'IyE': 'VoxelY', 'uXE': 'VoxelZ',
    'vby': 'SolarSystemIndex', 'jsv': 'PlanetIndex', ';l5': 'Inventory',
    ':No': 'Slots', 'Vn8': 'Type', 'elv': 'InventoryType', 'b2n': 'Id',
    '1o9': 'Amount', 'F9q': 'MaxAmount', 'eVk': 'DamageFactor',
    '3ZH': 'Index', '>Qh': 'X', 'XJ>': 'Y', 'hl?': 'ValidSlotIndices',
    'B@N': 'Class', '1o6': 'InventoryClass', '0H2':
    'SubstanceMaxStorageMultiplier', 'cTY': 'ProductMaxStorageMultiplier',
    '@bB': 'BaseStatValues', 'MMm': 'SpecialSlots', '=Tb': 'Width',
    'N9>': 'Height', 'iF:': 'IsCool', 'PMT': 'Inventory_TechOnly',
    'gan': 'Inventory_Cargo', '6<E': 'ShipInventory', 'Kgt': 'WeaponInventory',
    'QL1': 'BaseStatID', '>MX': 'Value', '4eu': 'GraveInventory',
    'BGQ': 'SpawnGrave', ';JQ': 'SpaceGrave', 'NFm': 'GraveUniverseAddress',
    '4ia': 'GravePosition', 'gIh': 'GraveMatrixLookAt', 'YJU': 'GraveMatrixUp',
    'rj2': 'ShipLayout', 'xbM': 'WeaponLayout', 'oJJ': 'CurrentShip',
    '<d2': 'ProceduralTexture', 'bnT': 'Samplers', 'e6e': 'CurrentWeapon',
    '93M': 'Filename', '@EL': 'Seed', 'QlJ': 'AltId', '9;o': 'Level',
    'NNR': 'GenerationSeed', '4kj': 'KnownTech', 'eZ<': 'KnownProducts',
    '24<': 'KnownSpecials', 'Ddk': 'KnownRefinerRecipes', ':JX': 'KnownWords',
    '9wn': 'Word', 'D;o': 'Races', 'dwb': 'MissionProgress', 'p0c': 'Mission',
    'tW6': 'Progress', '8?J': 'Data', 'eZ7': 'Participants', '5L6': 'UA',
    '5bU': 'BuildingSeed', 'M?f': 'ParticipantType', 'wMC': 'Position',
    '?eS': 'PostMissionIndex', ';R7': 'CurrentMissionID',
    'k?G': 'CurrentMissionSeed', 'Mg<': 'PreviousMissionID',
    'rej': 'PreviousMissionSeed', 'yq:': 'MissionVersion',
    'EQt': 'MissionRecurrences', 'jGk': 'MissionID', 'oF@':
    'RecurrenceDeadline', 'cmA': 'HoloExplorerInteraction',
    '<>B': 'HoloScepticInteraction', ':=:': 'HoloNooneInteraction',
    'fSZ': 'Health', 'KCM': 'ShipHealth', 'kLc': 'Shield',
    'NE3': 'ShipShield', 'dcK': 'Energy', 'wGS': 'Units', '7QL':
    'Nanites', 'kN;': 'Specials', 'cid': 'ThirdPersonShip',
    'i8O': 'TimeAlive', 'Lg8': 'TotalPlayTime', 'A1f': 'MarkerStack',
    '9@i': 'Table', 'FN5': 'Event', 'TWn': 'BuildingLocation',
    'iqv': 'BuildingClass', 'ADw': 'Time', '1JW': 'MissionSeed',
    'i;<': 'NewMPMarkerStack', 'gUR': 'Stats', ':rc': 'GroupId',
    '2Ak': 'Address', '>vs': 'IntValue', 'eoL': 'FloatValue',
    'm4I': 'TelemetryStats', 'a6j': 'StoredInteractions',
    'O49': 'Interactions', 'RQA': 'MaintenanceInteractions',
    '=yU': 'InventoryContainer', 'wx7': 'LastUpdateTimestamp',
    '4>;': 'DamageTimers', 'XV5': 'Flags',
    'VhC': 'PersonalMaintenanceInteractions', 'nwS': 'VisitedSystems',
    'seg': 'Hazard', '9Lm': 'BoltAmmo', 'VPy': 'ScatterAmmo',
    ':ML': 'PulseAmmo', 'cO>': 'LaserAmmo', 'vaP': 'FirstSpawnPosition',
    'E?v': 'SavedInteractionIndicies', 'SEK': 'SavedRaceIndicies',
    'Wu?': 'SavedInteractionDialogTable', 'E=X': 'Hash', '2Fk': 'Dialog',
    'wHR': 'InteractionProgressTable', 'vsV': 'AtlasStationAdressData',
    'B9>': 'NewAtlasStationAdressData', ':A3': 'VisitedAtlasStationsData',
    ':0x': 'FirstAtlasStationDiscovered', 'kPF': 'UsesThirdPersonCharacterCam',
    'DtI': 'ProgressionLevel', 'QNH': 'ProcTechIndex', 'eV1': 'IsNew',
    'wc3': 'UseSmallerBlackholeJumps', '<dk': 'UsedEntitlements',
    'aHM': 'PlanetPositions', '?=a': 'PlanetSeeds', '7Mc': 'PrimaryPlanet',
    '05J': 'TimeLastSpaceBattle', '8br': 'WarpsLastSpaceBattle',
    '8xx': 'ActiveSpaceBattleUA', 'IRi': 'TimeLastMiniStation',
    'x=M': 'WarpsLastMiniStation', 'gpU': 'MiniStationUA',
    'JvI': 'AnomalyPositionOverride', '5ST': 'GameStartAddress1',
    'EeN': 'GameStartAddress2', 'M2Z': 'GalacticMapRequests',
    'nTB': 'FirstShipPosition', 'G?:': 'HazardTimeAlive',
    'tSD': 'RevealBlackHoles', 'kYq': 'CurrentFreighterHomeSystemSeed',
    'bIR': 'CurrentFreighter', '>Yh': 'FreighterLayout',
    '8ZP': 'FreighterInventory', '0wS': 'FreighterInventory_TechOnly',
    'RB7': 'FreighterUniverseAddress', '9wg': 'FreighterMatrixAt',
    'fUl': 'FreighterMatrixUp', 'lpm': 'FreighterMatrixPos',
    'c5s': 'BaseBuildingObjects', 'b1:': 'Timestamp', 'r<7': 'ObjectID',
    'ofi': 'RegionSeed', 'CVX': 'UserData', 'wJ0': 'Up', 'aNu': 'At',
    '=o:': 'TerrainEditData', 'r:j': 'GalacticAddresses', 'eiy': 'BufferSizes',
    'w0G': 'BufferAges', 'cL6': 'BufferAnchors', 'Mes': 'Edits',
    '4Km': 'NPCWorkers', 'q08': 'ResourceElement', 'BKy': 'InteractionSeed',
    '1wj': 'HiredWorker', 'gNy': 'FreighterBase', 'pNt': 'BaseUA',
    'TJx': 'BaseOffset', 'F?0': 'PersistentPlayerBases', 'h4X': 'BaseVersion',
    'oHw': 'Forward', '@ZJ': 'Objects', 'B2h': 'RID', '3?K': 'Owner',
    'f5Q':'LID', 'K7E': 'UID', 'V?:': 'USN', '3I1': 'TS', 'NKm': 'Name',
    'peI': 'BaseType', 'DPp': 'PersistentBaseTypes',
    'nlG': 'TeleportEndpoints', 'gk4': 'Facing', 'iAF': 'TeleporterType',
    '=3B': 'Chest1Layout', '3Nc': 'Chest1Inventory', '@ip': 'Chest2Layout',
    'IDc': 'Chest2Inventory', '@Ik': 'Chest3Layout', 'M=:': 'Chest3Inventory',
    '@1E': 'Chest4Layout', 'iYp': 'Chest4Inventory', 'XgV': 'Chest5Layout',
    '<IP': 'Chest5Inventory', 'nw5': 'Chest6Layout', 'qYJ': 'Chest6Inventory',
    'tLo': 'Chest7Layout', '@e5': 'Chest7Inventory', '7b?': 'Chest8Layout',
    '5uh': 'Chest8Inventory', 'QBc': 'Chest9Layout', '5Tg': 'Chest9Inventory',
    'wqf': 'Chest10Layout', 'Bq<': 'Chest10Inventory',
    'v8;': 'ChestMagicLayout', ';?C': 'ChestMagicInventory',
    'wFm': 'ChestMagic2Layout', 'fCh': 'ChestMagic2Inventory',
    'Sjw': 'CurrentFreighterNPC', 'P;m': 'VehicleOwnership', 'NTx': 'Resource',
    'pMa': 'InventoryLayout', 'YTa': 'Location', 'l?l': 'Direction',
    '5sx': 'PrimaryVehicle', '@Cs': 'ShipOwnership',
    'QA1': 'InventorySpecialSlotType', 'aBE': 'PrimaryShip',
    '@Vn': 'MultiShipEnabled', 'rBG': 'PlayerWeaponName',
    'vxi': 'PlayerFreighterName', 'h=c': 'StartGameShipPosition',
    'nkF': 'TradingSupplyDataIndex', 'b69': 'TradingSupplyData',
    'Iu7': 'Supply', 'pfp': 'Demand', 'JWK': 'Product', 'HbG': 'LastPortal',
    'NQJ': 'VisitedPortal', '3fO': 'PortalSeed', 'K:U': 'LastPortalUA',
    'vrS': 'KnownPortalRunes', 'DaC': 'OnOtherSideOfPortal',
    'LIR': 'PortalMarkerPosition_Local', 'qW;': 'PortalMarkerPosition_Offset',
    ';DM': 'StartingPrimaryWeapon', 'Rfw': 'WeaponMode',
    'SYl': 'StartingSecondaryWeapon', 'l:j': 'CharacterCustomisationData',
    'VFd': 'SelectedPreset', 'wnR': 'CustomData', 'SMP': 'DescriptorGroups',
    'Aak': 'Colours', 'T>1': 'TextureOptions', 'gsg': 'BoneScales',
    'unY': 'Scale', '4hl': 'ShipUsesLegacyColours', '3Z>': 'FleetSeed',
    ';Du': 'FleetFrigates', 'SLc': 'ResourceSeed', '@ui': 'HomeSystemSeed',
    '4kx': 'TimeOfLastIncomeCollection', 'fH8': 'CustomName',
    'uw7': 'FrigateClass', 'SS2': 'Race', '0Hi': 'AlienRace',
    '5es': 'TotalNumberOfExpeditions', 'v=L': 'TotalNumberOfSuccessfulEvents',
    '5VG': 'TotalNumberOfFailedEvents', 'MuL': 'NumberOfTimesDamaged',
    'Mjm': 'TraitIDs', 'yJC': 'RepairsMade', '7hK': 'DamageTaken',
    'kw:': 'FleetExpeditions', 'o@4': 'SpawnPosition',
    '4j2': 'TerminalPosition', 'b>d': 'SpeedMultiplier', 'hea': 'Powerups',
    '3oW': 'InterventionEventMissionID', 'bc<': 'StartTime',
    '?DC': 'PauseTime', 'dfZ': 'TimeOfLastUAChange',
    'U87': 'NextEventToTrigger', 'xVB': 'ExpeditionCategory',
    'TPK': 'ExpeditionDuration', 'sbg': 'ActiveFrigateIndices',
    'WZs': 'DamagedFrigateIndices', '1xe': 'DestroyedFrigateIndices',
    'lD@': 'AllFrigateIndices',
    'omN': 'NumberOfSuccessfulEventsThisExpedition',
    'G;H': 'NumberOfFailedEventsThisExpedition', 'tUs': 'Events',
    'iaH': 'AffectedFrigateIndices', 'QJG': 'RepairingFrigateIndices',
    'fe2': 'AffectedFrigateResponses', 'Mx@': 'EventID',
    '7Q;': 'InterventionEventID', 'bbB': 'Success',
    'fvN': 'IsInterventionEvent', '8GD': 'AvoidedIntervention',
    'b78': 'InterventionPhoneCallActivated',
    'ieD': 'ExpeditionSeedsSelectedToday', 'nxr': 'LastKnownDay',
    '>db': 'SunTimer', 'vXX': 'MultiplayerLobbyID', 'YxD': 'MultiplayerUA',
    'x<b': 'MultiplayerSpawn', 'mEH': 'PlayerPositionInSystem',
    'l2U': 'PlayerTransformAt', 'tnP': 'ShipPositionInSystem',
    'l4H': 'ShipTransformAt', 'jk4': 'LastKnownPlayerState',
    'NGn': 'FreighterPositionInSystem', 'uAt': 'FreighterTransformAt',
    '5Sg': 'FreighterTransformUp', 'wyZ': 'RepairTechBuffer',
    'rnc': 'SpawnStateData', 'VuQ': 'GameKnowledgeData', 'yRy': 'Waypoints',
    'S8b': 'GalaxyWaypointType', 'SSo': 'EventId',
    'fDu': 'DiscoveryManagerData', 'ETO': 'DiscoveryData-v1',
    'fgt': 'ReserveStore', 'xxK': 'ReserveManaged', 'OsQ': 'Store', '?fB':
    'Record', '8P3': 'DD', '<Dn': 'DT', 'bEr': 'VP', 'q9a': 'DM','ksu': 'OWS',
    '=wD': 'FL', 'bLr': 'C', 'tiH': 'tiH', 'brV': 'Available', 'kVv': 'TSrec',
    ';FZ': 'Enqueued', 'RVl': 'Palette', 'Ty=': 'ColourAlt', 'xEg': 'Colour',
    '@6c': 'TextureOptionGroupName', '=Cv': 'TextureOptionName',
    'tIm': 'BoneName', 'q5u': 'CN', 'kpg': 'MultiplayerPrivileges',
    'M2L': 'EntitlementId', ';YC': 'MaintenanceContainer',
    'WX8': 'InventoryIndex', 'V?r': 'H', 'sHl': 'Message', 'D6b': 'PTK',
    '8GF': 'LastUABeforePortalWarp', 'eyv': 'LastCompletedTimestamp',
    'FML': 'LastBrokenTimestamp', 'k8N': 'HotActions', 'sM@': 'KeyActions',
    'TD8': 'Action', '<6x': 'QuickMenuActions', 'Y5p': 'Number',
    '7Un': 'StoryPortalSeed', 'aPn': 'ShopNumber', 'rk3': 'ShopTier',
    'QQp': 'HomeRealityIteration', 'MF2': 'KnownWordGroups',
    'RiP': 'CookingIngredientsLayout', 'Kha': 'CookingIngredientsInventory',
    '30s': 'OtherSideOfPortalReturnBase', 'Ue9': 'HasAccessToNexus',
    'hiE': 'NexusUniverseAddress', 'Fk@': 'NexusMatrixAt',
    'd72': 'NexusMatrixUp', 'Yel': 'NexusMatrixPos',
    'j3Y': 'PhotoModeSettings', '9hR': 'BannerIcon',
    '8DG': 'BannerMainColour', 'lmE': 'BannerBackgroundColour',
    'qFl': 'TelemetryUploadVersion', 'qAx': 'Fog', '22a': 'CloudAmount',
    'qLk': 'SunDir', 'OCG': 'SunDirSet', 'yGF': 'FoV', 'ARP': 'FoVSet',
    '?sW': 'DepthOfField', '14a': 'DepthOfFieldAmount', 'L85': 'Vignette',
    'HJQ': 'Filter', 'b76': 'FullyInstalled', 'MYl': 'Group',
    'a>;': 'CalcWarpOffset', 'BpT': 'OriginalBaseVersion',
    'J=S': 'LastEditedById', 'vyN': 'LastEditedByUsername'
}

def dump_file(save_filename, target_filename, decrypt=True):
    # Load original file
    save_file = open(save_filename, 'r', encoding='utf-8')
    inpd = save_file.read()
    if decrypt:
        inpd = inpd[:-1]
    save_data_raw = json.loads(inpd)
    save_file.close()
    # Parse json map
    # json_map_f = open('jsonmap.txt', 'r', encoding='utf-8')
    # json_map = {}
    # json_map_r = {}
    # for i in json_map_f.read().strip().split('\n'):
    #     _ = i.split('\t')
    #     if len(_) == 1:
    #         _.append(_[0])
    #     json_map[_[0]] = _[1]
    #     json_map_r[_[1]] = _[0]
    json_map_r = {}
    for i in json_map:
        json_map_r[json_map[i]] = i
    # Convert json
    def conv(s, mp):
        if isinstance(s, str):
            return s
        elif isinstance(s, list):
            return list(conv(i, mp) for i in s)
        elif isinstance(s, dict):
            return dict((mp.get(i, i), conv(s[i], mp)) for i in s)
        return s
    if decrypt:
        save_data = conv(save_data_raw, json_map)
        output = json.dumps(save_data, indent=4)
    else:
        save_data = conv(save_data_raw, json_map_r)
        output = json.dumps(save_data, indent=None,
                            separators=(',', ':')) + '\x00'
    # Save file
    t = open(target_filename, 'w')
    t.write(output)
    t.close()
    return

if __name__ == '__main__':
    if len(sys.argv) != 3:
        print('python extract.py GAMESAVE.hg READABLE.json')
        print('python extract.py READABLE.json GAMESAVE.hg')
        print('either of the above')
    else:
        src = sys.argv[1]
        trg = sys.argv[2]
        dec = True
        if not src.lower().endswith('.hg'):
            dec = False
        dump_file(src, trg, decrypt=dec)
    exit(0)
m-vandeneede commented 5 years ago

Ayy thanks for that dude! Although the project only lived for around 48 hours, this will definitely come in handy in the future!