SAVEGAME: Reduce size of savefile (#1148)

Storing less info in the save for Factions/Companies if it's still the default info
This commit is contained in:
Snarling 2024-03-11 08:58:10 -04:00 committed by GitHub
parent 4d5401f62e
commit e9d1ddfaf3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 54 additions and 587 deletions

@ -3,7 +3,7 @@ import { getCompaniesMetadata } from "./data/CompaniesMetadata";
import { Company } from "./Company"; import { Company } from "./Company";
import { Reviver, assertLoadingType } from "../utils/JSONReviver"; import { Reviver, assertLoadingType } from "../utils/JSONReviver";
import { CompanyName } from "./Enums"; import { CompanyName } from "./Enums";
import { createEnumKeyedRecord } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord } from "../Types/Record";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
export const Companies: Record<CompanyName, Company> = (() => { export const Companies: Record<CompanyName, Company> = (() => {
@ -11,6 +11,8 @@ export const Companies: Record<CompanyName, Company> = (() => {
return createEnumKeyedRecord(CompanyName, (name) => new Company(metadata[name])); return createEnumKeyedRecord(CompanyName, (name) => new Company(metadata[name]));
})(); })();
type SavegameCompany = { favor?: number; playerReputation?: number };
// Used to load Companies map from a save // Used to load Companies map from a save
export function loadCompanies(saveString: string): void { export function loadCompanies(saveString: string): void {
const loadedCompanies = JSON.parse(saveString, Reviver) as unknown; const loadedCompanies = JSON.parse(saveString, Reviver) as unknown;
@ -22,9 +24,21 @@ export function loadCompanies(saveString: string): void {
if (!loadedCompany) continue; if (!loadedCompany) continue;
if (typeof loadedCompany !== "object") continue; if (typeof loadedCompany !== "object") continue;
const company = Companies[loadedCompanyName]; const company = Companies[loadedCompanyName];
assertLoadingType<Company>(loadedCompany); assertLoadingType<SavegameCompany>(loadedCompany);
const { playerReputation: loadedRep, favor: loadedFavor } = loadedCompany; const { playerReputation: loadedRep, favor: loadedFavor } = loadedCompany;
if (typeof loadedRep === "number" && loadedRep > 0) company.playerReputation = loadedRep; if (typeof loadedRep === "number" && loadedRep > 0) company.playerReputation = loadedRep;
if (typeof loadedFavor === "number" && loadedFavor > 0) company.favor = loadedFavor; if (typeof loadedFavor === "number" && loadedFavor > 0) company.favor = loadedFavor;
} }
} }
// Most companies are usually at default values, so we'll only save the companies with non-default data
export function getCompaniesSave(): PartialRecord<CompanyName, SavegameCompany> {
const save: PartialRecord<CompanyName, SavegameCompany> = {};
for (const companyName of getEnumHelper("CompanyName").valueArray) {
const { favor, playerReputation } = Companies[companyName];
if (favor || playerReputation) {
save[companyName] = { favor: favor || undefined, playerReputation: playerReputation || undefined };
}
}
return save;
}

@ -3,8 +3,6 @@ import type { CompanyPosition } from "./CompanyPosition";
import { CompanyName, JobName, FactionName } from "@enums"; import { CompanyName, JobName, FactionName } from "@enums";
import { favorToRep, repToFavor } from "../Faction/formulas/favor"; import { favorToRep, repToFavor } from "../Faction/formulas/favor";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
export interface CompanyCtorParams { export interface CompanyCtorParams {
name: CompanyName; name: CompanyName;
info?: string; info?: string;
@ -41,8 +39,7 @@ export class Company {
playerReputation = 0; playerReputation = 0;
favor = 0; favor = 0;
constructor(p?: CompanyCtorParams) { constructor(p: CompanyCtorParams) {
if (!p) return;
this.name = p.name; this.name = p.name;
if (p.info) this.info = p.info; if (p.info) this.info = p.info;
p.companyPositions.forEach((jobName) => this.companyPositions.add(jobName)); p.companyPositions.forEach((jobName) => this.companyPositions.add(jobName));
@ -74,19 +71,4 @@ export class Company {
const newFavor = repToFavor(totalRep); const newFavor = repToFavor(totalRep);
return newFavor - this.favor; return newFavor - this.favor;
} }
/** Serialize the current object to a JSON save state. */
toJSON(): IReviverValue {
return Generic_toJSON("Company", this, Company.includedKeys);
}
/** Initializes a Company from a JSON save state. */
static fromJSON(value: IReviverValue): Company {
return Generic_fromJSON(Company, value.data, Company.includedKeys);
}
// Only these 2 keys are relevant to the save file
static includedKeys = ["favor", "playerReputation"] as const;
} }
constructorsForReviver.Company = Company;

@ -1,8 +1,6 @@
import { AugmentationName, FactionName, FactionDiscovery } from "@enums"; import { AugmentationName, FactionName, FactionDiscovery } from "@enums";
import { FactionInfo, FactionInfos } from "./FactionInfo"; import { FactionInfo, FactionInfos } from "./FactionInfo";
import { favorToRep, repToFavor } from "./formulas/favor"; import { favorToRep, repToFavor } from "./formulas/favor";
import { Generic_fromJSON, Generic_toJSON, IReviverValue, constructorsForReviver } from "../utils/JSONReviver";
import { getKeyList } from "../utils/helpers/getKeyList";
export class Faction { export class Faction {
/** /**
@ -32,7 +30,7 @@ export class Faction {
/** Amount of reputation player has with this faction */ /** Amount of reputation player has with this faction */
playerReputation = 0; playerReputation = 0;
constructor(name = FactionName.Sector12) { constructor(name: FactionName) {
this.name = name; this.name = name;
} }
@ -77,20 +75,4 @@ export class Faction {
const newFavor = repToFavor(totalRep); const newFavor = repToFavor(totalRep);
return newFavor - this.favor; return newFavor - this.favor;
} }
static savedKeys = getKeyList(Faction, {
removedKeys: ["augmentations", "name", "alreadyInvited", "isBanned", "isMember"],
});
/** Serialize the current object to a JSON save state. */
toJSON(): IReviverValue {
return Generic_toJSON("Faction", this, Faction.savedKeys);
}
/** Initializes a Faction object from a JSON save state. */
static fromJSON(value: IReviverValue): Faction {
return Generic_fromJSON(Faction, value.data, Faction.savedKeys);
}
} }
constructorsForReviver.Faction = Faction;

@ -4,7 +4,7 @@ import { FactionName, FactionDiscovery } from "@enums";
import { Faction } from "./Faction"; import { Faction } from "./Faction";
import { Reviver, assertLoadingType } from "../utils/JSONReviver"; import { Reviver, assertLoadingType } from "../utils/JSONReviver";
import { createEnumKeyedRecord, getRecordValues } from "../Types/Record"; import { PartialRecord, createEnumKeyedRecord, getRecordValues } from "../Types/Record";
import { Augmentations } from "../Augmentation/Augmentations"; import { Augmentations } from "../Augmentation/Augmentations";
import { getEnumHelper } from "../utils/EnumHelper"; import { getEnumHelper } from "../utils/EnumHelper";
@ -18,6 +18,8 @@ for (const aug of getRecordValues(Augmentations)) {
} }
} }
type SavegameFaction = { playerReputation?: number; favor?: number; discovery?: FactionDiscovery };
export function loadFactions(saveString: string, player: PlayerObject): void { export function loadFactions(saveString: string, player: PlayerObject): void {
const loadedFactions = JSON.parse(saveString, Reviver) as unknown; const loadedFactions = JSON.parse(saveString, Reviver) as unknown;
// This loading method allows invalid data in player save, but just ignores anything invalid // This loading method allows invalid data in player save, but just ignores anything invalid
@ -28,7 +30,7 @@ export function loadFactions(saveString: string, player: PlayerObject): void {
if (!loadedFaction) continue; if (!loadedFaction) continue;
const faction = Factions[loadedFactionName]; const faction = Factions[loadedFactionName];
if (typeof loadedFaction !== "object") continue; if (typeof loadedFaction !== "object") continue;
assertLoadingType<Faction>(loadedFaction); assertLoadingType<SavegameFaction>(loadedFaction);
const { playerReputation: loadedRep, favor: loadedFavor, discovery: loadedDiscovery } = loadedFaction; const { playerReputation: loadedRep, favor: loadedFavor, discovery: loadedDiscovery } = loadedFaction;
if (typeof loadedRep === "number" && loadedRep > 0) faction.playerReputation = loadedRep; if (typeof loadedRep === "number" && loadedRep > 0) faction.playerReputation = loadedRep;
if (typeof loadedFavor === "number" && loadedFavor > 0) faction.favor = loadedFavor; if (typeof loadedFavor === "number" && loadedFavor > 0) faction.favor = loadedFavor;
@ -56,3 +58,16 @@ export function loadFactions(saveString: string, player: PlayerObject): void {
Factions[invitedFaction].discovery = FactionDiscovery.known; Factions[invitedFaction].discovery = FactionDiscovery.known;
} }
} }
export function getFactionsSave(): PartialRecord<FactionName, SavegameFaction> {
const save: PartialRecord<FactionName, SavegameFaction> = {};
for (const factionName of getEnumHelper("FactionName").valueArray) {
const faction = Factions[factionName];
const discovery = faction.discovery === FactionDiscovery.unknown ? undefined : faction.discovery;
const { favor, playerReputation } = faction;
if (discovery || favor || playerReputation) {
save[factionName] = { favor: favor || undefined, playerReputation: playerReputation || undefined, discovery };
}
}
return save;
}

