feat(UIFixes)

This commit is contained in:
GetParanoid 2025-07-18 16:07:32 -05:00
parent 395345ad51
commit 407cffddd5
9 changed files with 388 additions and 0 deletions

Binary file not shown.

Binary file not shown.

View file

@ -0,0 +1,3 @@
{
"putToolsBack": true
}

View file

@ -0,0 +1,32 @@
{
"name": "uifixes",
"version": "4.2.1",
"main": "src/mod.js",
"license": "MIT",
"author": "Tyfon",
"sptVersion": "~3.11",
"loadBefore": [],
"loadAfter": [],
"incompatibilities": [],
"contributors": [],
"isBundleMod": false,
"scripts": {
"setup": "npm i",
"build": "node ./build.mjs",
"buildinfo": "node ./build.mjs --verbose"
},
"devDependencies": {
"@types/node": "22.12.0",
"@typescript-eslint/eslint-plugin": "7.2",
"@typescript-eslint/parser": "7.2",
"archiver": "^6.0",
"eslint": "8.57",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.1.3",
"fs-extra": "11.2",
"ignore": "^5.2",
"tsyringe": "4.8.0",
"typescript": "5.8.2",
"winston": "3.12"
}
}

View file

@ -0,0 +1,57 @@
import type { DependencyContainer } from "tsyringe";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { DatabaseService } from "@spt/services/DatabaseService";
import type { StaticRouterModService } from "@spt/services/mod/staticRouter/StaticRouterModService";
export const assortUnlocks = (container: DependencyContainer) => {
const logger = container.resolve<ILogger>("PrimaryLogger");
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
const databaseService = container.resolve<DatabaseService>("DatabaseService");
const loadAssortmentUnlocks = () => {
const traders = databaseService.getTraders();
const quests = databaseService.getQuests();
const result: Record<string, string> = {};
for (const traderId in traders) {
const trader = traders[traderId];
if (trader.questassort) {
for (const questStatus in trader.questassort) {
// Explicitly check that quest status is an expected value - some mods accidently import in such a way that adds a "default" value
if (!["started", "success", "fail"].includes(questStatus)) {
continue;
}
for (const assortId in trader.questassort[questStatus]) {
const questId = trader.questassort[questStatus][assortId];
if (!quests[questId]) {
logger.warning(
`UIFixes: Trader ${traderId} questassort references unknown quest ${JSON.stringify(questId)}! Check that whatever mod added that trader and/or quest is installed correctly.`
);
continue;
}
result[assortId] = quests[questId].name;
}
}
}
}
return result;
};
staticRouterModService.registerStaticRouter(
"UIFixesRoutes",
[
{
url: "/uifixes/assortUnlocks",
action: async (url, info, sessionId, output) => {
return JSON.stringify(loadAssortmentUnlocks());
}
}
],
"custom-static-ui-fixes"
);
};

View file

@ -0,0 +1,39 @@
import type { DependencyContainer } from "tsyringe";
import type { InRaidHelper } from "@spt/helpers/InRaidHelper";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { ICloner } from "@spt/utils/cloners/ICloner";
export const keepQuickBinds = (container: DependencyContainer) => {
const logger = container.resolve<ILogger>("PrimaryLogger");
const cloner = container.resolve<ICloner>("RecursiveCloner");
container.afterResolution(
"InRaidHelper",
(_, inRaidHelper: InRaidHelper) => {
const original = inRaidHelper.deleteInventory;
inRaidHelper.deleteInventory = (pmcData, sessionId) => {
// Copy the existing quickbinds
const fastPanel = cloner.clone(pmcData.Inventory.fastPanel);
// Nukes the inventory and the fastpanel
const result = original.call(inRaidHelper, pmcData, sessionId);
// Restore the quickbinds for items that still exist
try {
for (const index in fastPanel) {
if (pmcData.Inventory.items.find(i => i._id == fastPanel[index])) {
pmcData.Inventory.fastPanel[index] = fastPanel[index];
}
}
} catch (error) {
logger.error(`UIFixes: Failed to restore quickbinds\n ${error}`);
}
return result;
};
},
{ frequency: "Always" }
);
};

View file

