Improve addTrader example

Move some code to new class `traderHelper`
More comments
Added example of constructing weapon from scratch
exposed trader update time as parameter instead of hard coding
Add additional example for adding singular item with different params
Added ability to add buy restrictions to items
This commit is contained in:
Dev 2023-07-16 11:01:19 +01:00
parent 65659372dd
commit 421120f623
4 changed files with 358 additions and 206 deletions

View File

@ -13,7 +13,7 @@
"customization_seller": false,
"name": "Cat",
"surname": " ",
"nickname": "Cat",
"nickname": "Maxwell",
"location": "Here is the cat shop",
"avatar": "/files/trader/avatar/cat.jpg",
"balance_rub": 5000000,

View File

@ -9,19 +9,18 @@ import { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
import { ImageRouter } from "@spt-aki/routers/ImageRouter";
import { ConfigServer } from "@spt-aki/servers/ConfigServer";
import { ConfigTypes } from "@spt-aki/models/enums/ConfigTypes";
import { ITraderAssort, ITraderBase } from "@spt-aki/models/eft/common/tables/ITrader";
import { ITraderConfig, UpdateTime } from "@spt-aki/models/spt/config/ITraderConfig";
import { ITraderConfig } from "@spt-aki/models/spt/config/ITraderConfig";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables";
import { Money } from "@spt-aki/models/enums/Money";
// New trader settings
import * as baseJson from "../db/base.json";
import { TraderHelper } from "./traderHelpers";
class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod {
mod: string
logger: ILogger
class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod
{
private mod: string
private logger: ILogger
private traderHeper: TraderHelper
constructor() {
this.mod = "13AddTrader"; // Set name of mod so we can log it to console later
@ -31,19 +30,23 @@ class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod {
* Some work needs to be done prior to SPT code being loaded, registering the profile image + setting trader update time inside the trader config json
* @param container Dependency container
*/
public preAkiLoad(container: DependencyContainer): void {
public preAkiLoad(container: DependencyContainer): void
{
// Get a logger
this.logger = container.resolve<ILogger>("WinstonLogger");
this.logger.debug(`[${this.mod}] preAki Loading... `);
// Get SPT code/data we need later
const preAkiModLoader: PreAkiModLoader = container.resolve<PreAkiModLoader>("PreAkiModLoader");
const imageRouter: ImageRouter = container.resolve<ImageRouter>("ImageRouter");
const configServer = container.resolve<ConfigServer>("ConfigServer");
const traderConfig: ITraderConfig = configServer.getConfig<ITraderConfig>(ConfigTypes.TRADER);
this.registerProfileImage(preAkiModLoader, imageRouter);
this.setupTraderUpdateTime(traderConfig);
// Create helper class and use it to register our traders image/icon + set its stock refresh time
this.traderHeper = new TraderHelper();
this.traderHeper.registerProfileImage(baseJson, this.mod, preAkiModLoader, imageRouter, "cat.jpg");
this.traderHeper.setTraderUpdateTime(traderConfig, baseJson, 3600);
this.logger.debug(`[${this.mod}] preAki Loaded`);
}
@ -51,210 +54,33 @@ class SampleTrader implements IPreAkiLoadMod, IPostDBLoadMod {
* Majority of trader-related work occurs after the aki database has been loaded but prior to SPT code being run
* @param container Dependency container
*/
public postDBLoad(container: DependencyContainer): void {
public postDBLoad(container: DependencyContainer): void
{
this.logger.debug(`[${this.mod}] postDb Loading... `);
// Resolve SPT classes we'll use
const databaseServer: DatabaseServer = container.resolve<DatabaseServer>("DatabaseServer");
const configServer: ConfigServer = container.resolve<ConfigServer>("ConfigServer");
const traderConfig: ITraderConfig = configServer.getConfig(ConfigTypes.TRADER);
const jsonUtil: JsonUtil = container.resolve<JsonUtil>("JsonUtil");
// Get a reference to the database tables
const tables = databaseServer.getTables();
// Add new trader to the trader dictionary in DatabaseServer
this.addTraderToDb(baseJson, tables, jsonUtil);
// Add new trader to the trader dictionary in DatabaseServer - has no assorts (items) yet
this.traderHeper.addTraderToDb(baseJson, tables, jsonUtil);
this.addTraderToLocales(tables, baseJson.name, "Cat", baseJson.nickname, baseJson.location, "This is the cat shop");
// Add some singular items to trader (items without sub-items e.g. milk/bandage)
this.traderHeper.addSingleItemsToTrader(tables, baseJson._id);
// Add more complex items to trader (items with sub-items, e.g. guns)
this.traderHeper.addComplexItemsToTrader(tables, baseJson._id, jsonUtil);
// Add trader to locale file, ensures trader text shows properly on screen
// WARNING: adds the same text to ALL locales (e.g. chinese/french/english)
this.traderHeper.addTraderToLocales(baseJson, tables, baseJson.name, "Cat", baseJson.nickname, baseJson.location, "This is the cat shop");
this.logger.debug(`[${this.mod}] postDb Loaded`);
}
/**
* Add profile picture to our trader
* @param preAkiModLoader mod loader class - used to get the mods file path
* @param imageRouter image router class - used to register the trader image path so we see their image on trader page
*/
private registerProfileImage(preAkiModLoader: PreAkiModLoader, imageRouter: ImageRouter): void
{
// Reference the mod "res" folder
const imageFilepath = `./${preAkiModLoader.getModPath(this.mod)}res`;
// Register a route to point to the profile picture
imageRouter.addRoute(baseJson.avatar.replace(".jpg", ""), `${imageFilepath}/cat.jpg`);
}
/**
* Add record to trader config to set the refresh time of trader in seconds (default is 60 minutes)
* @param traderConfig trader config to add our trader to
*/
private setupTraderUpdateTime(traderConfig: ITraderConfig): void
{
// Add refresh time in seconds to config
const traderRefreshRecord: UpdateTime = { traderId: baseJson._id, seconds: 3600 }
traderConfig.updateTime.push(traderRefreshRecord);
}
/**
* Add our new trader to the database
* @param traderDetailsToAdd trader details
* @param tables database
* @param jsonUtil json utility class
*/
// rome-ignore lint/suspicious/noExplicitAny: traderDetailsToAdd comes from base.json, so no type
private addTraderToDb(traderDetailsToAdd: any, tables: IDatabaseTables, jsonUtil: JsonUtil): void
{
// Add trader to trader table, key is the traders id
tables.traders[traderDetailsToAdd._id] = {
assort: this.createAssortTable(tables, jsonUtil), // assorts are the 'offers' trader sells, can be a single item (e.g. carton of milk) or multiple items as a collection (e.g. a gun)
base: jsonUtil.deserialize(jsonUtil.serialize(traderDetailsToAdd)) as ITraderBase,
questassort: {
started: {},
success: {},
fail: {}
} // Empty object as trader has no assorts unlocked by quests
};
}
/**
* Create assorts for trader and add milk and a gun to it
* @returns ITraderAssort
*/
private createAssortTable(tables: IDatabaseTables, jsonUtil: JsonUtil): ITraderAssort
{
// Create a blank assort object, ready to have items added
const assortTable: ITraderAssort = {
nextResupply: 0,
items: [],
barter_scheme: {},
loyal_level_items: {}
}
const MILK_ID = "575146b724597720a27126d5"; // Can find item ids in `database\templates\items.json`
// View function documentation for what all the parameters are
this.addSingleItemToAssort(assortTable, MILK_ID, true, 9999999, 1, Money.ROUBLES, 1);
// Get the mp133 preset and add to the traders assort (Could make your own Items[] array, doesnt have to be presets)
const mp133GunPreset = tables.globals.ItemPresets["584148f2245977598f1ad387"]._items;
this.addCollectionToAssort(jsonUtil, assortTable, mp133GunPreset, false, 5, 1, Money.ROUBLES, 500);
return assortTable;
}
/**
* Add item to assortTable + barter scheme + loyalty level objects
* @param assortTable trader assorts to add item to
* @param itemTpl Items tpl to add to traders assort
* @param unlimitedCount Can an unlimited number of this item be purchased from trader
* @param stackCount Total size of item stack trader sells
* @param loyaltyLevel Loyalty level item can be purchased at
* @param currencyType What currency does item sell for
* @param currencyValue Amount of currency item can be purchased for
*/
private addSingleItemToAssort(assortTable: ITraderAssort, itemTpl: string, unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number)
{
// Define item in the table
const newItem: Item = {
_id: itemTpl,
_tpl: itemTpl,
parentId: "hideout",
slotId: "hideout",
upd: {
UnlimitedCount: unlimitedCount,
StackObjectsCount: stackCount
}
};
assortTable.items.push(newItem);
// Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made)
assortTable.barter_scheme[itemTpl] = [
[
{
count: currencyValue,
_tpl: currencyType
}
]
];
// Set loyalty level needed to unlock item
assortTable.loyal_level_items[itemTpl] = loyaltyLevel;
}
/**
* Add a complex item to trader assort (item with child items)
* @param assortTable trader assorts to add items to
* @param jsonUtil JSON utility class
* @param items Items array to add to assort
* @param unlimitedCount Can an unlimited number of this item be purchased from trader
* @param stackCount Total size of item stack trader sells
* @param loyaltyLevel Loyalty level item can be purchased at
* @param currencyType What currency does item sell for
* @param currencyValue Amount of currency item can be purchased for
*/
private addCollectionToAssort(jsonUtil: JsonUtil, assortTable: ITraderAssort, items: Item[], unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number): void
{
// Deserialize and serialize to ensure we dont alter the original data
const collectionToAdd: Item[] = jsonUtil.deserialize(jsonUtil.serialize(items));
// Update item base with values needed to make item sellable by trader
collectionToAdd[0].upd = {
UnlimitedCount: unlimitedCount,
StackObjectsCount: stackCount
}
collectionToAdd[0].parentId = "hideout";
collectionToAdd[0].slotId = "hideout";
// Push all the items into the traders assort table
assortTable.items.push(...collectionToAdd);
// Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made)
assortTable.barter_scheme[collectionToAdd[0]._id] = [
[
{
count: currencyValue,
_tpl: currencyType
}
]
];
// Set loyalty level needed to unlock item
assortTable.loyal_level_items[collectionToAdd[0]._id] = loyaltyLevel;
}
/**
* Add traders name/location/description to the locale table
* @param tables database tables
* @param fullName fullname of trader
* @param firstName first name of trader
* @param nickName nickname of trader
* @param location location of trader
* @param description description of trader
*/
private addTraderToLocales(tables: IDatabaseTables, fullName: string, firstName: string, nickName: string, location: string, description: string)
{
// For each language, add locale for the new trader
const locales = Object.values(tables.locales.global) as Record<string, string>[];
for (const locale of locales) {
locale[`${baseJson._id} FullName`] = fullName;
locale[`${baseJson._id} FirstName`] = firstName;
locale[`${baseJson._id} Nickname`] = nickName;
locale[`${baseJson._id} Location`] = location;
locale[`${baseJson._id} Description`] = description;
}
}
private addItemToLocales(tables: IDatabaseTables, itemTpl: string, name: string, shortName: string, Description: string)
{
// For each language, add locale for the new trader
const locales = Object.values(tables.locales.global) as Record<string, string>[];
for (const locale of locales) {
locale[`${itemTpl} Name`] = name;
locale[`${itemTpl} ShortName`] = shortName;
locale[`${itemTpl} Description`] = Description;
}
}
}
module.exports = { mod: new SampleTrader() }

View File

@ -0,0 +1,326 @@
import { PreAkiModLoader } from "@spt-aki/loaders/PreAkiModLoader";
import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITraderBase, ITraderAssort } from "@spt-aki/models/eft/common/tables/ITrader";
import { Money } from "@spt-aki/models/enums/Money";
import { ITraderConfig, UpdateTime } from "@spt-aki/models/spt/config/ITraderConfig";
import { IDatabaseTables } from "@spt-aki/models/spt/server/IDatabaseTables";
import { ImageRouter } from "@spt-aki/routers/ImageRouter";
import { JsonUtil } from "@spt-aki/utils/JsonUtil";
export class TraderHelper
{
/**
* Add profile picture to our trader
* @param baseJson json file for trader (db/base.json)
* @param preAkiModLoader mod loader class - used to get the mods file path
* @param imageRouter image router class - used to register the trader image path so we see their image on trader page
* @param traderImageName Filename of the trader icon to use
*/
public registerProfileImage(baseJson: any, modName: string, preAkiModLoader: PreAkiModLoader, imageRouter: ImageRouter, traderImageName: string): void
{
// Reference the mod "res" folder
const imageFilepath = `./${preAkiModLoader.getModPath(modName)}res`;
// Register a route to point to the profile picture - remember to remove the .jpg from it
imageRouter.addRoute(baseJson.avatar.replace(".jpg", ""), `${imageFilepath}/${traderImageName}`);
}
/**
* Add record to trader config to set the refresh time of trader in seconds (default is 60 minutes)
* @param traderConfig trader config to add our trader to
* @param baseJson json file for trader (db/base.json)
* @param refreshTimeSeconds How many sections between trader stock refresh
*/
public setTraderUpdateTime(traderConfig: ITraderConfig, baseJson: any, refreshTimeSeconds: number): void
{
// Add refresh time in seconds to config
const traderRefreshRecord: UpdateTime = {
traderId: baseJson._id,
seconds: refreshTimeSeconds };
traderConfig.updateTime.push(traderRefreshRecord);
}
/**
* Add our new trader to the database
* @param traderDetailsToAdd trader details
* @param tables database
* @param jsonUtil json utility class
*/
// rome-ignore lint/suspicious/noExplicitAny: traderDetailsToAdd comes from base.json, so no type
public addTraderToDb(traderDetailsToAdd: any, tables: IDatabaseTables, jsonUtil: JsonUtil): void
{
// Add trader to trader table, key is the traders id
tables.traders[traderDetailsToAdd._id] = {
assort: this.createAssortTable(), // assorts are the 'offers' trader sells, can be a single item (e.g. carton of milk) or multiple items as a collection (e.g. a gun)
base: jsonUtil.deserialize(jsonUtil.serialize(traderDetailsToAdd)) as ITraderBase, // Deserialise/serialise creates a copy of the json and allows us to cast it as an ITraderBase
questassort: {
started: {},
success: {},
fail: {}
} // questassort is empty as trader has no assorts unlocked by quests
};
}
/**
* Create basic data for trader + add empty assorts table for trader
* @param tables SPT db
* @param jsonUtil SPT JSON utility class
* @returns ITraderAssort
*/
private createAssortTable(): ITraderAssort
{
// Create a blank assort object, ready to have items added
const assortTable: ITraderAssort = {
nextResupply: 0,
items: [],
barter_scheme: {},
loyal_level_items: {}
}
return assortTable;
}
/**
* Add basic items to trader
* @param tables SPT db
* @param traderId Traders id (basejson/_id value)
*/
public addSingleItemsToTrader(tables: IDatabaseTables, traderId: string)
{
// Get the table that can hold our new items
const traderAssortTable = tables.traders[traderId].assort
// Add milk, unlimited stock for 1 rouble with no buy restrictions
const MILK_ID = "575146b724597720a27126d5"; // Can find item ids in `database\templates\items.json` or with https://db.sp-tarkov.com/search
this.addSingleItemToAssort(traderAssortTable, MILK_ID, true, 9999999, 1, Money.ROUBLES, 1, false, 0);
// Add salewa with 50 stock for 500 dollars + buy restriction of 2 per refresh
const SALEWA_ID = "544fb45d4bdc2dee738b4568";
this.addSingleItemToAssort(traderAssortTable, SALEWA_ID, false, 50, 1, Money.DOLLARS, 500, true, 2);
}
/**
* Add items with sub items to trader
* @param tables SPT db
* @param traderId Traders id (basejson/_id value)
* @param jsonUtil SPT JSON utility class
*/
public addComplexItemsToTrader(tables: IDatabaseTables, traderId: string, jsonUtil: JsonUtil)
{
// Get the table that can hold our new items
const traderAssortTable = tables.traders[traderId].assort
// Get the mp133 preset and add to the traders assort (Could make your own Items[] array, doesn't have to be from presets)
const mp133GunPreset = tables.globals.ItemPresets["584148f2245977598f1ad387"]._items;
this.addItemWithSubItemsToAssort(jsonUtil, traderAssortTable, mp133GunPreset, false, 5, 1, Money.ROUBLES, 500, false, 0);
// Create a pistol with some mods + add to trader
const customGlock17 = this.createGlock();
this.addItemWithSubItemsToAssort(jsonUtil, traderAssortTable, customGlock17, true, 69, 1, Money.EUROS, 5, true, 2);
}
/**
* Create a weapon from scratch, ready to be added to trader
* @returns Item[]
*/
private createGlock(): Item[]
{
// Create an array ready to hold weapon + all mods
const glock: Item[] = [];
// Add the base first
glock.push({ // Add the base weapon first
_id: "glockBase", // Ids dont matter, as long as they are unique (can use hashUtil.generate() if you dont want to type every id by hand)
_tpl: "5a7ae0c351dfba0017554310" // This is the weapons tpl, found on: https://db.sp-tarkov.com/search
});
// Add barrel
glock.push({
_id: "glockbarrel",
_tpl: "5a6b60158dc32e000a31138b",
parentId: "glockBase", // This is a sub item, you need to define its parent its attached to / inserted into
slotId: "mod_barrel" // Required for mods, you need to define what 'role' they have
});
// Add reciever
glock.push({
_id: "glockReciever",
_tpl:"5a9685b1a2750c0032157104",
parentId: "glockBase",
slotId: "mod_reciever"
});
// Add compensator
glock.push({
_id: "glockCompensator",
_tpl:"5a7b32a2e899ef00135e345a",
parentId: "glockReciever", // The parent of this mod is the reciever NOT weapon, be careful to get the correct parent
slotId: "mod_muzzle"
});
// Add Pistol grip
glock.push({
_id: "glockPistolGrip",
_tpl:"5a7b4960e899ef197b331a2d",
parentId: "glockBase",
slotId: "mod_pistol_grip"
});
// Add front sight
glock.push({
_id: "glockRearSight",
_tpl: "5a6f5d528dc32e00094b97d9",
parentId: "glockReciever",
slotId: "mod_sight_rear"
});
// Add rear sight
glock.push({
_id: "glockFrontSight",
_tpl: "5a6f58f68dc32e000a311390",
parentId: "glockReciever",
slotId: "mod_sight_front"
});
// Add magazine
glock.push({
_id: "glockMagazine",
_tpl: "630769c4962d0247b029dc60",
parentId: "glockBase",
slotId: "mod_magazine"
});
return glock;
}
/**
* Add item to assortTable + barter scheme + loyalty level objects
* @param assortTable Trader assorts to add item to
* @param itemTpl Items tpl to add to traders assort
* @param unlimitedCount Can an unlimited number of this item be purchased from trader
* @param stackCount Total size of item stack trader sells
* @param loyaltyLevel Loyalty level item can be purchased at
* @param currencyType What currency does item sell for
* @param currencyValue Amount of currency item can be purchased for
* @param hasBuyRestriction Does the item have a max purchase amount
* @param buyRestrictionMax How many times can item be purchased per trader refresh
*/
private addSingleItemToAssort(assortTable: ITraderAssort, itemTpl: string, unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number, hasBuyRestriction: boolean, buyRestrictionMax: number)
{
// Create item ready for insertion into assort table
const newItemToAdd: Item = {
_id: itemTpl,
_tpl: itemTpl,
parentId: "hideout", // Should always be "hideout"
slotId: "hideout", // Should always be "hideout"
upd: {
UnlimitedCount: unlimitedCount,
StackObjectsCount: stackCount
}
};
// Items can have a buy restriction per trader refresh cycle, optional
if(hasBuyRestriction)
{
newItemToAdd.upd.BuyRestrictionMax = buyRestrictionMax;
newItemToAdd.upd.BuyRestrictionCurrent = 0;
}
assortTable.items.push(newItemToAdd);
// Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made)
assortTable.barter_scheme[itemTpl] = [
[
{
count: currencyValue,
_tpl: currencyType
}
]
];
// Set loyalty level needed to unlock item
assortTable.loyal_level_items[itemTpl] = loyaltyLevel;
}
/**
* Add a complex item to trader assort (item with child items)
* @param assortTable trader assorts to add items to
* @param jsonUtil JSON utility class
* @param items Items array to add to assort
* @param unlimitedCount Can an unlimited number of this item be purchased from trader
* @param stackCount Total size of item stack trader sells
* @param loyaltyLevel Loyalty level item can be purchased at
* @param currencyType What currency does item sell for
* @param currencyValue Amount of currency item can be purchased for
* @param hasBuyRestriction Does the item have a max purchase amount
* @param buyRestrictionMax How many times can item be purchased per trader refresh
*/
private addItemWithSubItemsToAssort(jsonUtil: JsonUtil, assortTable: ITraderAssort, items: Item[], unlimitedCount: boolean, stackCount: number, loyaltyLevel: number, currencyType: Money, currencyValue: number, hasBuyRestriction: boolean, buyRestrictionMax: number): void
{
// Deserialize and serialize to ensure we dont alter the original data (clone it)
const collectionToAdd: Item[] = jsonUtil.deserialize(jsonUtil.serialize(items));
// Create upd object if its missing
if (!collectionToAdd[0].upd)
{
collectionToAdd[0].upd = {};
}
// Update item base with values needed to make item sellable by trader
collectionToAdd[0].upd = {
UnlimitedCount: unlimitedCount,
StackObjectsCount: stackCount
}
// Items can have a buy restriction per trader refresh cycle, optional
if(hasBuyRestriction)
{
collectionToAdd[0].upd.BuyRestrictionMax = buyRestrictionMax;
collectionToAdd[0].upd.BuyRestrictionCurrent = 0;
}
// First item should always have both properties set to 'hideout'
collectionToAdd[0].parentId = "hideout";
collectionToAdd[0].slotId = "hideout";
// Push all the items into the traders assort table
assortTable.items.push(...collectionToAdd);
// Barter scheme holds the cost of the item + the currency needed (doesnt need to be currency, can be any item, this is how barter traders are made)
assortTable.barter_scheme[collectionToAdd[0]._id] = [
[
{
count: currencyValue,
_tpl: currencyType
}
]
];
// Set loyalty level needed to unlock item
assortTable.loyal_level_items[collectionToAdd[0]._id] = loyaltyLevel;
}
/**
* Add traders name/location/description to the locale table
* @param baseJson json file for trader (db/base.json)
* @param tables database tables
* @param fullName Complete name of trader
* @param firstName First name of trader
* @param nickName Nickname of trader
* @param location Location of trader (e.g. "Here in the cat shop")
* @param description Description of trader
*/
public addTraderToLocales(baseJson: any, tables: IDatabaseTables, fullName: string, firstName: string, nickName: string, location: string, description: string)
{
// For each language, add locale for the new trader
const locales = Object.values(tables.locales.global) as Record<string, string>[];
for (const locale of locales) {
locale[`${baseJson._id} FullName`] = fullName;
locale[`${baseJson._id} FirstName`] = firstName;
locale[`${baseJson._id} Nickname`] = nickName;
locale[`${baseJson._id} Location`] = location;
locale[`${baseJson._id} Description`] = description;
}
}
}

View File

@ -2,7 +2,7 @@ import { DependencyContainer } from "tsyringe";
import { IPostAkiLoadMod } from "@spt-aki/models/external/IPostAkiLoadMod";
import { ILogger } from "@spt-aki/models/spt/utils/ILogger";
import {MoreCode } from "./MoreCode";
import { MoreCode } from "./MoreCode";
class Mod implements IPostAkiLoadMod
{