ModernUO/Projects/UOContent/Engines/Craft/Core/Repair.cs
Kamron Batman 9b554f69b0
feat(timers): Adds timer pooling, fixes timer related bugs, and changes timer api (#667)
### Changes/Fixes:
* Adds timer pooling.
* Allows pool to be configurable in ModernUO.json
* Pool replenishes itself asynchronously if depleted.
* Fixes an issue with barkeeps and town criers
* Fixes an issue with incognito buff icons not being removed
* Fixes an issue with polymorph name mod not being removed
* Fixes several places where timers go on forever even after an object is deleted, keeping a reference (memory leak)
* Eliminates the timer for MiningCart altogether.
* Deletes `AcidSlime` since it is a duplicate of `PoolOfAcid`
* Fixes HonorableExecution and standardizes the code for other Bushido moves.

## Changes to the Timer API:
```cs
public class Timer
{
  // Creates a timer that will be returned to the pool once execution stops.
  public static void StartTimer(Action callback);
  public static void StartTimer(TimeSpan delay, Action callback);
  public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback);
  public static void StartTimer(TimeSpan interval, int count, Action callback);
  public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback);

  // Creates a timer and returns a token for more control. Requires manual cancellation in order for the timer to be returned to the pool.
  // If the token is dereferenced, the timer will be dereferenced too. While not returning a timer to the pool is not considered hazardous, it does defeat the purpose of pooled timers.
  public static void StartTimer(Action callback, out TimerExecutionToken token);
  public static void StartTimer(TimeSpan delay, Action callback, out TimerExecutionToken token);
  public static void StartTimer(TimeSpan delay, TimeSpan interval, Action callback, out TimerExecutionToken token);
  public static void StartTimer(TimeSpan interval, int count, Action callback, out TimerExecutionToken token);
  public static void StartTimer(TimeSpan delay, TimeSpan interval, int count, Action callback, out TimerExecutionToken token);

  // If you aren't sure how to use the API above, or you don't care about performance, then you can use the old RunUO Timer.DelayCall
  public static DelayCallTimer DelayCall(Action callback);
  public static DelayCallTimer DelayCall(TimeSpan delay, Action callback);
  public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, Action callback);
  public static DelayCallTimer DelayCall(TimeSpan interval, int count, Action callback);
  public static DelayCallTimer DelayCall(TimeSpan delay, TimeSpan interval, int count, Action callback);
}

public struct TimerExecutionToken
{
  public bool Running { get; }
  public int Index { get; }
  public int RemainingCount { get; }
  public DateTime Next { get; }
}
```

## When to use `TimerExecutionToken`?
Use tokens when you want to gain the performance benefit of using a pooled timer, but you need one of the following:
* Access to the next time the timer will tick:`token.Next`
* Access to which interval, how many intervals there are, or how many are remaining: `token.Index`, `token.Count`, and `token.RemainingCount`
* Stop a timer manually.
* Determine if the timer is running: `timer.Running`
* See notes below about requirements for using tokens!

## Notes about using the TimerExecutionToken:
When you opt-in to receive a token, you must call `Cancel()` to return the timer. This can be done inside of the callback, or outside of the callback at any time.
If this is not called and your timer is an infinite interval, then you will create a potential memory leak, or null pointer exception in your callback.
If the timer ends and is stopped, but cancel is not called, then the timer will never return to the pool and stay referenced until the token is deleted or cancel is called. (Memory leak)

## Is this thread safe?
No. The ModernUO timer system is not thread safe at all. If you require a thread safe timer system, contact me and I'll help adapt this system. Keep in mind that there is a massive performance hit to make this thread safe when there are literally no use cases for it.

If you need to synchronize execution, meaning you want to execute code from another thread on the core thread. Let's say you have a discord bot that is pushing commands to the game server. Then use `EventLoopContext.Post(SendOrPostCallback callback, object state);`.
2021-08-07 14:33:35 -07:00

543 lines
21 KiB
C#

using System;
using Server.Items;
using Server.Mobiles;
using Server.Targeting;
namespace Server.Engines.Craft
{
public static class Repair
{
public static void Do(Mobile from, CraftSystem craftSystem, BaseTool tool)
{
from.Target = new InternalTarget(craftSystem, tool);
from.SendLocalizedMessage(1044276); // Target an item to repair.
}
public static void Do(Mobile from, CraftSystem craftSystem, RepairDeed deed)
{
from.Target = new InternalTarget(craftSystem, deed);
from.SendLocalizedMessage(1044276); // Target an item to repair.
}
private class InternalTarget : Target
{
private readonly CraftSystem m_CraftSystem;
private readonly RepairDeed m_Deed;
private readonly BaseTool m_Tool;
public InternalTarget(CraftSystem craftSystem, BaseTool tool) : base(2, false, TargetFlags.None)
{
m_CraftSystem = craftSystem;
m_Tool = tool;
}
public InternalTarget(CraftSystem craftSystem, RepairDeed deed) : base(2, false, TargetFlags.None)
{
m_CraftSystem = craftSystem;
m_Deed = deed;
}
private int GetWeakenChance(Mobile mob, SkillName skill, int curHits, int maxHits) => 40 + (maxHits - curHits) -
(int)((m_Deed?.SkillLevel ?? mob.Skills[skill].Value) / 10);
private bool CheckWeaken(Mobile mob, SkillName skill, int curHits, int maxHits) =>
GetWeakenChance(mob, skill, curHits, maxHits) > Utility.Random(100);
private int GetRepairDifficulty(int curHits, int maxHits) =>
(maxHits - curHits) * 1250 / Math.Max(maxHits, 1) - 250;
private bool CheckRepairDifficulty(Mobile mob, SkillName skill, int curHits, int maxHits)
{
var difficulty = GetRepairDifficulty(curHits, maxHits) * 0.1;
if (m_Deed != null)
{
var value = m_Deed.SkillLevel;
var minSkill = difficulty - 25.0;
var maxSkill = difficulty + 25;
if (value < minSkill)
{
return false; // Too difficult
}
if (value >= maxSkill)
{
return true; // No challenge
}
var chance = (value - minSkill) / (maxSkill - minSkill);
return chance >= Utility.RandomDouble();
}
return mob.CheckSkill(skill, difficulty - 25.0, difficulty + 25.0);
}
private bool CheckDeed(Mobile from)
{
if (m_Deed != null)
{
return m_Deed.Check(from);
}
return true;
}
private bool IsSpecialClothing(BaseClothing clothing)
{
// Clothing repairable but not craftable
if (m_CraftSystem is DefTailoring)
{
return clothing is BearMask
|| clothing is DeerMask
|| clothing is TheMostKnowledgePerson
|| clothing is TheRobeOfBritanniaAri
|| clothing is EmbroideredOakLeafCloak;
}
return false;
}
private bool IsSpecialWeapon(BaseWeapon weapon)
{
// Weapons repairable but not craftable
if (m_CraftSystem is DefTinkering)
{
return weapon is Cleaver
|| weapon is Hatchet
|| weapon is Pickaxe
|| weapon is ButcherKnife
|| weapon is SkinningKnife;
}
if (m_CraftSystem is DefCarpentry)
{
return weapon is Club
|| weapon is BlackStaff
|| weapon is MagicWand
// TODO: Make these items craftable
|| weapon is WildStaff;
}
if (m_CraftSystem is DefBlacksmithy)
{
return weapon is Pitchfork
// TODO: Make these items craftable
|| weapon is RadiantScimitar
|| weapon is WarCleaver
|| weapon is ElvenSpellblade
|| weapon is AssassinSpike
|| weapon is Leafblade
|| weapon is RuneBlade
|| weapon is ElvenMachete
|| weapon is OrnateAxe
|| weapon is DiamondMace;
}
// TODO: Make these items craftable
if (m_CraftSystem is DefBowFletching)
{
return weapon is ElvenCompositeLongbow
|| weapon is MagicalShortbow;
}
return false;
}
private bool IsSpecialArmor(BaseArmor armor)
{
// Armor repairable but not craftable
// TODO: Make these items craftable
if (m_CraftSystem is DefTailoring)
{
return armor is LeafTonlet
|| armor is LeafArms
|| armor is LeafChest
|| armor is LeafGloves
|| armor is LeafGorget
|| armor is LeafLegs
|| armor is HideChest
|| armor is HideGloves
|| armor is HideGorget
|| armor is HidePants
|| armor is HidePauldrons;
}
if (m_CraftSystem is DefCarpentry)
{
return armor is WingedHelm
|| armor is RavenHelm
|| armor is VultureHelm
|| armor is WoodlandArms
|| armor is WoodlandChest
|| armor is WoodlandGloves
|| armor is WoodlandGorget
|| armor is WoodlandLegs;
}
if (m_CraftSystem is DefBlacksmithy)
{
return armor is Circlet
|| armor is RoyalCirclet
|| armor is GemmedCirclet;
}
return false;
}
protected override void OnTarget(Mobile from, object targeted)
{
int number;
if (!CheckDeed(from))
{
return;
}
var usingDeed = m_Deed != null;
var toDelete = false;
// TODO: Make an IRepairable
if (m_CraftSystem.CanCraft(from, m_Tool, targeted.GetType()) == 1044267)
{
number = 1044282; // You must be near a forge and and anvil to repair items. * Yes, there are two and's *
}
else if (m_CraftSystem is DefTinkering && targeted is Golem g)
{
var damage = g.HitsMax - g.Hits;
if (g.IsDeadBondedPet)
{
number = 500426; // You can't repair that.
}
else if (damage <= 0)
{
number = 500423; // That is already in full repair.
}
else
{
var skillValue = usingDeed ? m_Deed.SkillLevel : from.Skills.Tinkering.Value;
if (skillValue < 60.0)
{
number =
1044153; // You don't have the required skills to attempt this item. //TODO: How does OSI handle this with deeds with golems?
}
else if (!from.CanBeginAction<Golem>())
{
number = 501789; // You must wait before trying again.
}
else
{
if (damage > (int)(skillValue * 0.3))
{
damage = (int)(skillValue * 0.3);
}
damage += 30;
if (!from.CheckSkill(SkillName.Tinkering, 0.0, 100.0))
{
damage /= 2;
}
var pack = from.Backpack;
if (pack != null)
{
var v = pack.ConsumeUpTo(typeof(IronIngot), (damage + 4) / 5);
if (v > 0)
{
g.Hits += v * 5;
number = 1044279; // You repair the item.
toDelete = true;
from.BeginAction<Golem>();
Timer.StartTimer(TimeSpan.FromSeconds(12.0), from.EndAction<Golem>);
}
else
{
number = 1044037; // You do not have sufficient metal to make that.
}
}
else
{
number = 1044037; // You do not have sufficient metal to make that.
}
}
}
}
else if (targeted is BaseWeapon weapon)
{
var skill = m_CraftSystem.MainSkill;
var toWeaken = 0;
if (Core.AOS)
{
toWeaken = 1;
}
else if (skill != SkillName.Tailoring)
{
var skillLevel = usingDeed ? m_Deed.SkillLevel : from.Skills[skill].Base;
if (skillLevel >= 90.0)
{
toWeaken = 1;
}
else if (skillLevel >= 70.0)
{
toWeaken = 2;
}
else
{
toWeaken = 3;
}
}
if (m_CraftSystem.CraftItems.SearchForSubclass(weapon.GetType()) == null && !IsSpecialWeapon(weapon))
{
number = usingDeed
? 1061136
: 1044277; // That item cannot be repaired. // You cannot repair that item with this type of repair contract.
}
else if (!weapon.IsChildOf(from.Backpack) && (!Core.ML || weapon.Parent != from))
{
number = 1044275; // The item must be in your backpack to repair it.
}
else if (!Core.AOS && weapon.PoisonCharges != 0)
{
number = 1005012; // You cannot repair an item while a caustic substance is on it.
}
else if (weapon.MaxHitPoints <= 0 || weapon.HitPoints == weapon.MaxHitPoints)
{
number = 1044281; // That item is in full repair
}
else if (weapon.MaxHitPoints <= toWeaken)
{
number = 1044278; // That item has been repaired many times, and will break if repairs are attempted again.
}
else
{
if (CheckWeaken(from, skill, weapon.HitPoints, weapon.MaxHitPoints))
{
weapon.MaxHitPoints -= toWeaken;
weapon.HitPoints = Math.Max(0, weapon.HitPoints - toWeaken);
}
if (CheckRepairDifficulty(from, skill, weapon.HitPoints, weapon.MaxHitPoints))
{
number = 1044279; // You repair the item.
m_CraftSystem.PlayCraftEffect(from);
weapon.HitPoints = weapon.MaxHitPoints;
}
else
{
number = usingDeed
? 1061137
: 1044280; // You fail to repair the item. [And the contract is destroyed]
m_CraftSystem.PlayCraftEffect(from);
}
toDelete = true;
}
}
else if (targeted is BaseArmor armor)
{
var skill = m_CraftSystem.MainSkill;
var toWeaken = 0;
if (Core.AOS)
{
toWeaken = 1;
}
else if (skill != SkillName.Tailoring)
{
var skillLevel = usingDeed ? m_Deed.SkillLevel : from.Skills[skill].Base;
if (skillLevel >= 90.0)
{
toWeaken = 1;
}
else if (skillLevel >= 70.0)
{
toWeaken = 2;
}
else
{
toWeaken = 3;
}
}
if (m_CraftSystem.CraftItems.SearchForSubclass(armor.GetType()) == null && !IsSpecialArmor(armor))
{
number = usingDeed
? 1061136
: 1044277; // That item cannot be repaired. // You cannot repair that item with this type of repair contract.
}
else if (!armor.IsChildOf(from.Backpack) && (!Core.ML || armor.Parent != from))
{
number = 1044275; // The item must be in your backpack to repair it.
}
else if (armor.MaxHitPoints <= 0 || armor.HitPoints == armor.MaxHitPoints)
{
number = 1044281; // That item is in full repair
}
else if (armor.MaxHitPoints <= toWeaken)
{
number = 1044278; // That item has been repaired many times, and will break if repairs are attempted again.
}
else
{
if (CheckWeaken(from, skill, armor.HitPoints, armor.MaxHitPoints))
{
armor.MaxHitPoints -= toWeaken;
armor.HitPoints = Math.Max(0, armor.HitPoints - toWeaken);
}
if (CheckRepairDifficulty(from, skill, armor.HitPoints, armor.MaxHitPoints))
{
number = 1044279; // You repair the item.
m_CraftSystem.PlayCraftEffect(from);
armor.HitPoints = armor.MaxHitPoints;
}
else
{
number = usingDeed
? 1061137
: 1044280; // You fail to repair the item. [And the contract is destroyed]
m_CraftSystem.PlayCraftEffect(from);
}
toDelete = true;
}
}
else if (targeted is BaseClothing clothing)
{
var skill = m_CraftSystem.MainSkill;
var toWeaken = 0;
if (Core.AOS)
{
toWeaken = 1;
}
else if (skill != SkillName.Tailoring)
{
var skillLevel = usingDeed ? m_Deed.SkillLevel : from.Skills[skill].Base;
if (skillLevel >= 90.0)
{
toWeaken = 1;
}
else if (skillLevel >= 70.0)
{
toWeaken = 2;
}
else
{
toWeaken = 3;
}
}
if (m_CraftSystem.CraftItems.SearchForSubclass(clothing.GetType()) == null &&
!IsSpecialClothing(clothing) && !(clothing is TribalMask || clothing is HornedTribalMask))
{
number = usingDeed
? 1061136
: 1044277; // That item cannot be repaired. // You cannot repair that item with this type of repair contract.
}
else if (!clothing.IsChildOf(from.Backpack) && (!Core.ML || clothing.Parent != from))
{
number = 1044275; // The item must be in your backpack to repair it.
}
else if (clothing.MaxHitPoints <= 0 || clothing.HitPoints == clothing.MaxHitPoints)
{
number = 1044281; // That item is in full repair
}
else if (clothing.MaxHitPoints <= toWeaken)
{
number = 1044278; // That item has been repaired many times, and will break if repairs are attempted again.
}
else
{
if (CheckWeaken(from, skill, clothing.HitPoints, clothing.MaxHitPoints))
{
clothing.MaxHitPoints -= toWeaken;
clothing.HitPoints = Math.Max(0, clothing.HitPoints - toWeaken);
}
if (CheckRepairDifficulty(from, skill, clothing.HitPoints, clothing.MaxHitPoints))
{
number = 1044279; // You repair the item.
m_CraftSystem.PlayCraftEffect(from);
clothing.HitPoints = clothing.MaxHitPoints;
}
else
{
number = usingDeed
? 1061137
: 1044280; // You fail to repair the item. [And the contract is destroyed]
m_CraftSystem.PlayCraftEffect(from);
}
toDelete = true;
}
}
else if (!usingDeed && targeted is BlankScroll scroll)
{
var skill = m_CraftSystem.MainSkill;
if (from.Skills[skill].Value >= 50.0)
{
scroll.Consume(1);
var deed = new RepairDeed(
RepairDeed.GetTypeFor(m_CraftSystem),
from.Skills[skill].Value,
from
);
from.AddToBackpack(deed);
number = 500442; // You create the item and put it in your backpack.
}
else
{
number = 1047005; // You must be at least apprentice level to create a repair service contract.
}
}
else if (targeted is Item)
{
number = usingDeed
? 1061136
: 1044277; // That item cannot be repaired. // You cannot repair that item with this type of repair contract.
}
else
{
number = 500426; // You can't repair that.
}
if (!usingDeed)
{
var context = m_CraftSystem.GetContext(from);
from.SendGump(new CraftGump(from, m_CraftSystem, m_Tool, number));
}
else
{
from.SendLocalizedMessage(number);
if (toDelete)
{
m_Deed.Delete();
}
}
}
}
}
}