Fixed give command to use dice coefficient making it more precise. Fixed give command giving invalid items to players. (!323)

Co-authored-by: clodan <clodan@clodan.com>
Reviewed-on: https://dev.sp-tarkov.com/SPT-AKI/Server/pulls/323
Co-authored-by: Alex <clodan@noreply.dev.sp-tarkov.com>
Co-committed-by: Alex <clodan@noreply.dev.sp-tarkov.com>
This commit is contained in:
Alex 2024-05-05 08:01:18 +00:00 committed by chomp
parent 79a5d32cb2
commit 84d5462955
3 changed files with 55 additions and 44 deletions

View File

@ -33,7 +33,6 @@
"dependencies": { "dependencies": {
"atomically": "~1.7", "atomically": "~1.7",
"buffer-crc32": "^1.0.0", "buffer-crc32": "^1.0.0",
"closest-match": "~1.3",
"date-fns": "~2.30", "date-fns": "~2.30",
"date-fns-tz": "~2.0", "date-fns-tz": "~2.0",
"i18n": "~0.15", "i18n": "~0.15",
@ -44,6 +43,7 @@
"reflect-metadata": "~0.2", "reflect-metadata": "~0.2",
"semver": "~7.6", "semver": "~7.6",
"source-map-support": "~0.5", "source-map-support": "~0.5",
"string-similarity-js": "~2.1",
"tsyringe": "~4.8", "tsyringe": "~4.8",
"typescript": "~5.4", "typescript": "~5.4",
"winston": "~3.12", "winston": "~3.12",

View File

@ -3,6 +3,7 @@ import { ISptCommand } from "@spt-aki/helpers/Dialogue/Commando/SptCommands/ISpt
import { ItemHelper } from "@spt-aki/helpers/ItemHelper"; import { ItemHelper } from "@spt-aki/helpers/ItemHelper";
import { PresetHelper } from "@spt-aki/helpers/PresetHelper"; import { PresetHelper } from "@spt-aki/helpers/PresetHelper";
import { Item } from "@spt-aki/models/eft/common/tables/IItem"; import { Item } from "@spt-aki/models/eft/common/tables/IItem";
import { ITemplateItem } from "@spt-aki/models/eft/common/tables/ITemplateItem";
import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest"; import { ISendMessageRequest } from "@spt-aki/models/eft/dialog/ISendMessageRequest";
import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile"; import { IUserDialogInfo } from "@spt-aki/models/eft/profile/IAkiProfile";
import { BaseClasses } from "@spt-aki/models/enums/BaseClasses"; import { BaseClasses } from "@spt-aki/models/enums/BaseClasses";
@ -13,7 +14,7 @@ import { LocaleService } from "@spt-aki/services/LocaleService";
import { MailSendService } from "@spt-aki/services/MailSendService"; import { MailSendService } from "@spt-aki/services/MailSendService";
import { HashUtil } from "@spt-aki/utils/HashUtil"; import { HashUtil } from "@spt-aki/utils/HashUtil";
import { JsonUtil } from "@spt-aki/utils/JsonUtil"; import { JsonUtil } from "@spt-aki/utils/JsonUtil";
import { closestMatch, distance } from "closest-match"; import { stringSimilarity } from "string-similarity-js";
import { inject, injectable } from "tsyringe"; import { inject, injectable } from "tsyringe";
@injectable() @injectable()
@ -28,7 +29,7 @@ export class GiveSptCommand implements ISptCommand
* spt give 5 <== this is the reply when the algo isn't sure about an item * spt give 5 <== this is the reply when the algo isn't sure about an item
*/ */
private static commandRegex = /^spt give (((([a-z]{2,5}) )?"(.+)"|\w+) )?([0-9]+)$/; private static commandRegex = /^spt give (((([a-z]{2,5}) )?"(.+)"|\w+) )?([0-9]+)$/;
private static maxAllowedDistance = 1.5; private static acceptableConfidence = 0.9;
protected savedCommand: Map<string, SavedCommand> = new Map<string, SavedCommand>(); protected savedCommand: Map<string, SavedCommand> = new Map<string, SavedCommand>();
@ -125,44 +126,51 @@ export class GiveSptCommand implements ISptCommand
if (isItemName) if (isItemName)
{ {
locale = result[4] ? result[4] : this.localeService.getDesiredGameLocale(); try
if (!this.localeService.getServerSupportedLocales().includes(locale)) {
locale = result[4] ? result[4] : (this.localeService.getDesiredGameLocale() ?? "en");
if (!this.localeService.getServerSupportedLocales().includes(locale))
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Unknown locale "${locale}". Use \"help\" for more information.`,
);
return request.dialogId;
}
}
catch (e)
{ {
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
`Unknown locale "${locale}". Use \"help\" for more information.`, `An error occurred while trying to use localized text. Locale will be defaulted to 'en'.`,
); );
return request.dialogId; this.logger.error(e);
locale = "en";
} }
const localizedGlobal = this.databaseServer.getTables().locales.global[locale]; const localizedGlobal = this.databaseServer.getTables().locales.global[locale];
const closestItemsMatchedByName = closestMatch( const closestItemsMatchedByName = this.itemHelper.getItems()
item.toLowerCase(), .filter((i) => this.isItemAllowed(i))
this.itemHelper.getItems().filter((i) => i._type !== "Node").filter((i) => .map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase())
!this.itemFilterService.isItemBlacklisted(i._id) .filter((i) => i !== undefined)
).map((i) => localizedGlobal[`${i?._id} Name`]?.toLowerCase()).filter((i) => i !== undefined), .map(i => ({match: stringSimilarity(item.toLocaleLowerCase(), i.toLocaleLowerCase()), itemName: i}))
true, .sort((a1, a2) => a2.match - a1.match);
) as string[];
if (closestItemsMatchedByName === undefined || closestItemsMatchedByName.length === 0) if (closestItemsMatchedByName[0].match >= GiveSptCommand.acceptableConfidence)
{ {
this.mailSendService.sendUserMessageToPlayer( item = closestItemsMatchedByName[0].itemName;
sessionId,
commandHandler,
"That item could not be found. Please refine your request and try again.",
);
return request.dialogId;
} }
else
if (closestItemsMatchedByName.length > 1)
{ {
let i = 1; let i = 1;
const slicedItems = closestItemsMatchedByName.slice(0, 10); const slicedItems = closestItemsMatchedByName.slice(0, 10);
// max 10 item names and map them // max 10 item names and map them
const itemList = slicedItems.map((itemName) => `${i++}. ${itemName}`).join("\n"); const itemList = slicedItems.map((match) => `${i++}. ${match.itemName} (conf: ${(match.match * 100).toFixed(2)})`)
this.savedCommand.set(sessionId, new SavedCommand(quantity, slicedItems, locale)); .join("\n");
this.savedCommand.set(sessionId, new SavedCommand(quantity, slicedItems.map(i => i.itemName), locale));
this.mailSendService.sendUserMessageToPlayer( this.mailSendService.sendUserMessageToPlayer(
sessionId, sessionId,
commandHandler, commandHandler,
@ -170,30 +178,15 @@ export class GiveSptCommand implements ISptCommand
); );
return request.dialogId; return request.dialogId;
} }
const dist = distance(item, closestItemsMatchedByName[0]);
if (dist > GiveSptCommand.maxAllowedDistance)
{
this.mailSendService.sendUserMessageToPlayer(
sessionId,
commandHandler,
`Found a possible match for "${item}" but uncertain. Match: "${
closestItemsMatchedByName[0]
}". Please refine your request and try again.`,
);
return request.dialogId;
}
// Only one available so we get that entry and use it
item = closestItemsMatchedByName[0];
} }
} }
// If item is an item name, we need to search using that item name and the locale which one we want otherwise // If item is an item name, we need to search using that item name and the locale which one we want otherwise
// item is just the tplId. // item is just the tplId.
const tplId = isItemName const tplId = isItemName
? this.itemHelper.getItems().filter((i) => !this.itemFilterService.isItemBlacklisted(i._id)).find((i) => ? this.itemHelper.getItems()
this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() === item .filter((i) => this.isItemAllowed(i))
)._id .find((i) => this.databaseServer.getTables().locales.global[locale][`${i?._id} Name`]?.toLowerCase() === item)._id
: item; : item;
const checkedItem = this.itemHelper.getItem(tplId); const checkedItem = this.itemHelper.getItem(tplId);
@ -285,4 +278,21 @@ export class GiveSptCommand implements ISptCommand
this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend); this.mailSendService.sendSystemMessageToPlayer(sessionId, "SPT GIVE", itemsToSend);
return request.dialogId; return request.dialogId;
} }
/**
* A "simple" function that checks if an item is supposed to be given to a player or not
* @param templateItem the template item to check
* @returns true if its obtainable, false if its not
*/
protected isItemAllowed(templateItem: ITemplateItem): boolean
{
return templateItem._type !== "Node" &&
!this.itemHelper.isQuestItem(templateItem._id) &&
!this.itemFilterService.isItemBlacklisted(templateItem._id) &&
(templateItem._props?.Prefab?.path ?? "") !== "" &&
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.HIDEOUT_AREA_CONTAINER) &&
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.LOOT_CONTAINER) &&
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.RANDOM_LOOT_CONTAINER) &&
!this.itemHelper.isOfBaseclass(templateItem._id, BaseClasses.MOB_CONTAINER);
}
} }

View File

@ -111,4 +111,5 @@ export enum BaseClasses
BARREL = "555ef6e44bdc2de9068b457e", BARREL = "555ef6e44bdc2de9068b457e",
CHARGING_HANDLE = "55818a6f4bdc2db9688b456b", CHARGING_HANDLE = "55818a6f4bdc2db9688b456b",
COMB_MUZZLE_DEVICE = "550aa4dd4bdc2dc9348b4569 ", COMB_MUZZLE_DEVICE = "550aa4dd4bdc2dc9348b4569 ",
HIDEOUT_AREA_CONTAINER = "63da6da4784a55176c018dba"
} }