diff --git a/ModExamples/13.1AddTraderWithDynamicAssorts/13.1AddTraderWithDynamicAssorts.csproj b/ModExamples/13.1AddTraderWithDynamicAssorts/13.1AddTraderWithDynamicAssorts.csproj new file mode 100644 index 00000000..7c4167ac --- /dev/null +++ b/ModExamples/13.1AddTraderWithDynamicAssorts/13.1AddTraderWithDynamicAssorts.csproj @@ -0,0 +1,32 @@ + + + + net9.0 + _13._1AddTraderWithDynamicAssorts + enable + enable + Library + + + + + + ..\TempReferences\Core.dll + false + false + false + + + ..\TempReferences\SptCommon.dll + false + false + false + + + ..\TempReferences\SptDependencyInjection.dll + false + false + false + + + diff --git a/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderHelper.cs b/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderHelper.cs new file mode 100644 index 00000000..106e30d8 --- /dev/null +++ b/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderHelper.cs @@ -0,0 +1,184 @@ +using Core.Models.Common; +using Core.Models.Eft.Common.Tables; +using Core.Models.Spt.Config; +using Core.Models.Spt.Server; +using Core.Utils; + +namespace _13._1AddTraderWithDynamicAssorts +{ + public class AddTraderHelper + { + /** + * Add record to trader config to set the refresh time of trader in seconds (default is 60 minutes) + * @param traderConfig trader config to add our trader to + * @param baseJson json file for trader (db/base.json) + * @param refreshTimeSecondsMin How many seconds between trader stock refresh min time + * @param refreshTimeSecondsMax How many seconds between trader stock refresh max time + */ + public void SetTraderUpdateTime(TraderConfig traderConfig, dynamic baseJson, int refreshTimeSecondsMin, int refreshTimeSecondsMax) + { + // Add refresh time in seconds to config + var traderRefreshRecord = new UpdateTime + { + TraderId = baseJson.id, + Seconds = new MinMax(refreshTimeSecondsMin, refreshTimeSecondsMax) + }; + + + traderConfig.UpdateTime.Add(traderRefreshRecord); + } + + /** + * Add our new trader to the database + * @param traderDetailsToAdd trader details + * @param tables database + * @param jsonUtil json utility class + */ + public void AddTraderToDb(dynamic traderDetailsToAdd, DatabaseTables tables, JsonUtil jsonUtil, object assortJson) + { + // Create trader data ready to add to database + var traderDataToAdd = new Trader + { + Assort = + jsonUtil.Deserialize( + jsonUtil.Serialize(assortJson)), // Deserialise/serialise creates a copy of the json + Base = + jsonUtil.Deserialize( + jsonUtil.Serialize(traderDetailsToAdd)), // Deserialise/serialise creates a copy of the json + QuestAssort = new Dictionary> // questassort is empty as trader has no assorts unlocked by quests + { + { "Started", new Dictionary() }, + { "Success", new Dictionary() }, + { "Fail", new Dictionary() } + } + }; + + // Add trader to trader table, key is the traders id + tables.Traders.Add(traderDetailsToAdd._id, traderDataToAdd); + } + + /** + * Add traders name/location/description to the locale table + * @param baseJson json file for trader (db/base.json) + * @param tables database tables + * @param fullName Complete name of trader + * @param firstName First name of trader + * @param nickName Nickname of trader + * @param location Location of trader (e.g. "Here in the cat shop") + * @param description Description of trader + */ + public void AddTraderToLocales(dynamic baseJson, DatabaseTables tables, string fullName, string firstName, string nickName, string location, string description) + { + // For each language, add locale for the new trader + var locales = tables.Locales.Global; + + foreach (var (key, value) in locales) { + value.Value[$"{baseJson._id} FullName"] = fullName; + value.Value[$"{baseJson._id} FirstName"] = firstName; + value.Value[$"{baseJson._id} Nickname"] = nickName; + value.Value[$"{baseJson._id} Location"] = location; + value.Value[$"{baseJson._id} Description"] = description; + } + } + + public List CreateGlock() + { + // Create an array ready to hold weapon + all mods + var glock = new List(); + + // Add the base first + glock.Add(new Item { // Add the base weapon first + Id = + NewItemIds.GLOCK_BASE, // Ids matter, MUST BE UNIQUE See mod.ts for more details + Template = + "5a7ae0c351dfba0017554310", // This is the weapons tpl, found on: https://db.sp-tarkov.com/search + }); + + // Add barrel + glock.Add(new Item { + Id = + NewItemIds.GLOCK_BARREL, + Template = + "5a6b60158dc32e000a31138b", + ParentId = + NewItemIds.GLOCK_BASE, // This is a sub item, you need to define its parent its attached to / inserted into + SlotId = + "mod_barrel", // Required for mods, you need to define what 'slot' the mod will fill on the weapon + }); + + // Add receiver + glock.Add( new Item { + Id = + NewItemIds.GLOCK_RECIEVER, + Template = + "5a9685b1a2750c0032157104", + ParentId = + NewItemIds.GLOCK_BASE, + SlotId = + "mod_reciever", + }); + + // Add compensator + glock.Add(new Item { + Id = + NewItemIds.GLOCK_COMPENSATOR, + Template = + "5a7b32a2e899ef00135e345a", + ParentId = + NewItemIds.GLOCK_RECIEVER, // The parent of this mod is the receiver NOT weapon, be careful to get the correct parent + SlotId = + "mod_muzzle", + }); + + // Add Pistol grip + glock.Add(new Item { + Id = + NewItemIds.GLOCK_PISTOL_GRIP, + Template = + "5a7b4960e899ef197b331a2d", + ParentId = + NewItemIds.GLOCK_BASE, + SlotId = + "mod_pistol_grip", + }); + + // Add front sight + glock.Add(new Item { + Id = + NewItemIds.GLOCK_FRONT_SIGHT, + Template = + "5a6f5d528dc32e00094b97d9", + ParentId = + NewItemIds.GLOCK_RECIEVER, + SlotId = + "mod_sight_rear", + }); + + // Add rear sight + glock.Add(new Item { + Id = + NewItemIds.GLOCK_REAR_SIGHT, + Template = + "5a6f58f68dc32e000a311390", + ParentId = + NewItemIds.GLOCK_RECIEVER, + SlotId = + "mod_sight_front", + }); + + // Add magazine + glock.Add(new Item { + Id = + NewItemIds.GLOCK_MAGAZINE, + Template = + "630769c4962d0247b029dc60", + ParentId = + NewItemIds.GLOCK_BASE, + SlotId = + "mod_magazine", + }); + + return glock; + } + } +} diff --git a/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderWithDynamicAssorts.cs b/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderWithDynamicAssorts.cs new file mode 100644 index 00000000..0ce41686 --- /dev/null +++ b/ModExamples/13.1AddTraderWithDynamicAssorts/AddTraderWithDynamicAssorts.cs @@ -0,0 +1,125 @@ +using Core.Models.Eft.Common.Tables; +using Core.Models.Enums; +using Core.Models.External; +using Core.Models.Spt.Config; +using Core.Models.Utils; +using Core.Routers; +using Core.Servers; +using Core.Services; +using Core.Utils; + +namespace _13._1AddTraderWithDynamicAssorts +{ + public class AddTraderWithDynamicAssorts : IPostDBLoadMod + { + private readonly ISptLogger _logger; + private readonly HashUtil _hashUtil; + private readonly JsonUtil _jsonUtil; + private readonly FileUtil _fileUtil; + private readonly DatabaseService _databaseService; + private readonly ImageRouter _imageRouter; + private readonly ConfigServer _configServer; + private readonly TraderConfig _traderConfig; + private readonly RagfairConfig _ragfairConfig; + + public AddTraderWithDynamicAssorts( + ISptLogger logger, + HashUtil hashUtil, + JsonUtil jsonUtil, + FileUtil fileUtil, + DatabaseService databaseService, + ImageRouter imageRouter, + ConfigServer configServer) + { + _logger = logger; + _hashUtil = hashUtil; + _jsonUtil = jsonUtil; + _fileUtil = fileUtil; + _databaseService = databaseService; + _imageRouter = imageRouter; + _configServer = configServer; + + _traderConfig = _configServer.GetConfig(); + _ragfairConfig = _configServer.GetConfig(); + } + + public void PostDBLoad() + { + var traderImagePath = "./db/cat.jpg"; + + var baseJson = _fileUtil.ReadFile("./db/base.json"); + var traderBase = _jsonUtil.Deserialize(baseJson); + + // Create helper class and use it to register our traders image/icon + set its stock refresh time + var addTraderHelper = new AddTraderHelper(); + _imageRouter.AddRoute(traderBase.Avatar.Replace(".jpg", ""), System.IO.Path.GetFullPath(traderImagePath)); + addTraderHelper.SetTraderUpdateTime(_traderConfig, traderBase, 3600, 4000); + + // Add trader to flea market + _ragfairConfig.Traders[traderBase.Id] = true; + + // Get a reference to the database tables + var tables = _databaseService.GetTables(); + + var fluentAssortCreator = new FluentTraderAssortCreator(_logger, _hashUtil); + + // Add milk + var milkAssort = fluentAssortCreator + .CreateSingleAssortItem(ItemTpl.DRINK_PACK_OF_MILK) + .AddStackCount(200) + .AddBuyRestriction(10) + .AddMoneyCost(Money.ROUBLES, 2000) + .AddLoyaltyLevel(1) + .Export(tables.Traders[traderBase.Id]); + + // Add 3x bitcoin + salewa for milk barter + fluentAssortCreator + .CreateSingleAssortItem(ItemTpl.DRINK_PACK_OF_MILK) + .AddStackCount(100) + .AddBarterCost(ItemTpl.BARTER_PHYSICAL_BITCOIN, 3) + .AddBarterCost(ItemTpl.MEDKIT_SALEWA_FIRST_AID_KIT, 1) + .AddLoyaltyLevel(1) + .Export(tables.Traders[traderBase.Id]); + + + // Add glock as a money purchase + fluentAssortCreator + .CreateComplexAssortItem(addTraderHelper.CreateGlock()) + .AddUnlimitedStackCount() + .AddMoneyCost(Money.ROUBLES, 20000) + .AddBuyRestriction(3) + .AddLoyaltyLevel(1) + .Export(tables.Traders[traderBase.Id]); + + // Add mp133 preset as a barter for mayonase + fluentAssortCreator + .CreateComplexAssortItem(tables.Globals.ItemPresets["584148f2245977598f1ad387"].Items) // Weapon preset id comes from globals.json + .AddStackCount(200) + .AddBarterCost(ItemTpl.FOOD_JAR_OF_DEVILDOG_MAYO, 1) + .AddBuyRestriction(3) + .AddLoyaltyLevel(1) + .Export(tables.Traders[traderBase.Id]); + + addTraderHelper.AddTraderToLocales( + baseJson, + _databaseService.GetTables(), + traderBase.Name, + "Cat", + traderBase.Nickname, + traderBase.Location, + "This is the cat shop. Meow."); + } + } + + public static class NewItemIds + { + public static string GLOCK_BASE = "66eeef3b2a166b73d2066a74"; + public static string GLOCK_BARREL = "66eeef3b2a166b73d2066a75"; + public static string GLOCK_RECIEVER = "66eeef3b2a166b73d2066a76"; + public static string GLOCK_COMPENSATOR = "66eeef3b2a166b73d2066a77"; + public static string GLOCK_PISTOL_GRIP = "66eeef3b2a166b73d2066a78"; + public static string GLOCK_REAR_SIGHT = "66eeef3b2a166b73d2066a79"; + public static string GLOCK_FRONT_SIGHT = "66eeef3b2a166b73d2066a7a"; + public static string GLOCK_MAGAZINE = "66eeef3b2a166b73d2066a7b"; + } +} diff --git a/ModExamples/13.1AddTraderWithDynamicAssorts/FluentTraderAssortCreator.cs b/ModExamples/13.1AddTraderWithDynamicAssorts/FluentTraderAssortCreator.cs new file mode 100644 index 00000000..88766830 --- /dev/null +++ b/ModExamples/13.1AddTraderWithDynamicAssorts/FluentTraderAssortCreator.cs @@ -0,0 +1,172 @@ +using Core.Models.Eft.Common.Tables; +using Core.Models.Utils; +using Core.Utils; + +namespace _13._1AddTraderWithDynamicAssorts; + +public class FluentTraderAssortCreator +{ + private readonly ISptLogger _logger; + private readonly HashUtil _hashUtil; + + private readonly List _itemsToSell = []; + private readonly Dictionary>> _barterScheme = new(); + private readonly Dictionary _loyaltyLevel = new(); + + public FluentTraderAssortCreator( + ISptLogger logger, + HashUtil hashUtil) + { + _logger = logger; + _hashUtil = hashUtil; + } + + public FluentTraderAssortCreator CreateSingleAssortItem(string itemTpl, string? itemId = null) + { + // Create item ready for insertion into assort table + var newItemToAdd = new Item + { + Id = itemId ?? _hashUtil.Generate(), + Template = itemTpl, + ParentId = "hideout", // Should always be "hideout" + SlotId = "hideout", // Should always be "hideout" + Upd = new Upd + { + UnlimitedCount = false, + StackObjectsCount = 100 + } + }; + + _itemsToSell.Add(newItemToAdd); + + return this; + } + + public FluentTraderAssortCreator CreateComplexAssortItem(List items) + { + items[0].ParentId = "hideout"; + items[0].SlotId = "hideout"; + + items[0].Upd ??= new Upd(); + + items[0].Upd.UnlimitedCount = false; + items[0].Upd.StackObjectsCount = 100; + + _itemsToSell.AddRange(items); + + return this; + } + + public FluentTraderAssortCreator AddStackCount(int stackCount) + { + _itemsToSell[0].Upd.StackObjectsCount = stackCount; + + return this; + } + + public FluentTraderAssortCreator AddUnlimitedStackCount() + { + _itemsToSell[0].Upd.StackObjectsCount = 999999; + _itemsToSell[0].Upd.UnlimitedCount = true; + + return this; + } + + public FluentTraderAssortCreator MakeStackCountUnlimited() + { + _itemsToSell[0].Upd.StackObjectsCount = 999999; + + return this; + } + + public FluentTraderAssortCreator AddBuyRestriction(int maxBuyLimit) + { + _itemsToSell[0].Upd.BuyRestrictionMax = maxBuyLimit; + _itemsToSell[0].Upd.BuyRestrictionCurrent = 0; + + return this; + } + + public FluentTraderAssortCreator AddLoyaltyLevel(int level) + { + _loyaltyLevel[_itemsToSell[0].Id] = level; + + return this; + } + + public FluentTraderAssortCreator AddMoneyCost(string currencyType, int amount) + { + var dataToAdd = new BarterScheme + { + Count = amount, + Template = currencyType + }; + + _barterScheme.Add(_itemsToSell[0].Id, [[dataToAdd]]); + + return this; + } + + public FluentTraderAssortCreator AddBarterCost(string itemTpl, int count) + { + var sellableItemId = _itemsToSell[0].Id; + + // No data at all, create + if (_barterScheme.Count == 0) + { + var dataToAdd = new BarterScheme + { + Count = count, + Template = itemTpl + }; + + _barterScheme[sellableItemId] = [[dataToAdd]]; + } + else + { + // Item already exists, add to + var existingData = _barterScheme[sellableItemId][0].FirstOrDefault(x => x.Template == itemTpl); + if (existingData is not null) + { + // itemtpl already a barter for item, add to count + existingData.Count += count; + } + else + { + // No barter for item, add it fresh + _barterScheme[sellableItemId][0].Add(new BarterScheme + { + Count = count, + Template = itemTpl + }); + } + } + + return this; + } + + /** + * Reset objet ready for reuse + * @returns + */ + public FluentTraderAssortCreator? Export(Trader data) + { + var itemBeingSoldId = _itemsToSell[0].Id; + if (!data.Assort.Items.Exists(x => x.Id == itemBeingSoldId)) + { + _logger.Error($"Unable to add complex item with item key: {_itemsToSell[0].Id}, key already used"); + + return null; + } + + data.Assort.Items.AddRange(_itemsToSell); + data.Assort.BarterScheme[itemBeingSoldId] = _barterScheme[itemBeingSoldId]; + data.Assort.LoyalLevelItems[itemBeingSoldId] = _loyaltyLevel[itemBeingSoldId]; + + _itemsToSell.Clear(); + _barterScheme.Clear(); + _loyaltyLevel.Clear(); + + return this; + } +} diff --git a/ModExamples/13.1AddTraderWithDynamicAssorts/package.json b/ModExamples/13.1AddTraderWithDynamicAssorts/package.json new file mode 100644 index 00000000..b49687fa --- /dev/null +++ b/ModExamples/13.1AddTraderWithDynamicAssorts/package.json @@ -0,0 +1,13 @@ +{ + "Name": "13.1AddTraderWithDynamicAssorts", + "Version": "1.0.0", + "SptVersion": "~4.0", + "LoadBefore": [], + "LoadAfter": [], + "IncompatibileMods": [], + "Url": "https://github.com/sp-tarkov/server-csharp/tree/develop/ExampleMods/Mods", + "IsBundleMod": false, + "Author": "SPT", + "Contributors": [], + "Licence": "MIT" +} diff --git a/ModExamples/ModExamples.sln b/ModExamples/ModExamples.sln index 32b15f79..d7d51c3d 100644 --- a/ModExamples/ModExamples.sln +++ b/ModExamples/ModExamples.sln @@ -38,6 +38,7 @@ EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "18.1CustomItemServiceLootBox", "18.1CustomItemServiceLootBox\18.1CustomItemServiceLootBox.csproj", "{B870C285-B435-4C40-89C4-0220D34CB9BE}" EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "20CustomChatBot", "20CustomChatBot\20CustomChatBot.csproj", "{32271491-8CF1-4014-9A8E-E1EA22EA4292}" +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "13.1AddTraderWithDynamicAssorts", "13.1AddTraderWithDynamicAssorts\13.1AddTraderWithDynamicAssorts.csproj", "{9038FA64-E484-4549-9728-C50F12BBE643}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -117,6 +118,10 @@ Global {32271491-8CF1-4014-9A8E-E1EA22EA4292}.Debug|Any CPU.Build.0 = Debug|Any CPU {32271491-8CF1-4014-9A8E-E1EA22EA4292}.Release|Any CPU.ActiveCfg = Release|Any CPU {32271491-8CF1-4014-9A8E-E1EA22EA4292}.Release|Any CPU.Build.0 = Release|Any CPU + {9038FA64-E484-4549-9728-C50F12BBE643}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9038FA64-E484-4549-9728-C50F12BBE643}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9038FA64-E484-4549-9728-C50F12BBE643}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9038FA64-E484-4549-9728-C50F12BBE643}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE