From 0aa6ef203962278619a415334954f5da1a141906 Mon Sep 17 00:00:00 2001 From: CWX Date: Sat, 25 Jan 2025 00:14:58 +0000 Subject: [PATCH] LocationLootGen done --- .../Core/Generators/LocationLootGenerator.cs | 484 ++++++++++++++++-- Libraries/Core/Models/Eft/Common/LooseLoot.cs | 14 +- Libraries/Core/Utils/App.cs | 2 +- 3 files changed, 443 insertions(+), 57 deletions(-) diff --git a/Libraries/Core/Generators/LocationLootGenerator.cs b/Libraries/Core/Generators/LocationLootGenerator.cs index 1c0fd89e..507494af 100644 --- a/Libraries/Core/Generators/LocationLootGenerator.cs +++ b/Libraries/Core/Generators/LocationLootGenerator.cs @@ -422,7 +422,8 @@ public class LocationLootGenerator( // Add forced loot to chosen item pool var tplsToAddToContainer = tplsForced.Concat(chosenTpls); - foreach (var tplToAdd in tplsToAddToContainer) { + foreach (var tplToAdd in tplsToAddToContainer) + { var chosenItemWithChildren = CreateStaticLootItem(tplToAdd, staticAmmoDist, parentId); if (chosenItemWithChildren is null) { @@ -457,7 +458,8 @@ public class LocationLootGenerator( result.Y.Value, (int)width, (int)height, - result.Rotation.GetValueOrDefault(false)); + result.Rotation.GetValueOrDefault(false) + ); var rotation = result.Rotation.GetValueOrDefault(false) ? 1 : 0; @@ -465,7 +467,8 @@ public class LocationLootGenerator( items[0].Location = new { X = result.X, Y = result.Y, R = rotation }; // Add loot to container before returning - foreach (var item in items) { + foreach (var item in items) + { containerClone.Template.Items.Add(item); } } @@ -596,7 +599,189 @@ public class LocationLootGenerator( Dictionary> staticAmmoDist, string locationName) { - throw new NotImplementedException(); + List loot = []; + List dynamicForcedSpawnPoints = []; + + // Remove christmas items from loot data + if (!_seasonalEventService.ChristmasEventEnabled()) + { + dynamicLootDist.Spawnpoints = dynamicLootDist.Spawnpoints.Where( + (point) => !point.Template.Id.StartsWith("christmas") + ) + .ToList(); + dynamicLootDist.SpawnpointsForced = dynamicLootDist.SpawnpointsForced.Where( + (point) => !point.Template.Id.StartsWith("christmas") + ) + .ToList(); + } + + // Build the list of forced loot from both `spawnpointsForced` and any point marked `IsAlwaysSpawn` + dynamicForcedSpawnPoints.AddRange(dynamicLootDist.SpawnpointsForced); + dynamicForcedSpawnPoints.AddRange(dynamicLootDist.Spawnpoints.Where((point) => point.Template.IsAlwaysSpawn ?? false)); + + // Add forced loot + AddForcedLoot(loot, dynamicForcedSpawnPoints, locationName, staticAmmoDist); + + var allDynamicSpawnpoints = dynamicLootDist.Spawnpoints; + + // Draw from random distribution + var desiredSpawnpointCount = Math.Round( + GetLooseLootMultiplerForLocation(locationName) * + _randomUtil.GetNormallyDistributedRandomNumber( + (double)dynamicLootDist.SpawnpointCount.Mean, + (double)dynamicLootDist.SpawnpointCount.Std + ) + ); + + // Positions not in forced but have 100% chance to spawn + List guaranteedLoosePoints = []; + + var blacklistedSpawnpoints = _locationConfig.LooseLootBlacklist[locationName]; + var spawnpointArray = new ProbabilityObjectArray, string, Spawnpoint>(_mathUtil, _cloner, []); + + foreach (var spawnpoint in allDynamicSpawnpoints) + { + // Point is blacklsited, skip + if (blacklistedSpawnpoints?.Contains(spawnpoint.Template.Id) ?? false) + { + _logger.Debug($"Ignoring loose loot location: {spawnpoint.Template.Id}"); + continue; + } + + // We've handled IsAlwaysSpawn above, so skip them + if (spawnpoint.Template.IsAlwaysSpawn ?? false) + { + continue; + } + + // 100%, add it to guaranteed + if (spawnpoint.Probability == 1) + { + guaranteedLoosePoints.Add(spawnpoint); + continue; + } + + spawnpointArray.Add(new ProbabilityObject(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 chosenSpawnpoints = []; + chosenSpawnpoints.AddRange(guaranteedLoosePoints); + + var randomSpawnpointCount = desiredSpawnpointCount - chosenSpawnpoints.Count; + // Only draw random spawn points if needed + if (randomSpawnpointCount > 0 && spawnpointArray.Count > 0) + { + // Add randomly chosen spawn points + foreach (var si in spawnpointArray.Draw((int)randomSpawnpointCount, false)) + { + 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(); + + // Do we have enough items in pool to fulfill requirement + var tooManySpawnPointsRequested = desiredSpawnpointCount - chosenSpawnpoints.Count > 0; + if (tooManySpawnPointsRequested) + { + _logger.Debug( + _localisationService.GetText( + "location-spawn_point_count_requested_vs_found", + new + { + requested = desiredSpawnpointCount + guaranteedLoosePoints.Count, + found = chosenSpawnpoints.Count, + mapName = locationName, + } + ) + ); + } + + // Iterate over spawnpoints + var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled(); + var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems(); + foreach (var spawnPoint in chosenSpawnpoints) + { + // Spawnpoint is invalid, skip it + if (spawnPoint.Template is null) + { + _logger.Warning( + _localisationService.GetText("location-missing_dynamic_template", spawnPoint.LocationId) + ); + + continue; + } + + // Ensure no blacklisted lootable items are in pool + spawnPoint.Template.Items = spawnPoint.Template.Items.Where( + (item) => !_itemFilterService.IsLootableItemBlacklisted(item.Template) + ) + .ToList(); + + // Ensure no seasonal items are in pool if not in-season + if (!seasonalEventActive) + { + spawnPoint.Template.Items = spawnPoint.Template.Items.Where( + (item) => !seasonalItemTplBlacklist.Contains(item.Template) + ) + .ToList(); + } + + // Spawn point has no items after filtering, skip + if (spawnPoint.Template.Items is null || spawnPoint.Template.Items.Count == 0) + { + _logger.Warning( + _localisationService.GetText("location-spawnpoint_missing_items", spawnPoint.Template.Id) + ); + + continue; + } + + // Get an array of allowed IDs after above filtering has occured + var validItemIds = spawnPoint.Template.Items.Select((item) => item.Id).ToList(); + + // Construct container to hold above filtered items, letting us pick an item for the spot + var itemArray = new ProbabilityObjectArray, string, double?>(_mathUtil, _cloner, []); + foreach (var itemDist in spawnPoint.ItemDistribution) + { + if (!validItemIds.Contains(itemDist.ComposedKey.Key)) + { + continue; + } + + itemArray.Add(new ProbabilityObject(itemDist.ComposedKey.Key, itemDist.RelativeProbability ?? 0, null)); + } + + if (itemArray.Count == 0) + { + _logger.Warning( + _localisationService.GetText("location-loot_pool_is_empty_skipping", spawnPoint.Template.Id) + ); + + continue; + } + + // Draw a random item from spawn points possible items + var chosenComposedKey = itemArray.Draw(1).FirstOrDefault(); + var createItemResult = CreateDynamicLootItem( + chosenComposedKey, + spawnPoint.Template.Items, + staticAmmoDist + ); + + // Root id can change when generating a weapon, ensure ids match + spawnPoint.Template.Root = createItemResult.Items.FirstOrDefault().Id; + + // Overwrite entire pool with chosen item + spawnPoint.Template.Items = createItemResult.Items; + + loot.Add(spawnPoint.Template); + } + + return loot; } /// @@ -606,12 +791,186 @@ public class LocationLootGenerator( /// Forced loot locations that must be added /// Name of map currently having force loot created for protected void AddForcedLoot(List lootLocationTemplates, - List forcedSpawnPoints, string locationName, + List forcedSpawnPoints, string locationName, Dictionary> staticAmmoDist) { - throw new NotImplementedException(); + var lootToForceSingleAmountOnMap = _locationConfig.ForcedLootSingleSpawnById[locationName]; + if (lootToForceSingleAmountOnMap is not null) + { + // Process loot items defined as requiring only 1 spawn position as they appear in multiple positions on the map + 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()) + { + _logger.Debug($"Unable to adjust loot item {itemTpl} as it does not exist inside {locationName} forced loot."); + continue; + } + + // Create probability array of all spawn positions for this spawn id + 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(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)) + { + var itemToAdd = items.FirstOrDefault((item) => item.LocationId == spawnPointLocationId); + var lootItem = itemToAdd?.Template; + if (lootItem is null) + { + _logger.Warning($"Item with spawn point id {spawnPointLocationId} could not be found, skipping"); + continue; + } + + var createItemResult = CreateDynamicLootItem( + lootItem.Items.FirstOrDefault().Id, + lootItem.Items, + staticAmmoDist + ); + + // Update root ID with the dynamically generated ID + lootItem.Root = createItemResult.Items.FirstOrDefault().Id; + lootItem.Items = createItemResult.Items; + lootLocationTemplates.Add(lootItem); + } + } + } + + var seasonalEventActive = _seasonalEventService.SeasonalEventEnabled(); + var seasonalItemTplBlacklist = _seasonalEventService.GetInactiveSeasonalEventItems(); + + // Add remaining forced loot to array + foreach (var forcedLootLocation in forcedSpawnPoints) + { + var firstLootItemTpl = forcedLootLocation.Template.Items.FirstOrDefault().Template; + + // Skip spawn positions processed already + if (lootToForceSingleAmountOnMap?.Contains(firstLootItemTpl) ?? false) + { + continue; + } + + // Skip adding seasonal items when seasonal event is not active + if (!seasonalEventActive && seasonalItemTplBlacklist.Contains(firstLootItemTpl)) + { + continue; + } + + var locationTemplateToAdd = forcedLootLocation.Template; + var createItemResult = CreateDynamicLootItem( + locationTemplateToAdd.Items.FirstOrDefault().Id, + forcedLootLocation.Template.Items, + staticAmmoDist + ); + + // Update root ID with the dynamically generated ID + forcedLootLocation.Template.Root = createItemResult.Items.FirstOrDefault().Id; + forcedLootLocation.Template.Items = createItemResult.Items; + + // Push forced location into array as long as it doesnt exist already + var existingLocation = lootLocationTemplates.Any( + (spawnPoint) => spawnPoint.Id == locationTemplateToAdd.Id + ); + if (!existingLocation) + { + lootLocationTemplates.Add(locationTemplateToAdd); + } + else + { + _logger.Debug( + $"Attempted to add a forced loot location with Id: {locationTemplateToAdd.Id} to map {locationName} that already has that id in use, skipping" + ); + } + } } + private ContainerItem CreateDynamicLootItem(string? chosenComposedKey, List items, Dictionary> staticAmmoDist) + { + var chosenItem = items.FirstOrDefault((item) => item.Id == chosenComposedKey); + var chosenTpl = chosenItem?.Template; + if (chosenTpl is null) { + throw new Exception($"Item for tpl {chosenComposedKey} was not found in the spawn point"); + } + var itemTemplate = _itemHelper.GetItem(chosenTpl).Value; + if (itemTemplate is null) { + _logger.Error($"Item tpl: {chosenTpl} cannot be found in database"); + } + + // Item array to return + List itemWithMods = []; + + // Money/Ammo - don't rely on items in spawnPoint.template.Items so we can randomise it ourselves + if (_itemHelper.IsOfBaseclasses(chosenTpl, [BaseClasses.MONEY, BaseClasses.AMMO])) { + var stackCount = + itemTemplate.Properties.StackMaxSize == 1 + ? 1 + : _randomUtil.GetInt((int)itemTemplate.Properties.StackMinRandom, (int)itemTemplate.Properties.StackMaxRandom); + + itemWithMods.Add(new Item { + Id = _hashUtil.Generate(), + Template = chosenTpl, + Upd = new Upd { StackObjectsCount = stackCount } + }); + } else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) { + // Fill with cartridges + List ammoBoxItem = [ new Item { Id = _hashUtil.Generate(), Template = chosenTpl }]; + _itemHelper.AddCartridgesToAmmoBox(ammoBoxItem, itemTemplate); + itemWithMods.AddRange(ammoBoxItem); + } else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) { + // Create array with just magazine + List magazineItem = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }]; + + if (_randomUtil.GetChance100(_locationConfig.StaticMagazineLootHasAmmoChancePercent)) { + // Add randomised amount of cartridges + _itemHelper.FillMagazineWithRandomCartridge( + magazineItem, + itemTemplate, // Magazine template + staticAmmoDist, + null, + _locationConfig.MinFillLooseMagazinePercent / 100 + ); + } + + itemWithMods.AddRange(magazineItem); + } else { + // Also used by armors to get child mods + // Get item + children and add into array we return + var itemWithChildren = _itemHelper.FindAndReturnChildrenAsItems(items, chosenItem.Id); + + // Ensure all IDs are unique + itemWithChildren = _itemHelper.ReplaceIDs(itemWithChildren); + + if (_locationConfig.TplsToStripChildItemsFrom.Contains(chosenItem.Template)) { + // Strip children from parent before adding + itemWithChildren = [itemWithChildren[0]]; + } + + itemWithMods.AddRange(itemWithChildren); + } + + // Get inventory size of item + var size = _itemHelper.GetItemSize(itemWithMods, itemWithMods[0].Id); + + return new ContainerItem { Items = itemWithMods, Width = size.Width, Height = size.Height }; + } + + private double GetLooseLootMultiplerForLocation(string location) + { + return _locationConfig.LooseLootMultiplier[location]; + } + + protected double getStaticLootMultiplerForLocation(string location) { + return _locationConfig.StaticLootMultiplier[location]; + } + + // TODO: rewrite, BIG yikes protected ContainerItem? CreateStaticLootItem( string chosenTpl, @@ -619,81 +978,109 @@ public class LocationLootGenerator( string? parentId = null) { var itemTemplate = _itemHelper.GetItem(chosenTpl).Value; - if (itemTemplate.Properties is null) { + if (itemTemplate.Properties is null) + { _logger.Error($"Unable to process item: ${{chosenTpl}}. it lacks _props"); return null; } + var width = itemTemplate.Properties.Width; var height = itemTemplate.Properties.Height; - List items = [ new Item { Id = _hashUtil.Generate(), Template = chosenTpl }]; + List items = [new Item { Id = _hashUtil.Generate(), Template = chosenTpl }]; var rootItem = items.FirstOrDefault(); // Use passed in parentId as override for new item - if (!string.IsNullOrEmpty(parentId)) { + if (!string.IsNullOrEmpty(parentId)) + { rootItem.ParentId = parentId; } if ( _itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MONEY) || _itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO) - ) { + ) + { // Edge case - some ammos e.g. flares or M406 grenades shouldn't be stacked var stackCount = itemTemplate.Properties.StackMaxSize == 1 - ? 1 - : _randomUtil.GetInt((int)(itemTemplate.Properties.StackMinRandom), (int)(itemTemplate.Properties.StackMaxRandom)); + ? 1 + : _randomUtil.GetInt((int)(itemTemplate.Properties.StackMinRandom), (int)(itemTemplate.Properties.StackMaxRandom)); rootItem.Upd = new Upd { StackObjectsCount = stackCount }; } // No spawn point, use default template - else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON)) { + else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.WEAPON)) + { List children = []; var defaultPreset = _cloner.Clone(_presetHelper.GetDefaultPreset(chosenTpl)); - if (defaultPreset?.Items is not null) { - try { + if (defaultPreset?.Items is not null) + { + try + { children = _itemHelper.ReparentItemAndChildren(defaultPreset.Items[0], defaultPreset.Items); - } catch (Exception e) { + } + catch (Exception e) + { // this item already broke it once without being reproducible tpl = "5839a40f24597726f856b511"; AKS-74UB Default // 5ea03f7400685063ec28bfa8 // ppsh default // 5ba26383d4351e00334c93d9 //mp7_devgru _logger.Error( - _localisationService.GetText("location-preset_not_found", new { - tpl = chosenTpl, - defaultId = defaultPreset.Id, - defaultName = defaultPreset.Name, - parentId, - }) + _localisationService.GetText( + "location-preset_not_found", + new + { + tpl = chosenTpl, + defaultId = defaultPreset.Id, + defaultName = defaultPreset.Name, + parentId, + } + ) ); throw; } - } else { + } + else + { // RSP30 (62178be9d0050232da3485d9/624c0b3340357b5f566e8766/6217726288ed9f0845317459) doesnt have any default presets and kills this code below as it has no chidren to reparent _logger.Debug($"createStaticLootItem() No preset found for weapon: {chosenTpl}"); } rootItem = items[0]; - if (rootItem is null) { + if (rootItem is null) + { _logger.Error( - _localisationService.GetText("location-missing_root_item", new { - tpl = chosenTpl, - parentId, - }) + _localisationService.GetText( + "location-missing_root_item", + new + { + tpl = chosenTpl, + parentId, + } + ) ); throw new Exception(_localisationService.GetText("location-critical_error_see_log")); } - try { - if (children?.Count > 0) { + try + { + if (children?.Count > 0) + { items = _itemHelper.ReparentItemAndChildren(rootItem, children); } - } catch (Exception e) { + } + catch (Exception e) + { _logger.Error( - _localisationService.GetText("location-unable_to_reparent_item", new { - tpl = chosenTpl, - parentId = parentId, - }) + _localisationService.GetText( + "location-unable_to_reparent_item", + new + { + tpl = chosenTpl, + parentId = parentId, + } + ) ); throw; @@ -705,7 +1092,8 @@ public class LocationLootGenerator( // BotGenerator var magazine = items.FirstOrDefault(item => item.SlotId == "mod_magazine"); // some weapon presets come without magazine; only fill the mag if it exists - if (magazine is not null) { + if (magazine is not null) + { var magTemplate = _itemHelper.GetItem(magazine.Template).Value; var weaponTemplate = _itemHelper.GetItem(chosenTpl).Value; @@ -732,10 +1120,14 @@ public class LocationLootGenerator( height = size.Height; } // No spawnpoint to fall back on, generate manually - else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) { + else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.AMMO_BOX)) + { _itemHelper.AddCartridgesToAmmoBox(items, itemTemplate); - } else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) { - if (_randomUtil.GetChance100(_locationConfig.MagazineLootHasAmmoChancePercent)) { + } + else if (_itemHelper.IsOfBaseclass(chosenTpl, BaseClasses.MAGAZINE)) + { + if (_randomUtil.GetChance100(_locationConfig.MagazineLootHasAmmoChancePercent)) + { // Create array with just magazine List magazineWithCartridges = [rootItem]; _itemHelper.FillMagazineWithRandomCartridge( @@ -750,18 +1142,24 @@ public class LocationLootGenerator( items.Remove(rootItem); items.AddRange(magazineWithCartridges); } - } else if (_itemHelper.ArmorItemCanHoldMods(chosenTpl)) { + } + else if (_itemHelper.ArmorItemCanHoldMods(chosenTpl)) + { var defaultPreset = _presetHelper.GetDefaultPreset(chosenTpl); - if (defaultPreset is not null) { + if (defaultPreset is not null) + { List presetAndMods = _itemHelper.ReplaceIDs(defaultPreset.Items); _itemHelper.RemapRootItemId(presetAndMods); // Use original items parentId otherwise item doesnt get added to container correctly presetAndMods[0].ParentId = rootItem.ParentId; items = presetAndMods; - } else { + } + else + { // We make base item above, at start of function, no need to do it here - if ((itemTemplate.Properties.Slots?.Count ?? 0) > 0) { + if ((itemTemplate.Properties.Slots?.Count ?? 0) > 0) + { items = _itemHelper.AddChildSlotItems( items, itemTemplate, diff --git a/Libraries/Core/Models/Eft/Common/LooseLoot.cs b/Libraries/Core/Models/Eft/Common/LooseLoot.cs index 40083f49..83e12da7 100644 --- a/Libraries/Core/Models/Eft/Common/LooseLoot.cs +++ b/Libraries/Core/Models/Eft/Common/LooseLoot.cs @@ -9,7 +9,7 @@ public record LooseLoot public SpawnpointCount? SpawnpointCount { get; set; } [JsonPropertyName("spawnpointsForced")] - public List? SpawnpointsForced { get; set; } + public List? SpawnpointsForced { get; set; } [JsonPropertyName("spawnpoints")] public List? Spawnpoints { get; set; } @@ -24,18 +24,6 @@ public record SpawnpointCount public double? Std { get; set; } } -public record SpawnpointsForced -{ - [JsonPropertyName("locationId")] - public string? LocationId { get; set; } - - [JsonPropertyName("probability")] - public double? Probability { get; set; } - - [JsonPropertyName("template")] - public SpawnpointTemplate? Template { get; set; } -} - public record SpawnpointTemplate { [JsonPropertyName("Id")] diff --git a/Libraries/Core/Utils/App.cs b/Libraries/Core/Utils/App.cs index 8582ecab..3e4de7c3 100644 --- a/Libraries/Core/Utils/App.cs +++ b/Libraries/Core/Utils/App.cs @@ -83,7 +83,7 @@ public class App _logger.Success(GetRandomisedStartMessage()); _logger.Success(".oooO Oooo."); - _logger.Success("( ( ) ( ) )"); + _logger.Success("( ( ) ( ) )"); // TODO: Remove later _logger.Success(" \\ ( ) /"); _logger.Success(" \\_) (_/"); }