@ -1,9 +1,9 @@
import { Skills } from "@nsdefs"; import { Skills } from "@nsdefs";
import { loadAliases, loadGlobalAliases, Aliases, GlobalAliases } from "./Alias"; import { loadAliases, loadGlobalAliases, Aliases, GlobalAliases } from "./Alias";
import { Companies, loadCompanies } from "./Company/Companies"; import { getCompaniesSave, loadCompanies } from "./Company/Companies";
import { CONSTANTS } from "./Constants"; import { CONSTANTS } from "./Constants";
import { Factions, loadFactions } from "./Faction/Factions"; import { getFactionsSave, loadFactions } from "./Faction/Factions";
import { loadAllGangs, AllGangs } from "./Gang/AllGangs"; import { loadAllGangs, AllGangs } from "./Gang/AllGangs";
import { Player, setPlayer, loadPlayer } from "./Player"; import { Player, setPlayer, loadPlayer } from "./Player";
import { import {
@ -26,11 +26,10 @@ import { dialogBoxCreate } from "./ui/React/DialogBox";
import { Reviver, constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "./utils/JSONReviver"; import { Reviver, constructorsForReviver, Generic_toJSON, Generic_fromJSON, IReviverValue } from "./utils/JSONReviver";
import { save } from "./db"; import { save } from "./db";
import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak"; import { AwardNFG, v1APIBreak } from "./utils/v1APIBreak";
import { AugmentationName, FactionName, LocationName, ToastVariant } from "@enums"; import { AugmentationName, LocationName, ToastVariant } from "@enums";
import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation"; import { PlayerOwnedAugmentation } from "./Augmentation/PlayerOwnedAugmentation";
import { pushGameSaved } from "./Electron"; import { pushGameSaved } from "./Electron";
import { defaultMonacoTheme } from "./ScriptEditor/ui/themes"; import { defaultMonacoTheme } from "./ScriptEditor/ui/themes";
import { Faction } from "./Faction/Faction";
import { safelyCreateUniqueServer } from "./Server/ServerHelpers"; import { safelyCreateUniqueServer } from "./Server/ServerHelpers";
import { SpecialServers } from "./Server/data/SpecialServers"; import { SpecialServers } from "./Server/data/SpecialServers";
import { v2APIBreak } from "./utils/v2APIBreak"; import { v2APIBreak } from "./utils/v2APIBreak";
@ -98,8 +97,8 @@ class BitburnerSaveObject {
this.AllServersSave = saveAllServers(); this.AllServersSave = saveAllServers();
Settings.ExcludeRunningScriptsFromSave = originalExcludeSetting; Settings.ExcludeRunningScriptsFromSave = originalExcludeSetting;
this.CompaniesSave = JSON.stringify(Companies); this.CompaniesSave = JSON.stringify(getCompaniesSave());
this.FactionsSave = JSON.stringify(Factions); this.FactionsSave = JSON.stringify(getFactionsSave());
this.AliasesSave = JSON.stringify(Object.fromEntries(Aliases.entries())); this.AliasesSave = JSON.stringify(Object.fromEntries(Aliases.entries()));
this.GlobalAliasesSave = JSON.stringify(Object.fromEntries(GlobalAliases.entries())); this.GlobalAliasesSave = JSON.stringify(Object.fromEntries(GlobalAliases.entries()));
this.StockMarketSave = JSON.stringify(StockMarket); this.StockMarketSave = JSON.stringify(StockMarket);
@ -382,7 +381,6 @@ function evaluateVersionCompatibility(ver: string | number): void {
} }
//Fix contract names //Fix contract names
if (ver < 16) { if (ver < 16) {
Factions[FactionName.ShadowsOfAnarchy] = new Faction(FactionName.ShadowsOfAnarchy);
//Iterate over all contracts on all servers //Iterate over all contracts on all servers
for (const server of GetAllServers()) { for (const server of GetAllServers()) {
for (const contract of server.contracts) { for (const contract of server.contracts) {

@ -30,15 +30,13 @@ export function Reviver(_key: string, value: unknown): any {
if (!ctor) { if (!ctor) {
// Known missing constructors with special handling. // Known missing constructors with special handling.
switch (value.ctor) { switch (value.ctor) {
case "AllServersMap": case "AllServersMap": // Reviver removed in v0.43.1
console.warn("Converting AllServersMap for v0.43.1"); case "Industry": // No longer part of save data since v2.3.0
case "Employee": // Entire object removed from game in v2.2.0 (employees abstracted)
case "Company": // Reviver removed in v2.6.1
case "Faction": // Reviver removed in v2.6.1
console.warn(`Legacy load type ${value.ctor} converted to expected format while loading.`);
return value.data; return value.data;
case "Industry":
console.warn("Converting a corp from pre-2.3");
return value.data; // Will immediately be overwritten by v2.3 save migration code
case "Employee":
console.warn("Converting a corp from pre-2.2");
return value.data; // Will immediately be overwritten by v2.3 save migration code
} }
// Missing constructor with no special handling. Throw error. // Missing constructor with no special handling. Throw error.
throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`); throw new Error(`Could not locate constructor named ${value.ctor}. If the save data is valid, this is a bug.`);

@ -2,548 +2,26 @@
exports[`Check Save File Continuity CompaniesSave continuity 1`] = ` exports[`Check Save File Continuity CompaniesSave continuity 1`] = `
{ {
"AeroCorp": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Aevum Police Headquarters": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Alpha Enterprises": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Bachman & Associates": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Blade Industries": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Carmichael Security": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Central Intelligence Agency": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Clarke Incorporated": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"CompuTek": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"DefComm": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"DeltaOne": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"ECorp": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"FoodNStuff": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Four Sigma": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Fulcrum Technologies": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Galactic Cybersystems": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Global Pharmaceuticals": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Helios Labs": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Icarus Microsystems": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Joe's Guns": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"KuaiGong International": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"LexoCorp": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"MegaCorp": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"NWO": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"National Security Agency": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"NetLink Technologies": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Noodle Bar": { "Noodle Bar": {
"ctor": "Company", "favor": 100,
"data": { "playerReputation": 100000,
"favor": 100,
"playerReputation": 100000,
},
},
"Nova Medical": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Omega Software": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"OmniTek Incorporated": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Omnia Cybersystems": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Rho Construction": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Solaris Space Systems": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Storm Technologies": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"SysCore Securities": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Universal Energy": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"VitaLife": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
},
"Watchdog Security": {
"ctor": "Company",
"data": {
"favor": 0,
"playerReputation": 0,
},
}, },
} }
`; `;
exports[`Check Save File Continuity FactionsSave continuity 1`] = ` exports[`Check Save File Continuity FactionsSave continuity 1`] = `
{ {
"Aevum": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Bachman & Associates": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"BitRunners": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Blade Industries": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Bladeburners": { "Bladeburners": {
"ctor": "Faction", "discovery": "known",
"data": { "playerReputation": 4000,
"discovery": "known",
"favor": 0,
"playerReputation": 4000,
},
},
"Chongqing": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Church of the Machine God": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Clarke Incorporated": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
}, },
"CyberSec": { "CyberSec": {
"ctor": "Faction", "discovery": "known",
"data": { "favor": 20,
"discovery": "known", "playerReputation": 1000000,
"favor": 20,
"playerReputation": 1000000,
},
},
"Daedalus": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"ECorp": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Four Sigma": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Fulcrum Secret Technologies": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Illuminati": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Ishima": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"KuaiGong International": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"MegaCorp": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"NWO": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Netburners": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"New Tokyo": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"NiteSec": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"OmniTek Incorporated": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Sector-12": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Shadows of Anarchy": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Silhouette": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
}, },
"Slum Snakes": { "Slum Snakes": {
"ctor": "Faction", "discovery": "known",
"data": {
"discovery": "known",
"favor": 0,
"playerReputation": 0,
},
},
"Speakers for the Dead": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Tetrads": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"The Black Hand": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"The Covenant": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"The Dark Army": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"The Syndicate": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Tian Di Hui": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
},
"Volhaven": {
"ctor": "Faction",
"data": {
"discovery": "unknown",
"favor": 0,
"playerReputation": 0,
},
}, },
} }
`; `;