Merge branch 'master' of https://dev.sp-tarkov.com/SPT-AKI/Server into 3.8.0

# Conflicts:
#	project/src/callbacks/BundleCallbacks.ts
#	project/src/controllers/RepeatableQuestController.ts
#	project/src/helpers/InRaidHelper.ts
#	project/src/loaders/PreAkiModLoader.ts
This commit is contained in:
Dev 2023-12-15 14:16:15 +00:00
commit 268a9b8abd
29 changed files with 5500 additions and 5330 deletions

View File

@ -1,4 +1,4 @@
FROM node:14 as builder
FROM node:18-alpine as builder
WORKDIR /app
@ -8,6 +8,6 @@ RUN npm i
COPY . .
RUN npm run gen:docs
FROM nginx:latest
FROM nginx:stable-alpine3.17
COPY --from=builder /app/docs /usr/share/nginx/html

View File

@ -763,7 +763,9 @@
"64e74a2fc2b4f829615ec332",
"64e74a274d49d23b2c39d317",
"64f09c02b63b74469b6c149f",
"64f07f7726cfa02c506f8ac0"
"64f07f7726cfa02c506f8ac0",
"64f69b4267e11a7c6206e010",
"64f5b4f71a5f313cb144c06c"
],
"laboratory": [
"6398a4cfb5992f573c6562b3",

View File

@ -617,39 +617,39 @@
"gifterSettings": [{
"map": "bigmap",
"zones": "ZoneDormitory,ZoneGasStation,ZoneScavBase",
"spawnChance": 15
"spawnChance": 45
}, {
"map": "factory4_day",
"zones": "BotZone",
"spawnChance": 10
"spawnChance": 30
}, {
"map": "laboratory",
"zones": "BotZoneFloor1,BotZoneFloor2",
"spawnChance": 15
"spawnChance": 35
}, {
"map": "interchange",
"zones": "ZoneCenterBot,ZoneCenter,ZoneOLI,ZoneIDEA,ZoneGoshan,ZoneIDEAPark,ZoneOLIPark",
"spawnChance": 15
"spawnChance": 45
}, {
"map": "lighthouse",
"zones": "Zone_TreatmentContainers,Zone_Chalet,Zone_RoofContainers,Zone_RoofBeach,Zone_Hellicopter",
"spawnChance": 15
"spawnChance": 45
}, {
"map": "rezervbase",
"zones": "ZoneRailStrorage,ZoneRailStrorage,ZoneRailStrorage,ZonePTOR1,ZonePTOR2,ZoneBarrack,ZoneBarrack,ZoneBarrack,ZoneSubStorage",
"spawnChance": 15
"spawnChance": 45
}, {
"map": "shoreline",
"zones": "ZonePort,ZoneGreenHouses,ZoneSanatorium1,ZoneGreenHouses,ZoneSanatorium2,ZoneMeteoStation,ZoneSanatorium1,ZoneSanatorium2",
"spawnChance": 15
"spawnChance": 45
}, {
"map": "tarkovstreets",
"zones": "ZoneColumn",
"spawnChance": 10
"spawnChance": 40
}, {
"map": "woods",
"zones": "ZoneScavBase2,ZoneWoodCutter",
"spawnChance": 15
"spawnChance": 45
}
],
"events": [{

View File

@ -8777,8 +8777,7 @@
"mod_magazine": [
"55d4887d4bdc2d962f8b4570",
"5c05413a0db834001c390617",
"5aaa5dfee5b5b000140293d3",
"6241c2c2117ad530666a5108"
"5aaa5dfee5b5b000140293d3"
],
"mod_pistol_grip": [
"5c0e2ff6d174af02a1659d4a",

View File

@ -8774,8 +8774,7 @@
"mod_magazine": [
"55d4887d4bdc2d962f8b4570",
"5c05413a0db834001c390617",
"5aaa5dfee5b5b000140293d3",
"6241c2c2117ad530666a5108"
"5aaa5dfee5b5b000140293d3"
],
"mod_pistol_grip": [
"5c0e2ff6d174af02a1659d4a",

View File

@ -24,7 +24,8 @@ export class BundleCallbacks
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
}
public sendBundle(sessionID: string, req: any, resp: any, body: any): any
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public sendBundle(sessionID: string, req: any, resp: any, body: any): void
{
this.logger.info(`[BUNDLE]: ${req.url}`);
@ -38,12 +39,14 @@ export class BundleCallbacks
/**
* Handle singleplayer/bundles
*/
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getBundles(url: string, info: any, sessionID: string): string
{
const local = this.httpConfig.ip === "127.0.0.1" || this.httpConfig.ip === "localhost";
return this.httpResponse.noBody(this.bundleLoader.getBundles(local));
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public getBundle(url: string, info: any, sessionID: string): string
{
return "BUNDLE";

View File

@ -864,15 +864,21 @@ export class HideoutController
globals.config.SkillsSettings.HideoutManagement.SkillPointsPerCraft,
true,
);
// manager Crafting skill
// Manager Crafting skill
if (craftingExpAmount > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.CRAFTING, craftingExpAmount);
this.profileHelper.addSkillPointsToPlayer(
pmcData,
SkillTypes.INTELLECT,
0.5 * (Math.round(craftingExpAmount / 15)),
);
const intellectAmountToGive = 0.5 * (Math.round(craftingExpAmount / 15));
if (intellectAmountToGive > 0)
{
this.profileHelper.addSkillPointsToPlayer(
pmcData,
SkillTypes.INTELLECT,
intellectAmountToGive,
);
}
}
area.lastRecipe = request.recipeId;
counterHoursCrafting.value = hoursCrafting;

View File

@ -72,8 +72,8 @@ export class RepeatableQuestController
* }
*
* The method checks if the player level requirement for repeatable quests (e.g. daily lvl5, weekly lvl15) is met and if the previously active quests
* are still valid. This is checked by endTime persisted in profile according to the resetTime configured for each repeatable kind (daily, weekly)
* in QuestConfig.js
* are still valid. This ischecked by endTime persisted in profile accordning to the resetTime configured for each repeatable kind (daily, weekly)
* in QuestCondig.js
*
* If the condition is met, new repeatableQuests are created, old quests (which are persisted in the profile.RepeatableQuests[i].activeQuests) are
* moved to profile.RepeatableQuests[i].inactiveQuests. This memory is required to get rid of old repeatable quest data in the profile, otherwise
@ -81,8 +81,10 @@ export class RepeatableQuestController
* (if the are on "Succeed" but not "Completed" we keep them, to allow the player to complete them and get the rewards)
* The new quests generated are again persisted in profile.RepeatableQuests
*
* @param {string} sessionId Player's session id
* @returns {array} array of "repeatableQuestObjects" as described above
* @param {string} _info Request from client
* @param {string} sessionID Player's session id
*
* @returns {array} array of "repeatableQuestObjects" as descibed above
*/
public getClientRepeatableQuests(_info: IEmptyRequestData, sessionID: string): IPmcDataRepeatableQuest[]
{
@ -322,7 +324,7 @@ export class RepeatableQuestController
{
const possibleLocations = Object.keys(repeatableConfig.locations);
// Set possible locations for elimination task, if target is savage, exclude labs from locations
// Set possible locations for elimination task, ift arget is savage, exclude labs from locations
questPool.pool.Elimination.targets[probabilityObject.key] = (probabilityObject.key === "Savage")
? { locations: possibleLocations.filter((x) => x !== "laboratory") }
: { locations: possibleLocations };
@ -397,8 +399,7 @@ export class RepeatableQuestController
// Get cost to replace existing quest
changeRequirement = this.jsonUtil.clone(currentRepeatablePool.changeRequirement[changeRequest.qid]);
delete currentRepeatablePool.changeRequirement[changeRequest.qid];
// TODO: somehow we need to reduce the questPool by the currently active quests (for all repeatable)
// TODO: somehow we need to reduce the questPool by the currently active quests (for all repeatables)
const repeatableConfig = this.questConfig.repeatableQuests.find((x) =>
x.name === currentRepeatablePool.name
@ -492,6 +493,7 @@ export class RepeatableQuestController
break;
}
}
return newRepeatableQuest;
}
}

View File

@ -78,6 +78,7 @@ export class LocationGenerator
staticAmmoDist: Record<string, IStaticAmmoDetails[]>,
): SpawnpointTemplate[]
{
let staticLootItemCount = 0;
const result: SpawnpointTemplate[] = [];
const locationId = locationBase.Id.toLowerCase();
@ -127,11 +128,13 @@ export class LocationGenerator
locationId,
);
result.push(containerWithLoot.template);
staticLootItemCount += containerWithLoot.template.Items.length;
}
this.logger.success(`Added ${guaranteedContainers.length} guaranteed containers`);
this.logger.debug(`Added ${guaranteedContainers.length} guaranteed containers`);
// randomisation is turned off globally or just turned off for this map
// Randomisation is turned off globally or just turned off for this map
if (
!(this.locationConfig.containerRandomisationSettings.enabled
&& this.locationConfig.containerRandomisationSettings.maps[locationId])
@ -150,8 +153,12 @@ export class LocationGenerator
locationId,
);
result.push(containerWithLoot.template);
staticLootItemCount += containerWithLoot.template.Items.length;
}
this.logger.success(`A total of ${staticLootItemCount} static items spawned`);
return result;
}
@ -230,9 +237,14 @@ export class LocationGenerator
);
result.push(containerWithLoot.template);
staticContainerCount++;
staticLootItemCount += containerWithLoot.template.Items.length;
}
}
this.logger.success(`A total of ${staticLootItemCount} static items spawned`);
this.logger.success(
this.localisationService.getText("location-containers_generated_success", staticContainerCount),
);

