Server/project/src/helpers/HideoutHelper.ts
chomp e7a44330fa Fix: Additional change for wall upgrading (!31)
Co-authored-by: Dev <dev@noreply.dev.sp-tarkov.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/31
2023-03-06 12:23:14 +00:00

993 lines
40 KiB
TypeScript

import { inject, injectable } from "tsyringe";
import { IPmcData } from "../models/eft/common/IPmcData";
import {
Common, HideoutArea, IHideoutImprovement, Production, Productive
} from "../models/eft/common/tables/IBotBase";
import { Upd } from "../models/eft/common/tables/IItem";
import { StageBonus } from "../models/eft/hideout/IHideoutArea";
import {
IHideoutContinuousProductionStartRequestData
} from "../models/eft/hideout/IHideoutContinuousProductionStartRequestData";
import { IHideoutProduction } from "../models/eft/hideout/IHideoutProduction";
import {
IHideoutSingleProductionStartRequestData
} from "../models/eft/hideout/IHideoutSingleProductionStartRequestData";
import {
IHideoutTakeProductionRequestData
} from "../models/eft/hideout/IHideoutTakeProductionRequestData";
import { IItemEventRouterResponse } from "../models/eft/itemEvent/IItemEventRouterResponse";
import { ConfigTypes } from "../models/enums/ConfigTypes";
import { HideoutAreas } from "../models/enums/HideoutAreas";
import { SkillTypes } from "../models/enums/SkillTypes";
import { IHideoutConfig } from "../models/spt/config/IHideoutConfig";
import { ILogger } from "../models/spt/utils/ILogger";
import { EventOutputHolder } from "../routers/EventOutputHolder";
import { ConfigServer } from "../servers/ConfigServer";
import { DatabaseServer } from "../servers/DatabaseServer";
import { LocalisationService } from "../services/LocalisationService";
import { PlayerService } from "../services/PlayerService";
import { HashUtil } from "../utils/HashUtil";
import { HttpResponseUtil } from "../utils/HttpResponseUtil";
import { TimeUtil } from "../utils/TimeUtil";
import { InventoryHelper } from "./InventoryHelper";
import { ProfileHelper } from "./ProfileHelper";
@injectable()
export class HideoutHelper
{
public static bitcoinFarm = "5d5c205bd582a50d042a3c0e";
public static waterCollector = "5d5589c1f934db045e6c5492";
public static bitcoin = "59faff1d86f7746c51718c9c";
public static expeditionaryFuelTank = "5d1b371186f774253763a656";
public static maxSkillPoint = 5000;
private static generatorOffMultipler = 0.2;
protected hideoutConfig: IHideoutConfig;
constructor(
@inject("WinstonLogger") protected logger: ILogger,
@inject("HashUtil") protected hashUtil: HashUtil,
@inject("TimeUtil") protected timeUtil: TimeUtil,
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
@inject("EventOutputHolder") protected eventOutputHolder: EventOutputHolder,
@inject("HttpResponseUtil") protected httpResponse: HttpResponseUtil,
@inject("ProfileHelper") protected profileHelper: ProfileHelper,
@inject("InventoryHelper") protected inventoryHelper: InventoryHelper,
@inject("PlayerService") protected playerService: PlayerService,
@inject("LocalisationService") protected localisationService: LocalisationService,
@inject("ConfigServer") protected configServer: ConfigServer
)
{
this.hideoutConfig = this.configServer.getConfig(ConfigTypes.HIDEOUT);
}
public registerProduction(pmcData: IPmcData, body: IHideoutSingleProductionStartRequestData | IHideoutContinuousProductionStartRequestData, sessionID: string): IItemEventRouterResponse
{
const recipe = this.databaseServer.getTables().hideout.production.find(p => p._id === body.recipeId);
if (!recipe)
{
this.logger.error(this.localisationService.getText("hideout-missing_recipe_in_db", body.recipeId));
return this.httpResponse.appendErrorToOutput(this.eventOutputHolder.getOutput(sessionID));
}
const modifiedProductionTime = recipe.productionTime - this.getCraftingSkillProductionTimeReduction(pmcData, recipe.productionTime);
// @Important: Here we need to be very exact:
// - normal recipe: Production time value is stored in attribute "productionType" with small "p"
// - scav case recipe: Production time value is stored in attribute "ProductionType" with capital "P"
pmcData.Hideout.Production[body.recipeId] = this.initProduction(body.recipeId, modifiedProductionTime);
}
/**
* This convenience function initializes new Production Object
* with all the constants.
*/
public initProduction(recipeId: string, productionTime: number): Production
{
return {
Progress: 0,
inProgress: true,
RecipeId: recipeId,
Products: [],
SkipTime: 0,
ProductionTime: productionTime,
StartTimestamp: this.timeUtil.getTimestamp()
};
}
public isProductionType(productive: Productive): productive is Production
{
return (productive as Production).Progress !== undefined || (productive as Production).RecipeId !== undefined;
}
// BALIST0N, I got bad news for you
// we do need to implement these after all
// ...
// with that I mean manual implementation
// RIP, GL whoever is going to do this
public applyPlayerUpgradesBonuses(pmcData: IPmcData, bonus: StageBonus): void
{
switch (bonus.type)
{
case "StashSize":
// TODO: bonus should only have type/templateId
for (const item in pmcData.Inventory.items)
{
if (pmcData.Inventory.items[item]._id === pmcData.Inventory.stash)
{
pmcData.Inventory.items[item]._tpl = bonus.templateId;
}
}
break;
case "MaximumEnergyReserve":
pmcData.Health.Energy.Maximum += 10;
break;
case "EnergyRegeneration":
case "HydrationRegeneration":
case "HealthRegeneration":
case "DebuffEndDelay":
case "QuestMoneyReward":
case "ExperienceRate":
case "SkillGroupLevelingBoost":
this.applySkillXPBoost(pmcData, bonus);
break;
case "ScavCooldownTimer":
case "InsuranceReturnTime":
case "RagfairCommission":
case "FuelConsumption":
// These skill is being applied automatically on the RagfairController, InsuranceController, ProfileController, HideoutController
// ScavCooldownTimer, InsuranceReturnTime, RagfairCommission, FuelConsumption
break;
case "AdditionalSlots":
// Some of these are also implemented on the HideoutController
break;
case "UnlockWeaponModification":
case "RepairArmorBonus":
case "RepairWeaponBonus":
case "UnlockArmorRepair":
case "UnlockWeaponRepair":
case "TextBonus":
// TODO: remove properties, only needs id/icon/type
break;
}
pmcData.Bonuses.push(bonus);
}
/**
* TODO:
* After looking at the skills there doesnt seem to be a configuration per skill to boost
* the XP gain PER skill. I THINK you should be able to put the variable "SkillProgress" (just like health has it)
* and be able to tune the skill gain PER skill, but I havent tested it and Im not sure!
* @param pmcData
* @param bonus
*/
protected applySkillXPBoost(pmcData: IPmcData, bonus: StageBonus): void
{
const skillGroupType = bonus.skillType;
if (skillGroupType)
{
switch (skillGroupType)
{
case "Physical":
case "Mental":
case "Combat":
case "Practical":
case "Special":
default:
break;
}
}
}
/**
* Process a players hideout, update areas that use resources + increment production timers
* @param sessionID Session id
*/
public updatePlayerHideout(sessionID: string): void
{
const pmcData = this.profileHelper.getPmcProfile(sessionID);
const hideoutProperties = {
btcFarmCGs: 0,
isGeneratorOn: false,
waterCollectorHasFilter: false
};
this.updateAreasWithResources(sessionID, pmcData, hideoutProperties);
this.updateProductionTimers(pmcData, hideoutProperties);
pmcData.Hideout.sptUpdateLastRunTimestamp = this.timeUtil.getTimestamp();
}
/**
* Update progress timer for water collector
* @param pmcData profile to update
* @param productionId id of water collection production to update
* @param hideoutProperties Hideout properties
*/
protected updateWaterCollectorProductionTimer(pmcData: IPmcData, productionId: string, hideoutProperties: { btcFarmCGs?: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void
{
let timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp);
if (!hideoutProperties.isGeneratorOn)
{
timeElapsed = Math.floor(timeElapsed * HideoutHelper.generatorOffMultipler);
}
if (hideoutProperties.waterCollectorHasFilter)
{
pmcData.Hideout.Production[productionId].Progress += timeElapsed;
}
}
/**
* Iterate over productions and update their progress timers
* @param pmcData Profile to check for productions and update
* @param hideoutProperties Hideout properties
*/
protected updateProductionTimers(pmcData: IPmcData, hideoutProperties: { btcFarmCGs: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void
{
const recipes = this.databaseServer.getTables().hideout.production;
// Check each production
for (const prodId in pmcData.Hideout.Production)
{
const craft = pmcData.Hideout.Production[prodId];
// Craft complete, skip processing (Don't skip continious crafts like bitcoin farm)
if (craft.Progress >= craft.ProductionTime && prodId !== HideoutHelper.bitcoinFarm)
{
continue;
}
if (craft.sptIsScavCase)
{
this.updateScavCaseProductionTimer(pmcData, prodId);
continue;
}
if (prodId === HideoutHelper.waterCollector)
{
this.updateWaterCollectorProductionTimer(pmcData, prodId, hideoutProperties);
continue;
}
if (prodId === HideoutHelper.bitcoinFarm)
{
pmcData.Hideout.Production[prodId] = this.updateBitcoinFarm(pmcData, hideoutProperties.btcFarmCGs, hideoutProperties.isGeneratorOn);
continue;
}
// Other recipes not covered by above
const recipe = recipes.find(r => r._id === prodId);
if (!recipe)
{
this.logger.error(this.localisationService.getText("hideout-missing_recipe_for_area", prodId));
continue;
}
this.updateProductionProgress(pmcData, prodId, recipe, hideoutProperties);
}
}
/**
* Update a productions progress value based on the amount of time that has passed
* @param pmcData Player profile
* @param prodId Production id being crafted
* @param recipe Recipe data being crafted
* @param hideoutProperties
*/
protected updateProductionProgress(pmcData: IPmcData, prodId: string, recipe: IHideoutProduction, hideoutProperties: { btcFarmCGs?: number; isGeneratorOn: boolean; waterCollectorHasFilter?: boolean; }): void
{
// Production is complete, no need to do any calculations
if (this.doesProgressMatchProductionTime(pmcData, prodId))
{
return;
}
// Get seconds since production started + now
let timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.Production[prodId].StartTimestamp) - pmcData.Hideout.Production[prodId].Progress;
if (!hideoutProperties.isGeneratorOn)
{
// Adjust for running without fuel
timeElapsed = Math.floor(timeElapsed * this.databaseServer.getTables().hideout.settings.generatorSpeedWithoutFuel);
}
// Increment progress by time passed
pmcData.Hideout.Production[prodId].Progress += timeElapsed;
// Limit progress to total production time if progress is over (dont run for continious crafts))
if (!recipe.continuous)
{
if (pmcData.Hideout.Production[prodId].Progress > pmcData.Hideout.Production[prodId].ProductionTime)
{
pmcData.Hideout.Production[prodId].Progress = pmcData.Hideout.Production[prodId].ProductionTime;
}
}
}
/**
* Check if a productions progress value matches its corresponding recipes production time value
* @param pmcData Player profile
* @param prodId Production id
* @param recipe Recipe being crafted
* @returns progress matches productionTime from recipe
*/
protected doesProgressMatchProductionTime(pmcData: IPmcData, prodId: string): boolean
{
return pmcData.Hideout.Production[prodId].Progress === pmcData.Hideout.Production[prodId].ProductionTime;
}
/**
* Update progress timer for scav case
* @param pmcData Profile to update
* @param productionId Id of scav case production to update
*/
protected updateScavCaseProductionTimer(pmcData: IPmcData, productionId: string): void
{
const timeElapsed = (this.timeUtil.getTimestamp() - pmcData.Hideout.Production[productionId].StartTimestamp) - pmcData.Hideout.Production[productionId].Progress;
pmcData.Hideout.Production[productionId].Progress += timeElapsed;
}
/**
* Iterate over hideout areas that use resources (fuel/filters etc) and update associated values
* @param sessionID Session id
* @param pmcData Profile to update areas of
* @param hideoutProperties hideout properties
*/
protected updateAreasWithResources(sessionID: string, pmcData: IPmcData, hideoutProperties: { btcFarmCGs: number; isGeneratorOn: boolean; waterCollectorHasFilter: boolean; }): void
{
for (const area of pmcData.Hideout.Areas)
{
switch (area.type)
{
case HideoutAreas.GENERATOR:
hideoutProperties.isGeneratorOn = area.active;
if (hideoutProperties.isGeneratorOn)
{
this.updateFuel(area, pmcData);
}
break;
case HideoutAreas.WATER_COLLECTOR:
this.updateWaterCollector(sessionID, pmcData, area, hideoutProperties.isGeneratorOn);
hideoutProperties.waterCollectorHasFilter = this.doesWaterCollectorHaveFilter(area);
break;
case HideoutAreas.AIR_FILTERING:
if (hideoutProperties.isGeneratorOn)
{
this.updateAirFilters(area, pmcData);
}
break;
case HideoutAreas.BITCOIN_FARM:
for (const slot of area.slots)
{
if (slot.item)
{
hideoutProperties.btcFarmCGs++;
}
}
break;
}
}
}
protected updateWaterCollector(sessionId: string, pmcData: IPmcData, area: HideoutArea, isGeneratorOn: boolean): void
{
if (area.level === 3)
{
const prod = pmcData.Hideout.Production[HideoutHelper.waterCollector];
if (prod && this.isProduction(prod))
{
area = this.updateWaterFilters(area, prod, isGeneratorOn, pmcData);
}
else
{
// continuousProductionStart()
// seem to not trigger consistently
const recipe: IHideoutSingleProductionStartRequestData = {
recipeId: HideoutHelper.waterCollector,
Action: "HideoutSingleProductionStart",
items: [],
timestamp: this.timeUtil.getTimestamp()
};
this.registerProduction(pmcData, recipe, sessionId);
}
}
}
protected doesWaterCollectorHaveFilter(waterCollector: HideoutArea): boolean
{
if (waterCollector.level === 3) // can put filters in from L3
{
// Has filter in at least one slot
return waterCollector.slots.some(x => x.item);
}
// No Filter
return false;
}
protected updateFuel(generatorArea: HideoutArea, pmcData: IPmcData): void
{
// 1 resource last 14 min 27 sec, 1/14.45/60 = 0.00115
// 10-10-2021 From wiki, 1 resource last 12 minutes 38 seconds, 1/12.63333/60 = 0.00131
let fuelDrainRate = this.databaseServer.getTables().hideout.settings.generatorFuelFlowRate * this.hideoutConfig.runIntervalSeconds;
// implemented moddable bonus for fuel consumption bonus instead of using solar power variable as before
const fuelBonus = pmcData.Bonuses.find(b => b.type === "FuelConsumption");
const fuelBonusPercent = 1.0 - (fuelBonus ? Math.abs(fuelBonus.value) : 0) / 100;
fuelDrainRate *= fuelBonusPercent;
// Hideout management resource consumption bonus:
const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData);
fuelDrainRate *= hideoutManagementConsumptionBonus;
let hasFuelRemaining = false;
let pointsConsumed = 0;
for (let i = 0; i < generatorArea.slots.length; i++)
{
if (generatorArea.slots[i].item)
{
let resourceValue = (generatorArea.slots[i].item[0].upd?.Resource)
? generatorArea.slots[i].item[0].upd.Resource.Value
: null;
if (resourceValue === 0)
{
continue;
}
else if (!resourceValue)
{
const fuelItem = HideoutHelper.expeditionaryFuelTank;
resourceValue = generatorArea.slots[i].item[0]._tpl === fuelItem
? resourceValue = 60 - fuelDrainRate
: resourceValue = 100 - fuelDrainRate;
pointsConsumed = fuelDrainRate;
}
else
{
pointsConsumed = (generatorArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + fuelDrainRate;
resourceValue -= fuelDrainRate;
}
resourceValue = Math.round(resourceValue * 10000) / 10000;
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
//check unit consumed for increment skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
pointsConsumed -= 10;
}
if (resourceValue > 0)
{
generatorArea.slots[i].item[0].upd = this.getAreaUpdObject(1, resourceValue, pointsConsumed);
this.logger.debug(`Generator: ${resourceValue} fuel left in slot ${i + 1}`);
hasFuelRemaining = true;
break; // Break here to avoid updating all the fuel tanks
}
else
{
generatorArea.slots[i].item[0].upd = this.getAreaUpdObject(1, 0, 0);
// Update remaining resources to be subtracted
fuelDrainRate = Math.abs(resourceValue);
}
}
}
if (!hasFuelRemaining)
{
generatorArea.active = false;
}
}
/**
* Adjust water filter objects resourceValue or delete when they reach 0 resource
* @param waterFilterArea water filter area to update
* @param production production object
* @param isGeneratorOn is generator enabled
* @param pmcData Player profile
* @returns Updated HideoutArea object
*/
protected updateWaterFilters(waterFilterArea: HideoutArea, production: Production, isGeneratorOn: boolean, pmcData: IPmcData): HideoutArea
{
let timeElapsed = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp;
// 100 resources last 8 hrs 20 min, 100/8.33/60/60 = 0.00333
let filterDrainRate = 0.00333;
// Hideout management resource consumption bonus:
const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData);
filterDrainRate *= hideoutManagementConsumptionBonus;
let productionTime = 0;
let pointsConsumed = 0;
const recipe = this.databaseServer.getTables().hideout.production.find(prod => prod._id === HideoutHelper.waterCollector);
productionTime = (recipe.productionTime || 0);
// Reduce time elapsed (and progress) when generator is off
if (!isGeneratorOn)
{
timeElapsed = Math.floor(timeElapsed * HideoutHelper.generatorOffMultipler);
}
if (timeElapsed > productionTime)
{
// We've gone beyond completion
filterDrainRate *= (productionTime - production.Progress);
}
else
{
filterDrainRate *= timeElapsed;
}
// Production hasn't completed
if (production.Progress < productionTime)
{
// Check all slots that take water filters
for (let i = 0; i < waterFilterArea.slots.length; i++)
{
// Has a water filter installed into slot
if (waterFilterArea.slots[i].item)
{
let resourceValue = (waterFilterArea.slots[i].item[0].upd?.Resource)
? waterFilterArea.slots[i].item[0].upd.Resource.Value
: null;
if (!resourceValue)
{
resourceValue = 100 - filterDrainRate;
pointsConsumed = filterDrainRate;
}
else
{
pointsConsumed = (waterFilterArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + filterDrainRate;
resourceValue -= filterDrainRate;
}
resourceValue = Math.round(resourceValue * 10000) / 10000;
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
//check unit consumed for increment skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
pointsConsumed -= 10;
}
// Filter has some juice left in it
if (resourceValue > 0)
{
waterFilterArea.slots[i].item[0].upd = this.getAreaUpdObject(1, resourceValue, pointsConsumed);
this.logger.debug(`Water filter: ${resourceValue} filter left on slot ${i + 1}`);
break; // Break here to avoid updating all filters
}
else
{
// Filter ran out / used up
delete waterFilterArea.slots[i].item;
// Update remaining resources to be subtracted
filterDrainRate = Math.abs(resourceValue);
}
}
}
}
return waterFilterArea;
}
protected getAreaUpdObject(stackCount: number, resourceValue: number, resourceUnitsConsumed: number): Upd
{
return {
StackObjectsCount: stackCount,
Resource: {
Value: resourceValue,
UnitsConsumed: resourceUnitsConsumed
}
};
}
protected updateAirFilters(airFilterArea: HideoutArea, pmcData: IPmcData): void
{
// 300 resources last 20 hrs, 300/20/60/60 = 0.00416
/* 10-10-2021 from WIKI (https://escapefromtarkov.fandom.com/wiki/FP-100_filter_absorber)
Lasts for 17 hours 38 minutes and 49 seconds (23 hours 31 minutes and 45 seconds with elite hideout management skill),
300/17.64694/60/60 = 0.004722
*/
let filterDrainRate = this.databaseServer.getTables().hideout.settings.airFilterUnitFlowRate * this.hideoutConfig.runIntervalSeconds;
// Hideout management resource consumption bonus:
const hideoutManagementConsumptionBonus = 1.0 - this.getHideoutManagementConsumptionBonus(pmcData);
filterDrainRate *= hideoutManagementConsumptionBonus;
let pointsConsumed = 0;
for (let i = 0; i < airFilterArea.slots.length; i++)
{
if (airFilterArea.slots[i].item)
{
let resourceValue = (airFilterArea.slots[i].item[0].upd?.Resource)
? airFilterArea.slots[i].item[0].upd.Resource.Value
: null;
if (!resourceValue)
{
resourceValue = 300 - filterDrainRate;
pointsConsumed = filterDrainRate;
}
else
{
pointsConsumed = (airFilterArea.slots[i].item[0].upd.Resource.UnitsConsumed || 0) + filterDrainRate;
resourceValue -= filterDrainRate;
}
resourceValue = Math.round(resourceValue * 10000) / 10000;
pointsConsumed = Math.round(pointsConsumed * 10000) / 10000;
//check unit consumed for increment skill point
if (pmcData && Math.floor(pointsConsumed / 10) >= 1)
{
this.playerService.incrementSkillLevel(pmcData, SkillTypes.HIDEOUT_MANAGEMENT, 1);
pointsConsumed -= 10;
}
if (resourceValue > 0)
{
airFilterArea.slots[i].item[0].upd = {
StackObjectsCount: 1,
Resource: {
Value: resourceValue,
UnitsConsumed: pointsConsumed
}
};
this.logger.debug(`Air filter: ${resourceValue} filter left on slot ${i + 1}`);
break; // Break here to avoid updating all filters
}
else
{
delete airFilterArea.slots[i].item;
// Update remaining resources to be subtracted
filterDrainRate = Math.abs(resourceValue);
}
}
}
}
protected updateBitcoinFarm(pmcData: IPmcData, btcFarmCGs: number, isGeneratorOn: boolean): Production
{
const btcProd = pmcData.Hideout.Production[HideoutHelper.bitcoinFarm];
const bitcoinProdData = this.databaseServer.getTables().hideout.production.find(p => p._id === "5d5c205bd582a50d042a3c0e");
const coinSlotCount = this.getBTCSlots(pmcData);
// Full on bitcoins, halt progress
if (this.isProduction(btcProd) && btcProd.Products.length >= coinSlotCount)
{
// Set progress to 0
btcProd.Progress = 0;
return btcProd;
}
if (this.isProduction(btcProd))
{
let timeElapsedSeconds = this.timeUtil.getTimestamp() - pmcData.Hideout.sptUpdateLastRunTimestamp;
if (!isGeneratorOn)
{
// Generator off, reduce time elapsed
timeElapsedSeconds = Math.floor(timeElapsedSeconds * HideoutHelper.generatorOffMultipler);
}
btcProd.Progress += timeElapsedSeconds;
// The wiki has a wrong formula!
// Do not change unless you validate it with the Client code files!
// This formula was found on the client files:
// *******************************************************
/*
public override int InstalledSuppliesCount
{
get
{
return this.int_1;
}
protected set
{
if (this.int_1 === value)
{
return;
}
this.int_1 = value;
base.Single_0 = ((this.int_1 === 0) ? 0f : (1f + (float)(this.int_1 - 1) * this.float_4));
}
}
*/
// **********************************************************
// At the time of writing this comment, this was GClass1667
// To find it in case of weird results, use DNSpy and look for usages on class AreaData
// Look for a GClassXXXX that has a method called "InitDetails" and the only parameter is the AreaData
// That should be the bitcoin farm production. To validate, try to find the snippet below:
/*
protected override void InitDetails(AreaData data)
{
base.InitDetails(data);
this.gclass1678_1.Type = EDetailsType.Farming;
}
*/
// BSG finally fixed their settings, they now get loaded from the settings and used in the client
const coinCraftTimeSeconds = bitcoinProdData.productionTime / (1 + (btcFarmCGs - 1) * this.databaseServer.getTables().hideout.settings.gpuBoostRate);
while (btcProd.Progress > coinCraftTimeSeconds)
{
if (btcProd.Products.length < coinSlotCount)
{
btcProd.Products.push({
"_id": this.hashUtil.generate(),
"_tpl": "59faff1d86f7746c51718c9c",
"upd": {
"StackObjectsCount": 1
}
});
btcProd.Progress -= coinCraftTimeSeconds;
}
else
{
btcProd.Progress = 0;
}
}
btcProd.StartTimestamp = this.timeUtil.getTimestamp();
return btcProd;
}
else
{
return null;
}
}
/**
* Get a count of how many BTC can be gathered by the profile
* @param pmcData Profile to look up
* @returns coin slot count
*/
protected getBTCSlots(pmcData: IPmcData): number
{
const bitcoinProduction = this.databaseServer.getTables().hideout.production.find(p => p._id === HideoutHelper.bitcoinFarm);
const productionSlots = bitcoinProduction?.productionLimitCount || 3;
const hasManagementSkillSlots = this.hasEliteHideoutManagementSkill(pmcData);
const managementSlotsCount = this.getManagementSkillsSlots() || 2;
return productionSlots + (hasManagementSkillSlots ? managementSlotsCount : 0);
}
/**
* Get a count of bitcoins player miner can hold
*/
protected getManagementSkillsSlots(): number
{
return this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.EliteSlots.BitcoinFarm.Container;
}
/**
* Does profile have elite hideout management skill
* @param pmcData Profile to look at
* @returns True if profile has skill
*/
protected hasEliteHideoutManagementSkill(pmcData: IPmcData): boolean
{
return this.getHideoutManagementSkill(pmcData)?.Progress >= 5100; // level 51+
}
/**
* Get the hideout management skill from player profile
* @param pmcData Profile to look at
* @returns Hideout management skill object
*/
protected getHideoutManagementSkill(pmcData: IPmcData): Common
{
return pmcData.Skills.Common.find(x => x.Id === "HideoutManagement");
}
protected getHideoutManagementConsumptionBonus(pmcData: IPmcData): number
{
const hideoutManagementSkill = this.getHideoutManagementSkill(pmcData);
if (!hideoutManagementSkill)
{
return 0;
}
let roundedLevel = Math.floor(hideoutManagementSkill.Progress / 100);
// If the level is 51 we need to round it at 50 so on elite you dont get 25.5%
// at level 1 you already get 0.5%, so it goes up until level 50. For some reason the wiki
// says that it caps at level 51 with 25% but as per dump data that is incorrect apparently
roundedLevel = (roundedLevel === 51) ? roundedLevel - 1 : roundedLevel;
return (roundedLevel * this.databaseServer.getTables().globals.config.SkillsSettings.HideoutManagement.ConsumptionReductionPerLevel) / 100;
}
/**
* Get the crafting skill details from player profile
* @param pmcData Player profile
* @returns crafting skill, null if not found
*/
protected getCraftingSkill(pmcData: IPmcData): Common
{
for (const skill of pmcData.Skills.Common)
{
if (skill.Id === SkillTypes.CRAFTING)
{
return skill;
}
}
return null;
}
/**
* Adjust craft time based on crafting skill level found in player profile
* @param pmcData Player profile
* @param productionTime Time to complete hideout craft in seconds
* @returns Adjusted craft time in seconds
*/
protected getCraftingSkillProductionTimeReduction(pmcData: IPmcData, productionTime: number): number
{
const craftingSkill = this.getCraftingSkill(pmcData);
if (!craftingSkill)
{
return productionTime;
}
const roundedLevel = Math.floor(Math.min(HideoutHelper.maxSkillPoint, craftingSkill.Progress) / 100);
const percentageToDrop = roundedLevel * 0.75;
return (productionTime * percentageToDrop) / 100;
}
public isProduction(productive: Productive): productive is Production
{
return (productive as Production).Progress !== undefined || (productive as Production).RecipeId !== undefined;
}
/**
* Gather crafted BTC from hideout area and add to inventory
* Reset production start timestamp if hideout area at full coin capacity
* @param pmcData Player profile
* @param request Take production request
* @param sessionId Session id
* @returns IItemEventRouterResponse
*/
public getBTC(pmcData: IPmcData, request: IHideoutTakeProductionRequestData, sessionId: string): IItemEventRouterResponse
{
const output = this.eventOutputHolder.getOutput(sessionId);
// Get how many coins were crafted and ready to pick up
const craftedCoinCount = pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length;
if (!craftedCoinCount)
{
const errorMsg = this.localisationService.getText("hideout-no_bitcoins_to_collect");
this.logger.error(errorMsg);
return this.httpResponse.appendErrorToOutput(output, errorMsg);
}
const coinsToAddToInventory = {
items: [{
// eslint-disable-next-line @typescript-eslint/naming-convention
item_id: HideoutHelper.bitcoin,
count: pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length
}],
tid: "ragfair"
};
const coinSlotCount = this.getBTCSlots(pmcData);
// Run callback after coins are added to player inventory
const callback = () =>
{
// Is at max capacity
if (pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products.length >= coinSlotCount)
{
// Set start to now
pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].StartTimestamp = this.timeUtil.getTimestamp();
}
// Remove crafted coins from production in profile
pmcData.Hideout.Production[HideoutHelper.bitcoinFarm].Products = [];
};
// Add FiR coins to player inventory
return this.inventoryHelper.addItem(pmcData, coinsToAddToInventory, output, sessionId, callback, true);
}
/**
* Upgrade hideout wall from starting level to interactable level if enough time has passed
* @param pmcProfile Profile to upgrade wall in
*/
public unlockHideoutWallInProfile(pmcProfile: IPmcData): void
{
// Sufficient time has passed since account created, upgrade wall to next level to be interactable
const wallUnlockTimestamp = this.hideoutConfig.hideoutWallAppearTimeSeconds + pmcProfile.Info.RegistrationDate;
if (wallUnlockTimestamp < this.timeUtil.getTimestamp())
{
// Get wall area
const wall = pmcProfile.Hideout.Areas.find(x => x.type === HideoutAreas.EMERGENCY_WALL);
if (!wall)
{
return;
}
if (wall.level === 0)
{
wall.level++;
wall.constructing = true;
return;
}
if (wall.level === 1)
{
if (this.hideoutImprovementIsComplete(pmcProfile.Hideout.Improvements["639199277a9178252d38c98f"]))
{
this.logger.debug("Improvement 639199277a9178252d38c98f found, upgrading hideout wall from level: 1 to 3");
wall.level = 3;
wall.constructing = false;
// 0 = no wall | Idle State
// 1 - EATS EVERYTHING without areas.json change to include improvements
// 2 - Should be Moppable wall / Interactable wall While Constructing = true Sledgehammer is Smashable
// 2 - While false UI is broken. While true mimics level 3 hideout
// 3 - Smashable wall / Sledgehammer
// 4 - Installable door
}
return;
}
// Workaround for old profiles that have the wall at level 2
if (wall.level === 2)
{
this.logger.debug("Old wall level 2 found, fixing");
if (this.hideoutImprovementIsComplete(pmcProfile.Hideout.Improvements["639199277a9178252d38c98f"]))
{
this.logger.debug("Wall level adjusted to 3");
wall.level++;
}
else
{
this.logger.debug("Wall level adjusted to 1");
wall.level--;
}
}
}
}
/**
* Hideout improvement is flagged as complete
* @param improvement hideout improvement object
* @returns true if complete
*/
protected hideoutImprovementIsComplete(improvement: IHideoutImprovement): boolean
{
if (improvement?.completed)
{
return true;
}
return false;
}
/**
* Iterate over hideout improvements not completed and check if they need to be adjusted
* @param pmcProfile Profile to adjust
*/
public setHideoutImprovementsToCompleted(pmcProfile: IPmcData): void
{
for (const improvementId in pmcProfile.Hideout.Improvements)
{
const improvementDetails = pmcProfile.Hideout.Improvements[improvementId];
if (improvementDetails.completed === false && improvementDetails.improveCompleteTimestamp < this.timeUtil.getTimestamp())
{
improvementDetails.completed = true;
}
}
}
}