Added AddTraderWithDynamicAssorts example

This commit is contained in:
Chomp
2025-02-10 17:20:59 +00:00
parent eef39e1a54
commit b277217672
6 changed files with 531 additions and 0 deletions
@@ -0,0 +1,32 @@
<Project Sdk="Microsoft.NET.Sdk.Web">
<PropertyGroup>
<TargetFramework>net9.0</TargetFramework>
<RootNamespace>_13._1AddTraderWithDynamicAssorts</RootNamespace>
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<OutputType>Library</OutputType>
</PropertyGroup>
<!-- TODO: Change to Nuget Package -->
<ItemGroup>
<Reference Include="Core">
<HintPath>..\TempReferences\Core.dll</HintPath>
<Private>false</Private>
<CopyLocal>false</CopyLocal>
<CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</Reference>
<Reference Include="SptCommon">
<HintPath>..\TempReferences\SptCommon.dll</HintPath>
<Private>false</Private>
<CopyLocal>false</CopyLocal>
<CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</Reference>
<Reference Include="SptDependencyInjection">
<HintPath>..\TempReferences\SptDependencyInjection.dll</HintPath>
<Private>false</Private>
<CopyLocal>false</CopyLocal>
<CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</Reference>
</ItemGroup>
</Project>
@@ -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<int>(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<TraderAssort>(
jsonUtil.Serialize(assortJson)), // Deserialise/serialise creates a copy of the json
Base =
jsonUtil.Deserialize<TraderBase>(
jsonUtil.Serialize(traderDetailsToAdd)), // Deserialise/serialise creates a copy of the json
QuestAssort = new Dictionary<string, Dictionary<string, string>> // questassort is empty as trader has no assorts unlocked by quests
{
{ "Started", new Dictionary<string, string>() },
{ "Success", new Dictionary<string, string>() },
{ "Fail", new Dictionary<string, string>() }
}
};
// 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<Item> CreateGlock()
{
// Create an array ready to hold weapon + all mods
var glock = new List<Item>();
// 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;
}
}
}
@@ -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<AddTraderWithDynamicAssorts> _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<AddTraderWithDynamicAssorts> 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<TraderConfig>();
_ragfairConfig = _configServer.GetConfig<RagfairConfig>();
}
public void PostDBLoad()
{
var traderImagePath = "./db/cat.jpg";
var baseJson = _fileUtil.ReadFile("./db/base.json");
var traderBase = _jsonUtil.Deserialize<TraderBase>(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";
}
}
@@ -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<AddTraderWithDynamicAssorts> _logger;
private readonly HashUtil _hashUtil;
private readonly List<Item> _itemsToSell = [];
private readonly Dictionary<string, List<List<BarterScheme>>> _barterScheme = new();
private readonly Dictionary<string, int> _loyaltyLevel = new();
public FluentTraderAssortCreator(
ISptLogger<AddTraderWithDynamicAssorts> 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<Item> 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;
}
}
@@ -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"
}
+5
View File
@@ -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