View File

@ -307,6 +307,8 @@ export class BotGeneratorHelper
slot: equipmentSlot,
}),
);
return { incompatible: true, reason: `item: ${tplToCheck} does not exist in the database` };
}
if (!itemToCheck._props)
@ -318,6 +320,8 @@ export class BotGeneratorHelper
slot: equipmentSlot,
}),
);
return { incompatible: true, reason: `item: ${tplToCheck} does not have a _props field` };
}
// Does an equipped item have a property that blocks the desired item - check for prop "BlocksX" .e.g BlocksEarpiece / BlocksFaceCover

View File

@ -21,6 +21,7 @@ import { SaveServer } from "@spt-aki/servers/SaveServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { ProfileFixerService } from "@spt-aki/services/ProfileFixerService";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { TimeUtil } from "@spt-aki/utils/TimeUtil";
import { ProfileHelper } from "./ProfileHelper";
@injectable()
@ -31,6 +32,7 @@ export class InRaidHelper
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("SaveServer") protected saveServer: SaveServer,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("ItemHelper") protected itemHelper: ItemHelper,
@ -129,7 +131,6 @@ export class InRaidHelper
* Reset a profile to a baseline, used post-raid
* Reset points earned during session property
* Increment exp
* Remove Labs keycard
* @param profileData Profile to update
* @param saveProgressRequest post raid save data request data
* @param sessionID Session id
@ -218,7 +219,7 @@ export class InRaidHelper
public updatePmcProfileDataPostRaid(pmcData: IPmcData, saveProgressRequest: ISaveProgressRequestData, sessionId: string): void
{
// Process failed quests then copy everything
this.processFailedQuests(sessionId, pmcData, pmcData.Quests, saveProgressRequest.profile);
this.processAlteredQuests(sessionId, pmcData, pmcData.Quests, saveProgressRequest.profile);
pmcData.Quests = saveProgressRequest.profile.Quests;
// No need to do this for scav, old scav is deleted and new one generated
@ -250,19 +251,20 @@ export class InRaidHelper
}
/**
* Look for quests with status = fail that were not failed pre-raid and run the failQuest() function
* Look for quests with a status different from what it began the raid with
* @param sessionId Player id
* @param pmcData Player profile
* @param preRaidQuests Quests prior to starting raid
* @param postRaidProfile Profile sent by client
* @param postRaidProfile Profile sent by client with post-raid quests
*/
protected processFailedQuests(
protected processAlteredQuests(
sessionId: string,
pmcData: IPmcData,
preRaidQuests: IQuestStatus[],
postRaidProfile: IPostRaidPmcData,
): void
{
// TODO: this may break when locked quests are added to profile but player has completed no quests prior to raid
if (!preRaidQuests)
{
// No quests to compare against, skip
@ -270,29 +272,45 @@ export class InRaidHelper
}
// Loop over all quests from post-raid profile
const newLockedQuests: IQuestStatus[] = [];
for (const postRaidQuest of postRaidProfile.Quests)
{
// Find matching pre-raid quest + not already failed
// postRaidQuest.status has a weird value, need to do some nasty casting to compare it
const postRaidQuestStatus = <string><unknown>postRaidQuest.status;
// Find matching pre-raid quest
const preRaidQuest = preRaidQuests?.find((x) => x.qid === postRaidQuest.qid);
if (!preRaidQuest)
{
// Some traders gives locked quests (LightKeeper) due to time-gating
if (postRaidQuestStatus === "Locked")
{
// Store new locked quest for future processing
newLockedQuests.push(postRaidQuest);
}
continue;
}
// Already failed before raid, skip
if (preRaidQuest.status === QuestStatus.Fail)
{
continue;
}
if (preRaidQuest.status === QuestStatus.Success)
// Already completed/failed before raid, skip
if ([QuestStatus.Fail, QuestStatus.Success].includes(preRaidQuest.status) )
{
continue;
}
// Quest failed inside raid, need to handle
// postRaidQuest.status has a weird value, need to do some nasty casting to compare it
const postRaidQuestStatus = <string><unknown>postRaidQuest.status;
// Quest with time-gate has unlocked
if (postRaidQuestStatus === "AvailableAfter" && postRaidQuest.availableAfter <= this.timeUtil.getTimestamp())
{
// Flag as ready to complete
postRaidQuest.status = QuestStatus.AvailableForStart;
postRaidQuest.statusTimers[QuestStatus.AvailableForStart] = this.timeUtil.getTimestamp();
this.logger.debug(`Time-locked quest ${postRaidQuest.qid} is now ready to start`);
continue;
}
// Quest failed inside raid
if (postRaidQuestStatus === "Fail")
{
// Send failed message
@ -347,6 +365,34 @@ export class InRaidHelper
postRaidQuest.completedConditions = [];
}
}
// Reclassify time-gated quests as time gated until a specific date
if (newLockedQuests.length > 0)
{
for (const lockedQuest of newLockedQuests)
{
// Get the quest from Db
const dbQuest = this.questHelper.getQuestFromDb(lockedQuest.qid, null);
if (!dbQuest)
{
this.logger.warning(`Unable to adjust locked quest: ${lockedQuest.qid} as it wasnt found in db. It may not become available later on`);
continue;
}
// Find the time requirement in AvailableForStart array (assuming there is one as quest in locked state === its time-gated)
const afsRequirement = dbQuest.conditions.AvailableForStart.find(x => x._parent === "Quest");
if (afsRequirement && afsRequirement._props.availableAfter > 0)
{
// Prereq quest has a wait
// Set quest as AvailableAfter and set timer
const timestamp = this.timeUtil.getTimestamp() + afsRequirement._props.availableAfter;
lockedQuest.availableAfter = timestamp;
lockedQuest.statusTimers.AvailableAfter = timestamp;
lockedQuest.status = 9;
}
}
}
}
/**
@ -447,10 +493,10 @@ export class InRaidHelper
&& this.itemHelper.itemIsInsideContainer(x, "SecuredContainer", postRaidProfile.Inventory.items));
});
for (const item of itemsToRemovePropertyFrom)
itemsToRemovePropertyFrom.forEach((item) =>
{
delete item.upd.SpawnedInSession;
}
});
return postRaidProfile;
}
@ -493,10 +539,11 @@ export class InRaidHelper
{
// Get inventory item ids to remove from players profile
const itemIdsToDeleteFromProfile = this.getInventoryItemsLostOnDeath(pmcData).map((x) => x._id);
for (const itemId of itemIdsToDeleteFromProfile)
itemIdsToDeleteFromProfile.forEach((x) =>
{
this.inventoryHelper.removeItem(pmcData, itemId, sessionID);
}
// Items inside containers are handed as part of function
this.inventoryHelper.removeItem(pmcData, x, sessionID);
});
// Remove contents of fast panel
pmcData.Inventory.fastPanel = {};
@ -510,25 +557,25 @@ export class InRaidHelper
protected getInventoryItemsLostOnDeath(pmcProfile: IPmcData): Item[]
{
const inventoryItems = pmcProfile.Inventory.items ?? [];
const equipment = pmcProfile?.Inventory?.equipment;
const questRaidItems = pmcProfile?.Inventory?.questRaidItems;
const equipmentRootId = pmcProfile?.Inventory?.equipment;
const questRaidItemContainerId = pmcProfile?.Inventory?.questRaidItems;
return inventoryItems.filter((x) =>
return inventoryItems.filter((item) =>
{
// Keep items flagged as kept after death
if (this.isItemKeptAfterDeath(pmcProfile, x))
if (this.isItemKeptAfterDeath(pmcProfile, item))
{
return false;
}
// Remove normal items or quest raid items
if (x.parentId === equipment || x.parentId === questRaidItems)
if (item.parentId === equipmentRootId || item.parentId === questRaidItemContainerId)
{
return true;
}
// Pocket items are not lost on death
if (x.slotId.startsWith("pocket"))
// Pocket items are lost on death
if (item.slotId.startsWith("pocket"))
{
return true;
}
@ -563,7 +610,7 @@ export class InRaidHelper
*/
protected isItemKeptAfterDeath(pmcData: IPmcData, itemToCheck: Item): boolean
{
// No parentid means its a base inventory item, always keep
// No parentId = base inventory item, always keep
if (!itemToCheck.parentId)
{
return true;
@ -583,7 +630,7 @@ export class InRaidHelper
}
// Is quest item + quest item not lost on death
if (!this.lostOnDeathConfig.questItems && itemToCheck.parentId === pmcData.Inventory.questRaidItems)
if (itemToCheck.parentId === pmcData.Inventory.questRaidItems && !this.lostOnDeathConfig.questItems)
{
return true;
}
@ -623,6 +670,9 @@ export class InRaidHelper
"pocket2",
"pocket3",
"pocket4",
"SpecialSlot1",
"SpecialSlot2",
"SpecialSlot3",
];
let inventoryItems: Item[] = [];

View File

@ -402,7 +402,7 @@ export class ProfileHelper
{
if (!pointsToAdd || pointsToAdd < 0)
{
this.logger.error(
this.logger.warning(
this.localisationService.getText("player-attempt_to_increment_skill_with_negative_value", skill),
);
return;
@ -432,6 +432,7 @@ export class ProfileHelper
}
profileSkill.Progress += pointsToAdd;
profileSkill.Progress = Math.min(profileSkill.Progress, 5100); // Prevent skill from ever going above level 51 (5100)
profileSkill.LastAccess = this.timeUtil.getTimestamp();
}

View File

@ -6,7 +6,6 @@ import { TraderAssortHelper } from "@spt-aki/helpers/TraderAssortHelper";
import { UtilityHelper } from "@spt-aki/helpers/UtilityHelper";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader";
import { IGetOffersResult } from "@spt-aki/models/eft/ragfair/IGetOffersResult";
import { ISearchRequestData } from "@spt-aki/models/eft/ragfair/ISearchRequestData";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { Money } from "@spt-aki/models/enums/Money";
@ -59,27 +58,27 @@ export class RagfairHelper
}
}
public filterCategories(sessionID: string, info: ISearchRequestData): string[]
public filterCategories(sessionID: string, request: ISearchRequestData): string[]
{
let result: string[] = [];
// Case: weapon builds
if (info.buildCount)
if (request.buildCount)
{
return Object.keys(info.buildItems);
return Object.keys(request.buildItems);
}
// Case: search
if (info.linkedSearchId)
if (request.linkedSearchId)
{
const data = this.ragfairLinkedItemService.getLinkedItems(info.linkedSearchId);
const data = this.ragfairLinkedItemService.getLinkedItems(request.linkedSearchId);
result = !data ? [] : [...data];
}
// Case: category
if (info.handbookId)
if (request.handbookId)
{
const handbook = this.getCategoryList(info.handbookId);
const handbook = this.getCategoryList(request.handbookId);
if (result.length)
{
@ -182,6 +181,12 @@ export class RagfairHelper
return [...[rootItem], ...list];
}
/**
* Return the symbol for a currency
* e.g. 5449016a4bdc2d6f028b456f return
* @param currencyTpl currency to get symbol for
* @returns symbol of currency
*/
public getCurrencySymbol(currencyTpl: string): string
{
switch (currencyTpl)

View File

@ -354,6 +354,7 @@ export class RagfairOfferHelper
const profileRagfairInfo = this.saveServer.getProfile(sessionID).characters.pmc.RagfairInfo;
const index = profileRagfairInfo.offers.findIndex((o) => o._id === offerId);
profileRagfairInfo.offers.splice(index, 1);
// Also delete from ragfair
this.ragfairOfferService.removeOfferById(offerId);
}
@ -436,15 +437,14 @@ export class RagfairOfferHelper
};
const stacks = this.itemHelper.splitStack(requestedItem);
for (const item of stacks)
{
const outItems = [item];
// TODO - is this code used?, may have been when adding barters to flea was still possible for player
if (requirement.onlyFunctional)
{
const presetItems = this.ragfairServerHelper.getPresetItemsByTpl(item);
if (presetItems.length)
{
outItems.push(presetItems[0]);

View File

@ -13,6 +13,7 @@ import { MessageType } from "@spt-aki/models/enums/MessageType";
import { Traders } from "@spt-aki/models/enums/Traders";
import { IQuestConfig } from "@spt-aki/models/spt/config/IQuestConfig";
import { IRagfairConfig } from "@spt-aki/models/spt/config/IRagfairConfig";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { SaveServer } from "@spt-aki/servers/SaveServer";
@ -35,6 +36,7 @@ export class RagfairServerHelper
protected static goodsReturnedTemplate = "5bdabfe486f7743e1665df6e 0"; // Your item was not sold
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("RandomUtil") protected randomUtil: RandomUtil,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@ -236,6 +238,11 @@ export class RagfairServerHelper
return MemberCategory.DEFAULT;
}
/**
* Get a player or traders nickname from their profile by their user id
* @param userID Sessionid/userid
* @returns Nickname of individual
*/
public getNickname(userID: string): string
{
if (this.isPlayer(userID))
@ -257,16 +264,25 @@ export class RagfairServerHelper
return (name.length > 15) ? this.getNickname(userID) : name;
}
public getPresetItems(item: any): Item[]
/**
* Given a preset id from globals.json, return an array of items[] with unique ids
* @param item Preset item
* @returns Array of weapon and its children
*/
public getPresetItems(item: Item): Item[]
{
const preset = this.jsonUtil.clone(this.databaseServer.getTables().globals.ItemPresets[item._id]._items);
return this.reparentPresets(item, preset);
}
/**
* Possible bug, returns all items associated with an items tpl, could be multiple presets from globals.json
* @param item Preset item
* @returns
*/
public getPresetItemsByTpl(item: Item): Item[]
{
const presets = [];
for (const itemId in this.databaseServer.getTables().globals.ItemPresets)
{
if (this.databaseServer.getTables().globals.ItemPresets[itemId]._items[0]._tpl === item._tpl)
@ -282,17 +298,17 @@ export class RagfairServerHelper
}
/**
* Generate new unique ids for the children while preserving hierarchy
* @param item base item
* @param preset
* Generate new unique ids for child items while preserving hierarchy
* @param rootItem Base/primary item of preset
* @param preset Primary item + children of primary item
* @returns Item array with new IDs
*/
public reparentPresets(item: Item, preset: Item[]): Item[]
public reparentPresets(rootItem: Item, preset: Item[]): Item[]
{
const oldRootId = preset[0]._id;
const idMappings = {};
idMappings[oldRootId] = item._id;
idMappings[oldRootId] = rootItem._id;
for (const mod of preset)
{
@ -314,8 +330,13 @@ export class RagfairServerHelper
}
}
// force item's details into first location of presetItems
preset[0] = item;
// Force item's details into first location of presetItems
if (preset[0]._tpl !== rootItem._tpl)
{
this.logger.warning(`Reassigning root item from ${preset[0]._tpl} to ${rootItem._tpl}`);
}
preset[0] = rootItem;
return preset;
}

View File

@ -48,6 +48,7 @@ export class RepairHelper
): void
{
this.logger.debug(`Adding ${amountToRepair} to ${itemToRepairDetails._name} using kit: ${useRepairKit}`);
const itemMaxDurability = this.jsonUtil.clone(itemToRepair.upd.Repairable.MaxDurability);
const itemCurrentDurability = this.jsonUtil.clone(itemToRepair.upd.Repairable.Durability);
const itemCurrentMaxDurability = this.jsonUtil.clone(itemToRepair.upd.Repairable.MaxDurability);
@ -105,6 +106,14 @@ export class RepairHelper
}
}
/**
* Repairing armor reduces the total durability value slightly, get a randomised (to 2dp) amount based on armor material
* @param armorMaterial What material is the armor being repaired made of
* @param isRepairKit Was a repair kit used
* @param armorMax Max amount of durability item can have
* @param traderQualityMultipler Different traders produce different loss values
* @returns Amount to reduce max durability by
*/
protected getRandomisedArmorRepairDegradationValue(
armorMaterial: string,
isRepairKit: boolean,
@ -112,6 +121,7 @@ export class RepairHelper
traderQualityMultipler: number,
): number
{
// Degradation value is based on the armor material
const armorMaterialSettings = this.databaseServer.getTables().globals.config.ArmorMaterials[armorMaterial];
const minMultiplier = isRepairKit
@ -128,6 +138,14 @@ export class RepairHelper
return Number(duraLossMultipliedByTraderMultiplier.toFixed(2));
}
/**
* Repairing weapons reduces the total durability value slightly, get a randomised (to 2dp) amount
* @param itemProps Weapon properties
* @param isRepairKit Was a repair kit used
* @param weaponMax ax amount of durability item can have
* @param traderQualityMultipler Different traders produce different loss values
* @returns Amount to reduce max durability by
*/
protected getRandomisedWeaponRepairDegradationValue(
itemProps: Props,
isRepairKit: boolean,
@ -149,19 +167,4 @@ export class RepairHelper
return Number(duraLossMultipliedByTraderMultiplier.toFixed(2));
}
/**
* Is the supplied tpl a weapon
* @param tpl tplId to check is a weapon
* @returns true if tpl is a weapon
*/
public isWeaponTemplate(tpl: string): boolean
{
const itemTemplates = this.databaseServer.getTables().templates.items;
const baseItem = itemTemplates[tpl];
const baseNode = itemTemplates[baseItem._parent];
const parentNode = itemTemplates[baseNode._parent];
return parentNode._id === BaseClasses.WEAPON;
}
}

