//#define CODELOCKCOMPONENT_LOG
// Code lock state enum for specific states
enum CodeLockStage
{
NONE,
NORMAL,
SETTING,
PROTECTION_STAGE_ONE,
PROTECTION_STAGE_TWO
}
enum CodeLockUIAction
{
NONE,
CONFIRM_INPUT,
CLEAR_INPUT,
DIGIT_INPUT
}
class CodeLockComponent : Managed
{
const int MIN_LOCK_PIN_LENGTH = 4; // min amount of digits the combination needs to contain
const int MAX_LOCK_PIN_LENGTH = 6; // max amount of digits the combination can contain
private const float DEFAULT_BRUTEFORCE_TIME = 30.0;
private const float DEFAULT_BRUTEFORCE_RESET_TIME = 3600.0;
protected int m_MinLockPinLength;
protected string m_InputPIN; // combination that is dialed on lock
protected string m_LockPIN; // actual combination of the lock
protected bool m_IsLocked; // lock state
protected EntityAI m_Parent; // code lock parent entity
protected int m_DoorIndex = -1; // code lock door index for parent building if code lock belongs to a door
protected bool m_HasPIN; // code state
protected float m_BruteForceProtectionTime;
protected float m_BruteForceResetTime;
protected int m_Stage;
protected ref Timer m_BruteForceUpdateTimer;
protected ref Timer m_BruteForceResetTimer;
protected int m_WrongCodeCount;
protected bool m_HasBruteForceProtection;
protected bool m_HandleBruteForceProtection;
protected CodeLockUIAction m_UIAction;
void CodeLockComponent(notnull EntityAI parent, int minLockPinLength = MIN_LOCK_PIN_LENGTH)
{
m_Parent = parent;
m_MinLockPinLength = minLockPinLength;
m_InputPIN = "";
m_LockPIN = "";
m_IsLocked = false;
m_HasPIN = false;
m_DoorIndex = -1;
m_Stage = CodeLockStage.NONE;
m_BruteForceProtectionTime = CfgGameplayHandler.GetExternalLockProtectionTime();
m_BruteForceResetTime = CfgGameplayHandler.GetExternalLockProtectionResetTime();
if (m_BruteForceProtectionTime <= 0)
m_BruteForceProtectionTime = DEFAULT_BRUTEFORCE_TIME;
if (m_BruteForceResetTime <= 0)
m_BruteForceResetTime = DEFAULT_BRUTEFORCE_RESET_TIME;
}
void SetStage(CodeLockStage stage)
{
m_Stage = stage;
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Lock stage=%1", typename.EnumToString(CodeLockStage, m_Stage)), ErrorExSeverity.INFO);
#endif
}
int GetStage()
{
return m_Stage;
}
void SetMinimalPinCodeLength(int length)
{
m_MinLockPinLength = length;
}
bool SetInputCode(string pin, bool force = true)
{
if (!force)
{
int pinLength = pin.Length();
if (pinLength < m_MinLockPinLength || pinLength > MAX_LOCK_PIN_LENGTH)
return false;
}
m_InputPIN = pin;
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("Set input pin=" + m_InputPIN, ErrorExSeverity.INFO);
#endif
return true;
}
protected bool ValidatePIN(string pin)
{
if (pin == "")
return false;
int pinLength = pin.Length();
return pinLength >= m_MinLockPinLength && pinLength <= MAX_LOCK_PIN_LENGTH;
}
bool SetLockCode(string pin, bool sync = true)
{
if (!ValidatePIN(pin))
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Invalid PIN length: %1 (min: %2, max: %3)", pin.Length(), m_MinLockPinLength, MAX_LOCK_PIN_LENGTH), ErrorExSeverity.WARNING);
#endif
return false;
}
m_LockPIN = pin;
m_HasPIN = true;
SetStage(CodeLockStage.NORMAL);
SetLockState(true, false);
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("Set lock pin=" + m_LockPIN, ErrorExSeverity.INFO);
#endif
if (sync)
SyncAndMarkDirty();
return true;
}
void ResetPIN(bool sync = true)
{
m_LockPIN = "";
m_HasPIN = false;
SetStage(CodeLockStage.SETTING);
if (sync)
SyncAndMarkDirty();
}
void SetLockState(bool state, bool sync = true)
{
if (m_IsLocked == state)
return;
m_IsLocked = state;
if (!state)
{
StopAllTimers();
m_HasBruteForceProtection = false;
m_HandleBruteForceProtection = false;
m_WrongCodeCount = 0;
if (m_HasPIN)
{
if (m_Stage >= CodeLockStage.PROTECTION_STAGE_ONE)
{
SetStage(CodeLockStage.PROTECTION_STAGE_TWO);
}
else
{
SetStage(CodeLockStage.NORMAL);
}
}
}
if (m_DoorIndex > -1)
HandleHierarchyRootLock(state);
if (g_Game.IsServer() || !g_Game.IsMultiplayer())
{
ItemBase parentIB = ItemBase.Cast(m_Parent);
if (parentIB)
{
if (state)
{
parentIB.StartItemSoundServer(SoundConstants.ITEM_DIGITAL_LOCK_LOCK);
}
else
{
parentIB.StartItemSoundServer(SoundConstants.ITEM_DIGITAL_LOCK_UNLOCK);
}
}
}
if (sync)
SyncAndMarkDirty();
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("Set lock state=" + m_IsLocked, ErrorExSeverity.INFO);
#endif
}
protected void HandleHierarchyRootLock(bool state)
{
if (m_DoorIndex == -1)
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Error! Invalid call of HandleHierarchyRootLock without valid door index! Parent=%1 | Door index=%2", m_Parent, m_DoorIndex), ErrorExSeverity.ERROR);
#endif
return;
}
Building building = Building.Cast(m_Parent.GetHierarchyRoot());
if (!building || !building.CanUseConstruction())
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Error! Invalid hierarchy root parent for lock! Parent=%1 | Hierarchy root parent=%2", m_Parent, m_Parent.GetHierarchyRoot()), ErrorExSeverity.ERROR);
#endif
return;
}
if (m_DoorIndex > building.GetDoorCount() - 1)
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Warning! Given door index is out of bounds! Door index=%1 | Doors count=%2", m_DoorIndex, building.GetDoorCount()), ErrorExSeverity.ERROR);
#endif
return;
}
if (state)
{
building.LockDoor(m_DoorIndex);
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Locked door! Door index=%1 | Doors count=%2", m_DoorIndex, building.GetDoorCount()), ErrorExSeverity.INFO);
#endif
}
else
{
building.UnlockDoor(m_DoorIndex);
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Unlocked door! Door index=%1 | Doors count=%2", m_DoorIndex, building.GetDoorCount()), ErrorExSeverity.INFO);
#endif
}
}
void OnStoreSave(ParamsWriteContext ctx)
{
ctx.Write(m_LockPIN);
ctx.Write(m_IsLocked);
ctx.Write(m_DoorIndex);
}
bool OnStoreLoad(ParamsReadContext ctx, int version)
{
if (!ctx.Read(m_LockPIN))
{
m_LockPIN = "";
m_HasPIN = false;
return false;
}
if (!ctx.Read(m_IsLocked))
return false;
if (!ctx.Read(m_DoorIndex))
return false;
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Loaded lock pin=%1", m_LockPIN), ErrorExSeverity.INFO);
ErrorEx(string.Format("Lock pin length=%1", m_LockPIN.Length()), ErrorExSeverity.INFO);
#endif
if (m_LockPIN != "" && m_LockPIN.Length() >= m_MinLockPinLength)
{
m_HasPIN = true;
}
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx(string.Format("Has pin=%1", m_HasPIN), ErrorExSeverity.INFO);
ErrorEx(string.Format("Is locked=%1", m_IsLocked), ErrorExSeverity.INFO);
#endif
SetLockState(m_IsLocked, false);
return true;
}
protected void CheckUIActionServer(CodeLockUIAction uiAction)
{
if (g_Game.IsServer() || !g_Game.IsMultiplayer())
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("UI Action=" + typename.EnumToString(CodeLockUIAction, uiAction), ErrorExSeverity.INFO);
#endif
m_UIAction = uiAction;
SyncAndMarkDirty();
g_Game.GetCallQueue(CALL_CATEGORY_SYSTEM).CallLater(ResetUIActionServer, 100);
}
}
protected void ResetUIActionServer()
{
m_UIAction = CodeLockUIAction.NONE;
SyncAndMarkDirty();
}
void CheckUIActionClient()
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("UI Action=" + typename.EnumToString(CodeLockUIAction, m_UIAction), ErrorExSeverity.INFO);
#endif
ItemBase parentIB = ItemBase.Cast(m_Parent);
switch(m_UIAction)
{
case CodeLockUIAction.CLEAR_INPUT:
{
parentIB.StartItemSoundClient(SoundConstants.ITEM_DIGITAL_LOCK_CLEAR_INPUT);
break;
}
case CodeLockUIAction.CONFIRM_INPUT:
{
parentIB.StartItemSoundClient(SoundConstants.ITEM_DIGITAL_LOCK_CONFIRM_INPUT);
break;
}
case CodeLockUIAction.DIGIT_INPUT:
{
parentIB.StartItemSoundClient(SoundConstants.ITEM_DIGITAL_LOCK_DIGIT_INPUT);
break;
}
}
m_UIAction = CodeLockUIAction.NONE;
}
// Called on server on RPC request
protected void CheckCodeRequest(string pin, PlayerIdentity identity)
{
bool success;
if (g_Game.IsServer() || !g_Game.IsMultiplayer())
{
if (!SetInputCode(pin))
return;
if (m_HasPIN)
{
// Check lock state and see if pin is valid
if (m_IsLocked)
{
#ifdef CODELOCKCOMPONENT_LOG
ErrorEx("Lock pin=" + GetInputPIN(), ErrorExSeverity.INFO);
ErrorEx("Actual pin=" + GetLockPIN(), ErrorExSeverity.INFO);
#endif
if (m_InputPIN == m_LockPIN)
{
// Lock is locked and given pin is same as lock pin. Unlock lock
// SetLockState will handle clearing brute force protection
SetLockState(false, false);
success = true;
}
else
{
// Handle brute force protection
ItemBase parentIB = ItemBase.Cast(m_Parent);
if (parentIB)
parentIB.StartItemSoundServer(SoundConstants.ITEM_DIGITAL_LOCK_ERROR);
HandleBruteForceProtectionServer();
success = false;
}
}
}
else
{
// Lock has no pin. Set pin!
SetLockCode(pin, false);
success = true;
}
}
if (g_Game.IsDedicatedServer())
{
m_Parent.SetSynchDirty();
}
// Force visual update after setting code
DigitalCodeLock codeLock;
if (Class.CastTo(codeLock, m_Parent) && codeLock.GetVisualManager())
{
codeLock.GetVisualManager().UpdateVisuals();
}
Param1<bool> response = new Param1<bool>(success);
GetGame().RPCSingleParam(m_Parent, ERPCs.RPC_CODE_LOCK_RESPONSE, response, true, identity);
}
protected void HandleBruteForceProtectionServer()
{
if (m_HasBruteForceProtection)
return;
m_WrongCodeCount++;
StopAllTimers();
if (ShouldTriggerProtectionStage())
{
ActivateBruteForceProtection(m_BruteForceProtectionTime, m_BruteForceResetTime);
}
}
private void ActivateBruteForceProtection(float protectionTime, float protectionResetTime)
{
if (m_Stage < CodeLockStage.PROTECTION_STAGE_ONE)
{
SetStage(CodeLockStage.PROTECTION_STAGE_ONE);
}
else if (m_Stage == CodeLockStage.PROTECTION_STAGE_ONE)
{
SetStage(CodeLockStage.PROTECTION_STAGE_TWO);
}
m_HasBruteForceProtection = true;
m_HandleBruteForceProtection = true;
m_BruteForceUpdateTimer = new Timer(CALL_CATEGORY_SYSTEM);
m_BruteForceUpdateTimer.Run(protectionTime, this, "CheckBruteForceState");
m_BruteForceResetTimer = new Timer(CALL_CATEGORY_SYSTEM);
m_BruteForceResetTimer.Run(protectionResetTime, this, "CheckBruteForceReset");
m_WrongCodeCount = 0;
UpdateParentVisuals();
if (g_Game.IsDedicatedServer())
m_Parent.SetSynchDirty();
}
void StopAllTimers()
{
if (m_BruteForceUpdateTimer)
{
m_BruteForceUpdateTimer.Stop();
m_BruteForceUpdateTimer = NULL;
}
if (m_BruteForceResetTimer)
{
m_BruteForceResetTimer.Stop();
m_BruteForceResetTimer = NULL;
}
}
private bool ShouldTriggerProtectionStage()
{
switch(m_Stage)
{
case CodeLockStage.NORMAL:
{
return m_WrongCodeCount >= CfgGameplayHandler.GetExternalLockProtectionCountStageOne();
}
case CodeLockStage.PROTECTION_STAGE_ONE:
case CodeLockStage.PROTECTION_STAGE_TWO:
{
return m_WrongCodeCount >= CfgGameplayHandler.GetExternalLockProtectionCountStageTwo();
}
}
return false;
}
protected void CheckBruteForceState()
{
if (m_BruteForceUpdateTimer)
{
m_BruteForceUpdateTimer.Stop();
m_BruteForceUpdateTimer = NULL;
}
m_HandleBruteForceProtection = false;
m_HasBruteForceProtection = false;
SyncAndMarkDirty();
}
protected void CheckBruteForceReset()
{
if (m_BruteForceResetTimer)
{
m_BruteForceResetTimer.Stop();
m_BruteForceResetTimer = NULL;
}
if (m_BruteForceUpdateTimer)
{
m_BruteForceUpdateTimer.Stop();
m_BruteForceUpdateTimer = NULL;
}
m_HasBruteForceProtection = false;
m_HandleBruteForceProtection = false;
m_WrongCodeCount = 0;
if (m_HasPIN)
SetStage(CodeLockStage.NORMAL);
SyncAndMarkDirty();
}
void OnParentDetached(bool sync = true)
{
m_DoorIndex = -1;
OnParentReset(false);
if (sync)
SyncAndMarkDirty();
}
void OnParentReset(bool sync = true)
{
SetLockState(false, false);
ResetPIN(false);
m_WrongCodeCount = 0;
m_HasBruteForceProtection = false;
SetStage(CodeLockStage.NONE);
if (m_BruteForceUpdateTimer)
{
m_BruteForceUpdateTimer.Stop();
m_BruteForceUpdateTimer = NULL;
}
if (m_BruteForceResetTimer)
{
m_BruteForceResetTimer.Stop();
m_BruteForceResetTimer = NULL;
}
if (sync)
SyncAndMarkDirty();
}
void OnRPC(PlayerIdentity sender, int type, ParamsReadContext ctx)
{
switch (type)
{
case ERPCs.RPC_CODE_LOCK_REQUEST:
{
Param1<string> code = new Param1<string>("");
if (!ctx.Read(code))
return;
string dialedCode = code.param1;
CheckCodeRequest(dialedCode, sender);
SetInputCode("", true);
break;
}
case ERPCs.RPC_CODE_LOCK_RESPONSE:
{
Param1<bool> response = new Param1<bool>(false);
if (!ctx.Read(response))
return;
DigitalCodeLockUIBase codeLockUI = DigitalCodeLockUIBase.Cast(GetGame().GetUIManager().FindMenu(MENU_DIGITAL_CODELOCK));
if (codeLockUI)
{
bool codeResponse = response.param1;
codeLockUI.CodeResponse(codeResponse);
SetInputCode("", true);
}
break;
}
case ERPCs.RPC_CODE_LOCK_UI_ACTION:
{
Param1<int> action = new Param1<int>(CodeLockUIAction.NONE);
if (!ctx.Read(action))
return;
CodeLockUIAction uiAction = action.param1;
if (uiAction != CodeLockUIAction.NONE)
CheckUIActionServer(uiAction);
break;
}
}
}
protected void UpdateParentVisuals()
{
if (!m_Parent)
return;
DigitalCodeLock codeLock;
if (Class.CastTo(codeLock, m_Parent) && codeLock.GetVisualManager())
{
codeLock.GetVisualManager().MarkDirty();
codeLock.GetVisualManager().UpdateVisuals();
}
}
protected void SyncAndMarkDirty()
{
// Always update parent visuals (will be marked dirty or updated depending on context)
UpdateParentVisuals();
if (g_Game.IsDedicatedServer())
m_Parent.SetSynchDirty();
}
EntityAI GetParent()
{
return m_Parent;
}
void SetDoorIndex(int index)
{
m_DoorIndex = index;
}
int GetDoorIndex()
{
return m_DoorIndex;
}
//! ToDo: Door parent position handling?!
vector GetPosition()
{
return m_Parent.GetPosition();
}
string GetInputPIN()
{
return m_InputPIN;
}
bool HasPIN()
{
return m_HasPIN;
}
string GetLockPIN()
{
return m_LockPIN;
}
bool IsLocked()
{
return m_IsLocked;
}
bool HasParent()
{
return m_Parent || m_DoorIndex > -1;
}
bool HasBruteForceProtection()
{
return m_HasBruteForceProtection;
}
bool ShouldHandleBruteForce()
{
return m_HandleBruteForceProtection;
}
int GetWrongCodeCount()
{
return m_WrongCodeCount;
}
int GetMinLockPINLength()
{
return m_MinLockPinLength;
}
CodeLockUIAction GetUIAction()
{
return m_UIAction;
}
}
class CodeLockComponentStatic : CodeLockComponent
{
const string SELECTION_INTERACT = "codelockinteract"; //! used on static objects
override vector GetPosition()
{
vector position = m_Parent.GetSelectionBasePositionLS(SELECTION_INTERACT);
return m_Parent.ModelToWorld(position);
}
}