Add new core config entry fixProfileBreakingInventoryItemIssues, defaults to off

Attempts to fix common issues that happen to profile inventory items:
Duplicate items with the same _id value
Item Tag names with non alphanumeric characters
StackObjectsCount null values
This commit is contained in:
Dev 2023-11-28 11:06:08 +00:00
parent 382cf4c785
commit f9cf3242c8
3 changed files with 87 additions and 5 deletions

View File

@ -7,7 +7,8 @@
"sptFriendNickname": "SPT", "sptFriendNickname": "SPT",
"fixes": { "fixes": {
"fixShotgunDispersion": true, "fixShotgunDispersion": true,
"removeModItemsFromProfile": false "removeModItemsFromProfile": false,
"fixProfileBreakingInventoryItemIssues": false
}, },
"features": { "features": {
"autoInstallModDependencies": false "autoInstallModDependencies": false

View File

@ -8,7 +8,7 @@ import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper"; import { WeightedRandomHelper } from "@spt-aki/helpers/WeightedRandomHelper";
import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader"; import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader";
import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData"; import { IEmptyRequestData } from "@spt-aki/models/eft/common/IEmptyRequestData";
import { Exit, ILocationBase } from "@spt-aki/models/eft/common/ILocationBase"; import { ILocationBase } from "@spt-aki/models/eft/common/ILocationBase";
import { ILooseLoot } from "@spt-aki/models/eft/common/ILooseLoot"; import { ILooseLoot } from "@spt-aki/models/eft/common/ILooseLoot";
import { IPmcData } from "@spt-aki/models/eft/common/IPmcData"; import { IPmcData } from "@spt-aki/models/eft/common/IPmcData";
import { BodyPartHealth } from "@spt-aki/models/eft/common/tables/IBotBase"; import { BodyPartHealth } from "@spt-aki/models/eft/common/tables/IBotBase";
@ -19,7 +19,6 @@ import { IGameKeepAliveResponse } from "@spt-aki/models/eft/game/IGameKeepAliveR
import { IGetRaidTimeRequest } from "@spt-aki/models/eft/game/IGetRaidTimeRequest"; import { IGetRaidTimeRequest } from "@spt-aki/models/eft/game/IGetRaidTimeRequest";
import { ExtractChange, IGetRaidTimeResponse } from "@spt-aki/models/eft/game/IGetRaidTimeResponse"; import { ExtractChange, IGetRaidTimeResponse } from "@spt-aki/models/eft/game/IGetRaidTimeResponse";
import { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails"; import { IServerDetails } from "@spt-aki/models/eft/game/IServerDetails";
import { IGetRaidConfigurationRequestData } from "@spt-aki/models/eft/match/IGetRaidConfigurationRequestData";
import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IAkiProfile } from "@spt-aki/models/eft/profile/IAkiProfile";
import { AccountTypes } from "@spt-aki/models/enums/AccountTypes"; import { AccountTypes } from "@spt-aki/models/enums/AccountTypes";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes"; import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
@ -42,6 +41,7 @@ import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { OpenZoneService } from "@spt-aki/services/OpenZoneService"; import { OpenZoneService } from "@spt-aki/services/OpenZoneService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService"; import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService"; import { SeasonalEventService } from "@spt-aki/services/SeasonalEventService";
import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil"; import { RandomUtil } from "@spt-aki/utils/RandomUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil"; import { TimeUtil } from "@spt-aki/utils/TimeUtil";
@ -61,6 +61,7 @@ export class GameController
@inject("DatabaseServer") protected databaseServer: DatabaseServer, @inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil, @inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil, @inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("PreAkiModLoader") protected preAkiModLoader: PreAkiModLoader, @inject("PreAkiModLoader") protected preAkiModLoader: PreAkiModLoader,
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper, @inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("RandomUtil") protected randomUtil: RandomUtil, @inject("RandomUtil") protected randomUtil: RandomUtil,
@ -143,6 +144,11 @@ export class GameController
this.logger.debug(`Started game with sessionId: ${sessionID} ${pmcProfile.Info?.Nickname}`); this.logger.debug(`Started game with sessionId: ${sessionID} ${pmcProfile.Info?.Nickname}`);
if (this.coreConfig.fixes.fixProfileBreakingInventoryItemIssues)
{
this.fixProfileBreakingInventoryItemIssues(pmcProfile)
}
if (pmcProfile.Health) if (pmcProfile.Health)
{ {
this.updateProfileHealthValues(pmcProfile); this.updateProfileHealthValues(pmcProfile);
@ -242,6 +248,79 @@ export class GameController
} }
} }
/**
* Attempt to fix common item issues that corrupt profiles
* @param pmcProfile Profile to check items of
*/
protected fixProfileBreakingInventoryItemIssues(pmcProfile: IPmcData): void
{
// Create a mapping of all inventory items, keyed by _id value
const itemMapping = pmcProfile.Inventory.items.reduce((acc, curr) =>
{
acc[curr._id] = acc[curr._id] || [];
acc[curr._id].push(curr);
return acc;
}, {});
for (const key in itemMapping)
{
// Only one item for this id, not a dupe
if (itemMapping[key].length === 1)
{
continue;
}
this.logger.warning(`${itemMapping[key].length - 1} duplicate(s) found for item: ${key}`);
const itemAJson = this.jsonUtil.serialize(itemMapping[key][0]);
const itemBJson = this.jsonUtil.serialize(itemMapping[key][1]);
if (itemAJson === itemBJson)
{
// Both items match, we can safely delete one
const indexOfItemToRemove = pmcProfile.Inventory.items.findIndex(x => x._id === key);
pmcProfile.Inventory.items.splice(indexOfItemToRemove, 1);
this.logger.warning(`Deleted duplicate item: ${key}`);
}
else
{
// Items are different, replace ID with unique value
// Only replace ID if items have no children, we dont want orphaned children
const itemsHaveChildren = pmcProfile.Inventory.items.some(x => x.parentId === key);
if (!itemsHaveChildren)
{
const itemToAdjustId = pmcProfile.Inventory.items.find(x => x._id === key);
itemToAdjustId._id = this.hashUtil.generate();
this.logger.warning(`Replace duplicate item Id: ${key} with ${itemToAdjustId._id}`);
}
}
}
// Iterate over all inventory items
for (const item of pmcProfile.Inventory.items.filter(x => x.slotId))
{
if (!item.upd)
{
// Ignore items without a upd object
continue;
}
// Check items with a tag that contains non alphanumeric characters
const regxp = /[^a-zA-Z0-9 .]/g;
if (regxp.test(item.upd.Tag?.Name))
{
this.logger.warning(`Fixed item: ${item._id}s Tag value, removed invalid characters`);
item.upd.Tag.Name = item.upd.Tag.Name.replace(regxp, '');
}
// Check items with StackObjectsCount (null)
if (item.upd.StackObjectsCount === null)
{
this.logger.warning(`Fixed item: ${item._id}s null StackObjectsCount value, now set to 1`);
item.upd.StackObjectsCount = 1;
}
}
}
/** /**
* Out of date/incorrectly made trader mods forget this data * Out of date/incorrectly made trader mods forget this data
*/ */
@ -482,7 +561,7 @@ export class GameController
} }
/** /**
* singleplayer/settings/getRaidTime * Handle singleplayer/settings/getRaidTime
*/ */
public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse public getRaidTime(sessionId: string, request: IGetRaidTimeRequest): IGetRaidTimeResponse
{ {

View File

@ -23,6 +23,8 @@ export interface IGameFixes
fixShotgunDispersion: boolean; fixShotgunDispersion: boolean;
/** Remove items added by mods when the mod no longer exists - can fix dead profiles stuck at game load*/ /** Remove items added by mods when the mod no longer exists - can fix dead profiles stuck at game load*/
removeModItemsFromProfile: boolean; removeModItemsFromProfile: boolean;
/** Fix issues that cause the game to not start due to inventory item issues */
fixProfileBreakingInventoryItemIssues: boolean;
} }
export interface IServerFeatures export interface IServerFeatures