View File

@ -17,6 +17,11 @@ export class SecureContainerHelper
constructor(@inject("ItemHelper") protected itemHelper: ItemHelper)
{}
/**
* Get an array of the item IDs (NOT tpls) inside a secure container
* @param items Inventory items to look for secure container in
* @returns Array of ids
*/
public getSecureContainerItems(items: Item[]): string[]
{
const secureContainer = items.find((x) => x.slotId === "SecuredContainer");

View File

@ -120,7 +120,7 @@ export class TradeHelper
this.incrementAssortBuyCount(itemPurchased, buyRequestData.count);
}
this.logger.debug(`Bought item: ${buyRequestData.item_id} from ${buyRequestData.tid}`);
this.logger.debug(`Bought item: ${buyRequestData.item_id} from: ${Traders[buyRequestData.tid]}`);
};
return this.inventoryHelper.addItem(pmcData, newReq, output, sessionID, callback, foundInRaid, upd);
@ -184,12 +184,18 @@ export class TradeHelper
}
}
/**
* Traders allow a limited number of purchases per refresh cycle (default 60 mins)
* @param assortBeingPurchased the item from trader being bought
* @param assortId Id of assort being purchased
* @param count How many are being bought
*/
protected checkPurchaseIsWithinTraderItemLimit(assortBeingPurchased: Item, assortId: string, count: number): void
{
if ((assortBeingPurchased.upd.BuyRestrictionCurrent + count) > assortBeingPurchased.upd?.BuyRestrictionMax)
{
throw new Error(
`Unable to purchase ${count} items, this would exceed your purchase limit of ${assortBeingPurchased.upd.BuyRestrictionMax} from the trader this refresh`,
`Unable to purchase ${count} items, this would exceed your purchase limit of ${assortBeingPurchased.upd.BuyRestrictionMax} from the traders assort: ${assortId} this refresh`,
);
}
}

