Merge branch 'develop' into linux-build-changes

This commit is contained in:
Cj
2025-06-15 22:49:39 -04:00
committed by GitHub
10 changed files with 229 additions and 357 deletions
@@ -22,7 +22,6 @@ public class LocationLootGenerator(
MathUtil _mathUtil,
HashUtil _hashUtil,
ItemHelper _itemHelper,
InventoryHelper _inventoryHelper,
DatabaseService _databaseService,
ContainerHelper _containerHelper,
PresetHelper _presetHelper,
@@ -33,8 +32,8 @@ public class LocationLootGenerator(
ICloner _cloner
)
{
protected LocationConfig _locationConfig = _configServer.GetConfig<LocationConfig>();
protected SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig<SeasonalEventConfig>();
protected readonly LocationConfig _locationConfig = _configServer.GetConfig<LocationConfig>();
protected readonly SeasonalEventConfig _seasonalEventConfig = _configServer.GetConfig<SeasonalEventConfig>();
/// Create a list of container objects with randomised loot
/// <param name="locationBase">Map base to generate containers for</param>
@@ -155,9 +154,10 @@ public class LocationLootGenerator(
return result;
}
var mapping = GetGroupIdToContainerMappings(mapData.Statics, staticRandomisableContainersOnMap);
// For each of the container groups, choose from the pool of containers, hydrate container with loot and add to result array
var mapping = GetGroupIdToContainerMappings(mapData.Statics, staticRandomisableContainersOnMap);
foreach (var (key, data) in mapping)
{
// Count chosen was 0, skip
@@ -177,7 +177,7 @@ public class LocationLootGenerator(
}
// EDGE CASE: These are containers without a group and have a probability < 100%
if (key == "")
if (key == string.Empty)
{
var containerIdsCopy = _cloner.Clone(data.ContainerIdsWithProbability);
// Roll each containers probability, if it passes, it gets added
@@ -235,7 +235,6 @@ public class LocationLootGenerator(
}
_logger.Success($"A total of: {staticLootItemCount} static items spawned");
_logger.Success(
_localisationService.GetText("location-containers_generated_success", staticContainerCount)
);
@@ -254,7 +253,7 @@ public class LocationLootGenerator(
staticContainer.Probability != 1 &&
!staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) &&
!_locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains(
staticContainer.Template.Items[0].Template
staticContainer.Template.Items.FirstOrDefault().Template
)
)
.ToList();
@@ -271,14 +270,14 @@ public class LocationLootGenerator(
staticContainer.Probability == 1 ||
staticContainer.Template.IsAlwaysSpawn.GetValueOrDefault(false) ||
_locationConfig.ContainerRandomisationSettings.ContainerTypesToNotRandomise.Contains(
staticContainer.Template.Items[0].Template
staticContainer.Template.Items.FirstOrDefault().Template
)
)
.ToList();
}
/// <summary>
/// Choose a number of containers based on their probabilty value to fulfil the desired count in
/// Choose a number of containers based on their probability value to fulfil the desired count in
/// containerData.chosenCount
/// </summary>
/// <param name="groupId">Name of the group the containers are being collected for</param>
@@ -346,12 +345,12 @@ public class LocationLootGenerator(
}
// Add an empty group for containers without a group id but still have a < 100% chance to spawn
// Likely bad BSG data, will be fixed...eventually, example of the groupids: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01`
mapping[""] = new ContainerGroupCount
// Likely bad BSG data, will be fixed...eventually, example of the groupIds: `NEED_TO_BE_FIXED1`,`NEED_TO_BE_FIXED_SE02`, `NEED_TO_BE_FIXED_NW_01`
mapping.Add(string.Empty, new ContainerGroupCount
{
ContainerIdsWithProbability = new Dictionary<string, double>(),
ChosenCount = -1
};
});
// Iterate over all containers and add to group keyed by groupId
// Containers without a group go into a group with empty key ""
@@ -413,12 +412,12 @@ public class LocationLootGenerator(
)
{
var containerClone = _cloner.Clone(staticContainer);
var containerTpl = containerClone.Template.Items[0].Template;
var containerTpl = containerClone.Template.Items.FirstOrDefault().Template;
// Create new unique parent id to prevent any collisions
var parentId = _hashUtil.Generate();
containerClone.Template.Root = parentId;
containerClone.Template.Items[0].Id = parentId;
containerClone.Template.Items.FirstOrDefault().Id = parentId;
var containerMap = _itemHelper.GetContainerMapping(containerTpl);
@@ -432,7 +431,7 @@ public class LocationLootGenerator(
// Get all possible loot items for container
var containerLootPool = GetPossibleLootItemsForContainer(containerTpl, staticLootDist);
// Some containers need to have items forced into it (quest keys etc)
// Some containers need to have items forced into it (quest keys etc.)
var tplsForced = staticForced
.Where(forcedStaticProp => forcedStaticProp.ContainerId == containerClone.Template.Id)
.Select(x => x.ItemTpl);
@@ -463,14 +462,9 @@ public class LocationLootGenerator(
continue;
}
if (tplToAdd == "5bf3e0490db83400196199af")
{
Console.WriteLine("yo");
}
// Check if item should have children removed
var items = _locationConfig.TplsToStripChildItemsFrom.Contains(tplToAdd)
? [chosenItemWithChildren.Items[0]] // Strip children from parent
? [chosenItemWithChildren.Items.FirstOrDefault()] // Strip children from parent
: chosenItemWithChildren.Items;
// look for open slot to put chosen item into
@@ -516,7 +510,7 @@ public class LocationLootGenerator(
}
/// <summary>
/// Look up a containers itemcountDistribution data and choose an item count based on the found weights
/// Look up a containers itemCountDistribution data and choose an item count based on the found weights
/// </summary>
/// <param name="containerTypeId">Container to get item count for</param>
/// <param name="staticLootDist">staticLoot.json</param>
@@ -546,8 +540,8 @@ public class LocationLootGenerator(
}
foreach (var itemCountDistribution in countDistribution)
// Add each count of items into array
{
// Add each count of items into array
itemCountArray.Add(
new ProbabilityObject<int, float?>(
itemCountDistribution.Count.Value,
@@ -588,14 +582,14 @@ public class LocationLootGenerator(
foreach (var icd in itemContainerDistribution)
{
if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(icd.Tpl))
// Skip seasonal event items if they're not enabled
{
// Skip seasonal event items if they're not enabled
continue;
}
// Ensure no blacklisted lootable items are in pool
if (_itemFilterService.IsLootableItemBlacklisted(icd.Tpl))
{
// Ensure no blacklisted lootable items are in pool
continue;
}
@@ -653,12 +647,11 @@ public class LocationLootGenerator(
// Add forced loot
AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist);
var allDynamicSpawnpoints = dynamicLootDist.Spawnpoints;
var allDynamicSpawnPoints = dynamicLootDist.Spawnpoints;
// Draw from random distribution
var desiredSpawnpointCount = Math.Round(
GetLooseLootMultiplierForLocation(locationName) *
_randomUtil.GetNormallyDistributedRandomNumber(
var desiredSpawnPointCount = Math.Round(
GetLooseLootMultiplierForLocation(locationName) * _randomUtil.GetNormallyDistributedRandomNumber(
(double) dynamicLootDist.SpawnpointCount.Mean,
(double) dynamicLootDist.SpawnpointCount.Std
)
@@ -667,59 +660,59 @@ public class LocationLootGenerator(
// Positions not in forced but have 100% chance to spawn
List<Spawnpoint> guaranteedLoosePoints = [];
var blacklistedSpawnpoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName);
var spawnpointArray = new ProbabilityObjectArray<string, Spawnpoint>(_mathUtil, _cloner);
var blacklistedSpawnPoints = _locationConfig.LooseLootBlacklist.GetValueOrDefault(locationName);
var spawnPointArray = new ProbabilityObjectArray<string, Spawnpoint>(_mathUtil, _cloner);
foreach (var spawnpoint in allDynamicSpawnpoints)
foreach (var spawnPoint in allDynamicSpawnPoints)
{
// Point is blacklisted, skip
if (blacklistedSpawnpoints?.Contains(spawnpoint.Template.Id) ?? false)
if (blacklistedSpawnPoints?.Contains(spawnPoint.Template.Id) ?? false)
{
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Ignoring loose loot location: {spawnpoint.Template.Id}");
_logger.Debug($"Ignoring loose loot location: {spawnPoint.Template.Id}");
}
continue;
}
// We've handled IsAlwaysSpawn above, so skip them
if (spawnpoint.Template.IsAlwaysSpawn ?? false)
if (spawnPoint.Template.IsAlwaysSpawn ?? false)
{
continue;
}
// 100%, add it to guaranteed
if (spawnpoint.Probability == 1)
if (spawnPoint.Probability == 1)
{
guaranteedLoosePoints.Add(spawnpoint);
guaranteedLoosePoints.Add(spawnPoint);
continue;
}
spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(spawnpoint.Template.Id, spawnpoint.Probability ?? 0, spawnpoint));
spawnPointArray.Add(new ProbabilityObject<string, Spawnpoint>(spawnPoint.Template.Id, spawnPoint.Probability ?? 0, spawnPoint));
}
// Select a number of spawn points to add loot to
// Add ALL loose loot with 100% chance to pool
List<Spawnpoint> chosenSpawnpoints = [];
chosenSpawnpoints.AddRange(guaranteedLoosePoints);
List<Spawnpoint> chosenSpawnPoints = [];
chosenSpawnPoints.AddRange(guaranteedLoosePoints);
var randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.Count;
var randomSpawnPointCount = desiredSpawnPointCount - chosenSpawnPoints.Count;
// Only draw random spawn points if needed
if (randomSpawnpointCount > 0 && spawnpointArray.Count > 0)
if (randomSpawnPointCount > 0 && spawnPointArray.Count > 0)
// Add randomly chosen spawn points
{
foreach (var si in spawnpointArray.Draw((int) randomSpawnpointCount, false))
foreach (var si in spawnPointArray.Draw((int) randomSpawnPointCount, false))
{
chosenSpawnpoints.Add(spawnpointArray.Data(si));
chosenSpawnPoints.Add(spawnPointArray.Data(si));
}
}
// Filter out duplicate locationIds // prob can be done better
chosenSpawnpoints = chosenSpawnpoints.GroupBy(spawnpoint => spawnpoint.LocationId).Select(group => group.First()).ToList();
chosenSpawnPoints = chosenSpawnPoints.GroupBy(spawnPoint => spawnPoint.LocationId).Select(group => group.First()).ToList();
// Do we have enough items in pool to fulfill requirement
var tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.Count > 0;
var tooManySpawnPointsRequested = desiredSpawnPointCount - chosenSpawnPoints.Count > 0;
if (tooManySpawnPointsRequested)
{
if (_logger.IsLogEnabled(LogLevel.Debug))
@@ -729,8 +722,8 @@ public class LocationLootGenerator(
"location-spawn_point_count_requested_vs_found",
new
{
requested = desiredSpawnpointCount + guaranteedLoosePoints.Count,
found = chosenSpawnpoints.Count,
requested = desiredSpawnPointCount + guaranteedLoosePoints.Count,
found = chosenSpawnPoints.Count,
mapName = locationName
}
)
@@ -738,12 +731,12 @@ public class LocationLootGenerator(
}
}
// Iterate over spawnpoints
// Iterate over spawnPoints
var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled();
var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems();
foreach (var spawnPoint in chosenSpawnpoints)
foreach (var spawnPoint in chosenSpawnPoints)
{
// Spawnpoint is invalid, skip it
// SpawnPoint is invalid, skip it
if (spawnPoint.Template is null)
{
_logger.Warning(
@@ -840,9 +833,8 @@ public class LocationLootGenerator(
foreach (var itemTpl in lootToForceSingleAmountOnMap)
{
// Get all spawn positions for item tpl in forced loot array
var items = forcedSpawnPoints.Where(forcedSpawnPoint => forcedSpawnPoint.Template.Items[0].Template == itemTpl
);
if (items is null || !items.Any())
var items = forcedSpawnPoints.Where(forcedSpawnPoint => forcedSpawnPoint.Template.Items.FirstOrDefault().Template == itemTpl);
if (!items.Any())
{
if (_logger.IsLogEnabled(LogLevel.Debug))
{
@@ -853,15 +845,15 @@ public class LocationLootGenerator(
}
// Create probability array of all spawn positions for this spawn id
var spawnpointArray = new ProbabilityObjectArray<string, Spawnpoint>(_mathUtil, _cloner);
var spawnPointArray = new ProbabilityObjectArray<string, Spawnpoint>(_mathUtil, _cloner);
foreach (var si in items)
// use locationId as template.Id is the same across all items
{
spawnpointArray.Add(new ProbabilityObject<string, Spawnpoint>(si.LocationId, si.Probability ?? 0, si));
spawnPointArray.Add(new ProbabilityObject<string, Spawnpoint>(si.LocationId, si.Probability ?? 0, si));
}
// Choose 1 out of all found spawn positions for spawn id and add to loot array
foreach (var spawnPointLocationId in spawnpointArray.Draw(1, false))
foreach (var spawnPointLocationId in spawnPointArray.Draw(1, false))
{
var itemToAdd = items.FirstOrDefault(item => item.LocationId == spawnPointLocationId);
var lootItem = itemToAdd?.Template;
@@ -1032,14 +1024,14 @@ public class LocationLootGenerator(
if (_locationConfig.TplsToStripChildItemsFrom.Contains(chosenItem.Template))
// Strip children from parent before adding
{
itemWithChildren = [itemWithChildren[0]];
itemWithChildren = [itemWithChildren.FirstOrDefault()];
}
itemWithMods.AddRange(itemWithChildren);
}
// Get inventory size of item
var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods[0].Id);
var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods.FirstOrDefault().Id);
return new ContainerItem
{
@@ -1100,13 +1092,13 @@ public class LocationLootGenerator(
// No spawn point, use default template
else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON))
{
rootItem = CreateWeaponItems(chosenTpl, staticAmmoDist, parentId, ref items);
rootItem = CreateWeaponRootAndChildren(chosenTpl, staticAmmoDist, parentId, ref items);
var size = _itemHelper.GetItemSize(items, rootItem.Id);
width = size.Width;
height = size.Height;
}
// No spawnpoint to fall back on, generate manually
// No spawnPoint to fall back on, generate manually
else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX))
{
_itemHelper.AddCartridgesToAmmoBox(items, itemTemplate);
@@ -1141,7 +1133,7 @@ public class LocationLootGenerator(
_itemHelper.RemapRootItemId(presetAndMods);
// Use original items parentId otherwise item doesn't get added to container correctly
presetAndMods[0].ParentId = rootItem.ParentId;
presetAndMods.FirstOrDefault().ParentId = rootItem.ParentId;
items = presetAndMods;
}
else
@@ -1160,15 +1152,30 @@ public class LocationLootGenerator(
return items;
}
protected Item? CreateWeaponItems(string chosenTpl, Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist, string? parentId, ref List<Item> items)
/// <summary>
/// Attempt to find default preset for passed in tpl and construct a weapon with children.
/// If no preset found, return chosenTpl as Item object
/// </summary>
/// <param name="chosenTpl">Tpl of item to get preset for</param>
/// <param name="cartridgePool">Pool of cartridges to pick from</param>
/// <param name="parentId"></param>
/// <param name="items">Root item + children</param>
/// <returns>Root Item</returns>
protected Item? CreateWeaponRootAndChildren(
string chosenTpl,
Dictionary<string,List<StaticAmmoDetails>> cartridgePool,
string? parentId,
ref List<Item> items)
{
List<Item> children = [];
// Look up a default preset for desired weapon tpl
var defaultPreset = _cloner.Clone(_presetHelper.GetDefaultPreset(chosenTpl));
if (defaultPreset?.Items is not null)
{
try
{
children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items[0], defaultPreset.Items);
children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items.FirstOrDefault(), defaultPreset.Items);
}
catch (Exception e)
{
@@ -1194,7 +1201,7 @@ public class LocationLootGenerator(
}
else
{
// RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no chidren to reparent
// RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesn't have any default presets and kills this code below as it has no children to re-parent
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"createStaticLootItem() No preset found for weapon: {chosenTpl}");
@@ -1259,7 +1266,7 @@ public class LocationLootGenerator(
_itemHelper.FillMagazineWithRandomCartridge(
magazineWithCartridges,
magTemplate,
staticAmmoDist,
cartridgePool,
weaponTemplate.Properties.AmmoCaliber,
0.25,
defaultWeapon.Properties.DefAmmo,
@@ -1274,7 +1281,10 @@ public class LocationLootGenerator(
return rootItem;
}
protected void GenerateStaticMagazineItem(Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist, Item? rootItem, TemplateItem itemTemplate,
protected void GenerateStaticMagazineItem(
Dictionary<string, List<StaticAmmoDetails>> staticAmmoDist,
Item? rootItem,
TemplateItem itemTemplate,
List<Item> items)
{
List<Item> magazineWithCartridges = [rootItem];
@@ -1292,7 +1302,7 @@ public class LocationLootGenerator(
}
}
public class ContainerGroupCount
public record ContainerGroupCount
{
[JsonPropertyName("containerIdsWithProbability")]
public Dictionary<string, double>? ContainerIdsWithProbability
@@ -4,32 +4,16 @@ using SPTarkov.Server.Core.Models.Spt.Config;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Servers;
using SPTarkov.Server.Core.Services;
using SPTarkov.Server.Core.Utils;
namespace SPTarkov.Server.Core.Generators;
[Injectable]
public class PmcWaveGenerator
public class PmcWaveGenerator(
ISptLogger<PmcWaveGenerator> logger,
DatabaseService databaseService,
ConfigServer configServer)
{
protected ConfigServer _configServer;
protected DatabaseService _databaseService;
protected ISptLogger<PmcWaveGenerator> _logger;
protected PmcConfig _pmcConfig;
protected RandomUtil _randomUtil;
public PmcWaveGenerator(
ISptLogger<PmcWaveGenerator> _logger,
RandomUtil _randomUtil,
DatabaseService _databaseService,
ConfigServer _configServer
)
{
this._logger = _logger;
this._randomUtil = _randomUtil;
this._databaseService = _databaseService;
this._configServer = _configServer;
_pmcConfig = _configServer.GetConfig<PmcConfig>();
}
protected readonly PmcConfig _pmcConfig = configServer.GetConfig<PmcConfig>();
/// <summary>
/// Add a pmc wave to a map
@@ -63,13 +47,8 @@ public class PmcWaveGenerator
return;
}
var location = _databaseService.GetLocation(name);
if (location is null)
{
return;
}
location.Base.BossLocationSpawn.AddRange(pmcWavesToAdd);
var location = databaseService.GetLocation(name);
location?.Base.BossLocationSpawn.AddRange(pmcWavesToAdd);
}
/// <summary>
@@ -1,58 +0,0 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Services.Cache;
[Injectable]
public class BundleHashCacheService(
ISptLogger<BundleHashCacheService> _logger,
HashUtil _hashUtil,
JsonUtil _jsonUtil,
FileUtil _fileUtil
)
{
protected static readonly string _bundleHashCachePath = "./user/cache/bundleHashCache.json";
protected Dictionary<string, string> _bundleHashes = new();
public string GetStoredValue(string key)
{
_bundleHashes.TryGetValue(key, out var value);
return value;
}
public void StoreValue(string key, string value)
{
_bundleHashes.Add(key, value);
_fileUtil.WriteFile(_bundleHashCachePath, _jsonUtil.Serialize(_bundleHashes));
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Bundle {key} hash stored in {_bundleHashCachePath}");
}
}
public bool MatchWithStoredHash(string bundlePath, string hash)
{
return GetStoredValue(bundlePath) == hash;
}
public bool CalculateAndMatchHash(string bundlePath)
{
var fileContents = _fileUtil.ReadFile(bundlePath);
var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.MD5, fileContents);
return MatchWithStoredHash(bundlePath, generatedHash);
}
public void CalculateAndStoreHash(string bundlePath)
{
var fileContents = _fileUtil.ReadFile(bundlePath);
var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.MD5, fileContents);
StoreValue(bundlePath, generatedHash);
}
}
@@ -1,56 +0,0 @@
using SPTarkov.DI.Annotations;
using SPTarkov.Server.Core.Models.Utils;
using SPTarkov.Server.Core.Utils;
using LogLevel = SPTarkov.Server.Core.Models.Spt.Logging.LogLevel;
namespace SPTarkov.Server.Core.Services.Cache;
[Injectable]
public class ModHashCacheService(
ISptLogger<ModHashCacheService> _logger,
JsonUtil _jsonUtil,
HashUtil _hashUtil,
FileUtil _fileUtil
)
{
protected readonly string _modCachePath = "./user/cache/modCache.json";
protected readonly Dictionary<string, string> _modHashes = new();
public string? GetStoredValue(string key)
{
_modHashes.TryGetValue(key, out var value);
return value;
}
public void StoreValue(string key, string value)
{
_modHashes.TryAdd(key, value);
_fileUtil.WriteFile(_modCachePath, _jsonUtil.Serialize(_modHashes));
if (_logger.IsLogEnabled(LogLevel.Debug))
{
_logger.Debug($"Mod {key} hash stored in: {_modCachePath}");
}
}
public bool MatchWithStoredHash(string modName, string hash)
{
return GetStoredValue(modName) == hash;
}
public bool CalculateAndCompareHash(string modName, string modContent)
{
var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.SHA1, modContent);
return MatchWithStoredHash(modName, generatedHash);
}
public void CalculateAndStoreHash(string modName, string modContent)
{
var generatedHash = _hashUtil.GenerateHashForData(HashingAlgorithm.SHA1, modContent);
StoreValue(modName, generatedHash);
}
}
@@ -26,7 +26,7 @@ public class DatabaseService(
HashUtil _hashUtil
)
{
protected bool isDataValid = true;
private bool _isDataValid = true;
/// <returns> assets/database/ </returns>
public DatabaseTables GetTables()
@@ -105,13 +105,15 @@ public class DatabaseService(
/// </summary>
/// <param name="locationId"> Desired location ID </param>
/// <returns> assets/database/locations/ </returns>
public Location GetLocation(string locationId)
public Location? GetLocation(string locationId)
{
var locations = GetLocations();
var desiredLocation = locations.GetByJsonProp<Location>(locationId.ToLower());
if (desiredLocation == null)
{
throw new Exception(_localisationService.GetText("database-no_location_found_with_id", locationId));
_logger.Error(_localisationService.GetText("database-no_location_found_with_id", locationId));
return null;
}
return desiredLocation;
@@ -297,12 +299,14 @@ public class DatabaseService(
/// </summary>
/// <param name="traderId"> Desired trader ID </param>
/// <returns> assets/database/traders/ </returns>
public Trader GetTrader(string traderId)
public Trader? GetTrader(string traderId)
{
var traders = GetTraders();
if (!traders.TryGetValue(traderId, out var desiredTrader))
{
throw new Exception(_localisationService.GetText("database-no_trader_found_with_id", traderId));
_logger.Error(_localisationService.GetText("database-no_trader_found_with_id", traderId));
return null;
}
return desiredTrader;
@@ -326,13 +330,13 @@ public class DatabaseService(
{
var start = Stopwatch.StartNew();
isDataValid =
_isDataValid =
ValidateTable(GetQuests(), "quest") &&
ValidateTable(GetTraders(), "trader") &&
ValidateTable(GetItems(), "item") &&
ValidateTable(GetCustomization(), "customization");
if (!isDataValid)
if (!_isDataValid)
{
_logger.Error(_localisationService.GetText("database-invalid_data"));
}
@@ -370,6 +374,6 @@ public class DatabaseService(
/// <returns> True if the database contains valid data, false otherwise </returns>
public bool IsDatabaseValid()
{
return isDataValid;
return _isDataValid;
}
}
@@ -13,13 +13,10 @@ public class LocaleService(
ConfigServer _configServer
)
{
// we have to LazyLoad the data from the database and then combine it with the custom data before returning it
protected readonly LocaleConfig _localeConfig = _configServer.GetConfig<LocaleConfig>();
protected readonly Dictionary<string, Dictionary<string, string>> customClientLocales = new();
/// <summary>
/// Get the eft globals db file based on the configured locale in config/locale.json, if not found, fall back to 'en'
/// This will contain Custom locales added by mods
/// </summary>
/// <returns> Dictionary of locales for desired language - en/fr/cn </returns>
public Dictionary<string, string> GetLocaleDb(string? language = null)
@@ -29,23 +26,21 @@ public class LocaleService(
: language;
// if it can't get locales for language provided, default to en
if (TryGetLocaleDbWithCustomLocales(languageToUse, out var localeToReturn) ||
TryGetLocaleDbWithCustomLocales("en", out localeToReturn))
if (TryGetLocaleDb(languageToUse, out var localeToReturn) || TryGetLocaleDb("en", out localeToReturn))
{
// TODO: need to see if this needs to be cloned
return RemovePraporTestMessage(localeToReturn);
return localeToReturn;
}
throw new Exception($"unable to get locales from either {languageToUse} or en");
}
/// <summary>
/// Attempts to retrieve the locale database for the specified language key, including custom locales if available.
/// Attempts to retrieve the locale database for the specified language key
/// </summary>
/// <param name="languageKey">The language key for which the locale database should be retrieved.</param>
/// <param name="localeToReturn">The resulting locale database as a dictionary, or null if the operation fails.</param>
/// <returns>True if the locale database was successfully retrieved, otherwise false.</returns>
protected bool TryGetLocaleDbWithCustomLocales(string languageKey, out Dictionary<string, string>? localeToReturn)
protected bool TryGetLocaleDb(string languageKey, out Dictionary<string, string>? localeToReturn)
{
localeToReturn = null;
if (!_databaseServer.GetTables().Locales.Global.TryGetValue(languageKey, out var keyedLocales))
@@ -55,42 +50,9 @@ public class LocaleService(
localeToReturn = keyedLocales.Value;
if (customClientLocales.TryGetValue(languageKey, out var customClientLocale))
{
// there were custom locales for this language
localeToReturn = CombineDbWithCustomLocales(localeToReturn, customClientLocale);
}
return true;
}
/// <summary>
/// Combines the provided database locales with custom locales, ensuring that all entries are merged into a single dictionary.
/// Custom locale entries will overwrite existing keys from the database locales if conflicts occur.
/// </summary>
/// <param name="dbLocales">The dictionary containing locale entries from the database.</param>
/// <param name="customLocales">The dictionary containing custom locale entries to be merged.</param>
/// <returns>A dictionary representing the merged result of database and custom locales.</returns>
protected Dictionary<string, string> CombineDbWithCustomLocales(Dictionary<string, string> dbLocales, Dictionary<string, string> customLocales)
{
try
{
return dbLocales
.Concat(customLocales)
.GroupBy(kvp => kvp.Key)
.ToDictionary(
group => group.Key,
group => group.Last().Value
);
}
catch (Exception e)
{
Console.WriteLine(e);
throw;
}
}
/// <summary>
/// Gets the game locale key from the locale.json file,
/// if value is 'system' get system-configured locale
@@ -239,33 +201,6 @@ public class LocaleService(
return GetLocaleDb().Keys.Where(x => x.StartsWith(partialKey)).ToList();
}
public void AddCustomClientLocale(string locale, string localeKey, string localeValue)
{
AddToDictionary(locale, localeKey, localeValue, customClientLocales);
}
public void RemoveCustomClientLocale(string locale, string localeKey)
{
customClientLocales.Remove(localeKey);
}
private void AddToDictionary(string locale, string localeKey, string localeValue,
Dictionary<string, Dictionary<string, string>> dictionaryToAddTo)
{
dictionaryToAddTo.TryAdd(locale, new Dictionary<string, string>());
if (!dictionaryToAddTo.TryGetValue(locale, out var localeDictToAddTo))
{
_logger.Error($"Unable to get custom locale dictionary keyed by: {locale}");
return;
}
if (!localeDictToAddTo.TryAdd(localeKey, localeValue))
{
localeDictToAddTo[localeKey] = localeValue;
}
}
/// <summary>
/// Blank out the "test" mail message from prapor
/// </summary>
@@ -243,9 +243,17 @@ public class CustomItemService(
newLocaleDetails ??= localeDetails[localeDetails.Keys.FirstOrDefault()];
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Name", newLocaleDetails.Name);
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} ShortName", newLocaleDetails.ShortName);
localeService.AddCustomClientLocale(shortNameKey.Key, $"{newItemId} Description", newLocaleDetails.Description);
if (databaseService.GetLocales().Global.TryGetValue(shortNameKey.Key, out var lazyLoad))
{
lazyLoad.AddTransformer(localeData =>
{
localeData.Add($"{newItemId} Name", newLocaleDetails.Name);
localeData.Add($"{newItemId} ShortName", newLocaleDetails.ShortName);
localeData.Add($"{newItemId} Description", newLocaleDetails.Description);
return localeData;
});
}
}
}
@@ -102,6 +102,8 @@ public class PostDbLoadService(
RemoveNewBeginningRequirementFromPrestige();
RemovePraporTestMessage();
ValidateQuestAssortUnlocksExist();
if (_seasonalEventService.IsAutomaticEventDetectionEnabled())
@@ -191,6 +193,19 @@ public class PostDbLoadService(
}
}
private void RemovePraporTestMessage()
{
foreach((var locale, var lazyLoad) in _databaseService.GetLocales().Global)
{
lazyLoad.AddTransformer(lazyloadedData =>
{
lazyloadedData["61687e2c3e526901fa76baf9"] = "";
return lazyloadedData;
});
}
}
protected void CloneExistingCraftsAndAddNew()
{
var hideoutCraftDb = _databaseService.GetHideout().Production;
@@ -264,34 +279,38 @@ public class PostDbLoadService(
continue;
}
var mapLooseLoot = _databaseService.GetLocation(mapId).LooseLoot.Value;
if (mapLooseLoot is null)
_databaseService.GetLocation(mapId).LooseLoot.AddTransformer(looselootData =>
{
_logger.Warning(
_localisationService.GetText("location-map_has_no_loose_loot_data", mapId)
);
continue;
}
foreach (var positionToAdd in positionsToAdd)
{
// Exists already, add new items to existing positions pool
var existingLootPosition = mapLooseLoot.Spawnpoints.FirstOrDefault(x =>
x.Template.Id == positionToAdd.Template.Id
);
if (existingLootPosition is not null)
if (looselootData is null)
{
existingLootPosition.Template.Items.AddRange(positionToAdd.Template.Items);
existingLootPosition.ItemDistribution.AddRange(positionToAdd.ItemDistribution);
_logger.Warning(
_localisationService.GetText("location-map_has_no_loose_loot_data", mapId)
);
continue;
return looselootData;
}
// New position, add entire object
mapLooseLoot.Spawnpoints.Add(positionToAdd);
}
foreach (var positionToAdd in positionsToAdd)
{
// Exists already, add new items to existing positions pool
var existingLootPosition = looselootData.Spawnpoints.FirstOrDefault(x =>
x.Template.Id == positionToAdd.Template.Id
);
if (existingLootPosition is not null)
{
existingLootPosition.Template.Items.AddRange(positionToAdd.Template.Items);
existingLootPosition.ItemDistribution.AddRange(positionToAdd.ItemDistribution);
continue;
}
// New position, add entire object
looselootData.Spawnpoints.Add(positionToAdd);
}
return looselootData;
});
}
}
@@ -396,35 +415,39 @@ public class PostDbLoadService(
foreach (var (mapId, mapAdjustments) in _lootConfig.LooseLootSpawnPointAdjustments)
{
var mapLooseLootData = _databaseService.GetLocation(mapId).LooseLoot.Value;
if (mapLooseLootData is null)
_databaseService.GetLocation(mapId).LooseLoot.AddTransformer(looselootData =>
{
_logger.Warning(
_localisationService.GetText("location-map_has_no_loose_loot_data", mapId)
);
continue;
}
foreach (var (lootKey, newChanceValue) in mapAdjustments)
{
var lootPostionToAdjust = mapLooseLootData.Spawnpoints.FirstOrDefault(spawnPoint =>
spawnPoint.Template.Id == lootKey
);
if (lootPostionToAdjust is null)
if (looselootData is null)
{
_logger.Warning(
_localisationService.GetText(
"location-unable_to_adjust_loot_position_on_map",
new { lootKey, mapId }
)
_localisationService.GetText("location-map_has_no_loose_loot_data", mapId)
);
continue;
return looselootData;
}
lootPostionToAdjust.Probability = newChanceValue;
}
foreach (var (lootKey, newChanceValue) in mapAdjustments)
{
var lootPostionToAdjust = looselootData.Spawnpoints.FirstOrDefault(spawnPoint =>
spawnPoint.Template.Id == lootKey
);
if (lootPostionToAdjust is null)
{
_logger.Warning(
_localisationService.GetText(
"location-unable_to_adjust_loot_position_on_map",
new { lootKey, mapId }
)
);
continue;
}
lootPostionToAdjust.Probability = newChanceValue;
}
return looselootData;
});
}
}
@@ -518,11 +541,11 @@ public class PostDbLoadService(
}
foreach (var area in _databaseService.GetHideout().Areas)
foreach (var (_, stage) in area.Stages)
// Only adjust crafts ABOVE the override
{
stage.ConstructionTime = Math.Min(stage.ConstructionTime.Value, overrideSeconds);
}
foreach (var (_, stage) in area.Stages)
// Only adjust crafts ABOVE the override
{
stage.ConstructionTime = Math.Min(stage.ConstructionTime.Value, overrideSeconds);
}
}
protected void UnlockHideoutLootCrateCrafts()
@@ -1030,8 +1030,16 @@ public class SeasonalEventService(
protected void RenameBitcoin()
{
_localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name", "Physical SPT Coin");
_localeService.AddCustomClientLocale("en", $"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName", "0.2SPT");
if(_databaseService.GetLocales().Global.TryGetValue("en", out var lazyLoad))
{
lazyLoad.AddTransformer(localeData =>
{
localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} Name"] = "Physical SPT Coin";
localeData[$"{ItemTpl.BARTER_PHYSICAL_BITCOIN} ShortName"] = "0.2SPT";
return localeData;
});
}
}
/// <summary>
@@ -2,6 +2,8 @@
public class LazyLoad<T>(Func<T> deserialize)
{
private readonly List<Func<T?, T?>> _lazyLoadTransformers = [];
private readonly ReaderWriterLockSlim _lazyLoadTransformersLock = new();
private static readonly TimeSpan _autoCleanerTimeout = TimeSpan.FromSeconds(30);
private bool _isLoaded;
private T? _result;
@@ -9,10 +11,23 @@ public class LazyLoad<T>(Func<T> deserialize)
private Timer? autoCleanerTimeout;
/// <summary>
/// <see cref="OnLazyLoad" /> can be subscribed to for mods to modify. It is fired right after lazy loading is complete
/// and any modification passed to <see cref="OnLazyLoadEventArgs.Value" /> will stay for the duration of this <see cref="LazyLoad{T}"/>'s lifecycle
/// Adds a transformer to modify the value during lazy loading. Transformers execute
/// in registration order and the final result is cached until auto-cleanup.
/// </summary>
public event EventHandler<OnLazyLoadEventArgs<T>>? OnLazyLoad;
/// <param name="transformer">Function that transforms the value</param>
public void AddTransformer(Func<T?, T?> transformer)
{
_lazyLoadTransformersLock.EnterWriteLock();
try
{
_lazyLoadTransformers.Add(transformer);
}
finally
{
_lazyLoadTransformersLock.ExitWriteLock();
}
}
public T? Value
{
@@ -23,12 +38,21 @@ public class LazyLoad<T>(Func<T> deserialize)
_result = deserialize();
_isLoaded = true;
OnLazyLoadEventArgs<T> args = new(_result);
OnLazyLoad?.Invoke(this, args);
if (args.Value != null)
_lazyLoadTransformersLock.EnterReadLock();
try
{
_result = args.Value;
foreach (var transform in _lazyLoadTransformers)
{
_result = transform(_result);
}
}
catch(Exception)
{
throw;
}
finally
{
_lazyLoadTransformersLock.ExitReadLock();
}
autoCleanerTimeout = new Timer(
@@ -43,15 +67,10 @@ public class LazyLoad<T>(Func<T> deserialize)
_autoCleanerTimeout,
Timeout.InfiniteTimeSpan
);
}
}
autoCleanerTimeout?.Change(_autoCleanerTimeout, Timeout.InfiniteTimeSpan);
return _result;
}
}
}
public class OnLazyLoadEventArgs<T>(T value) : EventArgs
{
public T Value { get; set; } = value;
}