From b478dea62874d70a4d8654f73da353545612d119 Mon Sep 17 00:00:00 2001 From: Trap Date: Sat, 28 Dec 2024 20:18:34 +0100 Subject: [PATCH] feat: configurable exfil prompt message --- PTT-Plugin/Data/ExfilsTargets.cs | 16 +++- configs/Default/config.json | 12 ++- src/config.ts | 5 ++ src/helpers.ts | 32 ++++++++ src/path-to-tarkov-controller.ts | 100 +++++++++++++++++++++++- src/services/ExfilsTooltipsTemplater.ts | 76 +++--------------- src/services/LocaleResolver.ts | 30 +++++++ 7 files changed, 196 insertions(+), 75 deletions(-) create mode 100644 src/services/LocaleResolver.ts diff --git a/PTT-Plugin/Data/ExfilsTargets.cs b/PTT-Plugin/Data/ExfilsTargets.cs index eb92cf73..c562db7e 100644 --- a/PTT-Plugin/Data/ExfilsTargets.cs +++ b/PTT-Plugin/Data/ExfilsTargets.cs @@ -32,15 +32,25 @@ public class ExfilTarget public string transitSpawnPointId; // transit only public string offraidPosition; // empty on transit - // TODO: i18n support (use the offraid position displayName) public string GetCustomActionName() { if (isTransit) { - return $"Transit to {transitMapId}"; + string transitTemplate = "PTT_TRANSITS_PROMPT_TEMPLATE".Localized(); + return string.Format(transitTemplate, transitMapId.Localized()); } - return $"Extract to {offraidPosition}"; + string extractTemplate = "PTT_EXTRACTS_PROMPT_TEMPLATE".Localized(); + string offraidPositionDisplayNameKey = $"PTT_OFFRAIDPOS_DISPLAY_NAME_{offraidPosition}"; + string offraidPositionDisplayName = offraidPositionDisplayNameKey.Localized(); + + // when the offraid position display name cannot be resolved + if (offraidPositionDisplayName == offraidPositionDisplayNameKey) + { + return string.Format(extractTemplate, offraidPosition); + } + + return string.Format(extractTemplate, offraidPositionDisplayName); } public string GetCustomExitName(ExfiltrationPoint exfil) diff --git a/configs/Default/config.json b/configs/Default/config.json index 9dfd6ec4..84685ad1 100644 --- a/configs/Default/config.json +++ b/configs/Default/config.json @@ -1123,6 +1123,14 @@ } }, "exfiltrations_tooltips_template": "$exfilDisplayName -> $offraidPositionDisplayName", + "extracts_prompt_template": { + "en": "Extract: {0}", + "fr": "Extraction: {0}" + }, + "transits_prompt_template": { + "en": "Transit: {0}", + "fr": "Transit: {0}" + }, "offraid_positions": { "PraporHideout": { "displayName": { @@ -1186,8 +1194,8 @@ }, "FactoryZB-1011": { "displayName": { - "en": "Customs/Factory", - "fr": "Douanes/Usine" + "en": "Bunker 11 Customs/Factory", + "fr": "Bunker 11 Douanes/Usine" } }, "SniperRB": { diff --git a/src/config.ts b/src/config.ts index a95c7012..4b75970c 100644 --- a/src/config.ts +++ b/src/config.ts @@ -46,6 +46,7 @@ type AvailableLocales = { }; export type ByLocale = Partial>; +export type ByLocaleFull = AvailableLocales; export const INDEXED_AVAILABLE_LOCALES: AvailableLocales = { ch: true, @@ -75,6 +76,8 @@ export const isLocalAvailable = (givenLocale: string): boolean => { export const AVAILABLE_LOCALES: string[] = Object.keys(INDEXED_AVAILABLE_LOCALES); +export const DEFAULT_FALLBACK_LANGUAGE = 'en'; + type ByProfileId = Record; export type MapName = keyof ByMap; @@ -226,6 +229,8 @@ type RawConfig = { infiltrations_config?: InfiltrationsConfig; exfiltrations_config?: Record; // TODO: validate in config-analysis exfiltrations_tooltips_template?: string; // TODO(config-analysis): error when unknown template variable usage is found + transits_prompt_template?: ByLocale; // TODO: validate in config-analysis + extracts_prompt_template?: ByLocale; // TODO: validate in config-analysis offraid_positions?: Record; // TODO: validate in config-analysis }; diff --git a/src/helpers.ts b/src/helpers.ts index 364c20b0..41dedbfe 100644 --- a/src/helpers.ts +++ b/src/helpers.ts @@ -7,6 +7,7 @@ import type { StaticRouterModService } from '@spt/services/mod/staticRouter/Stat import type { AccessVia, Config, PositionXYZ, Profile, SpawnPoint, StashConfig } from './config'; import { EMPTY_STASH, SLOT_ID_HIDEOUT, SLOT_ID_LOCKED_STASH, VANILLA_STASH_IDS } from './config'; import type { IItem } from '@spt/models/eft/common/tables/IItem'; +import type { AllLocalesInDb } from './services/LocaleResolver'; export function checkAccessVia(access_via: AccessVia, value: string): boolean { return access_via === '*' || access_via[0] === '*' || access_via.includes(value); @@ -309,3 +310,34 @@ export const retrieveMainStashIdFromItems = ( return null; }; + +export type LocalesMutationReport = { + nbLocalesImpacted: number; + nbTotalValuesUpdated: number; +}; + +export function mutateLocales( + allLocales: AllLocalesInDb, + partialLocales: Partial, +): LocalesMutationReport { + const report: LocalesMutationReport = { + nbLocalesImpacted: 0, + nbTotalValuesUpdated: 0, + }; + + void Object.keys(allLocales).forEach(localeName => { + if (partialLocales[localeName]) { + const values = allLocales[localeName]; + const newValues = partialLocales[localeName] ?? {}; + const nbNewValues = Object.keys(newValues).length; + + if (nbNewValues > 0) { + void Object.assign(values, newValues); // mutation here + report.nbLocalesImpacted += 1; + report.nbTotalValuesUpdated += nbNewValues; + } + } + }); + + return report; +} diff --git a/src/path-to-tarkov-controller.ts b/src/path-to-tarkov-controller.ts index 2b7ae979..9fb3f089 100644 --- a/src/path-to-tarkov-controller.ts +++ b/src/path-to-tarkov-controller.ts @@ -5,8 +5,16 @@ import type { ConfigServer } from '@spt/servers/ConfigServer'; import type { DatabaseServer } from '@spt/servers/DatabaseServer'; import type { SaveServer } from '@spt/servers/SaveServer'; -import type { Config, ConfigGetter, MapName, Profile, SpawnConfig } from './config'; -import { MAPLIST, VANILLA_STASH_IDS } from './config'; +import type { + ByLocale, + Config, + ConfigGetter, + LocaleName, + MapName, + Profile, + SpawnConfig, +} from './config'; +import { DEFAULT_FALLBACK_LANGUAGE, MAPLIST, VANILLA_STASH_IDS } from './config'; import { changeRestrictionsInRaid, @@ -16,6 +24,7 @@ import { disableRunThrough, isIgnoredArea, isPlayerSpawnPoint, + mutateLocales, PTT_INFILTRATION, } from './helpers'; @@ -40,6 +49,7 @@ import { fixRepeatableQuestsForPmc } from './fix-repeatable-quests'; import { KeepFoundInRaidTweak } from './keep-fir-tweak'; import { ExfilsTooltipsTemplater } from './services/ExfilsTooltipsTemplater'; import type { RaidCache } from './event-watcher'; +import type { AllLocalesInDb } from './services/LocaleResolver'; type IndexedLocations = Record; @@ -122,6 +132,8 @@ export class PathToTarkovController { this.tradersAvailabilityService.init(quests); this.injectTooltipsInLocales(config); + this.injectPromptTemplatesInLocales(config); + this.injectOffraidPositionDisplayNamesInLocales(config); this.tradersController.initTraders(config); const nbAddedTemplates = this.stashController.initSecondaryStashTemplates( @@ -248,7 +260,7 @@ export class PathToTarkovController { } const partialLocales = this.tooltipsTemplater.computeLocales(config); - const report = ExfilsTooltipsTemplater.mutateLocales(allLocales, partialLocales); + const report = mutateLocales(allLocales, partialLocales); const nbValuesUpdated = report.nbTotalValuesUpdated / report.nbLocalesImpacted; this.debug( @@ -256,6 +268,88 @@ export class PathToTarkovController { ); } + // TODO: refactor in a dedicated service + // TODO: make it dynamic (aka intercept instead of mutating the db) + private injectPromptTemplatesInLocales(config: Config): void { + const allLocales = this.db.getTables()?.locales?.global; + + if (!allLocales) { + throw new Error('Path To Tarkov: no locales found in db'); + } + + // 1. prepare transits_prompt_template + const DEFAULT_TRANSITS_PROMPT_TEMPLATE_KEY = 'PTT_TRANSITS_PROMPT_TEMPLATE'; + const DEFAULT_TRANSITS_PROMPT_TEMPLATE_VALUE = 'Transit: {0}'; + const DEFAULT_TRANSITS_PROMPT_TEMPLATE: ByLocale = { + [DEFAULT_FALLBACK_LANGUAGE]: DEFAULT_TRANSITS_PROMPT_TEMPLATE_VALUE, + }; + const transitsPromptTemplate = + config.transits_prompt_template ?? DEFAULT_TRANSITS_PROMPT_TEMPLATE; + + // 2. prepare extracts_prompt_template + const DEFAULT_EXTRACTS_PROMPT_TEMPLATE_KEY = 'PTT_EXTRACTS_PROMPT_TEMPLATE'; + const DEFAULT_EXTRACTS_PROMPT_TEMPLATE_VALUE = 'Extract: {0}'; + const DEFAULT_EXTRACTS_PROMPT_TEMPLATE: ByLocale = { + [DEFAULT_FALLBACK_LANGUAGE]: DEFAULT_EXTRACTS_PROMPT_TEMPLATE_VALUE, + }; + const extractsPromptTemplate = + config.extracts_prompt_template ?? DEFAULT_EXTRACTS_PROMPT_TEMPLATE; + + // 3. prepare new locales + const newLocales: Partial = {}; + Object.keys(allLocales).forEach(locale => { + const localeValues: Record = { + [DEFAULT_TRANSITS_PROMPT_TEMPLATE_KEY]: + transitsPromptTemplate[locale as LocaleName] ?? DEFAULT_TRANSITS_PROMPT_TEMPLATE_VALUE, + [DEFAULT_EXTRACTS_PROMPT_TEMPLATE_KEY]: + extractsPromptTemplate[locale as LocaleName] ?? DEFAULT_EXTRACTS_PROMPT_TEMPLATE_VALUE, + }; + newLocales[locale] = localeValues; + }); + + // 4. mutate locales + const report = mutateLocales(allLocales, newLocales); + + const nbValuesUpdated = report.nbTotalValuesUpdated / report.nbLocalesImpacted; + this.debug( + `${nbValuesUpdated} prompt templates values updated for ${report.nbLocalesImpacted} locales (total of ${report.nbTotalValuesUpdated})`, + ); + } + + private injectOffraidPositionDisplayNamesInLocales(config: Config): void { + const allLocales = this.db.getTables()?.locales?.global; + + if (!allLocales) { + throw new Error('Path To Tarkov: no locales found in db'); + } + + // 1. create new locales + const newLocales: Partial = {}; + Object.keys(allLocales).forEach(locale => { + const localeValues: Record = {}; + + const offraidPositions = config.offraid_positions ?? {}; + Object.keys(offraidPositions).forEach(offraidPosition => { + const displayNameValue = ExfilsTooltipsTemplater.resolveOffraidPositionDisplayName(config, { + offraidPosition, + locale: locale as LocaleName, + }); + + localeValues[`PTT_OFFRAIDPOS_DISPLAY_NAME_${offraidPosition}`] = displayNameValue; + }); + + newLocales[locale] = localeValues; + }); + + // 2. mutate locales + const report = mutateLocales(allLocales, newLocales); + + const nbValuesUpdated = report.nbTotalValuesUpdated / report.nbLocalesImpacted; + this.debug( + `${nbValuesUpdated} prompt templates values updated for ${report.nbLocalesImpacted} locales (total of ${report.nbTotalValuesUpdated})`, + ); + } + private getRespawnOffraidPosition = (sessionId: string): string => { const profile: Profile = this.saveServer.getProfile(sessionId); const profileTemplateId = profile.info.edition; diff --git a/src/services/ExfilsTooltipsTemplater.ts b/src/services/ExfilsTooltipsTemplater.ts index b771b014..c5173da7 100644 --- a/src/services/ExfilsTooltipsTemplater.ts +++ b/src/services/ExfilsTooltipsTemplater.ts @@ -1,57 +1,25 @@ import { parseExilTargetFromPTTConfig } from '../exfils-targets'; import { AVAILABLE_LOCALES, + DEFAULT_FALLBACK_LANGUAGE, type ByLocale, type Config, type LocaleName, type MapName, } from '../config'; import { deepClone } from '../utils'; - -const DEFAULT_FALLBACK_LANGUAGE = 'en'; +import type { AllLocalesInDb } from './LocaleResolver'; +import { LocaleResolver } from './LocaleResolver'; +import { mutateLocales } from '../helpers'; const EXFIL_DISPLAY_NAME_VARIABLE = '$exfilDisplayName'; const OFFRAID_POSITION_DISPLAY_NAME_VARIABLE = '$offraidPositionDisplayName'; -type AllLocalesInDb = Record>; - -type LocaleKey = string; - -type LocaleKeysLowerCaseMapping = { - [localeName: string]: { - [lowerCasedLocaleKey: string]: LocaleKey; - }; -}; - -class ExfilsLocaleResolver { - private readonly localeKeysMapping: LocaleKeysLowerCaseMapping = {}; - - constructor(allLocales: AllLocalesInDb) { - void Object.keys(allLocales).forEach(localeName => { - const localeValues: Record = {}; - this.localeKeysMapping[localeName] = localeValues; - - void Object.keys(allLocales[localeName]).forEach(localeKey => { - localeValues[localeKey.toLowerCase()] = localeKey; - }); - }); - } - - public retrieveKey(exfilName: string, locale: LocaleName): string { - return this.localeKeysMapping?.[locale]?.[exfilName.toLowerCase()] ?? exfilName; - } -} - export type MinimumConfigForTooltipsTemplater = Pick< Config, 'exfiltrations' | 'exfiltrations_config' | 'exfiltrations_tooltips_template' | 'offraid_positions' >; -export type LocalesMutationReport = { - nbLocalesImpacted: number; - nbTotalValuesUpdated: number; -}; - export type ComputeLocaleValueParameter = { locale: LocaleName; localeKey: string; @@ -64,11 +32,11 @@ export class ExfilsTooltipsTemplater { // this is used to be sure to keep the vanilla locales even after mutations are applied to the database private readonly snapshotLocales: AllLocalesInDb; - private readonly localeResolver: ExfilsLocaleResolver; + private readonly localeResolver: LocaleResolver; constructor(allLocales: AllLocalesInDb) { this.snapshotLocales = deepClone(allLocales); - this.localeResolver = new ExfilsLocaleResolver(allLocales); + this.localeResolver = new LocaleResolver(allLocales); } public computeLocales(config: MinimumConfigForTooltipsTemplater): Partial { @@ -123,36 +91,10 @@ export class ExfilsTooltipsTemplater { }, {}, ); - void ExfilsTooltipsTemplater.mutateLocales(mergedLocales, partialLocales); + void mutateLocales(mergedLocales, partialLocales); return mergedLocales[locale]; } - public static mutateLocales( - allLocales: AllLocalesInDb, - partialLocales: Partial, - ): LocalesMutationReport { - const report: LocalesMutationReport = { - nbLocalesImpacted: 0, - nbTotalValuesUpdated: 0, - }; - - void Object.keys(allLocales).forEach(localeName => { - if (partialLocales[localeName]) { - const values = allLocales[localeName]; - const newValues = partialLocales[localeName] ?? {}; - const nbNewValues = Object.keys(newValues).length; - - if (nbNewValues > 0) { - void Object.assign(values, newValues); // mutation here - report.nbLocalesImpacted += 1; - report.nbTotalValuesUpdated += nbNewValues; - } - } - }); - - return report; - } - private computeLocaleValue( config: MinimumConfigForTooltipsTemplater, params: ComputeLocaleValueParameter, @@ -194,9 +136,9 @@ export class ExfilsTooltipsTemplater { return resolvedDisplayName; } - private static resolveOffraidPositionDisplayName( + public static resolveOffraidPositionDisplayName( config: MinimumConfigForTooltipsTemplater, - { offraidPosition, locale }: ComputeLocaleValueParameter, + { offraidPosition, locale }: { offraidPosition: string; locale: LocaleName }, ): string { const offraidPositionDefinition = config.offraid_positions?.[offraidPosition]; const resolvedDisplayName = ExfilsTooltipsTemplater.resolveDisplayName( diff --git a/src/services/LocaleResolver.ts b/src/services/LocaleResolver.ts new file mode 100644 index 00000000..ba72767d --- /dev/null +++ b/src/services/LocaleResolver.ts @@ -0,0 +1,30 @@ +import { type LocaleName } from '../config'; + +export type AllLocalesInDb = Record>; + +type LocaleKey = string; + +type LocaleKeysLowerCaseMapping = { + [localeName: string]: { + [lowerCasedLocaleKey: string]: LocaleKey; + }; +}; + +export class LocaleResolver { + private readonly localeKeysMapping: LocaleKeysLowerCaseMapping = {}; + + constructor(allLocales: AllLocalesInDb) { + void Object.keys(allLocales).forEach(localeName => { + const localeValues: Record = {}; + this.localeKeysMapping[localeName] = localeValues; + + void Object.keys(allLocales[localeName]).forEach(localeKey => { + localeValues[localeKey.toLowerCase()] = localeKey; + }); + }); + } + + public retrieveKey(exfilName: string, locale: LocaleName): string { + return this.localeKeysMapping?.[locale]?.[exfilName.toLowerCase()] ?? exfilName; + } +}