View File

@ -149,6 +149,12 @@ export class TraderHelper
}
}
/**
* Get the starting standing of a trader based on the current profiles type (e.g. EoD, Standard etc)
* @param traderId Trader id to get standing for
* @param rawProfileTemplate Raw profile from profiles.json to look up standing from
* @returns Standing value
*/
protected getStartingStanding(traderId: string, rawProfileTemplate: ProfileTraderTemplate): number
{
// Edge case for Lightkeeper, 0 standing means seeing `Make Amends - Buyout` quest

View File

@ -18,6 +18,11 @@ export class WeightedRandomHelper
return chosenItem.item;
}
/**
* Choos an item from the passed in array based on the weightings of each
* @param itemArray Items and weights to use
* @returns Chosen item from array
*/
public getWeightedValue<T>(itemArray: { [key: string]: unknown; } | ArrayLike<unknown>): T
{
const itemKeys = Object.keys(itemArray);

View File

@ -146,10 +146,10 @@ export class PreAkiModLoader implements IModLoader
const modOrder = this.vfs.readFile(this.modOrderPath, { encoding: "utf8" });
try
{
for (const [index, mod] of (this.jsonUtil.deserialize<any>(modOrder).order as string[]).entries())
this.jsonUtil.deserialize<any>(modOrder, this.modOrderPath).order.forEach((mod: string, index: number) =>
{
this.order[mod] = index;
}
});
}
catch (error)
{
@ -213,10 +213,11 @@ export class PreAkiModLoader implements IModLoader
validMods.sort((prev, next) => this.sortMods(prev, next, missingFromOrderJSON));
// log the missing mods from order.json
for (const missingMod of Object.keys(missingFromOrderJSON))
{
this.logger.debug(this.localisationService.getText("modloader-mod_order_missing_from_json", missingMod));
}
Object.keys(missingFromOrderJSON).forEach((
missingMod,
) => (this.logger.debug(
this.localisationService.getText("modloader-mod_order_missing_from_json", missingMod),
)));
// add mods
for (const mod of validMods)
@ -428,9 +429,10 @@ export class PreAkiModLoader implements IModLoader
public sortModsLoadOrder(): string[]
{
// if loadorder.json exists: load it, otherwise generate load order
if (this.vfs.exists(`${this.basepath}loadorder.json`))
const loadOrderPath = `${this.basepath}loadorder.json`
if (this.vfs.exists(loadOrderPath))
{
return this.jsonUtil.deserialize(this.vfs.readFile(`${this.basepath}loadorder.json`));
return this.jsonUtil.deserialize(this.vfs.readFile(loadOrderPath), loadOrderPath);
}
else
{
@ -660,14 +662,15 @@ export class PreAkiModLoader implements IModLoader
}
// Check if config exists
if (!this.vfs.exists(`${modPath}/package.json`))
const modPackagePath = `${modPath}/package.json`;
if (!this.vfs.exists(modPackagePath))
{
this.logger.error(this.localisationService.getText("modloader-missing_package_json", modName));
return false;
}
// Validate mod
const config = this.jsonUtil.deserialize<IPackageJsonData>(this.vfs.readFile(`${modPath}/package.json`));
const config = this.jsonUtil.deserialize<IPackageJsonData>(this.vfs.readFile(modPackagePath), modPackagePath);
const checks = ["name", "author", "version", "license"];
let issue = false;

View File

@ -196,14 +196,14 @@ export class EventOutputHolder
for (const productionKey in productions)
{
const production = productions[productionKey];
if (production.sptIsComplete && production.sptIsContinuous)
if (production?.sptIsComplete && production?.sptIsContinuous)
{
// Water collector / Bitcoin etc
production.sptIsComplete = false;
production.Progress = 0;
production.StartTimestamp = this.timeUtil.getTimestamp();
}
else if (!production.inProgress)
else if (!production?.inProgress)
{
// Normal completed craft, delete
delete productions[productionKey];

View File

@ -11,6 +11,7 @@ import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { LocalisationService } from "@spt-aki/services/LocalisationService";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { RandomUtil } from "@spt-aki/utils/RandomUtil";
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
@injectable()
export class WebSocketServer
@ -22,6 +23,7 @@ export class WebSocketServer
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("JsonUtil") protected jsonUtil: JsonUtil,
@inject("HttpServerHelper") protected httpServerHelper: HttpServerHelper,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
)
{
this.httpConfig = this.configServer.getConfig(ConfigTypes.HTTP);
@ -92,11 +94,13 @@ export class WebSocketServer
// Strip request and break it into sections
const splitUrl = req.url.substring(0, req.url.indexOf("?")).split("/");
const sessionID = splitUrl.pop();
const playerProfile = this.profileHelper.getFullProfile(sessionID);
const playerInfoText = `${playerProfile.info.username} (${sessionID})`;
this.logger.info(this.localisationService.getText("websocket-player_connected", sessionID));
this.logger.info(this.localisationService.getText("websocket-player_connected", playerInfoText));
const logger = this.logger;
const msgToLog = this.localisationService.getText("websocket-received_message", sessionID);
const msgToLog = this.localisationService.getText("websocket-received_message", playerInfoText);
ws.on("message", function message(msg)
{
logger.info(`${msgToLog} ${msg}`);

View File

@ -27,7 +27,7 @@ export class HashCacheService
// get mod hash file
if (!this.modHashes)
{ // empty
this.modHashes = this.jsonUtil.deserialize(this.vfs.readFile(`${this.modCachePath}`));
this.modHashes = this.jsonUtil.deserialize(this.vfs.readFile(`${this.modCachePath}`), this.modCachePath);
}
}

View File

@ -524,7 +524,7 @@ export class ProfileFixerService
{
// Old profiles had quests with a bad status of 0 (invalid) added to profile, remove them
// E.g. compensation for damage showing before standing check was added to getClientQuests()
if (quest.status === 0 && !isDevProfile)
if (quest.status === 0 && quest.availableAfter === 0 && !isDevProfile)
{
questsToDelete.push(quest);

View File

@ -129,6 +129,7 @@ export class RaidTimeAdjustmentService
const chosenRaidReductionPercent = Number.parseInt(this.weightedRandomHelper.getWeightedValue<string>(
mapSettings.reductionPercentWeights,
));
const raidTimeRemainingPercent = 100 - chosenRaidReductionPercent;
// How many minutes raid will last
const newRaidTimeMinutes = Math.floor(this.randomUtil.reduceValueByPercent(baseEscapeTimeMinutes, chosenRaidReductionPercent));
@ -141,8 +142,8 @@ export class RaidTimeAdjustmentService
// Store time reduction percent in app context so loot gen can pick it up later
this.applicationContext.addValue(ContextVariableType.RAID_ADJUSTMENTS,
{
dynamicLootPercent: Math.max(chosenRaidReductionPercent, mapSettings.minDynamicLootPercent),
staticLootPercent: Math.max(chosenRaidReductionPercent, mapSettings.minStaticLootPercent),
dynamicLootPercent: Math.max(raidTimeRemainingPercent, mapSettings.minDynamicLootPercent),
staticLootPercent: Math.max(raidTimeRemainingPercent, mapSettings.minStaticLootPercent),
simulatedRaidStartSeconds: simulatedRaidStartTimeMinutes * 60
});
}

View File

@ -141,6 +141,7 @@ export class RepairService
*/
public addRepairSkillPoints(sessionId: string, repairDetails: RepairDetails, pmcData: IPmcData): void
{
// Handle kit repair of weapon
if (
repairDetails.repairedByKit
&& this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON)
@ -148,10 +149,13 @@ export class RepairService
{
const skillPoints = this.getWeaponRepairSkillPoints(repairDetails);
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.WEAPON_TREATMENT, skillPoints, true);
if (skillPoints > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.WEAPON_TREATMENT, skillPoints, true);
}
}
// Handle kit repairs of armor
// Handle kit repair of armor
if (
repairDetails.repairedByKit
&& this.itemHelper.isOfBaseclasses(repairDetails.repairedItem._tpl, [BaseClasses.ARMOR, BaseClasses.VEST])
@ -180,16 +184,25 @@ export class RepairService
}
// Handle giving INT to player - differs if using kit/trader and weapon vs armor
let intellectGainedFromRepair: number;
const intellectGainedFromRepair = this.getIntellectGainedFromRepair(repairDetails);
if (intellectGainedFromRepair > 0)
{
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair);
}
}
protected getIntellectGainedFromRepair(repairDetails: RepairDetails): number
{
if (repairDetails.repairedByKit)
{
// Weapons/armor have different multipliers
const intRepairMultiplier =
(this.itemHelper.isOfBaseclass(repairDetails.repairedItem._tpl, BaseClasses.WEAPON))
? this.repairConfig.repairKitIntellectGainMultiplier.weapon
: this.repairConfig.repairKitIntellectGainMultiplier.armor;
// limit gain to a max value defined in config.maxIntellectGainPerRepair
intellectGainedFromRepair = Math.min(
// Limit gain to a max value defined in config.maxIntellectGainPerRepair
return Math.min(
repairDetails.repairPoints * intRepairMultiplier,
this.repairConfig.maxIntellectGainPerRepair.kit,
);
@ -197,13 +210,11 @@ export class RepairService
else
{
// Trader repair - Not as accurate as kit, needs data from live
intellectGainedFromRepair = Math.min(
return Math.min(
repairDetails.repairAmount / 10,
this.repairConfig.maxIntellectGainPerRepair.trader,
);
}
this.profileHelper.addSkillPointsToPlayer(pmcData, SkillTypes.INTELLECT, intellectGainedFromRepair);
}
/**

View File

@ -301,6 +301,8 @@ export class SeasonalEventService
*/
protected updateGlobalEvents(sessionId: string, globalConfig: IConfig, eventType: SeasonalEventType): void
{
this.logger.success(`${eventType} event is active`);
switch (eventType.toLowerCase())
{
case SeasonalEventType.HALLOWEEN.toLowerCase():