2024-04-16 18:29:40 +00:00
using LootDumpProcessor.Logger ;
2023-08-12 19:08:38 +01:00
using LootDumpProcessor.Model ;
using LootDumpProcessor.Model.Output ;
using LootDumpProcessor.Model.Output.LooseLoot ;
using LootDumpProcessor.Model.Processing ;
using LootDumpProcessor.Storage ;
using LootDumpProcessor.Storage.Collections ;
2024-04-16 18:29:40 +00:00
using LootDumpProcessor.Utils ;
2023-08-12 19:08:38 +01:00
using NumSharp ;
namespace LootDumpProcessor.Process.Processor ;
2024-04-16 18:29:40 +00:00
public static class LooseLootProcessor
2023-08-12 19:08:38 +01:00
{
public static PreProcessedLooseLoot PreProcessLooseLoot ( List < Template > looseloot )
{
var looseloot_ci = new PreProcessedLooseLoot
{
Counts = new Dictionary < string , int > ( )
} ;
var temporalItemProperties = new SubdivisionedKeyableDictionary < string , List < Template > > ( ) ;
looseloot_ci . ItemProperties = ( AbstractKey ) temporalItemProperties . GetKey ( ) ;
looseloot_ci . MapSpawnpointCount = looseloot . Count ;
var uniqueIds = new Dictionary < string , object > ( ) ;
// sometimes the rotation changes very slightly in the dumps for the same location / rotation spawnpoint
// use rounding to make sure it is not generated to two spawnpoint
foreach ( var looseLootTemplate in looseloot )
{
// the bsg ids are insane.
// Sometimes the last 7 digits vary but they spawn the same item at the same position
// e.g. for the quest item "60a3b65c27adf161da7b6e14" at "loot_bunker_quest (3)555192"
// so the first approach was to remove the last digits.
// We then saw, that sometimes when the last digits differ for the same string, also the position
// differs.
// We decided to group over the position/rotation/useGravity since they make out a distinct spot
var saneId = looseLootTemplate . GetSaneId ( ) ;
if ( ! uniqueIds . ContainsKey ( saneId ) )
{
uniqueIds [ saneId ] = looseLootTemplate . Id ;
if ( looseloot_ci . Counts . ContainsKey ( saneId ) )
looseloot_ci . Counts [ saneId ] + + ;
else
looseloot_ci . Counts [ saneId ] = 1 ;
}
if ( ! temporalItemProperties . TryGetValue ( saneId , out var templates ) )
{
templates = new FlatKeyableList < Template > ( ) ;
temporalItemProperties . Add ( saneId , templates ) ;
}
templates . Add ( looseLootTemplate ) ;
}
DataStorageFactory . GetInstance ( ) . Store ( temporalItemProperties ) ;
return looseloot_ci ;
}
public static Dictionary < string , LooseLootRoot > CreateLooseLootDistribution (
Dictionary < string , int > map_counts ,
Dictionary < string , IKey > looseloot_counts
)
{
var forcedConfidence = LootDumpProcessorContext . GetConfig ( ) . ProcessorConfig . SpawnPointToleranceForForced / 100 ;
var probabilities = new Dictionary < string , Dictionary < string , double > > ( ) ;
var looseLootDistribution = new Dictionary < string , LooseLootRoot > ( ) ;
foreach ( var _tup_1 in map_counts )
{
var mapName = _tup_1 . Key ;
var mapCount = _tup_1 . Value ;
probabilities [ mapName ] = new Dictionary < string , double > ( ) ;
var looseLootCounts = DataStorageFactory . GetInstance ( ) . GetItem < LooseLootCounts > ( looseloot_counts [ mapName ] ) ;
var counts = DataStorageFactory . GetInstance ( )
. GetItem < FlatKeyableDictionary < string , int > > ( looseLootCounts . Counts ) ;
foreach ( var ( idi , cnt ) in counts )
{
probabilities [ mapName ] [ idi ] = ( double ) ( ( decimal ) cnt / mapCount ) ;
}
// No longer used, dispose
counts = null ;
2024-01-17 00:45:38 +00:00
// we want to cleanup the data, so we calculate the mean for the values we get raw
// For whatever reason, we sometimes get dumps that have A LOT more loose loot point than
// the average
var initialMean = np . mean ( np . array ( looseLootCounts . MapSpawnpointCount ) ) . ToArray < double > ( ) . First ( ) ;
var looseLootCountTolerancePercentage = LootDumpProcessorContext . GetConfig ( ) . ProcessorConfig . LooseLootCountTolerancePercentage / 100 ;
// We calculate here a high point to check, anything above this value will be ignored
// The data that was inside those loose loot points still counts for them though!
var high = initialMean * ( 1 + looseLootCountTolerancePercentage ) ;
looseLootCounts . MapSpawnpointCount = looseLootCounts . MapSpawnpointCount . Where ( v = > v < = high ) . ToList ( ) ;
2023-08-12 19:08:38 +01:00
looseLootDistribution [ mapName ] = new LooseLootRoot
{
SpawnPointCount = new SpawnPointCount
{
Mean = np . mean ( np . array ( looseLootCounts . MapSpawnpointCount ) ) ,
Std = np . std ( np . array ( looseLootCounts . MapSpawnpointCount ) )
} ,
SpawnPointsForced = new List < SpawnPointsForced > ( ) ,
SpawnPoints = new List < SpawnPoint > ( )
} ;
var itemProperties = DataStorageFactory . GetInstance ( )
. GetItem < FlatKeyableDictionary < string , IKey > > ( looseLootCounts . ItemProperties ) ;
foreach ( var ( spawnPoint , itemList ) in itemProperties )
{
var itemsCounts = new Dictionary < ComposedKey , int > ( ) ;
var savedItemProperties = DataStorageFactory . GetInstance ( ) . GetItem < FlatKeyableList < Template > > ( itemList ) ;
foreach ( var savedTemplateProperties in savedItemProperties )
{
var key = new ComposedKey ( savedTemplateProperties ) ;
if ( itemsCounts . ContainsKey ( key ) )
itemsCounts [ key ] + = 1 ;
else
itemsCounts [ key ] = 1 ;
}
// Group by arguments to create possible positions / rotations per spawnpoint
// check if grouping is unique
var itemListSorted = savedItemProperties . Select ( template = > ( template . GetSaneId ( ) , template ) )
. GroupBy ( g = > g . Item1 ) . ToList ( ) ;
if ( itemListSorted . Count > 1 )
{
throw new Exception ( "More than one saneKey found" ) ;
}
var spawnPoints = itemListSorted . First ( ) . Select ( v = > v . template ) . ToList ( ) ;
var locationId = spawnPoints [ 0 ] . GetLocationId ( ) ;
var template = ProcessorUtil . Copy ( spawnPoints [ 0 ] ) ;
//template.Root = null; // Why do we do this, not null in bsg data
var itemDistribution = itemsCounts . Select ( kv = > new ItemDistribution
{
ComposedKey = kv . Key ,
RelativeProbability = kv . Value
} ) . ToList ( ) ;
// If any of the items is a quest item or forced loose loot items, or the item normally appreas 99.5%
2023-09-19 11:58:04 +01:00
// Only add position to forced loot if it has only 1 item in the array.
if ( itemDistribution . Count = = 1 & & itemDistribution . Any ( item = >
2023-08-12 19:08:38 +01:00
LootDumpProcessorContext . GetTarkovItems ( ) . IsQuestItem ( item . ComposedKey ? . FirstItem ? . Tpl ) | |
LootDumpProcessorContext . GetForcedLooseItems ( ) [ mapName ] . Contains ( item . ComposedKey ? . FirstItem ? . Tpl ) )
)
{
var spawnPointToAdd = new SpawnPointsForced
{
LocationId = locationId ,
Probability = probabilities [ mapName ] [ spawnPoint ] ,
Template = template
} ;
looseLootDistribution [ mapName ] . SpawnPointsForced . Add ( spawnPointToAdd ) ;
}
else if ( probabilities [ mapName ] [ spawnPoint ] > forcedConfidence )
{
var spawnPointToAdd = new SpawnPointsForced
{
LocationId = locationId ,
Probability = probabilities [ mapName ] [ spawnPoint ] ,
Template = template
} ;
looseLootDistribution [ mapName ] . SpawnPointsForced . Add ( spawnPointToAdd ) ;
2024-04-16 18:29:40 +00:00
if ( LoggerFactory . GetInstance ( ) . CanBeLogged ( LogLevel . Warning ) )
LoggerFactory . GetInstance ( ) . Log (
$"Item: {template.Id} has > {LootDumpProcessorContext.GetConfig().ProcessorConfig.SpawnPointToleranceForForced}% spawn chance in spawn point: {spawnPointToAdd.LocationId} but isn't in forced loot, adding to forced" ,
LogLevel . Warning
) ;
2023-08-12 19:08:38 +01:00
}
else // Normal spawn point, add to non-forced spawnpoint array
{
var spawnPointToAdd = new SpawnPoint
{
LocationId = locationId ,
Probability = probabilities [ mapName ] [ spawnPoint ] ,
Template = template ,
ItemDistribution = itemDistribution
} ;
template . Items = new List < Item > ( ) ;
var group = spawnPoints . GroupBy ( template = > new ComposedKey ( template ) )
. ToDictionary ( g = > g . Key , g = > g . ToList ( ) ) ;
foreach ( var distribution in itemDistribution )
{
if ( group . TryGetValue ( distribution . ComposedKey , out var items ) )
{
// We need to reparent the IDs to match the composed key ID
var itemDistributionItemList = items . First ( ) . Items ;
// Find the item with no parent id, this is essentially the "Root" of the actual item
var firstItemInTemplate =
itemDistributionItemList . Find ( i = > string . IsNullOrEmpty ( i . ParentId ) ) ;
// Save the original ID reference, we need to replace it on child items
var originalId = firstItemInTemplate . Id ;
// Put the composed key instead
firstItemInTemplate . Id = distribution . ComposedKey . Key ;
// Reparent any items with the original id on it
itemDistributionItemList . Where ( i = > i . ParentId = = originalId )
. ToList ( )
. ForEach ( i = > i . ParentId = firstItemInTemplate . Id ) ;
template . Items . AddRange ( itemDistributionItemList ) ;
}
else
{
2024-04-16 18:29:40 +00:00
if ( LoggerFactory . GetInstance ( ) . CanBeLogged ( LogLevel . Error ) )
LoggerFactory . GetInstance ( ) . Log (
$"Item template {distribution.ComposedKey?.FirstItem?.Tpl} was on loose loot distribution for spawn point {template.Id} but the spawn points didnt contain a template matching it." ,
LogLevel . Error
) ;
2023-08-12 19:08:38 +01:00
}
}
looseLootDistribution [ mapName ] . SpawnPoints . Add ( spawnPointToAdd ) ;
}
}
// # Test for duplicate position
// # we removed most of them by "rounding away" the jitter in rotation,
// # there are still a few duplicate locations with distinct difference in rotation left though
// group_fun = lambda x: (
// x["template"]["Position"]["x"],
// x["template"]["Position"]["y"],
// x["template"]["Position"]["z"],
// )
// test = sorted(loose_loot_distribution[mi]["spawnpoints"], key=group_fun)
// test_grouped = groupby(test, group_fun)
// test_len = []
// for k, g in test_grouped:
// gl = list(g)
// test_len.append(len(gl))
// if len(gl) > 1:
// print(gl)
//
// print(mi, np.unique(test_len, return_counts=True))
looseLootDistribution [ mapName ] . SpawnPoints =
looseLootDistribution [ mapName ] . SpawnPoints . OrderBy ( x = > x . Template . Id ) . ToList ( ) ;
// Cross check with forced loot in dumps vs items defined in forced_loose.yaml
var forcedTplsInConfig = new HashSet < string > (
( from forceditem in LootDumpProcessorContext . GetForcedLooseItems ( ) [ mapName ]
select forceditem ) . ToList ( ) ) ;
var forcedTplsFound = new HashSet < string > (
( from forceditem in looseLootDistribution [ mapName ] . SpawnPointsForced
select forceditem . Template . Items [ 0 ] . Tpl ) . ToList ( ) ) ;
// All the tpls that are defined in the forced_loose.yaml for this map that are not found as forced
foreach ( var itemTpl in forcedTplsInConfig )
{
if ( ! forcedTplsFound . Contains ( itemTpl ) )
{
2024-04-16 18:29:40 +00:00
if ( LoggerFactory . GetInstance ( ) . CanBeLogged ( LogLevel . Error ) )
LoggerFactory . GetInstance ( ) . Log (
$"Expected item: {itemTpl} defined in forced_loose.yaml config not found in forced loot" ,
LogLevel . Error
) ;
2023-08-12 19:08:38 +01:00
}
}
// All the tpls that are found as forced in output file but not in the forced_loose.yaml config
foreach ( var itemTpl in forcedTplsFound )
{
if ( ! forcedTplsInConfig . Contains ( itemTpl ) )
{
2024-04-16 18:29:40 +00:00
if ( LoggerFactory . GetInstance ( ) . CanBeLogged ( LogLevel . Warning ) )
LoggerFactory . GetInstance ( ) . Log (
$"Map: {mapName} Item: {itemTpl} not defined in forced_loose.yaml config but was flagged as forced by code" ,
LogLevel . Warning
) ;
2023-08-12 19:08:38 +01:00
}
}
}
return looseLootDistribution ;
}
}