@ -0,0 +1,61 @@
import type { DependencyContainer } from "tsyringe";
import type { ItemHelper } from "@spt/helpers/ItemHelper";
import type { ITemplateItem } from "@spt/models/eft/common/tables/ITemplateItem";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { RagfairLinkedItemService } from "@spt/services/RagfairLinkedItemService";
import type { DatabaseService } from "@spt/services/DatabaseService";
export const linkedSlotSearch = (container: DependencyContainer) => {
const logger = container.resolve<ILogger>("PrimaryLogger");
const itemHelper = container.resolve<ItemHelper>("ItemHelper");
const databaseService = container.resolve<DatabaseService>("DatabaseService");
container.afterResolution(
"RagfairLinkedItemService",
(_, linkedItemService: RagfairLinkedItemService) => {
const original = linkedItemService.getLinkedItems;
linkedItemService.getLinkedItems = linkedSearchId => {
const [tpl, slotName] = linkedSearchId.split(":", 2);
if (slotName) {
logger.info(`UIFixes: Finding items for specific slot ${tpl}:${slotName}`);
const allItems = databaseService.getItems();
const resultSet = getSpecificFilter(allItems[tpl], slotName);
// Default Inventory, for equipment slots
if (tpl === "55d7217a4bdc2d86028b456d") {
const categories = [...resultSet];
const items = Object.keys(allItems).filter(tpl => itemHelper.isOfBaseclasses(tpl, categories));
// Send the categories along too, some of them might actually be items
return new Set(items.concat(categories));
}
return resultSet;
}
return original.call(linkedItemService, tpl);
};
},
{ frequency: "Always" }
);
};
const getSpecificFilter = (item: ITemplateItem, slotName: string): Set<string> => {
const results = new Set<string>();
// For whatever reason, all chamber slots have the name "patron_in_weapon"
const groupName = slotName === "patron_in_weapon" ? "Chambers" : "Slots";
const group = item._props[groupName] ?? [];
const sub = group.find(slot => slot._name === slotName);
for (const filter of sub?._props?.filters ?? []) {
for (const f of filter.Filter) {
results.add(f);
}
}
return results;
};

View file

@ -0,0 +1,30 @@
import type { DependencyContainer } from "tsyringe";
import type { IPreSptLoadMod } from "@spt/models/external/IPreSptLoadMod";
import { assortUnlocks } from "./assortUnlocks";
import { keepQuickBinds } from "./keepQuickBinds";
import { linkedSlotSearch } from "./linkedSlotSearch";
import { putToolsBack } from "./putToolsBack";
import config from "../config/config.json";
class UIFixes implements IPreSptLoadMod {
public preSptLoad(container: DependencyContainer): void {
// Keep quickbinds for items that aren't actually lost on death
keepQuickBinds(container);
// Better tool return - starting production
if (config.putToolsBack) {
putToolsBack(container);
}
// Slot-specific linked search
linkedSlotSearch(container);
// Show unlocking quest on locked offers
assortUnlocks(container);
}
}
export const mod = new UIFixes();

View file

