ace/Source/ACE.Server/WorldObjects/Lock.cs

315 lines
13 KiB
C#

using System;
using System.Diagnostics;
using log4net;
using ACE.Common;
using ACE.Common.Extensions;
using ACE.Entity.Enum;
using ACE.Entity.Enum.Properties;
using ACE.Server.Entity;
using ACE.Server.Entity.Actions;
using ACE.Server.Network.GameEvent.Events;
using ACE.Server.Network.GameMessages.Messages;
namespace ACE.Server.WorldObjects
{
public enum UnlockResults : ushort
{
UnlockSuccess = 0,
PickLockFailed = 1,
IncorrectKey = 2,
AlreadyUnlocked = 3,
CannotBePicked = 4,
Open = 5
}
public interface Lock
{
UnlockResults Unlock(uint unlockerGuid, Key key, string keyCode = null);
UnlockResults Unlock(uint unlockerGuid, uint playerLockpickSkillLvl, ref int difficulty);
}
public class UnlockerHelper
{
private static readonly ILog log = LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
public static void ConsumeUnlocker(Player player, WorldObject unlocker, WorldObject target, bool success)
{
// is Sonic Screwdriver supposed to be consumed on use?
// it doesn't have a Structure, and it doesn't have PropertyBool.UnlimitedUse
var unlimitedUses = unlocker.Structure == null || (unlocker.GetProperty(PropertyBool.UnlimitedUse) ?? false);
var isLockpick = unlocker.WeenieType == WeenieType.Lockpick;
var msg = "";
if (isLockpick)
{
if (success)
msg = "You have successfully picked the lock! It is now unlocked.\n ";
else
msg = "You have failed to pick the lock. It is still locked. ";
}
else if (success)
{
msg = $"The {target.Name} has been unlocked.\n";
}
else
{
msg = $"The {target.Name} is still locked.\n";
}
if (!unlimitedUses)
{
msg += $"Your {(isLockpick ? "lockpicks" : "key")} ";
if (unlocker.Structure > 0)
unlocker.Structure--;
else
unlocker.Structure = 0;
if (unlocker.Structure == 0)
{
msg += $"{(isLockpick ? "are" : "is")} used up.";
}
else
{
msg += $"{(isLockpick ? "have" : "has")} {unlocker.Structure} use{(unlocker.Structure > 1 ? "s" : "")} left.";
}
unlocker.Value -= unlocker.StructureUnitValue;
if (unlocker.Value < 0) // fix negative value
unlocker.Value = 0;
}
player.Session.Network.EnqueueSend(new GameMessageSystemChat(msg, ChatMessageType.Broadcast));
if (!unlimitedUses)
{
if (unlocker.Structure == 0)
{
if (!player.TryConsumeFromInventoryWithNetworking(unlocker, 1))
log.Warn($"UnlockerHelper.ConsumeUnlocker: TryConsumeFromInventoryWithNetworking failed for {unlocker.Name} (0x{unlocker.Guid}:{unlocker.WeenieClassId}), used on {target.Name} (0x{target.Guid}:{target.WeenieClassId}) and used by {player.Name} (0x{player.Guid})");
}
else
{
player.Session.Network.EnqueueSend(new GameMessagePublicUpdatePropertyInt(unlocker, PropertyInt.Structure, (int)unlocker.Structure));
}
}
player.SendUseDoneEvent();
}
public static uint GetEffectiveLockpickSkill(Player player, WorldObject unlocker)
{
var lockpickSkill = player.GetCreatureSkill(Skill.Lockpick).Current;
var additiveBonus = unlocker.GetProperty(PropertyInt.LockpickMod) ?? 0;
var multiplicativeBonus = unlocker.GetProperty(PropertyFloat.LockpickMod) ?? 1.0f;
// is this really 10x bonus, or +10% bonus?
if (multiplicativeBonus > 1.0f)
multiplicativeBonus = 1.0f + multiplicativeBonus * 0.01f;
var effectiveSkill = (int)Math.Round(lockpickSkill * multiplicativeBonus + additiveBonus);
effectiveSkill = Math.Max(0, effectiveSkill);
//Console.WriteLine($"Base skill: {lockpickSkill}");
//Console.WriteLine($"Effective skill: {effectiveSkill}");
return (uint)effectiveSkill;
}
public static void UseUnlocker(Player player, WorldObject unlocker, WorldObject target)
{
ActionChain chain = new ActionChain();
chain.AddAction(player, () =>
{
if (unlocker.WeenieType == WeenieType.Lockpick &&
player.Skills[Skill.Lockpick].AdvancementClass != SkillAdvancementClass.Trained &&
player.Skills[Skill.Lockpick].AdvancementClass != SkillAdvancementClass.Specialized)
{
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.YouArentTrainedInLockpicking));
return;
}
if (target is Lock @lock)
{
UnlockResults result = UnlockResults.IncorrectKey;
var difficulty = 0;
if (unlocker.WeenieType == WeenieType.Lockpick)
{
var effectiveLockpickSkill = GetEffectiveLockpickSkill(player, unlocker);
result = @lock.Unlock(player.Guid.Full, effectiveLockpickSkill, ref difficulty);
}
else if (unlocker is Key woKey)
{
if (target is Door woDoor)
{
if (woDoor.LockCode == "") // the door isn't to be opened with keys
{
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.YouCannotLockOrUnlockThat));
return;
}
}
result = @lock.Unlock(player.Guid.Full, woKey);
}
switch (result)
{
case UnlockResults.UnlockSuccess:
if (unlocker.WeenieType == WeenieType.Lockpick)
{
// the source guid for this sound must be the player, else the sound will not play
// which differs from PicklockFail and LockSuccess being in the target sound table
player.EnqueueBroadcast(new GameMessageSound(player.Guid, Sound.Lockpicking, 1.0f));
var lockpickSkill = player.GetCreatureSkill(Skill.Lockpick);
Proficiency.OnSuccessUse(player, lockpickSkill, difficulty);
}
ConsumeUnlocker(player, unlocker, target, true);
break;
case UnlockResults.Open:
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.YouCannotLockWhatIsOpen));
break;
case UnlockResults.AlreadyUnlocked:
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.LockAlreadyUnlocked));
break;
case UnlockResults.PickLockFailed:
target.EnqueueBroadcast(new GameMessageSound(target.Guid, Sound.PicklockFail, 1.0f));
ConsumeUnlocker(player, unlocker, target, false);
break;
case UnlockResults.CannotBePicked:
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.YouCannotLockOrUnlockThat));
break;
case UnlockResults.IncorrectKey:
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.KeyDoesntFitThisLock));
break;
}
}
else
{
player.Session.Network.EnqueueSend(new GameEventUseDone(player.Session, WeenieError.YouCannotLockOrUnlockThat));
}
});
chain.EnqueueChain();
}
}
public class LockHelper
{
/// <summary>
/// Returns TRUE if wo is a lockable item that can be picked
/// </summary>
public static bool IsPickable(WorldObject wo)
{
if (!(wo is Lock)) return false;
var resistLockpick = wo.ResistLockpick;
// TODO: find out if ResistLockpick >= 9999 is a special 'unpickable' value in acclient,
// similar to ResistMagic >= 9999 being equivalent to Unenchantable?
if (resistLockpick == null || resistLockpick >= 9999 )
return false;
return true;
}
public static int? GetResistLockpick(WorldObject wo)
{
if (!(wo is Lock)) return null;
// if base ResistLockpick without enchantments is unpickable,
// do not apply enchantments
var isPickable = IsPickable(wo);
if (!isPickable)
return wo.ResistLockpick;
var resistLockpick = wo.ResistLockpick.Value;
var enchantmentMod = wo.EnchantmentManager.GetResistLockpick();
var difficulty = resistLockpick + enchantmentMod;
// minimum 0 difficulty
difficulty = Math.Max(0, difficulty);
return difficulty;
}
public static string GetLockCode(WorldObject me)
{
string myLockCode = null;
if (me is Door woDoor)
myLockCode = woDoor.LockCode;
else if (me is Chest woChest)
myLockCode = woChest.LockCode;
return myLockCode;
}
public static UnlockResults Unlock(WorldObject target, Key key, string keyCode = null)
{
if (keyCode == null)
keyCode = key?.KeyCode;
string myLockCode = GetLockCode(target);
if (myLockCode == null) return UnlockResults.IncorrectKey;
if (target.IsOpen)
return UnlockResults.Open;
// there is only 1 instance of an 'opens all' key in PY16 data, 'keysonicscrewdriver'
// which uses keyCode '_bohemund's_magic_key_'
// when LSD added the rare skeleton key (keyrarevolatileuniversal),
// they used PropertyBool.OpensAnyLock, which appears to have been used for something else in retail on Writables:
// https://github.com/ACEmulator/ACE-World-16PY/blob/master/Database/3-Core/9%20WeenieDefaults/SQL/Key/Key/09181%20Sonic%20Screwdriver.sql
// https://github.com/ACEmulator/ACE-World-16PY/search?q=OpensAnyLock
if (keyCode != null && (keyCode.Equals(myLockCode, StringComparison.OrdinalIgnoreCase) || keyCode.Equals("_bohemund's_magic_key_")) ||
key != null && key.OpensAnyLock)
{
if (!target.IsLocked)
return UnlockResults.AlreadyUnlocked;
target.IsLocked = false;
var updateProperty = new GameMessagePublicUpdatePropertyBool(target, PropertyBool.Locked, target.IsLocked);
var sound = new GameMessageSound(target.Guid, Sound.LockSuccess, 1.0f);
target.EnqueueBroadcast(updateProperty, sound);
return UnlockResults.UnlockSuccess;
}
return UnlockResults.IncorrectKey;
}
public static UnlockResults Unlock(WorldObject target, uint playerLockpickSkillLvl, ref int difficulty)
{
var isPickable = IsPickable(target);
if (!isPickable)
return UnlockResults.CannotBePicked;
int? myResistLockpick = GetResistLockpick(target);
difficulty = myResistLockpick.Value;
if (target.IsOpen)
return UnlockResults.Open;
if (!target.IsLocked)
return UnlockResults.AlreadyUnlocked;
var pickChance = SkillCheck.GetSkillChance((int)playerLockpickSkillLvl, difficulty);
#if DEBUG
Debug.WriteLine($"{pickChance.FormatChance()} chance of UnlockSuccess");
#endif
var dice = ThreadSafeRandom.Next(0.0f, 1.0f);
if (dice >= pickChance)
return UnlockResults.PickLockFailed;
target.IsLocked = false;
target.EnqueueBroadcast(new GameMessagePublicUpdatePropertyBool(target, PropertyBool.Locked, target.IsLocked));
//target.CurrentLandblock?.EnqueueBroadcastSound(target, Sound.Lockpicking);
return UnlockResults.UnlockSuccess;
}
}
}