From 7ba650787ee3409e2e20ccfbf9754bb84ae1c33a Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:25:19 -0300 Subject: [PATCH 1/3] fix: duplicate rows in deserialized aggregate collections --- .../Updates/AddElfSoldierBuffPlugIn.cs | 75 ++++++++++--------- .../VersionSeasonSix/NpcInitialization.cs | 1 + .../Json/ReferenceResolvingConverter.cs | 19 +++-- 3 files changed, 52 insertions(+), 43 deletions(-) diff --git a/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs b/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs index ef6b7ba82..5f8548b31 100644 --- a/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs @@ -62,41 +62,46 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio return; } - var buffEffect = context.CreateNew(); - gameConfiguration.MagicEffects.Add(buffEffect); - buffEffect.Number = (short)MagicEffectNumber.ElfSoldierBuff; - buffEffect.Name = "Elf Soldier Buff"; - buffEffect.InformObservers = true; - buffEffect.StopByDeath = true; - - // Duration: 60 minutes - buffEffect.Duration = context.CreateNew(); - buffEffect.Duration.ConstantValue.Value = 3600; - - // Defense boost: 50 + (Level / 5) - var defensePowerUp = context.CreateNew(); - defensePowerUp.TargetAttribute = Stats.DefenseFinal.GetPersistent(gameConfiguration); - defensePowerUp.Boost = context.CreateNew(); - defensePowerUp.Boost.ConstantValue.Value = 50; - defensePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; - var defensePerLevel = context.CreateNew(); - defensePerLevel.InputAttribute = Stats.Level.GetPersistent(gameConfiguration); - defensePerLevel.InputOperand = 1f / 5; - defensePerLevel.InputOperator = InputOperator.Multiply; - defensePowerUp.Boost.RelatedValues.Add(defensePerLevel); - buffEffect.PowerUpDefinitions.Add(defensePowerUp); - - // Damage boost: 45 + (Level / 3) - var damagePowerUp = context.CreateNew(); - damagePowerUp.TargetAttribute = Stats.GreaterDamageBonus.GetPersistent(gameConfiguration); - damagePowerUp.Boost = context.CreateNew(); - damagePowerUp.Boost.ConstantValue.Value = 45; - var damagePerLevel = context.CreateNew(); - damagePerLevel.InputAttribute = Stats.Level.GetPersistent(gameConfiguration); - damagePerLevel.InputOperand = 1f / 3; - damagePerLevel.InputOperator = InputOperator.Multiply; - damagePowerUp.Boost.RelatedValues.Add(damagePerLevel); - buffEffect.PowerUpDefinitions.Add(damagePowerUp); + var buffEffect = gameConfiguration.MagicEffects.FirstOrDefault(e => e.Number == (short)MagicEffectNumber.ElfSoldierBuff); + if (buffEffect is null) + { + buffEffect = context.CreateNew(); + gameConfiguration.MagicEffects.Add(buffEffect); + buffEffect.Number = (short)MagicEffectNumber.ElfSoldierBuff; + buffEffect.Name = "Elf Soldier Buff"; + buffEffect.InformObservers = true; + buffEffect.StopByDeath = true; + + // Duration: 60 minutes + buffEffect.Duration = context.CreateNew(); + buffEffect.Duration.ConstantValue.Value = 3600; + + // Defense boost: 50 + (Level / 5) + var defensePowerUp = context.CreateNew(); + defensePowerUp.TargetAttribute = Stats.DefenseFinal.GetPersistent(gameConfiguration); + defensePowerUp.Boost = context.CreateNew(); + defensePowerUp.Boost.ConstantValue.Value = 50; + defensePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; + var defensePerLevel = context.CreateNew(); + defensePerLevel.InputAttribute = Stats.Level.GetPersistent(gameConfiguration); + defensePerLevel.InputOperand = 1f / 5; + defensePerLevel.InputOperator = InputOperator.Multiply; + defensePowerUp.Boost.RelatedValues.Add(defensePerLevel); + buffEffect.PowerUpDefinitions.Add(defensePowerUp); + + // Damage boost: 45 + (Level / 3) + var damagePowerUp = context.CreateNew(); + damagePowerUp.TargetAttribute = Stats.GreaterDamageBonus.GetPersistent(gameConfiguration); + damagePowerUp.Boost = context.CreateNew(); + damagePowerUp.Boost.ConstantValue.Value = 45; + damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; + var damagePerLevel = context.CreateNew(); + damagePerLevel.InputAttribute = Stats.Level.GetPersistent(gameConfiguration); + damagePerLevel.InputOperand = 1f / 3; + damagePerLevel.InputOperator = InputOperator.Multiply; + damagePowerUp.Boost.RelatedValues.Add(damagePerLevel); + buffEffect.PowerUpDefinitions.Add(damagePowerUp); + } var buff = context.CreateNew(); buff.MagicEffectDefinition = buffEffect; diff --git a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs index 5c3eed168..10e7d810d 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs @@ -151,6 +151,7 @@ public override void Initialize() damagePowerUp.TargetAttribute = Stats.GreaterDamageBonus.GetPersistent(this.GameConfiguration); damagePowerUp.Boost = this.Context.CreateNew(); damagePowerUp.Boost.ConstantValue.Value = 45; + damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; var damagePerLevel = this.Context.CreateNew(); damagePerLevel.InputAttribute = Stats.Level.GetPersistent(this.GameConfiguration); damagePerLevel.InputOperand = 1f / 3; diff --git a/src/Persistence/Json/ReferenceResolvingConverter.cs b/src/Persistence/Json/ReferenceResolvingConverter.cs index 35eda046b..b1f69df8b 100644 --- a/src/Persistence/Json/ReferenceResolvingConverter.cs +++ b/src/Persistence/Json/ReferenceResolvingConverter.cs @@ -47,11 +47,12 @@ static ReferenceResolvingConverter() else if (x.CollectionInterface != null && x.Property.Name.StartsWith("Raw")) { propertyType = x.CollectionInterface.GetGenericArguments()[0]; + var collectionExpr = Expression.Property(tParam, x.Property); + var itemExpr = Expression.Convert(objParam, propertyType); + var containsCall = Expression.Call(collectionExpr, x.CollectionInterface.GetMethod("Contains")!, itemExpr); + var addCall = Expression.Call(collectionExpr, x.CollectionInterface.GetMethod("Add")!, itemExpr); adder = Expression.Lambda>( - Expression.Call( - Expression.Property(tParam, x.Property), - x.CollectionInterface.GetMethod("Add")!, - Expression.Convert(objParam, propertyType)), + Expression.IfThen(Expression.Not(containsCall), addCall), tParam, objParam) .Compile(); @@ -66,11 +67,13 @@ static ReferenceResolvingConverter() propertyType = propertyType.GetProperties().First(p => p.PropertyType.BaseType == baseType).PropertyType; jsonPropertyName = basePropertyName; + + var collectionExpr = Expression.Property(tParam, baseCollectionProperty); + var itemExpr = Expression.Convert(objParam, propertyType); + var containsCall = Expression.Call(collectionExpr, baseCollectionProperty.PropertyType.GetMethod("Contains")!, itemExpr); + var addCall = Expression.Call(collectionExpr, baseCollectionProperty.PropertyType.GetMethod("Add")!, itemExpr); adder = Expression.Lambda>( - Expression.Call( - Expression.Property(tParam, baseCollectionProperty), - baseCollectionProperty.PropertyType.GetMethod("Add")!, - Expression.Convert(objParam, propertyType)), + Expression.IfThen(Expression.Not(containsCall), addCall), tParam, objParam) .Compile(); From 691374b54418f6584ea7ec00bdf333e020782bea Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:39:07 -0300 Subject: [PATCH 2/3] code review: apply gemini suggestions --- src/Persistence/Json/ReferenceResolvingConverter.cs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/Persistence/Json/ReferenceResolvingConverter.cs b/src/Persistence/Json/ReferenceResolvingConverter.cs index b1f69df8b..9b6897610 100644 --- a/src/Persistence/Json/ReferenceResolvingConverter.cs +++ b/src/Persistence/Json/ReferenceResolvingConverter.cs @@ -47,10 +47,12 @@ static ReferenceResolvingConverter() else if (x.CollectionInterface != null && x.Property.Name.StartsWith("Raw")) { propertyType = x.CollectionInterface.GetGenericArguments()[0]; - var collectionExpr = Expression.Property(tParam, x.Property); + + var collectionExpr = Expression.Convert(Expression.Property(tParam, x.Property), x.CollectionInterface); var itemExpr = Expression.Convert(objParam, propertyType); var containsCall = Expression.Call(collectionExpr, x.CollectionInterface.GetMethod("Contains")!, itemExpr); var addCall = Expression.Call(collectionExpr, x.CollectionInterface.GetMethod("Add")!, itemExpr); + adder = Expression.Lambda>( Expression.IfThen(Expression.Not(containsCall), addCall), tParam, @@ -68,10 +70,12 @@ static ReferenceResolvingConverter() propertyType = propertyType.GetProperties().First(p => p.PropertyType.BaseType == baseType).PropertyType; jsonPropertyName = basePropertyName; - var collectionExpr = Expression.Property(tParam, baseCollectionProperty); + var baseCollectionInterface = DetermineCollectionInterface(baseCollectionProperty)!; + var collectionExpr = Expression.Convert(Expression.Property(tParam, baseCollectionProperty), baseCollectionInterface); var itemExpr = Expression.Convert(objParam, propertyType); - var containsCall = Expression.Call(collectionExpr, baseCollectionProperty.PropertyType.GetMethod("Contains")!, itemExpr); - var addCall = Expression.Call(collectionExpr, baseCollectionProperty.PropertyType.GetMethod("Add")!, itemExpr); + var containsCall = Expression.Call(collectionExpr, baseCollectionInterface.GetMethod("Contains")!, itemExpr); + var addCall = Expression.Call(collectionExpr, baseCollectionInterface.GetMethod("Add")!, itemExpr); + adder = Expression.Lambda>( Expression.IfThen(Expression.Not(containsCall), addCall), tParam, From 23dbb5e6a739e9b9b7e31ae4090d8291215bfc41 Mon Sep 17 00:00:00 2001 From: Eduardo <6845999+eduardosmaniotto@users.noreply.github.com> Date: Fri, 17 Jul 2026 20:53:10 -0300 Subject: [PATCH 3/3] revert to previouly hardcoded buff values --- .../Initialization/Updates/AddElfSoldierBuffPlugIn.cs | 2 +- .../Initialization/VersionSeasonSix/NpcInitialization.cs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs b/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs index 5f8548b31..7e47d3b9a 100644 --- a/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs +++ b/src/Persistence/Initialization/Updates/AddElfSoldierBuffPlugIn.cs @@ -94,7 +94,7 @@ protected override async ValueTask ApplyAsync(IContext context, GameConfiguratio damagePowerUp.TargetAttribute = Stats.GreaterDamageBonus.GetPersistent(gameConfiguration); damagePowerUp.Boost = context.CreateNew(); damagePowerUp.Boost.ConstantValue.Value = 45; - damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; + damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddRaw; var damagePerLevel = context.CreateNew(); damagePerLevel.InputAttribute = Stats.Level.GetPersistent(gameConfiguration); damagePerLevel.InputOperand = 1f / 3; diff --git a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs index 10e7d810d..ba77909fe 100644 --- a/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs +++ b/src/Persistence/Initialization/VersionSeasonSix/NpcInitialization.cs @@ -151,7 +151,7 @@ public override void Initialize() damagePowerUp.TargetAttribute = Stats.GreaterDamageBonus.GetPersistent(this.GameConfiguration); damagePowerUp.Boost = this.Context.CreateNew(); damagePowerUp.Boost.ConstantValue.Value = 45; - damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddFinal; + damagePowerUp.Boost.ConstantValue.AggregateType = AggregateType.AddRaw; var damagePerLevel = this.Context.CreateNew(); damagePerLevel.InputAttribute = Stats.Level.GetPersistent(this.GameConfiguration); damagePerLevel.InputOperand = 1f / 3;