Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

New Feature: BTR #319

Closed
wants to merge 7 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
242 changes: 242 additions & 0 deletions Source/AkiSupport/Singleplayer/Utils/TraderServicesManager.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,242 @@
using Comfort.Common;
using EFT;
using EFT.Quests;
using HarmonyLib;
using Newtonsoft.Json;
using StayInTarkov.Networking;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
using static BackendConfigSettingsClass;
//using TraderServiceClass = GClass1794;
// Globalusings.cs ServiceData1
using QuestDictClass = GClass2133<string>;
using StandingListClass = GClass2135<float>;


namespace StayInTarkov.AkiSupport.Singleplayer.Utils
{

public class TraderServicesManager
{
/// <summary>
/// Subscribe to this event to trigger trader service logic.
/// </summary>
public event Action<ETraderServiceType, string> OnTraderServicePurchased;

private static TraderServicesManager _instance;

public static TraderServicesManager Instance
{
get
{
if (_instance == null)
{
_instance = new TraderServicesManager();
}

return _instance;
}
}

private Dictionary<ETraderServiceType, Dictionary<string, bool>> _servicePurchased { get; set; }
private HashSet<string> _cachedTraders = new();
private FieldInfo _playerQuestControllerField;

public TraderServicesManager()
{
_servicePurchased = new Dictionary<ETraderServiceType, Dictionary<string, bool>>();
_playerQuestControllerField = AccessTools.Field(typeof(Player), "_questController");
}

public void Clear()
{
_servicePurchased.Clear();
_cachedTraders.Clear();
}

public void GetTraderServicesDataFromServer(string traderId)
{
Dictionary<ETraderServiceType, ServiceData> servicesData = Singleton<BackendConfigSettingsClass>.Instance.ServicesData;
var gameWorld = Singleton<GameWorld>.Instance;
var player = gameWorld?.MainPlayer;

if (gameWorld == null || player == null)
{
Debug.LogError("GetTraderServicesDataFromServer - Error fetching game objects");
return;
}

if (!player.Profile.TradersInfo.TryGetValue(traderId, out Profile.TraderInfo traderInfo))
{
Debug.LogError("GetTraderServicesDataFromServer - Error fetching profile trader info");
return;
}

// Only request data from the server if it's not already cached
if (!_cachedTraders.Contains(traderId))
{
var json = AkiBackendCommunication.Instance.GetJsonBLOCKING($"/singleplayer/traderServices/getTraderServices/{traderId}");
var traderServiceModels = JsonConvert.DeserializeObject<List<TraderServiceModel>>(json);

foreach (var traderServiceModel in traderServiceModels)
{
ETraderServiceType serviceType = traderServiceModel.ServiceType;
ServiceData serviceData;

// Only populate trader services that don't exist yet
if (!servicesData.ContainsKey(traderServiceModel.ServiceType))
{
var traderService = new ServiceData1() // TraderServiceClass
{
TraderId = traderId,
ServiceType = serviceType,
UniqueItems = traderServiceModel.ItemsToReceive ?? new MongoID[0],
ItemsToPay = traderServiceModel.ItemsToPay ?? new Dictionary<MongoID, int>(),

// SubServices seem to be populated dynamically in the client (For BTR taxi atleast), so we can just ignore it
// NOTE: For future reference, this is a dict of `point id` to `price` for the BTR taxi
SubServices = new Dictionary<string, int>()
};

// Convert our format to the backend settings format
serviceData = new ServiceData(traderService);

// Populate requirements if provided
if (traderServiceModel.Requirements != null)
{
if (traderServiceModel.Requirements.Standings != null)
{
serviceData.TraderServiceRequirements.Standings = new StandingListClass();
serviceData.TraderServiceRequirements.Standings.AddRange(traderServiceModel.Requirements.Standings);

// BSG has a bug in their code, we _need_ to initialize this if Standings isn't null
serviceData.TraderServiceRequirements.CompletedQuests = new QuestDictClass();
}

if (traderServiceModel.Requirements.CompletedQuests != null)
{
serviceData.TraderServiceRequirements.CompletedQuests = new QuestDictClass();
serviceData.TraderServiceRequirements.CompletedQuests.Concat(traderServiceModel.Requirements.CompletedQuests);
}
}

servicesData[serviceData.ServiceType] = serviceData;
}
}

_cachedTraders.Add(traderId);
}

// Update service availability
foreach (var servicesDataPair in servicesData)
{
// Only update this trader's services
if (servicesDataPair.Value.TraderId != traderId)
{
continue;
}

var IsServiceAvailable = this.IsServiceAvailable(player, servicesDataPair.Value.TraderServiceRequirements);

// Check whether we've purchased this service yet
var traderService = servicesDataPair.Key;
var WasPurchasedInThisRaid = IsServicePurchased(traderService, traderId);
traderInfo.SetServiceAvailability(traderService, IsServiceAvailable, WasPurchasedInThisRaid);
}
}

private bool IsServiceAvailable(Player player, ServiceRequirements requirements)
{
// Handle standing requirements
if (requirements.Standings != null)
{
foreach (var entry in requirements.Standings)
{
if (!player.Profile.TradersInfo.ContainsKey(entry.Key) ||
player.Profile.TradersInfo[entry.Key].Standing < entry.Value)
{
return false;
}
}
}

// Handle quest requirements
if (requirements.CompletedQuests != null)
{
AbstractQuestControllerClass questController = _playerQuestControllerField.GetValue(player) as AbstractQuestControllerClass;
foreach (string questId in requirements.CompletedQuests)
{
var conditional = questController.Quests.GetConditional(questId);
if (conditional == null || conditional.QuestStatus != EQuestStatus.Success)
{
return false;
}
}
}

return true;
}

public void AfterPurchaseTraderService(ETraderServiceType serviceType, AbstractQuestControllerClass questController, string subServiceId = null)
{
GameWorld gameWorld = Singleton<GameWorld>.Instance;
Player player = gameWorld?.MainPlayer;

if (gameWorld == null || player == null)
{
Debug.LogError("TryPurchaseTraderService - Error fetching game objects");
return;
}

// Service doesn't exist
if (!Singleton<BackendConfigSettingsClass>.Instance.ServicesData.TryGetValue(serviceType, out var serviceData))
{
return;
}

SetServicePurchased(serviceType, subServiceId, serviceData.TraderId);
}

public void SetServicePurchased(ETraderServiceType serviceType, string subserviceId, string traderId)
{
if (_servicePurchased.TryGetValue(serviceType, out var traderDict))
{
traderDict[traderId] = true;
}
else
{
_servicePurchased[serviceType] = new Dictionary<string, bool>();
_servicePurchased[serviceType][traderId] = true;
}

if (OnTraderServicePurchased != null)
{
OnTraderServicePurchased.Invoke(serviceType, subserviceId);
}
}

public void RemovePurchasedService(ETraderServiceType serviceType, string traderId)
{
if (_servicePurchased.TryGetValue(serviceType, out var traderDict))
{
traderDict[traderId] = false;
}
}

public bool IsServicePurchased(ETraderServiceType serviceType, string traderId)
{
if (_servicePurchased.TryGetValue(serviceType, out var traderDict))
{
if (traderDict.TryGetValue(traderId, out var result))
{
return result;
}
}

return false;
}
}
}
37 changes: 37 additions & 0 deletions Source/AkiSupport/Singleplayer/Utils/TradingServiceModel.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
using EFT;
using Newtonsoft.Json;
using System.Collections.Generic;

