Create new script for associating productions with quests (!421)
See `src\tools\ProductionQuestsGen\ProductionQuestsGen.ts` for usage I've run the tool and committed the updated production.json as an example of it successfully managing to find the quest association for most productions (Aside from new event quest productions) Co-authored-by: DrakiaXYZ <565558+TheDgtl@users.noreply.github.com> Reviewed-on: https://dev.sp-tarkov.com/SPT/Server/pulls/421 Co-authored-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com> Co-committed-by: DrakiaXYZ <drakiaxyz@noreply.dev.sp-tarkov.com>
This commit is contained in:
parent
f3e3594e1b
commit
9b17a9b350
@ -2088,7 +2088,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "6396701b9113f06a7c3b2379"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -2122,7 +2123,8 @@
|
||||
"type": "Area"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "625d700cc48e6c62a440fab5"
|
||||
},
|
||||
{
|
||||
"templateId": "590c2d8786f774245b1f03f3",
|
||||
@ -2317,7 +2319,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "63966fbeea19ac7ed845db2e"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -2392,7 +2395,8 @@
|
||||
"type": "Item"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5a27bc8586f7741b543d8ea4"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -3349,7 +3353,8 @@
|
||||
"type": "Item"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5c1141f386f77430ff393792"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -3701,7 +3706,8 @@
|
||||
"type": "Item"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "60e71c11d54b755a3b53eb65"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -3760,7 +3766,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "60e71b62a0beca400d69efc4"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -4458,7 +4465,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5a27bb8386f7741c770d2d0a"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -4499,7 +4507,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "60e71c9ad54b755a3b53eb66"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -4738,7 +4747,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "625d6ffaf7308432be1d44c5"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -4783,7 +4793,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5ae4497b86f7744cf402ed00"
|
||||
},
|
||||
{
|
||||
"count": 2,
|
||||
@ -6378,7 +6389,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "63966faeea19ac7ed845db2c"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -6430,7 +6442,8 @@
|
||||
"type": "Item"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5c0d4e61d09282029f53920e"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -6500,7 +6513,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "63966fccac6f8f3c677b9d89"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -6986,7 +7000,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "6396700fea19ac7ed845db32"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -7058,7 +7073,8 @@
|
||||
"type": "Tool"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "64e7b9bffd30422ed03dad38"
|
||||
}
|
||||
]
|
||||
},
|
||||
@ -7673,7 +7689,8 @@
|
||||
"type": "Item"
|
||||
},
|
||||
{
|
||||
"type": "QuestComplete"
|
||||
"type": "QuestComplete",
|
||||
"questId": "5ae4495086f77443c122bc40"
|
||||
},
|
||||
{
|
||||
"count": 1,
|
||||
@ -8283,4 +8300,4 @@
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
@ -30,7 +30,8 @@
|
||||
"run:profiler": "gulp run:profiler",
|
||||
"gen:types": "tsc -p tsconfig.typedef.json --resolveJsonModule",
|
||||
"gen:docs": "typedoc --options ./typedoc.json --entryPointStrategy expand ./src",
|
||||
"gen:items": "ts-node -r tsconfig-paths/register ./src/tools/ItemTplGenerator/ItemTplGeneratorProgram.ts"
|
||||
"gen:items": "ts-node -r tsconfig-paths/register ./src/tools/ItemTplGenerator/ItemTplGeneratorProgram.ts",
|
||||
"gen:productionquests": "ts-node -r tsconfig-paths/register ./src/tools/ProductionQuestsGen/ProductionQuestsGenProgram.ts"
|
||||
},
|
||||
"dependencies": {
|
||||
"atomically": "~1.7",
|
||||
|
159
project/src/tools/ProductionQuestsGen/ProductionQuestsGen.ts
Normal file
159
project/src/tools/ProductionQuestsGen/ProductionQuestsGen.ts
Normal file
@ -0,0 +1,159 @@
|
||||
/**
|
||||
* Automatically update the `questId` property for production `QuestComplete` requirements in `assets/database/hideout/production.json`
|
||||
* Based on data from both `production.json` and `quests.json`.
|
||||
*
|
||||
* Usage:
|
||||
* - Run this script using npm: `npm run gen:productionquests`
|
||||
*
|
||||
* Notes:
|
||||
* - Some productions may output "Unable to find quest" if new quests or event quests haven't been dumped
|
||||
* - Some productions may output "Quest ... is already associated" if a quest unlocks multiple assorts, this can be ignored
|
||||
* - The list of "blacklistedProductions" is to stop spurious errors when we know a production is no longer necessary (Old events)
|
||||
*/
|
||||
import * as fs from "node:fs";
|
||||
import * as path from "node:path";
|
||||
import { OnLoad } from "@spt/di/OnLoad";
|
||||
import { QuestRewardType } from "@spt/models/enums/QuestRewardType";
|
||||
import { ILogger } from "@spt/models/spt/utils/ILogger";
|
||||
import { DatabaseServer } from "@spt/servers/DatabaseServer";
|
||||
import { inject, injectAll, injectable } from "tsyringe";
|
||||
|
||||
@injectable()
|
||||
export class ProductionQuestsGen {
|
||||
private questProductionOutputList: QuestProductionOutput[] = [];
|
||||
private questProductionMap: Record<string, string> = {};
|
||||
|
||||
private blacklistedProductions = [
|
||||
"6617cdb6b24b0ea24505f618", // Old event quest production "Radio Repeater" alt recipe
|
||||
"66140c4a9688754de10dac07", // Old event quest production "Documents with decrypted data"
|
||||
"661e6c26750e453380391f55", // Old event quest production "Documents with decrypted data"
|
||||
"660c2dbaa2a92e70cc074863", // Old event quest production "Decrypted flash drive"
|
||||
];
|
||||
|
||||
constructor(
|
||||
@inject("DatabaseServer") protected databaseServer: DatabaseServer,
|
||||
@inject("PrimaryLogger") protected logger: ILogger,
|
||||
@injectAll("OnLoad") protected onLoadComponents: OnLoad[],
|
||||
) {}
|
||||
|
||||
async run(): Promise<void> {
|
||||
// Load all of the onload components, this gives us access to most of SPTs injections
|
||||
for (const onLoad of this.onLoadComponents) {
|
||||
await onLoad.onLoad();
|
||||
}
|
||||
|
||||
// Build up our dataset
|
||||
this.buildQuestProductionList();
|
||||
this.updateProductionQuests();
|
||||
|
||||
// Dump the new data to disk
|
||||
const currentDir = path.dirname(__filename);
|
||||
const projectDir = path.resolve(currentDir, "..", "..", "..");
|
||||
const hideoutDir = path.join(projectDir, "assets", "database", "hideout");
|
||||
const productionOutPath = path.join(hideoutDir, "production.json");
|
||||
fs.writeFileSync(
|
||||
productionOutPath,
|
||||
JSON.stringify(this.databaseServer.getTables().hideout.production, null, 2),
|
||||
"utf-8",
|
||||
);
|
||||
}
|
||||
|
||||
private updateProductionQuests(): void {
|
||||
// Loop through all productions, and try to associate any with a `QuestComplete` type with its quest
|
||||
for (const production of this.databaseServer.getTables().hideout.production.recipes) {
|
||||
// Skip blacklisted productions
|
||||
if (this.blacklistedProductions.includes(production._id)) continue;
|
||||
|
||||
// If the production has no quest requirement, or more than 1, skip it
|
||||
const questCompleteList = production.requirements.filter((req) => req.type === "QuestComplete");
|
||||
if (questCompleteList.length === 0) continue;
|
||||
if (questCompleteList.length > 1) {
|
||||
this.logger.error(`Error, production ${production._id} contains multiple QuestComplete requirements`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Try to find the quest that matches this production
|
||||
const questProductionOutputs = this.questProductionOutputList.filter(
|
||||
(output) => output.ItemTemplate === production.endProduct && output.Quantity === production.count,
|
||||
);
|
||||
|
||||
// Make sure we found valid data
|
||||
if (!this.isValidQuestProduction(production, questProductionOutputs, questCompleteList[0])) continue;
|
||||
|
||||
// Update the production quest ID
|
||||
this.questProductionMap[questProductionOutputs[0].QuestId] = production._id;
|
||||
questCompleteList[0].questId = questProductionOutputs[0].QuestId;
|
||||
this.logger.success(
|
||||
`Updated ${production._id}, ${production.endProduct} with quantity ${production.count} to target quest ${questProductionOutputs[0].QuestId}`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
private isValidQuestProduction(production, questProductionOutputs, questComplete): boolean {
|
||||
// A lot of error handling for edge cases
|
||||
if (questProductionOutputs.length === 0) {
|
||||
this.logger.error(
|
||||
`Unable to find quest for production ${production._id}, endProduct ${production.endProduct} with quantity ${production.count}. Potential new or removed quest`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (questProductionOutputs.length > 1) {
|
||||
this.logger.error(
|
||||
`Multiple quests match production ${production._id}, endProduct ${production.endProduct} with quantity ${production.count}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (questComplete.questId && questComplete.questId !== questProductionOutputs[0].QuestId) {
|
||||
this.logger.error(
|
||||
`Multiple productions match quest. EndProduct ${production.endProduct} with quantity ${production.count}, existing quest ${questComplete.questId}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (this.questProductionMap[questProductionOutputs[0].QuestId]) {
|
||||
this.logger.warning(
|
||||
`Quest ${questProductionOutputs[0].QuestId} is already associated with production ${this.questProductionMap[questProductionOutputs[0].QuestId]}. Potential conflict`,
|
||||
);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
// Build a list of all quests and what production they unlock
|
||||
private buildQuestProductionList(): void {
|
||||
for (const quest of Object.values(this.databaseServer.getTables().templates.quests)) {
|
||||
for (const rewardState of Object.values(quest.rewards)) {
|
||||
for (const reward of rewardState) {
|
||||
if (reward.type !== QuestRewardType.PRODUCTIONS_SCHEME) continue;
|
||||
|
||||
// Make the assumption all productions only output a single item template
|
||||
const output: QuestProductionOutput = {
|
||||
QuestId: quest._id,
|
||||
ItemTemplate: reward.items[0]._tpl,
|
||||
Quantity: 0,
|
||||
};
|
||||
|
||||
for (const item of reward.items) {
|
||||
// Skip any item that has a parent, we only care about the root item(s)
|
||||
if (item.parentId) continue;
|
||||
if (item._tpl !== output.ItemTemplate) {
|
||||
this.logger.error(
|
||||
`Production scheme has multiple output items. ${output.ItemTemplate} !== ${item._tpl}`,
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
output.Quantity += item.upd.StackObjectsCount;
|
||||
}
|
||||
|
||||
this.questProductionOutputList.push(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class QuestProductionOutput {
|
||||
public QuestId: string;
|
||||
public ItemTemplate: string;
|
||||
public Quantity: number;
|
||||
}
|
@ -0,0 +1,38 @@
|
||||
import "reflect-metadata";
|
||||
import "source-map-support/register";
|
||||
|
||||
import { ErrorHandler } from "@spt/ErrorHandler";
|
||||
import { Container } from "@spt/di/Container";
|
||||
import { ProductionQuestsGen } from "@spt/tools/ProductionQuestsGen/ProductionQuestsGen";
|
||||
import { Lifecycle, container } from "tsyringe";
|
||||
|
||||
export class ProductionQuestsGenProgram {
|
||||
private errorHandler: ErrorHandler;
|
||||
constructor() {
|
||||
// set window properties
|
||||
process.stdout.setEncoding("utf8");
|
||||
process.title = "SPT ProductionQuestsGen";
|
||||
this.errorHandler = new ErrorHandler();
|
||||
}
|
||||
|
||||
public async start(): Promise<void> {
|
||||
try {
|
||||
Container.registerTypes(container);
|
||||
const childContainer = container.createChildContainer();
|
||||
childContainer.register<ProductionQuestsGen>("ProductionQuestsGen", ProductionQuestsGen, {
|
||||
lifecycle: Lifecycle.Singleton,
|
||||
});
|
||||
Container.registerListTypes(childContainer);
|
||||
Container.registerPostLoadTypes(container, childContainer);
|
||||
await childContainer.resolve<ProductionQuestsGen>("ProductionQuestsGen").run();
|
||||
} catch (err: any) {
|
||||
this.errorHandler.handleCriticalError(err instanceof Error ? err : new Error(err));
|
||||
}
|
||||
|
||||
// Kill the process, something holds it open so we need to manually kill it
|
||||
process.exit();
|
||||
}
|
||||
}
|
||||
|
||||
const program = new ProductionQuestsGenProgram();
|
||||
program.start();
|
Loading…
Reference in New Issue
Block a user