ace/Source/ACE.Server/Managers/RecipeManager.cs
Coldeve-ACE a31112ea51
Common Logging Changes (#4345)
* Update ShardDatabaseOfflineTools.cs

* Update Corpse.cs

* Update Creature_Death.cs

* Update Player_Death.cs

* Update Player_Inventory.cs

* Update WorldObject_Decay.cs

* Update HouseManager.cs

* Update Player_House.cs

* Update RecipeManager.cs

* Update Player_Allegiance.cs

* Update AuthenticationDatabase.cs

* Update ShardDatabase.cs

* Update WorldDatabase.cs

* Update Player_Magic.cs

* Update Player_Skills.cs

* Update Player_Attributes.cs

* Update Player_Combat.cs

* Update PhysicsObj.cs

* Update Player_Vitals.cs

* Update Player_Missile.cs

* Update Player_Melee.cs

* Update Monster_Melee.cs

* Update Session.cs
2025-08-11 22:38:12 -04:00

1576 lines
70 KiB
C#

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text.RegularExpressions;
using log4net;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Database;
using ACE.Database.Models.World;
using ACE.DatLoader;
using ACE.DatLoader.FileTypes;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Entity.Models;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Entity.Mutations;
using ACE.Server.Factories;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
using ACE.Server.WorldObjects;
using Microsoft.EntityFrameworkCore.ChangeTracking;
namespace ACE.Server.Managers
{
public partial class RecipeManager
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public static Recipe GetRecipe(Player player, WorldObject source, WorldObject target)
{
// PY16 recipes
var cookbook = DatabaseManager.World.GetCachedCookbook(source.WeenieClassId, target.WeenieClassId);
if (cookbook != null)
return cookbook.Recipe;
// if none exists, try finding new recipe
return GetNewRecipe(player, source, target);
}
public static void UseObjectOnTarget(Player player, WorldObject source, WorldObject target, bool confirmed = false)
{
if (player.IsBusy)
{
player.SendUseDoneEvent(WeenieError.YoureTooBusy);
return;
}
var allowCraftInCombat = PropertyManager.GetBool("allow_combat_mode_crafting").Item;
if (!allowCraftInCombat && player.CombatMode != CombatMode.NonCombat)
{
player.SendUseDoneEvent(WeenieError.YouMustBeInPeaceModeToTrade);
return;
}
if (source == target)
{
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"The {source.NameWithMaterial} cannot be combined with itself.", ChatMessageType.Craft));
player.Session.Network.EnqueueSend(new GameEventCommunicationTransientString(player.Session, $"You can't use the {source.NameWithMaterial} on itself."));
player.SendUseDoneEvent();
return;
}
var recipe = GetRecipe(player, source, target);
if (recipe == null)
{
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"The {source.NameWithMaterial} cannot be used on the {target.NameWithMaterial}.", ChatMessageType.Craft));
player.SendUseDoneEvent();
return;
}
// verify requirements
if (!VerifyRequirements(recipe, player, source, target))
{
player.SendUseDoneEvent(WeenieError.YouDoNotPassCraftingRequirements);
return;
}
if (recipe.IsTinkering())
log.InfoFormat("[TINKERING] {0}.UseObjectOnTarget({1}, {2}) | Status: {3}confirmed", player.Name, source.NameWithMaterial, target.NameWithMaterial, (confirmed ? "" : "un"));
var percentSuccess = GetRecipeChance(player, source, target, recipe);
if (percentSuccess == null)
{
player.SendUseDoneEvent();
return;
}
var showDialog = HasDifficulty(recipe) && player.GetCharacterOption(CharacterOption.UseCraftingChanceOfSuccessDialog);
if (!confirmed && player.LumAugSkilledCraft > 0)
player.SendMessage($"Your Aura of the Craftman augmentation increased your skill by {player.LumAugSkilledCraft}!");
var motionCommand = MotionCommand.ClapHands;
var actionChain = new ActionChain();
var nextUseTime = 0.0f;
player.IsBusy = true;
if (allowCraftInCombat && player.CombatMode != CombatMode.NonCombat)
{
// Drop out of combat mode. This depends on the server property "allow_combat_mode_craft" being True.
// If not, this action would have aborted due to not being in NonCombat mode.
var stanceTime = player.SetCombatMode(CombatMode.NonCombat);
actionChain.AddDelaySeconds(stanceTime);
nextUseTime += stanceTime;
}
var motion = new Motion(player, motionCommand);
var currentStance = player.CurrentMotionState.Stance; // expected to be MotionStance.NonCombat
var clapTime = !confirmed ? Physics.Animation.MotionTable.GetAnimationLength(player.MotionTableId, currentStance, motionCommand) : 0.0f;
if (!confirmed)
{
actionChain.AddAction(player, () => player.SendMotionAsCommands(motionCommand, currentStance));
actionChain.AddDelaySeconds(clapTime);
nextUseTime += clapTime;
}
if (showDialog && !confirmed)
{
actionChain.AddAction(player, () => ShowDialog(player, source, target, recipe, percentSuccess.Value));
actionChain.AddAction(player, () => player.IsBusy = false);
}
else
{
actionChain.AddAction(player, () => HandleRecipe(player, source, target, recipe, percentSuccess.Value));
actionChain.AddAction(player, () =>
{
if (!showDialog)
player.SendUseDoneEvent();
player.IsBusy = false;
});
}
actionChain.EnqueueChain();
player.NextUseTime = DateTime.UtcNow.AddSeconds(nextUseTime);
}
public static bool HasDifficulty(Recipe recipe)
{
if (recipe.IsTinkering())
return true;
return recipe.Skill > 0 && recipe.Difficulty > 0;
}
public static double? GetRecipeChance(Player player, WorldObject source, WorldObject target, Recipe recipe)
{
if (recipe.IsTinkering())
return GetTinkerChance(player, source, target, recipe);
if (!HasDifficulty(recipe))
return 1.0;
var playerSkill = player.GetCreatureSkill((Skill)recipe.Skill);
if (playerSkill == null)
{
// this shouldn't happen, but sanity check for unexpected nulls
log.Warn($"RecipeManager.GetRecipeChance({player.Name}, {source.Name}, {target.Name}): recipe {recipe.Id} missing skill");
return null;
}
// check for pre-MoA skill
// convert into appropriate post-MoA skill
// pre-MoA melee weapons: get highest melee weapons skill
var newSkill = player.ConvertToMoASkill(playerSkill.Skill);
playerSkill = player.GetCreatureSkill(newSkill);
//Console.WriteLine("Required skill: " + skill.Skill);
if (playerSkill.AdvancementClass < SkillAdvancementClass.Trained)
{
player.SendWeenieError(WeenieError.YouAreNotTrainedInThatTradeSkill);
return null;
}
//Console.WriteLine("Skill difficulty: " + recipe.Recipe.Difficulty);
var playerCurrentPlusLumAugSkilledCraft = playerSkill.Current + (uint)player.LumAugSkilledCraft;
var successChance = SkillCheck.GetSkillChance(playerCurrentPlusLumAugSkilledCraft, recipe.Difficulty);
return successChance;
}
public static double? GetTinkerChance(Player player, WorldObject tool, WorldObject target, Recipe recipe)
{
// calculate % success chance
var toolWorkmanship = tool.Workmanship ?? 0;
var itemWorkmanship = target.Workmanship ?? 0;
var tinkeredCount = target.NumTimesTinkered;
var materialType = tool.MaterialType ?? MaterialType.Unknown;
var salvageMod = GetMaterialMod(materialType);
var workmanshipMod = 1.0f;
if (toolWorkmanship >= itemWorkmanship)
workmanshipMod = 2.0f;
var recipeSkill = (Skill)recipe.Skill;
var skill = player.GetCreatureSkill(recipeSkill);
// tinkering skill must be trained
if (skill.AdvancementClass < SkillAdvancementClass.Trained)
{
player.Session.Network.EnqueueSend(new GameMessageSystemChat($"You are not trained in {skill.Skill.ToSentence()}.", ChatMessageType.Broadcast));
return null;
}
// thanks to Endy's Tinkering Calculator for this formula!
var attemptMod = TinkeringDifficulty[tinkeredCount];
var difficulty = (int)Math.Floor(((salvageMod * 5.0f) + (itemWorkmanship * salvageMod * 2.0f) - (toolWorkmanship * workmanshipMod * salvageMod / 5.0f)) * attemptMod);
var playerCurrentPlusLumAugSkilledCraft = skill.Current + (uint)player.LumAugSkilledCraft;
var successChance = SkillCheck.GetSkillChance((int)playerCurrentPlusLumAugSkilledCraft, difficulty);
// imbue: divide success by 3
if (recipe.IsImbuing())
{
successChance /= 3.0f;
if (player.AugmentationBonusImbueChance > 0)
successChance += player.AugmentationBonusImbueChance * 0.05f;
}
// todo: remove this once foolproof salvage recipes are updated
if (foolproofTinkers.Contains((WeenieClassName)tool.WeenieClassId))
successChance = 1.0;
return successChance;
}
/// <summary>
/// Returns the modifier for a bag of salvaging material
/// </summary>
public static float GetMaterialMod(MaterialType material)
{
switch (material)
{
case MaterialType.Gold:
case MaterialType.Oak:
return 10.0f;
case MaterialType.Alabaster:
case MaterialType.ArmoredilloHide:
case MaterialType.Brass:
case MaterialType.Bronze:
case MaterialType.Ceramic:
case MaterialType.Granite:
case MaterialType.Linen:
case MaterialType.Marble:
case MaterialType.Moonstone:
case MaterialType.Opal:
case MaterialType.Pine:
case MaterialType.ReedSharkHide:
case MaterialType.Velvet:
case MaterialType.Wool:
return 11.0f;
case MaterialType.Ebony:
case MaterialType.GreenGarnet:
case MaterialType.Iron:
case MaterialType.Mahogany:
case MaterialType.Porcelain:
case MaterialType.Satin:
case MaterialType.Steel:
case MaterialType.Teak:
return 12.0f;
case MaterialType.Bloodstone:
case MaterialType.Carnelian:
case MaterialType.Citrine:
case MaterialType.Hematite:
case MaterialType.LavenderJade:
case MaterialType.Malachite:
case MaterialType.RedJade:
case MaterialType.RoseQuartz:
return 25.0f;
default:
return 20.0f;
}
}
/// <summary>
/// Thanks to Endy's Tinkering Calculator for these values!
/// </summary>
public static List<float> TinkeringDifficulty = new List<float>()
{
// attempt #
1.0f, // 1
1.1f, // 2
1.3f, // 3
1.6f, // 4
2.0f, // 5
2.5f, // 6
3.0f, // 7
3.5f, // 8
4.0f, // 9
4.5f // 10
};
public static void ShowDialog(Player player, WorldObject source, WorldObject target, Recipe recipe, double successChance)
{
var percent = successChance * 100;
// retail messages:
// You determine that you have a 100 percent chance to succeed.
// You determine that you have a 99 percent chance to succeed.
// You determine that you have a 38 percent chance to succeed. 5 percent is due to your augmentation.
var floorMsg = $"You determine that you have a {percent.Round()} percent chance to succeed.";
var numAugs = recipe.IsImbuing() ? player.AugmentationBonusImbueChance : 0;
if (numAugs > 0)
floorMsg += $"\n{numAugs * 5} percent is due to your augmentation.";
if (!player.ConfirmationManager.EnqueueSend(new Confirmation_CraftInteration(player.Guid, source.Guid, target.Guid), floorMsg))
{
player.SendUseDoneEvent(WeenieError.ConfirmationInProgress);
return;
}
if (PropertyManager.GetBool("craft_exact_msg").Item)
{
var exactMsg = $"You have a {(float)percent} percent chance of using {source.NameWithMaterial} on {target.NameWithMaterial}.";
player.Session.Network.EnqueueSend(new GameMessageSystemChat(exactMsg, ChatMessageType.Craft));
}
player.SendUseDoneEvent();
}
public static void HandleRecipe(Player player, WorldObject source, WorldObject target, Recipe recipe, double successChance)
{
// re-verify
if (!VerifyRequirements(recipe, player, source, target))
{
player.SendWeenieError(WeenieError.YouDoNotPassCraftingRequirements);
return;
}
var success = ThreadSafeRandom.Next(0.0f, 1.0f) < successChance;
if (recipe.IsImbuing())
{
player.ImbueAttempts++;
if (success) player.ImbueSuccesses++;
}
var modified = CreateDestroyItems(player, recipe, source, target, successChance, success);
if (modified != null)
{
if (modified.Contains(source.Guid.Full))
UpdateObj(player, source);
if (modified.Contains(target.Guid.Full))
UpdateObj(player, target);
}
if (success && recipe.Skill > 0 && recipe.Difficulty > 0)
{
var skill = player.GetCreatureSkill((Skill)recipe.Skill);
Proficiency.OnSuccessUse(player, skill, recipe.Difficulty);
}
}
/// <summary>
/// Sends an UpdateObj to the client for modified sources / targets
/// </summary>
private static void UpdateObj(Player player, WorldObject obj)
{
if (Debug)
Console.WriteLine($"{player.Name}.UpdateObj({obj.Name})");
player.EnqueueBroadcast(new GameMessageUpdateObject(obj));
if (obj.CurrentWieldedLocation != null)
{
// retail possibly required sources / targets to be in the player's inventory,
// and not equipped. this scenario might already be prevented beforehand in VerifyUse()
player.EnqueueBroadcast(new GameMessageObjDescEvent(player));
return;
}
// client automatically moves item to first slot in container
// when an UpdateObject is sent. we must mimic this process on the server for persistance
// only run this for items in the player's inventory
// ie. skip for items on landblock, such as chorizite ore
var invObj = player.FindObject(obj.Guid.Full, Player.SearchLocations.MyInventory);
if (invObj != null)
player.MoveItemToFirstContainerSlot(obj);
}
public static bool TryMutateNative(Player player, WorldObject source, WorldObject target, Recipe recipe, uint dataId)
{
// legacy method, unused by default
switch (dataId)
{
// armor tinkering
case 0x38000011: // Steel
target.ArmorLevel += 20;
break;
// mutations apparently didn't cap to 2.0 here, clamps are applied in damage calculations though
case 0x38000017: // Alabaster
//target.ArmorModVsPierce = Math.Min((target.ArmorModVsPierce ?? 0) + 0.2f, 2.0f);
target.ArmorModVsPierce += 0.2f;
break;
case 0x38000018: // Bronze
//target.ArmorModVsSlash = Math.Min((target.ArmorModVsSlash ?? 0) + 0.2f, 2.0f);
target.ArmorModVsSlash += 0.2f;
break;
case 0x38000013: // Marble
//target.ArmorModVsBludgeon = Math.Min((target.ArmorModVsBludgeon ?? 0) + 0.2f, 2.0f);
target.ArmorModVsBludgeon += 0.2f;
break;
case 0x38000012: // ArmoredilloHide
//target.ArmorModVsAcid = Math.Min((target.ArmorModVsAcid ?? 0) + 0.4f, 2.0f);
target.ArmorModVsAcid += 0.4f;
break;
case 0x38000016: // Ceramic
//target.ArmorModVsFire = Math.Min((target.ArmorModVsFire ?? 0) + 0.4f, 2.0f);
target.ArmorModVsFire += 0.4f;
break;
case 0x38000014: // Wool
//target.ArmorModVsCold = Math.Min((target.ArmorModVsCold ?? 0) + 0.4f, 2.0f);
target.ArmorModVsCold += 0.4f;
break;
case 0x38000015: // ReedSharkHide
//target.ArmorModVsElectric = Math.Min((target.ArmorModVsElectric ?? 0) + 0.4f, 2.0f);
target.ArmorModVsElectric += 0.4f;
break;
case 0x38000038: // Peridot
//AddImbuedEffect(target, ImbuedEffectType.MeleeDefense);
target.ImbuedEffect = ImbuedEffectType.MeleeDefense;
break;
case 0x38000039: // YellowTopaz
//AddImbuedEffect(target, ImbuedEffectType.MissileDefense);
target.ImbuedEffect = ImbuedEffectType.MissileDefense;
break;
case 0x38000037: // Zircon
//AddImbuedEffect(target, ImbuedEffectType.MagicDefense);
target.ImbuedEffect = ImbuedEffectType.MagicDefense;
break;
// item tinkering
case 0x3800001E: // Pine
//target.Value = (int)Math.Round((target.Value ?? 1) * 0.75f);
target.Value = (int?)(target.Value * 0.75f);
break;
case 0x3800001F: // Gold
//target.Value = (int)Math.Round((target.Value ?? 1) * 1.25f);
target.Value = (int?)(target.Value * 1.25f);
break;
case 0x38000019: // Linen
//target.EncumbranceVal = (int)Math.Round((target.EncumbranceVal ?? 1) * 0.75f);
target.EncumbranceVal = (int?)(target.EncumbranceVal * 0.75f);
break;
// Ivory is handled purely in recipe mod?
case 0x38000043: // Leather
target.Retained = true;
break;
case 0x3900004E: // Sandstone: 43 -> 4E
target.Retained = false;
break;
case 0x3800002F: // Moonstone
target.ItemMaxMana += 500;
break;
case 0x39000042:
// legacy, these are handled in recipe mods
//target.SetProperty(PropertyString.ItemHeritageGroupRestriction, "Aluvian"); // Teak
//target.SetProperty(PropertyString.ItemHeritageGroupRestriction, "Gharu'ndim"); // Ebony
//target.SetProperty(PropertyString.ItemHeritageGroupRestriction, "Sho"); // Porcelain
//target.SetProperty(PropertyString.ItemHeritageGroupRestriction, "Viamontian"); // Satin
break;
case 0x38000035: // Copper
// handled in requirements, only here for legacy support?
if (target.ItemSkillLimit != Skill.MissileDefense || target.ItemSkillLevelLimit == null)
return false;
// change activation requirement: missile defense -> melee defense
target.ItemSkillLimit = Skill.MeleeDefense;
target.ItemSkillLevelLimit = (int)(target.ItemSkillLevelLimit / 0.7f);
break;
case 0x38000034: // Silver
// handled in requirements, only here for legacy support?
if (target.ItemSkillLimit != Skill.MeleeDefense || target.ItemSkillLevelLimit == null)
return false;
// change activation requirement: melee defense -> missile defense
target.ItemSkillLimit = Skill.MissileDefense;
target.ItemSkillLevelLimit = (int)(target.ItemSkillLevelLimit * 0.7f);
break;
case 0x38000036: // Silk
// remove allegiance rank limit, set difficulty to spellcraft
target.ItemAllegianceRankLimit = null;
target.ItemDifficulty = target.ItemSpellcraft;
break;
// armatures / trinkets
// these are handled in recipe mod
case 0x39000048: // Amber
case 0x39000049: // Diamond
case 0x39000050: // GromnieHide
case 0x39000051: // Pyreal
case 0x39000052: // Ruby
case 0x39000053: // Sapphire
return false;
// magic item tinkering
case 0x38000025: // Sunstone
//AddImbuedEffect(target, ImbuedEffectType.ArmorRending);
target.ImbuedEffect = ImbuedEffectType.ArmorRending;
break;
case 0x38000024: // FireOpal
//AddImbuedEffect(target, ImbuedEffectType.CripplingBlow);
target.ImbuedEffect = ImbuedEffectType.CripplingBlow;
break;
case 0x38000023: // BlackOpal
//AddImbuedEffect(target, ImbuedEffectType.CriticalStrike);
target.ImbuedEffect = ImbuedEffectType.CriticalStrike;
break;
case 0x3800002E: // Opal
//target.ManaConversionMod += 0.01f;
target.ManaConversionMod = (target.ManaConversionMod ?? 0.0f) + 0.01f;
break;
case 0x3800004B: // GreenGarnet: 44 -> 4B
target.ElementalDamageMod = (target.ElementalDamageMod ?? 0.0f) + 0.01f; // + 1% vs. monsters, + 0.25% vs. players
break;
case 0x38000041:
// these are handled in recipe mods already
// SmokeyQuartz
//AddSpell(player, target, SpellId.CANTRIPCOORDINATION1);
// RoseQuartz
//AddSpell(player, target, SpellId.CANTRIPQUICKNESS1);
// RedJade
//AddSpell(player, target, SpellId.CANTRIPHEALTHGAIN1);
// Malachite
//AddSpell(player, target, SpellId.WarriorsVigor);
// LavenderJade
//AddSpell(player, target, SpellId.CANTRIPMANAGAIN1);
// LapisLazuli
//AddSpell(player, target, SpellId.CANTRIPWILLPOWER1);
// Hematite
//AddSpell(player, target, SpellId.WarriorsVitality);
// Citrine
//AddSpell(player, target, SpellId.CANTRIPSTAMINAGAIN1);
// Carnelian
//AddSpell(player, target, SpellId.CANTRIPSTRENGTH1);
// Bloodstone
//AddSpell(player, target, SpellId.CANTRIPENDURANCE1);
// Azurite
//AddSpell(player, target, SpellId.WizardsIntellect);
// Agate
//AddSpell(player, target, SpellId.CANTRIPFOCUS1);
target.ImbuedEffect = ImbuedEffectType.Spellbook;
break;
// weapon tinkering
case 0x3800001A: // Iron
target.Damage += 1;
break;
case 0x3800001B: // Mahogany
target.DamageMod += 0.04f;
break;
case 0x3800001C: // Granite / Lucky Rabbit's Foot
target.DamageVariance *= 0.8f;
break;
case 0x3800001D: // Oak
target.WeaponTime = Math.Max(0, (target.WeaponTime ?? 0) - 50);
break;
case 0x38000020: // Brass
target.WeaponDefense += 0.01f;
break;
case 0x38000021: // Velvet
target.WeaponOffense += 0.01f;
break;
// only 1 imbue can be applied per piece of armor?
case 0x3800003A: // Emerald
//AddImbuedEffect(target, ImbuedEffectType.AcidRending);
target.ImbuedEffect = ImbuedEffectType.AcidRending;
break;
case 0x3800003B: // WhiteSapphire
//AddImbuedEffect(target, ImbuedEffectType.BludgeonRending);
target.ImbuedEffect = ImbuedEffectType.BludgeonRending;
break;
case 0x3800003C: // Aquamarine
//AddImbuedEffect(target, ImbuedEffectType.ColdRending);
target.ImbuedEffect = ImbuedEffectType.ColdRending;
break;
case 0x3800003D: // Jet
//AddImbuedEffect(target, ImbuedEffectType.ElectricRending);
target.ImbuedEffect = ImbuedEffectType.ElectricRending;
break;
case 0x3800003E: // RedGarnet
//AddImbuedEffect(target, ImbuedEffectType.FireRending);
target.ImbuedEffect = ImbuedEffectType.FireRending;
break;
case 0x3800003F: // BlackGarnet
//AddImbuedEffect(target, ImbuedEffectType.PierceRending);
target.ImbuedEffect = ImbuedEffectType.PierceRending;
break;
case 0x38000040: // ImperialTopaz
//AddImbuedEffect(target, ImbuedEffectType.SlashRending);
target.ImbuedEffect = ImbuedEffectType.SlashRending;
break;
// addons
case 0x3800000F: // Stamps
target.IconOverlayId = target.IconOverlaySecondary;
break;
case 0x38000046: // Fetish of the Dark Idols
// shouldn't exist on player items, but just recreating original script here
if (target.ImbuedEffect >= ImbuedEffectType.IgnoreAllArmor)
target.ImbuedEffect = ImbuedEffectType.Undef;
target.ImbuedEffect |= ImbuedEffectType.IgnoreSomeMagicProjectileDamage;
//target.AbsorbMagicDamage = 0.25f; // not in original mods / mutation?
break;
case 0x39000000: // Paragon Weapons
target.ItemMaxLevel = (target.ItemMaxLevel ?? 0) + 1;
target.ItemBaseXp = 2000000000;
target.ItemTotalXp = target.ItemTotalXp ?? 0;
break;
default:
log.Error($"{player.Name}.RecipeManager.Tinkering_ModifyItem({source.Name} ({source.Guid}), {target.Name} ({target.Guid})) - unknown mutation id: {dataId:X8}");
return false;
}
if (incItemTinkered.Contains(dataId))
HandleTinkerLog(source, target);
return true;
}
// only needed for legacy method
// ideally this wouldn't even be needed for the legacy method, and recipe.IsTinkering() would suffice
// however, that would break for rare salvages, which have 0 difficulty and salvage_Type 0
private static readonly HashSet<uint> incItemTinkered = new HashSet<uint>()
{
0x38000011, // Steel
0x38000012, // Armoredillo Hide
0x38000013, // Marble
0x38000014, // Wool
0x38000015, // Reedshark Hide
0x38000016, // Ceramic
0x38000017, // Alabaster
0x38000018, // Bronze
0x38000019, // Linen
0x3800001A, // Iron
0x3800001B, // Mahogany
0x3800001C, // Granite
0x3800001D, // Oak
0x3800001E, // Pine
0x3800001F, // Gold
0x38000020, // Brass
0x38000021, // Velvet
0x38000023, // Black Opal
0x38000024, // Fire Opal
0x38000025, // Sunstone
0x3800002E, // Opal
0x3800002F, // Moonstone
0x38000034, // Silver
0x38000035, // Copper
0x38000036, // Silk
0x38000037, // Zircon
0x38000038, // Peridot
0x38000039, // Yellow Topaz
0x3800003A, // Emerald
0x3800003B, // White Sapphire
0x3800003C, // Aquamarine
0x3800003D, // Jet
0x3800003E, // Red Garnet
0x3800003F, // Black Garnet
0x38000040, // Imperial Topaz
0x38000041, // Cantrips
0x38000042, // Heritage
0x3800004B, // Green Garnet
};
public static void AddSpell(Player player, WorldObject target, SpellId spell, int difficulty = 25)
{
target.Biota.GetOrAddKnownSpell((int)spell, target.BiotaDatabaseLock, out _);
target.ChangesDetected = true;
if (difficulty != 0)
{
target.ItemSpellcraft = (target.ItemSpellcraft ?? 0) + difficulty;
target.ItemDifficulty = (target.ItemDifficulty ?? 0) + difficulty;
}
if (target.UiEffects == null)
{
target.UiEffects = UiEffects.Magical;
player.Session.Network.EnqueueSend(new GameMessagePublicUpdatePropertyInt(target, PropertyInt.UiEffects, (int)target.UiEffects));
}
}
// derrick's input => output mappings
public static Dictionary<ImbuedEffectType, uint> IconUnderlay = new Dictionary<ImbuedEffectType, uint>()
{
{ ImbuedEffectType.ColdRending, 0x06003353 },
{ ImbuedEffectType.ElectricRending, 0x06003354 },
{ ImbuedEffectType.AcidRending, 0x06003355 },
{ ImbuedEffectType.ArmorRending, 0x06003356 },
{ ImbuedEffectType.CripplingBlow, 0x06003357 },
{ ImbuedEffectType.CriticalStrike, 0x06003358 },
{ ImbuedEffectType.FireRending, 0x06003359 },
{ ImbuedEffectType.BludgeonRending, 0x0600335a },
{ ImbuedEffectType.PierceRending, 0x0600335b },
{ ImbuedEffectType.SlashRending, 0x0600335c },
};
public static ImbuedEffectType GetImbuedEffects(WorldObject target)
{
var imbuedEffects = 0;
imbuedEffects |= target.GetProperty(PropertyInt.ImbuedEffect) ?? 0;
imbuedEffects |= target.GetProperty(PropertyInt.ImbuedEffect2) ?? 0;
imbuedEffects |= target.GetProperty(PropertyInt.ImbuedEffect3) ?? 0;
imbuedEffects |= target.GetProperty(PropertyInt.ImbuedEffect4) ?? 0;
imbuedEffects |= target.GetProperty(PropertyInt.ImbuedEffect5) ?? 0;
return (ImbuedEffectType)imbuedEffects;
}
public static bool VerifyRequirements(Recipe recipe, Player player, WorldObject source, WorldObject target)
{
if (!VerifyUse(player, source, target))
return false;
if (!VerifyRequirements(recipe, player, target, RequirementType.Target)) return false;
if (!VerifyRequirements(recipe, player, source, RequirementType.Source)) return false;
if (!VerifyRequirements(recipe, player, player, RequirementType.Player)) return false;
return true;
}
public static bool VerifyUse(Player player, WorldObject source, WorldObject target)
{
var usable = source.ItemUseable ?? Usable.Undef;
if (usable == Usable.Undef)
{
log.Warn($"{player.Name}.RecipeManager.VerifyUse({source.Name} ({source.Guid}), {target.Name} ({target.Guid})) - source not usable, falling back on defaults");
// re-verify
if (player.FindObject(source.Guid.Full, Player.SearchLocations.MyInventory) == null)
return false;
// almost always MyInventory, but sometimes can be applied to equipped
if (player.FindObject(target.Guid.Full, Player.SearchLocations.MyInventory | Player.SearchLocations.MyEquippedItems) == null)
return false;
return true;
}
var sourceUse = usable.GetSourceFlags();
var targetUse = usable.GetTargetFlags();
return VerifyUse(player, source, sourceUse) && VerifyUse(player, target, targetUse);
}
public static bool VerifyUse(Player player, WorldObject obj, Usable usable)
{
var searchLocations = Player.SearchLocations.None;
// TODO: figure out other Usable flags
if (usable.HasFlag(Usable.Contained))
searchLocations |= Player.SearchLocations.MyInventory | Player.SearchLocations.MyEquippedItems;
if (usable.HasFlag(Usable.Wielded))
searchLocations |= Player.SearchLocations.MyEquippedItems;
if (usable.HasFlag(Usable.Remote))
searchLocations |= Player.SearchLocations.LocationsICanMove; // TODO: moveto for this type
return player.FindObject(obj.Guid.Full, searchLocations) != null;
}
public static bool Debug = false;
public static bool VerifyRequirements(Recipe recipe, Player player, WorldObject obj, RequirementType reqType)
{
var boolReqs = recipe.RecipeRequirementsBool.Where(i => i.Index == (int)reqType).ToList();
var intReqs = recipe.RecipeRequirementsInt.Where(i => i.Index == (int)reqType).ToList();
var floatReqs = recipe.RecipeRequirementsFloat.Where(i => i.Index == (int)reqType).ToList();
var strReqs = recipe.RecipeRequirementsString.Where(i => i.Index == (int)reqType).ToList();
var iidReqs = recipe.RecipeRequirementsIID.Where(i => i.Index == (int)reqType).ToList();
var didReqs = recipe.RecipeRequirementsDID.Where(i => i.Index == (int)reqType).ToList();
var totalReqs = boolReqs.Count + intReqs.Count + floatReqs.Count + strReqs.Count + iidReqs.Count + didReqs.Count;
if (Debug && totalReqs > 0)
Console.WriteLine($"{reqType} Requirements: {totalReqs}");
foreach (var requirement in boolReqs)
{
bool? value = obj.GetProperty((PropertyBool)requirement.Stat);
double? normalized = value != null ? (double?)Convert.ToDouble(value.Value) : null;
if (Debug)
Console.WriteLine($"PropertyBool.{(PropertyBool)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, normalized, Convert.ToDouble(requirement.Value), requirement.Message))
return false;
}
foreach (var requirement in intReqs)
{
int? value = obj.GetProperty((PropertyInt)requirement.Stat);
double? normalized = value != null ? (double?)Convert.ToDouble(value.Value) : null;
if (Debug)
Console.WriteLine($"PropertyInt.{(PropertyInt)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, normalized, Convert.ToDouble(requirement.Value), requirement.Message))
return false;
}
foreach (var requirement in floatReqs)
{
double? value = obj.GetProperty((PropertyFloat)requirement.Stat);
if (Debug)
Console.WriteLine($"PropertyFloat.{(PropertyFloat)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, value, requirement.Value, requirement.Message))
return false;
}
foreach (var requirement in strReqs)
{
string value = obj.GetProperty((PropertyString)requirement.Stat);
if (Debug)
Console.WriteLine($"PropertyString.{(PropertyString)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, value, requirement.Value, requirement.Message))
return false;
}
foreach (var requirement in iidReqs)
{
uint? value = obj.GetProperty((PropertyInstanceId)requirement.Stat);
double? normalized = value != null ? (double?)Convert.ToDouble(value.Value) : null;
if (Debug)
Console.WriteLine($"PropertyInstanceId.{(PropertyInstanceId)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, normalized, Convert.ToDouble(requirement.Value), requirement.Message))
return false;
}
foreach (var requirement in didReqs)
{
uint? value = obj.GetProperty((PropertyDataId)requirement.Stat);
double? normalized = value != null ? (double?)Convert.ToDouble(value.Value) : null;
if (Debug)
Console.WriteLine($"PropertyDataId.{(PropertyDataId)requirement.Stat} {(CompareType)requirement.Enum} {requirement.Value}, current: {value}");
if (!VerifyRequirement(player, (CompareType)requirement.Enum, normalized, Convert.ToDouble(requirement.Value), requirement.Message))
return false;
}
if (Debug && totalReqs > 0)
Console.WriteLine($"-----");
return true;
}
public static bool VerifyRequirement(Player player, CompareType compareType, double? prop, double val, string failMsg)
{
var success = true;
switch (compareType)
{
case CompareType.GreaterThan:
if ((prop ?? 0) > val)
success = false;
break;
case CompareType.LessThanEqual:
if ((prop ?? 0) <= val)
success = false;
break;
case CompareType.LessThan:
if ((prop ?? 0) < val)
success = false;
break;
case CompareType.GreaterThanEqual:
if ((prop ?? 0) >= val)
success = false;
break;
case CompareType.NotEqual:
if ((prop ?? 0) != val)
success = false;
break;
case CompareType.NotEqualNotExist:
if (prop == null || prop.Value != val)
success = false;
break;
case CompareType.Equal:
if ((prop ?? 0) == val)
success = false;
break;
case CompareType.NotExist:
if (prop == null)
success = false;
break;
case CompareType.Exist:
if (prop != null)
success = false;
break;
case CompareType.NotHasBits:
if (((int)(prop ?? 0) & (int)val) == 0)
success = false;
break;
case CompareType.HasBits:
if (((int)(prop ?? 0) & (int)val) == (int)val)
success = false;
break;
}
if (!success)
player.Session.Network.EnqueueSend(new GameMessageSystemChat(failMsg, ChatMessageType.Craft));
return success;
}
public static bool VerifyRequirement(Player player, CompareType compareType, string prop, string val, string failMsg)
{
var success = true;
switch (compareType)
{
case CompareType.NotEqual:
if (!(prop ?? "").Equals(val))
success = false;
break;
case CompareType.NotEqualNotExist:
if (prop == null || !prop.Equals(val))
success = false;
break;
case CompareType.Equal:
if ((prop ?? "").Equals(val))
success = false;
break;
case CompareType.NotExist:
if (prop == null)
success = false;
break;
case CompareType.Exist:
if (prop != null)
success = false;
break;
}
if (!success)
player.Session.Network.EnqueueSend(new GameMessageSystemChat(failMsg, ChatMessageType.Craft));
return success;
}
/// <summary>
/// Returns a list of object guids that have been modified
/// </summary>
public static HashSet<uint> CreateDestroyItems(Player player, Recipe recipe, WorldObject source, WorldObject target, double successChance, bool success)
{
var destroyTargetChance = success ? recipe.SuccessDestroyTargetChance : recipe.FailDestroyTargetChance;
var destroySourceChance = success ? recipe.SuccessDestroySourceChance : recipe.FailDestroySourceChance;
var destroyTarget = ThreadSafeRandom.Next(0.0f, 1.0f) < destroyTargetChance;
var destroySource = ThreadSafeRandom.Next(0.0f, 1.0f) < destroySourceChance;
var createItem = success ? recipe.SuccessWCID : recipe.FailWCID;
var createAmount = success ? recipe.SuccessAmount : recipe.FailAmount;
if (createItem > 0 && DatabaseManager.World.GetCachedWeenie(createItem) == null)
{
log.Error($"RecipeManager.CreateDestroyItems: Recipe.Id({recipe.Id}) couldn't find {(success ? "Success" : "Fail")}WCID {createItem} in database.");
player.Session.Network.EnqueueSend(new GameEventWeenieError(player.Session, WeenieError.CraftGeneralErrorUiMsg));
return null;
}
if (destroyTarget)
{
var destroyTargetAmount = success ? recipe.SuccessDestroyTargetAmount : recipe.FailDestroyTargetAmount;
var destroyTargetMessage = success ? recipe.SuccessDestroyTargetMessage : recipe.FailDestroyTargetMessage;
DestroyItem(player, recipe, target, destroyTargetAmount, destroyTargetMessage);
}
if (destroySource)
{
var destroySourceAmount = success ? recipe.SuccessDestroySourceAmount : recipe.FailDestroySourceAmount;
var destroySourceMessage = success ? recipe.SuccessDestroySourceMessage : recipe.FailDestroySourceMessage;
DestroyItem(player, recipe, source, destroySourceAmount, destroySourceMessage);
}
WorldObject result = null;
if (createItem > 0)
result = CreateItem(player, createItem, createAmount);
var modified = ModifyItem(player, recipe, source, target, result, success);
// broadcast different messages based on recipe type
if (!recipe.IsTinkering())
{
var message = success ? recipe.SuccessMessage : recipe.FailMessage;
player.Session.Network.EnqueueSend(new GameMessageSystemChat(message, ChatMessageType.Craft));
log.Info($"[CRAFTING] {player.Name} used {source.NameWithMaterial} on {target.NameWithMaterial} {(success ? "" : "un")}successfully. {(destroySource ? $"| {source.NameWithMaterial} was destroyed " : "")}{(destroyTarget ? $"| {target.NameWithMaterial} was destroyed " : "")}| {message}");
}
else
BroadcastTinkering(player, source, target, successChance, success);
return modified;
}
public static void BroadcastTinkering(Player player, WorldObject tool, WorldObject target, double chance, bool success)
{
// retail AC had some inconsistency with respect to the messages broadcast by tinkering.
//
// Largely this revolved around the name of the weenies that represented each of the salvage bags.
// First, there were name changes that were a result of the client side pre-pending of material type which resulted in bags being named "Salvage", "Salvaged"
// Second, these bags that were generated from loot by players always ended with the number of materials in the bag, such as (100), (88), (1)
// while non-lootgen bags did not include the number, so the name of the bag when displayed or broadcast varied like "Steel Salvage (100)", "Steel Salvaged (100)", "Steel Salvage"
// Third, Foolproof bags, again depending on weenie names, also had their own variations which resulted in display names like "Foolproof Black Garnet Gem", "Zircon Foolproof Zircon", "Imperial Topaz Foolproof Imperial Topaz"
// in many cases, there are multiple weenies of a particular salvage type, used by various systems, which resulted in each tinker operation having varied output even when doing essentially the same thing
// Finally, items that were inscribed were surprisingly identified in the broadcast like "Reed Shark Hide Studded Leather Sleeves inscribed by Callaway", "Copper Frost Bow inscribed by Mini Bonsai"
//
// ACE output, as seen below, has for the most part standardized the message due to weenie name changes, salvage coding and recipe handling differences
//
var sourceName = Regex.Replace(tool.NameWithMaterial, @" \(\d+\)$", "");
var msg = $"{player.Name} {(success ? "successfully applies" : "fails to apply")} the {sourceName} (workmanship {(tool.Workmanship ?? 0):#.00}) to the {target.NameWithMaterial}{((target.Inscription != null && target.ScribeName != null) ? $" inscribed by {target.ScribeName}" : "")}.{(!success ? " The target is destroyed." : "")}";
// send local broadcast
player.EnqueueBroadcast(new GameMessageSystemChat(msg, ChatMessageType.Craft), WorldObject.LocalBroadcastRange, ChatMessageType.Craft);
log.Info($"[TINKERING] {msg} | Chance: {chance}");
}
public static WorldObject CreateItem(Player player, uint wcid, uint amount)
{
var wo = WorldObjectFactory.CreateNewWorldObject(wcid);
if (wo == null)
{
log.Warn($"RecipeManager.CreateItem({player.Name}, {wcid}, {amount}): failed to create {wcid}");
return null;
}
if (amount > 1)
wo.SetStackSize((int)amount);
player.TryCreateInInventoryWithNetworking(wo);
return wo;
}
public static void DestroyItem(Player player, Recipe recipe, WorldObject item, uint amount, string msg)
{
if (item.OwnerId == player.Guid.Full || player.GetInventoryItem(item.Guid) != null)
{
if (!player.TryConsumeFromInventoryWithNetworking(item, (int)amount))
log.Warn($"RecipeManager.DestroyItem({player.Name}, {item.Name}, {amount}, {msg}): failed to remove {item.Name}");
}
else if (item.WielderId == player.Guid.Full)
{
if (!player.TryDequipObjectWithNetworking(item.Guid, out _, Player.DequipObjectAction.ConsumeItem))
log.Warn($"RecipeManager.DestroyItem({player.Name}, {item.Name}, {amount}, {msg}): failed to remove {item.Name}");
}
else
{
item.Destroy();
}
if (!string.IsNullOrEmpty(msg))
{
var destroyMessage = new GameMessageSystemChat(msg, ChatMessageType.Craft);
player.Session.Network.EnqueueSend(destroyMessage);
}
}
public static WorldObject GetSourceMod(RecipeSourceType sourceType, Player player, WorldObject source)
{
switch (sourceType)
{
case RecipeSourceType.Player:
return player;
case RecipeSourceType.Source:
return source;
}
log.Warn($"RecipeManager.GetSourceMod({sourceType}, {player.Name}, {source.Name}) - unknown source type");
return null;
}
public static WorldObject GetTargetMod(ModificationType type, WorldObject source, WorldObject target, Player player, WorldObject result)
{
switch (type)
{
case ModificationType.SuccessSource:
case ModificationType.FailureSource:
return source;
default:
return target;
case ModificationType.SuccessPlayer:
case ModificationType.FailurePlayer:
return player;
case ModificationType.SuccessResult:
case ModificationType.FailureResult:
return result ?? target;
}
}
/// <summary>
/// Returns a list of object guids that have been modified
/// </summary>
public static HashSet<uint> ModifyItem(Player player, Recipe recipe, WorldObject source, WorldObject target, WorldObject result, bool success)
{
var modified = new HashSet<uint>();
foreach (var mod in recipe.RecipeMod)
{
if (mod.ExecutesOnSuccess != success)
continue;
// adjust vitals
if (mod.Health != 0)
ModifyVital(player, PropertyAttribute2nd.Health, mod.Health);
if (mod.Stamina != 0)
ModifyVital(player, PropertyAttribute2nd.Stamina, mod.Stamina);
if (mod.Mana != 0)
ModifyVital(player, PropertyAttribute2nd.Mana, mod.Mana);
// apply type mods
foreach (var boolMod in mod.RecipeModsBool)
ModifyBool(player, boolMod, source, target, result, modified);
foreach (var intMod in mod.RecipeModsInt)
ModifyInt(player, intMod, source, target, result, modified);
foreach (var floatMod in mod.RecipeModsFloat)
ModifyFloat(player, floatMod, source, target, result, modified);
foreach (var stringMod in mod.RecipeModsString)
ModifyString(player, stringMod, source, target, result, modified);
foreach (var iidMod in mod.RecipeModsIID)
ModifyInstanceID(player, iidMod, source, target, result, modified);
foreach (var didMod in mod.RecipeModsDID)
ModifyDataID(player, didMod, source, target, result, modified);
// run mutation script, if applicable
if (mod.DataId != 0)
TryMutate(player, source, target, recipe, (uint)mod.DataId, modified);
}
return modified;
}
private static void ModifyVital(Player player, PropertyAttribute2nd attribute2nd, int value)
{
var vital = player.GetCreatureVital(attribute2nd);
var vitalChange = (uint)Math.Abs(player.UpdateVitalDelta(vital, value));
if (attribute2nd == PropertyAttribute2nd.Health)
{
if (value >= 0)
player.DamageHistory.OnHeal(vitalChange);
else
player.DamageHistory.Add(player, DamageType.Health, vitalChange);
if (player.Health.Current <= 0)
{
// should this be possible?
//var lastDamager = player != null ? new DamageHistoryInfo(player) : null;
var lastDamager = new DamageHistoryInfo(player);
player.OnDeath(lastDamager, DamageType.Health, false);
player.Die();
}
}
}
public static void ModifyBool(Player player, RecipeModsBool boolMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)boolMod.Enum;
var prop = (PropertyBool)boolMod.Stat;
var value = boolMod.Value;
var targetMod = GetTargetMod((ModificationType)boolMod.Index, source, target, player, result);
// always SetValue?
if (op != ModificationOperation.SetValue)
{
log.Warn($"RecipeManager.ModifyBool({source.Name}, {target.Name}): unhandled operation {op}");
return;
}
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug)
Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
}
public static void ModifyInt(Player player, RecipeModsInt intMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)intMod.Enum;
var prop = (PropertyInt)intMod.Stat;
var value = intMod.Value;
var sourceMod = GetSourceMod((RecipeSourceType)intMod.Source, player, source);
var targetMod = GetTargetMod((ModificationType)intMod.Index, source, target, player, result);
switch (op)
{
case ModificationOperation.SetValue:
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.Add:
player.UpdateProperty(targetMod, prop, (targetMod.GetProperty(prop) ?? 0) + value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.IncProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.CopyFromSourceToTarget:
player.UpdateProperty(target, prop, sourceMod.GetProperty(prop) ?? 0);
modified.Add(target.Guid.Full);
if (Debug) Console.WriteLine($"{target.Name}.SetProperty({prop}, {sourceMod.GetProperty(prop) ?? 0}) - {op}");
break;
case ModificationOperation.CopyFromSourceToResult:
player.UpdateProperty(result, prop, player.GetProperty(prop) ?? 0); // ??
modified.Add(result.Guid.Full);
if (Debug) Console.WriteLine($"{result.Name}.SetProperty({prop}, {player.GetProperty(prop) ?? 0}) - {op}");
break;
case ModificationOperation.AddSpell:
targetMod.Biota.GetOrAddKnownSpell(intMod.Stat, target.BiotaDatabaseLock, out var added);
modified.Add(targetMod.Guid.Full);
if (added)
targetMod.ChangesDetected = true;
if (Debug) Console.WriteLine($"{targetMod.Name}.AddSpell({intMod.Stat}) - {op}");
break;
case ModificationOperation.SetBitsOn:
var bits = targetMod.GetProperty(prop) ?? 0;
bits |= value;
player.UpdateProperty(targetMod, prop, bits);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, 0x{bits:X}) - {op}");
break;
case ModificationOperation.SetBitsOff:
bits = targetMod.GetProperty(prop) ?? 0;
bits &= ~value;
player.UpdateProperty(targetMod, prop, bits);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, 0x{bits:X}) - {op}");
break;
default:
log.Warn($"RecipeManager.ModifyInt({source.Name}, {target.Name}): unhandled operation {op}");
break;
}
}
public static void ModifyFloat(Player player, RecipeModsFloat floatMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)floatMod.Enum;
var prop = (PropertyFloat)floatMod.Stat;
var value = floatMod.Value;
var sourceMod = GetSourceMod((RecipeSourceType)floatMod.Source, player, source);
var targetMod = GetTargetMod((ModificationType)floatMod.Index, source, target, player, result);
switch (op)
{
case ModificationOperation.SetValue:
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.Add:
player.UpdateProperty(targetMod, prop, (targetMod.GetProperty(prop) ?? 0) + value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.IncProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.CopyFromSourceToTarget:
player.UpdateProperty(target, prop, sourceMod.GetProperty(prop) ?? 0);
modified.Add(target.Guid.Full);
if (Debug) Console.WriteLine($"{target.Name}.SetProperty({prop}, {sourceMod.GetProperty(prop) ?? 0}) - {op}");
break;
case ModificationOperation.CopyFromSourceToResult:
player.UpdateProperty(result, prop, player.GetProperty(prop) ?? 0);
modified.Add(result.Guid.Full);
if (Debug) Console.WriteLine($"{result.Name}.SetProperty({prop}, {player.GetProperty(prop) ?? 0}) - {op}");
break;
default:
log.Warn($"RecipeManager.ModifyFloat({source.Name}, {target.Name}): unhandled operation {op}");
break;
}
}
public static void ModifyString(Player player, RecipeModsString stringMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)stringMod.Enum;
var prop = (PropertyString)stringMod.Stat;
var value = stringMod.Value;
var sourceMod = GetSourceMod((RecipeSourceType)stringMod.Source, player, source);
var targetMod = GetTargetMod((ModificationType)stringMod.Index, source, target, player, result);
switch (op)
{
case ModificationOperation.SetValue:
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.CopyFromSourceToTarget:
player.UpdateProperty(target, prop, sourceMod.GetProperty(prop) ?? sourceMod.Name);
modified.Add(target.Guid.Full);
if (Debug) Console.WriteLine($"{target.Name}.SetProperty({prop}, {sourceMod.GetProperty(prop) ?? sourceMod.Name}) - {op}");
break;
case ModificationOperation.CopyFromSourceToResult:
player.UpdateProperty(result, prop, player.GetProperty(prop) ?? player.Name);
modified.Add(result.Guid.Full);
if (Debug) Console.WriteLine($"{result.Name}.SetProperty({prop}, {player.GetProperty(prop) ?? player.Name}) - {op}");
break;
default:
log.Warn($"RecipeManager.ModifyString({source.Name}, {target.Name}): unhandled operation {op}");
break;
}
}
public static void ModifyInstanceID(Player player, RecipeModsIID iidMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)iidMod.Enum;
var prop = (PropertyInstanceId)iidMod.Stat;
var value = iidMod.Value;
var sourceMod = GetSourceMod((RecipeSourceType)iidMod.Source, player, source);
var targetMod = GetTargetMod((ModificationType)iidMod.Index, source, target, player, result);
switch (op)
{
case ModificationOperation.SetValue:
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.CopyFromSourceToTarget:
player.UpdateProperty(target, prop, ModifyInstanceIDRuleSet(prop, sourceMod, targetMod));
modified.Add(target.Guid.Full);
if (Debug) Console.WriteLine($"{target.Name}.SetProperty({prop}, {ModifyInstanceIDRuleSet(prop, sourceMod, targetMod)}) - {op}");
break;
case ModificationOperation.CopyFromSourceToResult:
player.UpdateProperty(result, prop, ModifyInstanceIDRuleSet(prop, player, targetMod)); // ??
modified.Add(result.Guid.Full);
if (Debug) Console.WriteLine($"{result.Name}.SetProperty({prop}, {ModifyInstanceIDRuleSet(prop, player, targetMod)}) - {op}");
break;
default:
log.Warn($"RecipeManager.ModifyInstanceID({source.Name}, {target.Name}): unhandled operation {op}");
break;
}
}
private static uint ModifyInstanceIDRuleSet(PropertyInstanceId property, WorldObject sourceMod, WorldObject targetMod)
{
switch (property)
{
case PropertyInstanceId.AllowedWielder:
case PropertyInstanceId.AllowedActivator:
return sourceMod.Guid.Full;
default:
break;
}
return sourceMod.GetProperty(property) ?? 0;
}
public static void ModifyDataID(Player player, RecipeModsDID didMod, WorldObject source, WorldObject target, WorldObject result, HashSet<uint> modified)
{
var op = (ModificationOperation)didMod.Enum;
var prop = (PropertyDataId)didMod.Stat;
var value = didMod.Value;
var sourceMod = GetSourceMod((RecipeSourceType)didMod.Source, player, source);
var targetMod = GetTargetMod((ModificationType)didMod.Index, source, target, player, result);
switch (op)
{
case ModificationOperation.SetValue:
player.UpdateProperty(targetMod, prop, value);
modified.Add(targetMod.Guid.Full);
if (Debug) Console.WriteLine($"{targetMod.Name}.SetProperty({prop}, {value}) - {op}");
break;
case ModificationOperation.CopyFromSourceToTarget:
player.UpdateProperty(target, prop, sourceMod.GetProperty(prop) ?? 0);
modified.Add(target.Guid.Full);
if (Debug) Console.WriteLine($"{target.Name}.SetProperty({prop}, {sourceMod.GetProperty(prop) ?? 0}) - {op}");
break;
case ModificationOperation.CopyFromSourceToResult:
player.UpdateProperty(result, prop, player.GetProperty(prop) ?? 0);
modified.Add(result.Guid.Full);
if (Debug) Console.WriteLine($"{result.Name}.SetProperty({prop}, {player.GetProperty(prop) ?? 0}) - {op}");
break;
default:
log.Warn($"RecipeManager.ModifyDataID({source.Name}, {target.Name}): unhandled operation {op}");
break;
}
}
///// <summary>
///// flag to use c# logic instead of mutate script logic
///// </summary>
//private const bool useMutateNative = false;
public static bool TryMutate(Player player, WorldObject source, WorldObject target, Recipe recipe, uint dataId, HashSet<uint> modified)
{
//if (useMutateNative)
// return TryMutateNative(player, source, target, recipe, dataId);
var numTimesTinkered = target.NumTimesTinkered;
var mutationScript = MutationCache.GetMutation(dataId);
if (mutationScript == null)
{
log.Error($"RecipeManager.TryApplyMutation({dataId:X8}, {target.Name}) - couldn't find mutation script");
return false;
}
var result = mutationScript.TryMutate(target);
if (numTimesTinkered != target.NumTimesTinkered)
HandleTinkerLog(source, target);
modified.Add(target.Guid.Full);
return result;
}
private static void HandleTinkerLog(WorldObject source, WorldObject target)
{
if (target.TinkerLog != null)
target.TinkerLog += ",";
target.TinkerLog += (uint?)source.MaterialType ?? source.WeenieClassId;
}
public static uint MaterialDualDID = 0x27000000;
public static string GetMaterialName(MaterialType materialType)
{
var dualDIDs = DatManager.PortalDat.ReadFromDat<DualDidMapper>(MaterialDualDID);
if (!dualDIDs.ClientEnumToName.TryGetValue((uint)materialType, out var materialName))
{
log.Error($"RecipeManager.GetMaterialName({materialType}): couldn't find material name");
return materialType.ToString();
}
return materialName.Replace("_", " ");
}
// todo: remove this once foolproof salvage recipes are updated
private static readonly HashSet<WeenieClassName> foolproofTinkers = new HashSet<WeenieClassName>()
{
// rare foolproof
WeenieClassName.W_MATERIALRAREFOOLPROOFAQUAMARINE_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFBLACKGARNET_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFBLACKOPAL_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFEMERALD_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFFIREOPAL_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFIMPERIALTOPAZ_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFJET_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFPERIDOT_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFREDGARNET_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFSUNSTONE_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFWHITESAPPHIRE_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFYELLOWTOPAZ_CLASS,
WeenieClassName.W_MATERIALRAREFOOLPROOFZIRCON_CLASS,
// regular foolproof
WeenieClassName.W_MATERIALACE36619FOOLPROOFAQUAMARINE,
WeenieClassName.W_MATERIALACE36620FOOLPROOFBLACKGARNET,
WeenieClassName.W_MATERIALACE36621FOOLPROOFBLACKOPAL,
WeenieClassName.W_MATERIALACE36622FOOLPROOFEMERALD,
WeenieClassName.W_MATERIALACE36623FOOLPROOFFIREOPAL,
WeenieClassName.W_MATERIALACE36624FOOLPROOFIMPERIALTOPAZ,
WeenieClassName.W_MATERIALACE36625FOOLPROOFJET,
WeenieClassName.W_MATERIALACE36626FOOLPROOFREDGARNET,
WeenieClassName.W_MATERIALACE36627FOOLPROOFSUNSTONE,
WeenieClassName.W_MATERIALACE36628FOOLPROOFWHITESAPPHIRE,
WeenieClassName.W_MATERIALACE36634FOOLPROOFPERIDOT,
WeenieClassName.W_MATERIALACE36635FOOLPROOFYELLOWTOPAZ,
WeenieClassName.W_MATERIALACE36636FOOLPROOFZIRCON,
};
}
public static class RecipeExtensions
{
public static bool IsTinkering(this Recipe recipe)
{
return recipe.SalvageType > 0;
}
public static bool IsImbuing(this Recipe recipe)
{
return recipe.SalvageType == 2;
}
}
}