namespace StayInTarkov.AkiSupport.Singleplayer.Utils
{

public class TraderServiceModel
{
[JsonProperty("serviceType")]
public ETraderServiceType ServiceType { get; set; }

[JsonProperty("itemsToPay")]
public Dictionary<MongoID, int> ItemsToPay { get; set; }

[JsonProperty("subServices")]
public Dictionary<string, int> SubServices { get; set; }

[JsonProperty("itemsToReceive")]
public MongoID[] ItemsToReceive { get; set; }

[JsonProperty("requirements")]
public TraderServiceRequirementsModel Requirements { get; set; }
}

public class TraderServiceRequirementsModel
{
[JsonProperty("completedQuests")]
public string[] CompletedQuests { get; set; }

[JsonProperty("standings")]
public Dictionary<string, float> Standings { get; set; }

[JsonProperty("side")]
public ESideType Side { get; set; }
}
}
75 changes: 75 additions & 0 deletions Source/Coop/NetworkPacket/BTR/BTRInteractionPacket.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
using BepInEx.Logging;
using Comfort.Common;
using EFT;
using HarmonyLib.Tools;
using StayInTarkov.Coop.Matchmaker;
using StayInTarkov.Coop.NetworkPacket.Player;
using StayInTarkov.Coop.Players;
using StayInTarkov.Multiplayer.BTR;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace StayInTarkov.Coop.NetworkPacket.BTR
{
public sealed class BTRInteractionPacket : BasePlayerPacket
{
public BTRInteractionPacket() : base("", nameof(BTRInteractionPacket))
{
Logger = BepInEx.Logging.Logger.CreateLogSource(nameof(BTRInteractionPacket));
}

public PlayerInteractPacket InteractPacket;

public override byte[] Serialize()
{
var ms = new MemoryStream();
using BinaryWriter writer = new(ms);
this.WriteHeaderAndProfileId(writer);
writer.Write(InteractPacket.HasInteraction);
writer.Write((int)InteractPacket.InteractionType);
writer.Write(InteractPacket.SideId);
writer.Write(InteractPacket.SlotId);
writer.Write(InteractPacket.Fast);
return ms.ToArray();
}

public override ISITPacket Deserialize(byte[] bytes)
{
using BinaryReader reader = new(new MemoryStream(bytes));
this.ReadHeaderAndProfileId(reader);

InteractPacket = new()
{
HasInteraction = reader.ReadBoolean(),
InteractionType = (EInteractionType)reader.ReadInt(),
SideId = reader.ReadByte(),
SlotId = reader.ReadByte(),
Fast = reader.ReadBoolean()
};
return this;
}

public override void Process()
{
Logger.LogDebug($"{nameof(Process)}");
if (SITMatchmaking.IsServer)
{
var mainPlayer = Singleton<GameWorld>.Instance.MainPlayer;
Singleton<BTRManager>.Instance.PlayerInteractWithDoor(mainPlayer, this.InteractPacket);
}
base.Process();
}

protected override void Process(CoopPlayerClient client)
{
Logger.LogDebug($"{nameof(Process)}(client)");
Singleton<BTRManager>.Instance.PlayerInteractWithDoor(client, this.InteractPacket);
}

}
}
Loading
Loading