@ -0,0 +1,166 @@
import type { DependencyContainer } from "tsyringe";
import type { HideoutHelper } from "@spt/helpers/HideoutHelper";
import type { InventoryHelper } from "@spt/helpers/InventoryHelper";
import type { ItemHelper } from "@spt/helpers/ItemHelper";
import type { IPmcData } from "@spt/models/eft/common/IPmcData";
import type { IItem } from "@spt/models/eft/common/tables/IItem";
import type { IHideoutSingleProductionStartRequestData } from "@spt/models/eft/hideout/IHideoutSingleProductionStartRequestData";
import type { ILogger } from "@spt/models/spt/utils/ILogger";
import type { ICloner } from "@spt/utils/cloners/ICloner";
const returnToProperty = "uifixes.returnTo";
export const putToolsBack = (container: DependencyContainer) => {
const logger = container.resolve<ILogger>("PrimaryLogger");
const cloner = container.resolve<ICloner>("RecursiveCloner");
const itemHelper = container.resolve<ItemHelper>("ItemHelper");
container.afterResolution(
"HideoutHelper",
(_, hideoutHelper: HideoutHelper) => {
const original = hideoutHelper.registerProduction;
hideoutHelper.registerProduction = (pmcData, body, sessionID) => {
const result = original.call(hideoutHelper, pmcData, body, sessionID);
// The items haven't been deleted yet, augment the list with their parentId
try {
const bodyAsSingle = body as IHideoutSingleProductionStartRequestData;
if (bodyAsSingle && bodyAsSingle.tools?.length > 0) {
const requestTools = bodyAsSingle.tools;
const tools = pmcData.Hideout.Production[body.recipeId].sptRequiredTools;
for (let i = 0; i < tools.length; i++) {
const originalTool = pmcData.Inventory.items.find(x => x._id === requestTools[i].id);
// If the tool is in the stash itself, skip it. Same check as InventoryHelper.isItemInStash
if (
originalTool.parentId === pmcData.Inventory.stash &&
originalTool.slotId === "hideout"
) {
continue;
}
tools[i][returnToProperty] = [originalTool.parentId, originalTool.slotId];
}
}
} catch (error) {
logger.error(`UIFixes: Failed to save tool origin\n ${error}`);
}
return result;
};
},
{ frequency: "Always" }
);
// Better tool return - returning the tool
container.afterResolution(
"InventoryHelper",
(_, inventoryHelper: InventoryHelper) => {
const original = inventoryHelper.addItemToStash;
inventoryHelper.addItemToStash = (sessionId, request, pmcData, output) => {
const itemWithModsToAddClone = cloner.clone(request.itemWithModsToAdd);
// If a tool marked with uifixes is there, try to return it to its original container
const tool = itemWithModsToAddClone[0];
if (tool[returnToProperty]) {
try {
const [containerId, slotId] = tool[returnToProperty];
// Clean the item
delete tool[returnToProperty];
const [foundContainerFS2D, foundSlotId] = findGridFS2DForItems(
inventoryHelper,
containerId,
slotId,
itemWithModsToAddClone,
pmcData
);
if (foundContainerFS2D) {
// At this point everything should succeed
inventoryHelper.placeItemInContainer(
foundContainerFS2D,
itemWithModsToAddClone,
containerId,
foundSlotId
);
// protected function, bypass typescript
inventoryHelper["setFindInRaidStatusForItem"](itemWithModsToAddClone, request.foundInRaid);
// Add item + mods to output and profile inventory
output.profileChanges[sessionId].items.new.push(...itemWithModsToAddClone);
pmcData.Inventory.items.push(...itemWithModsToAddClone);
logger.debug(
`Added ${itemWithModsToAddClone[0].upd?.StackObjectsCount ?? 1} item: ${
itemWithModsToAddClone[0]._tpl
} with: ${itemWithModsToAddClone.length - 1} mods to ${containerId}`
);
return;
}
} catch (error) {
logger.error(`UIFixes: Encounted an error trying to put tool back.\n ${error}`);
}
logger.info("UIFixes: Unable to put tool back in its original container, returning it to stash.");
}
return original.call(inventoryHelper, sessionId, request, pmcData, output);
};
},
{ frequency: "Always" }
);
function findGridFS2DForItems(
inventoryHelper: InventoryHelper,
containerId: string,
startingGrid: string,
items: IItem[],
pmcData: IPmcData
): [number[][], string] | undefined {
const container = pmcData.Inventory.items.find(x => x._id === containerId);
if (!container) {
return;
}
const [foundTemplate, containerTemplate] = itemHelper.getItem(container._tpl);
if (!foundTemplate || !containerTemplate) {
return;
}
let originalGridIndex = containerTemplate._props.Grids.findIndex(g => g._name === startingGrid);
if (originalGridIndex < 0) {
originalGridIndex = 0;
}
// Loop through grids, starting from the original grid
for (
let gridIndex = originalGridIndex;
gridIndex < containerTemplate._props.Grids.length + originalGridIndex;
gridIndex++
) {
const grid = containerTemplate._props.Grids[gridIndex % containerTemplate._props.Grids.length];
const gridItems = pmcData.Inventory.items.filter(
x => x.parentId === containerId && x.slotId === grid._name
);
const containerFS2D = inventoryHelper.getContainerMap(
grid._props.cellsH,
grid._props.cellsV,
gridItems,
containerId
);
// will change the array so clone it
if (inventoryHelper.canPlaceItemInContainer(cloner.clone(containerFS2D), items)) {
return [containerFS2D, grid._name];
}
}
}
};