Reenable post-scav swapping with server support; add /uifixes/assortUnlocks
This commit is contained in:
60
Patches/AssortUnlocksPatch.cs
Normal file
60
Patches/AssortUnlocksPatch.cs
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
using Aki.Common.Http;
|
||||||
|
using Aki.Reflection.Patching;
|
||||||
|
using EFT.UI;
|
||||||
|
using EFT.UI.Ragfair;
|
||||||
|
using HarmonyLib;
|
||||||
|
using Newtonsoft.Json;
|
||||||
|
using System;
|
||||||
|
using System.Collections.Generic;
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace UIFixes
|
||||||
|
{
|
||||||
|
public class AssortUnlocksPatch : ModulePatch
|
||||||
|
{
|
||||||
|
private static bool Loading = false;
|
||||||
|
private static Dictionary<string, string> AssortUnlocks = null;
|
||||||
|
|
||||||
|
protected override MethodBase GetTargetMethod()
|
||||||
|
{
|
||||||
|
return AccessTools.Method(typeof(OfferView), nameof(OfferView.method_10));
|
||||||
|
}
|
||||||
|
|
||||||
|
[PatchPostfix]
|
||||||
|
public static async void Postfix(OfferView __instance, HoverTooltipArea ____hoverTooltipArea)
|
||||||
|
{
|
||||||
|
if (!Settings.ShowRequiredQuest.Value)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (AssortUnlocks == null && !Loading)
|
||||||
|
{
|
||||||
|
Loading = true;
|
||||||
|
|
||||||
|
string response = await RequestHandler.GetJsonAsync("/uifixes/assortUnlocks");
|
||||||
|
if (!String.IsNullOrEmpty(response))
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
AssortUnlocks = JsonConvert.DeserializeObject<Dictionary<string, string>>(response);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Logger.LogError(ex);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Loading = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (__instance.Offer_0.Locked)
|
||||||
|
{
|
||||||
|
if (AssortUnlocks != null && AssortUnlocks.TryGetValue(__instance.Offer_0.Item.Id, out string questName))
|
||||||
|
{
|
||||||
|
____hoverTooltipArea.SetMessageText(____hoverTooltipArea.String_1 + " (" + questName.Localized() + ")", true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -23,7 +23,6 @@ namespace UIFixes
|
|||||||
new ToggleOnOpenPatch().Enable();
|
new ToggleOnOpenPatch().Enable();
|
||||||
|
|
||||||
new OfferItemFixMaskPatch().Enable();
|
new OfferItemFixMaskPatch().Enable();
|
||||||
new OfferViewLockedQuestPatch().Enable();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public class DoNotToggleOnMouseOverPatch : ModulePatch
|
public class DoNotToggleOnMouseOverPatch : ModulePatch
|
||||||
@@ -77,89 +76,5 @@ namespace UIFixes
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public class OfferViewLockedQuestPatch : ModulePatch
|
|
||||||
{
|
|
||||||
private static readonly Dictionary<string, string> QuestUnlocks = [];
|
|
||||||
|
|
||||||
protected override MethodBase GetTargetMethod()
|
|
||||||
{
|
|
||||||
return AccessTools.Method(typeof(OfferView), nameof(OfferView.method_10));
|
|
||||||
}
|
|
||||||
|
|
||||||
[PatchPostfix]
|
|
||||||
public static void Postfix(OfferView __instance, HoverTooltipArea ____hoverTooltipArea)
|
|
||||||
{
|
|
||||||
if (!Settings.ShowRequiredQuest.Value)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (__instance.Offer_0.Locked)
|
|
||||||
{
|
|
||||||
string questName = null;
|
|
||||||
if (QuestUnlocks.ContainsKey(__instance.Offer_0.Id))
|
|
||||||
{
|
|
||||||
questName = QuestUnlocks[__instance.Offer_0.Id];
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
// Filter by as much data available. There are some unlocks that are ambiguous without access to the server questassorts
|
|
||||||
// Using a tuple of (quest, rewards) to avoid doing all the reward checks more than once
|
|
||||||
var questsAndRewards = R.QuestCache.Instance.GetAllQuestTemplates()
|
|
||||||
.Select(q => (quest: q, rewards: q.Rewards[EQuestStatus.Success]
|
|
||||||
.Where(r => r.type == ERewardType.AssortmentUnlock &&
|
|
||||||
r.traderId == __instance.Offer_0.User.Id &&
|
|
||||||
r.loyaltyLevel == __instance.Offer_0.LoyaltyLevel &&
|
|
||||||
r.items.First(i => i._id == r.target)._tpl == __instance.Offer_0.Item.TemplateId)))
|
|
||||||
.Where(x => x.rewards.Any());
|
|
||||||
|
|
||||||
if (questsAndRewards.Count() > 1)
|
|
||||||
{
|
|
||||||
// Some of the ambiguous unlocks are weapons with full loadouts we can actually compare
|
|
||||||
List<Item> items = [];
|
|
||||||
Item.smethod_0(__instance.Offer_0.Item, items, (item, container) => true); // complete list of items, including the top level item
|
|
||||||
|
|
||||||
// Hashset.SetEquals compares lists, ignoring order (don't care) and duplicates (don't have any)
|
|
||||||
var allItemTemplateIds = new HashSet<string>(items.Select(i => i.TemplateId));
|
|
||||||
questsAndRewards = questsAndRewards.Where(x => x.rewards.Any(r => allItemTemplateIds.SetEquals(r.items.Select(i => i._tpl))));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (questsAndRewards.Count() > 1)
|
|
||||||
{
|
|
||||||
// Some quests are USEC/Bear versions with the same name
|
|
||||||
questsAndRewards = questsAndRewards.Distinct(x => x.quest.Name);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Zeus thermal scope is the only remaining ambiguous item in the base game
|
|
||||||
if (questsAndRewards.Count() > 1 && __instance.Offer_0.Item.TemplateId == "63fc44e2429a8a166c7f61e6") // Zeus thermal scope
|
|
||||||
{
|
|
||||||
if (__instance.Offer_0.Requirements.Any(r => r.TemplateId == "5fc64ea372b0dd78d51159dc")) // cultist knife
|
|
||||||
{
|
|
||||||
questsAndRewards = questsAndRewards.Where(x => x.quest.Id == "64ee99639878a0569d6ec8c9"); // Broadcast - Part 5
|
|
||||||
}
|
|
||||||
else if (__instance.Offer_0.Requirements.Any(r => r.TemplateId == "5c0530ee86f774697952d952")) // ledx
|
|
||||||
{
|
|
||||||
questsAndRewards = questsAndRewards.Where(x => x.quest.Id == "64e7b971f9d6fa49d6769b44"); // The Huntsman Path - Big Game
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (questsAndRewards.Count() == 1)
|
|
||||||
{
|
|
||||||
questName = questsAndRewards.First().quest.Name;
|
|
||||||
}
|
|
||||||
|
|
||||||
// If it's not clear by now, it's either missing or still too ambiguous ¯\_(ツ)_/¯
|
|
||||||
// Cache the result, even if empty
|
|
||||||
QuestUnlocks.Add(__instance.Offer_0.Id, questName);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!String.IsNullOrEmpty(questName))
|
|
||||||
{
|
|
||||||
____hoverTooltipArea.SetMessageText(____hoverTooltipArea.String_1 + " (" + questName + ")", true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -63,15 +63,6 @@ namespace UIFixes
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: Remove in 3.9.0 when server bug is fixed
|
|
||||||
// If on the scav inventory screen (aka post-raid scav transfer), swap must be blocked not only between the scav inventory and the stash (normally blocked anyway),
|
|
||||||
// but even within the scav inventory itself, due to the server not handling it.
|
|
||||||
if ((itemContext.ViewType == EItemViewType.ScavInventory || targetItemContext.ViewType == EItemViewType.ScavInventory)
|
|
||||||
&& (itemContext.Item.Owner.ID != PatchConstants.BackEndSession.Profile.Id || targetItemContext.Item.Owner.ID != PatchConstants.BackEndSession.Profile.Id))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (itemContext.Item == targetItemContext.Item || targetItemContext.Item.GetAllParentItems().Contains(itemContext.Item))
|
if (itemContext.Item == targetItemContext.Item || targetItemContext.Item.GetAllParentItems().Contains(itemContext.Item))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ namespace UIFixes
|
|||||||
FleaPrevSearchPatches.Enable();
|
FleaPrevSearchPatches.Enable();
|
||||||
KeepOfferWindowOpenPatches.Enable();
|
KeepOfferWindowOpenPatches.Enable();
|
||||||
AddOfferClickablePricesPatches.Enable();
|
AddOfferClickablePricesPatches.Enable();
|
||||||
|
new AssortUnlocksPatch().Enable();
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool InRaid()
|
public static bool InRaid()
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
<TargetFramework>net471</TargetFramework>
|
<TargetFramework>net471</TargetFramework>
|
||||||
<AssemblyName>Tyfon.UIFixes</AssemblyName>
|
<AssemblyName>Tyfon.UIFixes</AssemblyName>
|
||||||
<Description>SPT UI Fixes</Description>
|
<Description>SPT UI Fixes</Description>
|
||||||
<Version>1.5.1</Version>
|
<Version>1.6.0</Version>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
<LangVersion>latest</LangVersion>
|
<LangVersion>latest</LangVersion>
|
||||||
<Configurations>Debug;Release;Dist</Configurations>
|
<Configurations>Debug;Release;Dist</Configurations>
|
||||||
@@ -21,6 +21,9 @@
|
|||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
|
<Reference Include="Aki.Common">
|
||||||
|
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Aki.Common.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Aki.Reflection">
|
<Reference Include="Aki.Reflection">
|
||||||
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Aki.Reflection.dll</HintPath>
|
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Aki.Reflection.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
@@ -33,6 +36,9 @@
|
|||||||
<Reference Include="ItemComponent.Types">
|
<Reference Include="ItemComponent.Types">
|
||||||
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\ItemComponent.Types.dll</HintPath>
|
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\ItemComponent.Types.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
<Reference Include="Newtonsoft.Json">
|
||||||
|
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Newtonsoft.Json.dll</HintPath>
|
||||||
|
</Reference>
|
||||||
<Reference Include="Sirenix.Serialization">
|
<Reference Include="Sirenix.Serialization">
|
||||||
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Sirenix.Serialization.dll</HintPath>
|
<HintPath>$(PathToSPT)\EscapeFromTarkov_Data\Managed\Sirenix.Serialization.dll</HintPath>
|
||||||
</Reference>
|
</Reference>
|
||||||
|
|||||||
@@ -38,6 +38,8 @@ import ignore from "ignore";
|
|||||||
import archiver from "archiver";
|
import archiver from "archiver";
|
||||||
import winston from "winston";
|
import winston from "winston";
|
||||||
|
|
||||||
|
const sptPath = "/SPT/3.8.1-debug";
|
||||||
|
|
||||||
// Get the command line arguments to determine whether to use verbose logging.
|
// Get the command line arguments to determine whether to use verbose logging.
|
||||||
const args = process.argv.slice(2);
|
const args = process.argv.slice(2);
|
||||||
const verbose = args.includes("--verbose") || args.includes("-v");
|
const verbose = args.includes("--verbose") || args.includes("-v");
|
||||||
@@ -113,6 +115,12 @@ async function main() {
|
|||||||
await copyFiles(currentDir, projectDir, buildIgnorePatterns);
|
await copyFiles(currentDir, projectDir, buildIgnorePatterns);
|
||||||
logger.log("success", "Files successfully copied to temporary directory.");
|
logger.log("success", "Files successfully copied to temporary directory.");
|
||||||
|
|
||||||
|
// Copy output to SPT installation for testing
|
||||||
|
logger.log("info", "Copying output to SPT installation");
|
||||||
|
const sptModPath = path.join(sptPath, "/user/mods/", projectShortName);
|
||||||
|
await fs.copy(projectDir, sptModPath);
|
||||||
|
logger.log("success", `Files successfully copied to ${sptModPath}`);
|
||||||
|
|
||||||
// Create a zip archive of the project files.
|
// Create a zip archive of the project files.
|
||||||
logger.log("info", "Beginning folder compression...");
|
logger.log("info", "Beginning folder compression...");
|
||||||
const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`);
|
const zipFilePath = path.join(path.dirname(projectDir), `${projectName}.zip`);
|
||||||
@@ -134,7 +142,7 @@ async function main() {
|
|||||||
logger.log("success", "------------------------------------");
|
logger.log("success", "------------------------------------");
|
||||||
logger.log("success", "Build script completed successfully!");
|
logger.log("success", "Build script completed successfully!");
|
||||||
logger.log("success", "Your mod package has been created in the 'dist' directory:");
|
logger.log("success", "Your mod package has been created in the 'dist' directory:");
|
||||||
logger.log("success", `/${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`);
|
logger.log("success", `${path.relative(process.cwd(), path.join(distDir, `${projectName}.zip`))}`);
|
||||||
logger.log("success", "------------------------------------");
|
logger.log("success", "------------------------------------");
|
||||||
if (!verbose) {
|
if (!verbose) {
|
||||||
logger.log("success", "To see a detailed build log, use `npm run buildinfo`.");
|
logger.log("success", "To see a detailed build log, use `npm run buildinfo`.");
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "tyfon-uifixes",
|
"name": "uifixes",
|
||||||
"version": "1.0.0",
|
"version": "1.6.0",
|
||||||
"main": "src/mod.js",
|
"main": "src/mod.js",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"author": "Tyfon",
|
"author": "Tyfon",
|
||||||
|
|||||||
@@ -1,34 +1,87 @@
|
|||||||
import { DependencyContainer } from "tsyringe";
|
import { DependencyContainer } from "tsyringe";
|
||||||
|
|
||||||
import { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod";
|
import type { IPreAkiLoadMod } from "@spt-aki/models/external/IPreAkiLoadMod";
|
||||||
import { InventoryController } from "@spt-aki/controllers/InventoryController";
|
import type { InventoryController } from "@spt-aki/controllers/InventoryController";
|
||||||
import { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
import type { ProfileHelper } from "@spt-aki/helpers/ProfileHelper";
|
||||||
|
import type { StaticRouterModService } from "@spt-aki/services/mod/staticRouter/StaticRouterModService";
|
||||||
|
import type { DatabaseServer } from "@spt-aki/servers/DatabaseServer";
|
||||||
|
import type { ILogger } from "@spt-aki/models/spt/utils/ILogger";
|
||||||
|
|
||||||
class UIFixes implements IPreAkiLoadMod {
|
class UIFixes implements IPreAkiLoadMod {
|
||||||
private container: DependencyContainer;
|
private databaseServer: DatabaseServer;
|
||||||
private profileHelper: ProfileHelper;
|
private logger: ILogger;
|
||||||
|
|
||||||
preAkiLoad(container: DependencyContainer): void {
|
public preAkiLoad(container: DependencyContainer): void {
|
||||||
this.container = container;
|
this.databaseServer = container.resolve<DatabaseServer>("DatabaseServer");
|
||||||
this.profileHelper = container.resolve<ProfileHelper>("ProfileHelper");
|
this.logger = container.resolve<ILogger>("WinstonLogger");
|
||||||
|
|
||||||
|
const profileHelper = container.resolve<ProfileHelper>("ProfileHelper");
|
||||||
|
const staticRouterModService = container.resolve<StaticRouterModService>("StaticRouterModService");
|
||||||
|
|
||||||
// Handle scav profile for post-raid scav transfer swaps (fixed in 3.9.0)
|
// Handle scav profile for post-raid scav transfer swaps (fixed in 3.9.0)
|
||||||
container.afterResolution(
|
container.afterResolution(
|
||||||
"InventoryController",
|
"InventoryController",
|
||||||
(_, result: InventoryController) => {
|
(_, inventoryController: InventoryController) => {
|
||||||
const original = result.swapItem;
|
const original = inventoryController.swapItem;
|
||||||
|
|
||||||
result.swapItem = (pmcData, request, sessionID) => {
|
inventoryController.swapItem = (pmcData, request, sessionID) => {
|
||||||
let playerData = pmcData;
|
let playerData = pmcData;
|
||||||
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id) {
|
if (request.fromOwner?.type === "Profile" && request.fromOwner.id !== playerData._id) {
|
||||||
playerData = this.profileHelper.getScavProfile(sessionID);
|
playerData = profileHelper.getScavProfile(sessionID);
|
||||||
}
|
}
|
||||||
|
|
||||||
return original(playerData, request, sessionID);
|
return original.call(inventoryController, playerData, request, sessionID);
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
{ frequency: "Always" }
|
{ frequency: "Always" }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
staticRouterModService.registerStaticRouter(
|
||||||
|
"UIFixesRoutes",
|
||||||
|
[
|
||||||
|
{
|
||||||
|
url: "/uifixes/assortUnlocks",
|
||||||
|
action: (url, info, sessionId, output) => {
|
||||||
|
return JSON.stringify(this.loadAssortmentUnlocks());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"custom-static-ui-fixes"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private loadAssortmentUnlocks() {
|
||||||
|
const traders = this.databaseServer.getTables().traders;
|
||||||
|
const quests = this.databaseServer.getTables().templates.quests;
|
||||||
|
|
||||||
|
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]) {
|
||||||
|
this.logger.error(
|
||||||
|
`Trader ${traderId} questassort references unknown quest ${JSON.stringify(questId)}!`
|
||||||
|
);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
result[assortId] = quests[questId].name;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user