Merge pull request 'issue #9: add classic sash gameplay support' (#2) from issue-9-sashes into main
Some checks failed
build / Linux asan (push) Has been cancelled
build / Linux release (push) Has been cancelled
build / FreeBSD build (push) Has been cancelled

Reviewed-on: #2
This commit was merged in pull request #2.
This commit is contained in:
2026-04-16 23:04:52 +02:00
11 changed files with 1133 additions and 2 deletions

View File

@@ -113,6 +113,7 @@ enum ECostumeSubTypes
{
COSTUME_BODY = ARMOR_BODY, // [중요!!] ECostumeSubTypes enum value는 종류별로 EArmorSubTypes의 그것과 같아야 함.
COSTUME_HAIR = ARMOR_HEAD, // 이는 코스츔 아이템에 추가 속성을 붙이겠다는 사업부의 요청에 따라서 기존 로직을 활용하기 위함임.
COSTUME_SASH = ARMOR_NUM_TYPES,
COSTUME_NUM_TYPES,
};
@@ -325,6 +326,7 @@ enum EItemWearableFlag
WEARABLE_COSTUME_BODY = (1 << 12),
WEARABLE_COSTUME_HAIR = (1 << 13),
WEARABLE_BELT = (1 << 14),
WEARABLE_COSTUME_SASH = (1 << 15),
};
enum ELimitTypes

View File

@@ -123,6 +123,7 @@ enum EWearPositions
WEAR_RING2, // 22 : 신규 반지슬롯2 (오른쪽)
WEAR_BELT, // 23 : 신규 벨트슬롯
WEAR_COSTUME_SASH, // 24
WEAR_MAX = 32 //
};
@@ -233,6 +234,7 @@ enum EParts
PART_WEAPON,
PART_HEAD,
PART_HAIR,
PART_ACCE,
PART_MAX_NUM,
PART_WEAPON_SUB,

View File

@@ -122,7 +122,7 @@ int get_Item_SubType_Value(int type_value, string inputString)
"RESOURCE_STONE", "RESOURCE_METIN", "RESOURCE_ORE" };
static string arSub16[] = { "UNIQUE_NONE", "UNIQUE_BOOK", "UNIQUE_SPECIAL_RIDE", "UNIQUE_3", "UNIQUE_4", "UNIQUE_5",
"UNIQUE_6", "UNIQUE_7", "UNIQUE_8", "UNIQUE_9", "USE_SPECIAL"};
static string arSub28[] = { "COSTUME_BODY", "COSTUME_HAIR" };
static string arSub28[] = { "COSTUME_BODY", "COSTUME_HAIR", "COSTUME_SASH" };
static string arSub29[] = { "DS_SLOT1", "DS_SLOT2", "DS_SLOT3", "DS_SLOT4", "DS_SLOT5", "DS_SLOT6" };
static string arSub31[] = { "EXTRACT_DRAGON_SOUL", "EXTRACT_DRAGON_HEART" };

View File

@@ -214,6 +214,9 @@ void CHARACTER::Initialize()
m_pkPoisonEvent = NULL;
m_pkFireEvent = NULL;
m_pkCheckSpeedHackEvent = NULL;
m_pkAutoPickupEvent = NULL;
m_pkStoneQueueEvent = NULL;
m_pkSwitchbotEvent = NULL;
m_speed_hack_count = 0;
m_pkAffectEvent = NULL;
@@ -304,6 +307,20 @@ void CHARACTER::Initialize()
// REFINE_NPC
m_dwRefineNPCVID = 0;
// END_OF_REFINE_NPC
m_bStoneQueueScrollCell = 0;
m_iStoneQueueCurrentIndex = 0;
m_iStoneQueueSuccessCount = 0;
m_iStoneQueueFailCount = 0;
m_vecStoneQueueSlots.clear();
for (size_t i = 0; i < m_switchbotSlots.size(); ++i)
{
m_switchbotSlots[i].active = false;
m_switchbotSlots[i].itemCell = 0;
m_switchbotSlots[i].attrType = 0;
m_switchbotSlots[i].minValue = 0;
m_switchbotSlots[i].attempts = 0;
}
m_iSwitchbotSpeedIndex = 1;
m_dwPolymorphRace = 0;
@@ -561,6 +578,9 @@ void CHARACTER::Destroy()
// MINING
event_cancel(&m_pkMiningEvent);
// END_OF_MINING
event_cancel(&m_pkAutoPickupEvent);
event_cancel(&m_pkStoneQueueEvent);
event_cancel(&m_pkSwitchbotEvent);
for (itertype(m_mapMobSkillEvent) it = m_mapMobSkillEvent.begin(); it != m_mapMobSkillEvent.end(); ++it)
@@ -953,6 +973,7 @@ void CHARACTER::EncodeInsertPacket(LPENTITY entity)
addPacket.awPart[CHR_EQUIPPART_WEAPON] = GetPart(PART_WEAPON);
addPacket.awPart[CHR_EQUIPPART_HEAD] = GetPart(PART_HEAD);
addPacket.awPart[CHR_EQUIPPART_HAIR] = GetPart(PART_HAIR);
addPacket.awPart[CHR_EQUIPPART_ACCE] = GetPart(PART_ACCE);
addPacket.bPKMode = m_bPKMode;
addPacket.dwMountVnum = GetMountVnum();
@@ -1090,6 +1111,7 @@ void CHARACTER::UpdatePacket()
pack.awPart[CHR_EQUIPPART_WEAPON] = GetPart(PART_WEAPON);
pack.awPart[CHR_EQUIPPART_HEAD] = GetPart(PART_HEAD);
pack.awPart[CHR_EQUIPPART_HAIR] = GetPart(PART_HAIR);
pack.awPart[CHR_EQUIPPART_ACCE] = GetPart(PART_ACCE);
pack.bMovingSpeed = GetLimitPoint(POINT_MOV_SPEED);
pack.bAttackSpeed = GetLimitPoint(POINT_ATT_SPEED);
@@ -1788,6 +1810,7 @@ void CHARACTER::SetPlayerProto(const TPlayerTable * t)
m_pointsInstant.bBasePart = t->part_base;
SetPart(PART_HAIR, t->parts[PART_HAIR]);
SetPart(PART_ACCE, t->parts[PART_ACCE]);
m_points.iRandomHP = t->sRandomHP;
m_points.iRandomSP = t->sRandomSP;
@@ -2290,6 +2313,7 @@ void CHARACTER::ComputePoints()
SetPart(PART_WEAPON, GetOriginalPart(PART_WEAPON));
SetPart(PART_HEAD, GetOriginalPart(PART_HEAD));
SetPart(PART_HAIR, GetOriginalPart(PART_HAIR));
SetPart(PART_ACCE, GetOriginalPart(PART_ACCE));
SetPoint(POINT_PARTY_ATTACKER_BONUS, lAttackerBonus);
SetPoint(POINT_PARTY_TANKER_BONUS, lTankerBonus);
@@ -4447,6 +4471,9 @@ WORD CHARACTER::GetOriginalPart(BYTE bPartPos) const
case PART_HAIR:
return GetPart(PART_HAIR);
case PART_ACCE:
return GetPart(PART_ACCE);
default:
return 0;
}
@@ -6476,6 +6503,493 @@ void CHARACTER::SetBlockModeForce(BYTE bFlag)
ChatPacket(CHAT_TYPE_COMMAND, "setblockmode %d", m_pointsInstant.bBlockMode);
}
bool CHARACTER::IsAutoPickupEnabled() const
{
return GetQuestFlag("autopickup.enabled") > 0;
}
int CHARACTER::GetAutoPickupFilterMode() const
{
return GetQuestFlag("autopickup.mode") > 0 ? 1 : 0;
}
int CHARACTER::GetAutoPickupFilterMask() const
{
return GetQuestFlag("autopickup.mask") & 31;
}
bool CHARACTER::HasAutoPickupVip() const
{
return GetPremiumRemainSeconds(PREMIUM_AUTOLOOT) > 0 || IsEquipUniqueGroup(UNIQUE_GROUP_AUTOLOOT);
}
int CHARACTER::GetAutoPickupRadius() const
{
return HasAutoPickupVip() ? 300 : 220;
}
void CHARACTER::SendAutoPickupState()
{
ChatPacket(CHAT_TYPE_COMMAND,
"AutoPickupState %d %d %d %d",
IsAutoPickupEnabled() ? 1 : 0,
GetAutoPickupFilterMode(),
GetAutoPickupFilterMask(),
HasAutoPickupVip() ? 1 : 0);
}
void CHARACTER::StopAutoPickupEvent()
{
event_cancel(&m_pkAutoPickupEvent);
}
void CHARACTER::RefreshAutoPickup()
{
if (GetQuestFlag("autopickup.initialized") <= 0)
{
SetQuestFlag("autopickup.initialized", 1);
SetQuestFlag("autopickup.mode", 0);
SetQuestFlag("autopickup.mask", 31);
}
if (IsAutoPickupEnabled())
StartAutoPickupEvent();
else
StopAutoPickupEvent();
SendAutoPickupState();
}
namespace
{
const int kStoneQueueTick = PASSES_PER_SEC(1);
const int kSwitchbotSlotMax = 5;
const int kSwitchbotSpeedSeconds[] = { 3, 2, 1 };
enum
{
HYUNIRON_CHN = 1,
MUSIN_SCROLL = 3,
BDRAGON_SCROLL = 6,
};
int GetStoneQueueRefineType(LPITEM scrollItem, LPITEM targetItem)
{
if (!scrollItem || !targetItem)
return -1;
if (scrollItem->GetType() != ITEM_USE || scrollItem->GetSubType() != USE_TUNING)
return -1;
if (scrollItem->GetValue(0) == MUSIN_SCROLL)
return REFINE_TYPE_MUSIN;
if (scrollItem->GetValue(0) == HYUNIRON_CHN)
return REFINE_TYPE_HYUNIRON;
if (scrollItem->GetValue(0) == BDRAGON_SCROLL)
{
if (targetItem->GetRefineSet() != 702)
return -1;
return REFINE_TYPE_BDRAGON;
}
if (targetItem->GetRefineSet() == 501)
return -1;
return REFINE_TYPE_SCROLL;
}
EVENTFUNC(stone_queue_event)
{
char_event_info* info = dynamic_cast<char_event_info*>(event->info);
if (!info)
{
sys_err("stone_queue_event> <Factor> Null pointer");
return 0;
}
LPCHARACTER ch = info->ch;
if (!ch)
return 0;
return ch->ProcessStoneQueueTick() ? kStoneQueueTick : 0;
}
bool IsSwitchbotScroll(LPITEM item)
{
if (!item || item->GetType() != ITEM_USE)
return false;
return item->GetSubType() == USE_CHANGE_ATTRIBUTE || item->GetSubType() == USE_CHANGE_ATTRIBUTE2;
}
bool IsSwitchbotEligibleItem(LPITEM item)
{
if (!item)
return false;
if (item->IsEquipped() || item->IsExchanging())
return false;
if (item->GetType() == ITEM_COSTUME)
return false;
if (item->GetAttributeSetIndex() == -1 || item->GetAttributeCount() == 0)
return false;
return item->GetType() == ITEM_WEAPON || item->GetType() == ITEM_ARMOR;
}
bool IsSwitchbotTargetReached(LPITEM item, BYTE attrType, short minValue)
{
if (!item || attrType == 0 || minValue <= 0)
return false;
for (int i = 0; i < ITEM_ATTRIBUTE_MAX_NUM; ++i)
{
if (item->GetAttributeType(i) == attrType && item->GetAttributeValue(i) >= minValue)
return true;
}
return false;
}
EVENTFUNC(switchbot_event)
{
char_event_info* info = dynamic_cast<char_event_info*>(event->info);
if (!info)
{
sys_err("switchbot_event> <Factor> Null pointer");
return 0;
}
LPCHARACTER ch = info->ch;
if (!ch)
return 0;
return ch->ProcessSwitchbotTick() ? PASSES_PER_SEC(ch->GetSwitchbotSpeedSeconds()) : 0;
}
}
int CHARACTER::GetStoneQueueMax() const
{
return GetPremiumRemainSeconds(PREMIUM_GOLD) > 0 ? 8 : 3;
}
void CHARACTER::SendStoneQueueState()
{
ChatPacket(CHAT_TYPE_COMMAND,
"StoneQueueState %d %d %d %d %d",
GetStoneQueueMax(),
m_pkStoneQueueEvent ? 1 : 0,
m_iStoneQueueCurrentIndex,
m_iStoneQueueSuccessCount,
m_iStoneQueueFailCount);
}
void CHARACTER::CancelStoneQueue(bool resetResults)
{
event_cancel(&m_pkStoneQueueEvent);
m_vecStoneQueueSlots.clear();
m_bStoneQueueScrollCell = 0;
if (resetResults)
{
m_iStoneQueueCurrentIndex = 0;
m_iStoneQueueSuccessCount = 0;
m_iStoneQueueFailCount = 0;
}
SendStoneQueueState();
}
void CHARACTER::StartStoneQueue(const std::vector<BYTE>& slots, BYTE scrollCell)
{
CancelStoneQueue();
m_vecStoneQueueSlots = slots;
m_bStoneQueueScrollCell = scrollCell;
char_event_info* info = AllocEventInfo<char_event_info>();
info->ch = this;
m_pkStoneQueueEvent = event_create(stone_queue_event, info, 1);
SendStoneQueueState();
}
void CHARACTER::OnStoneQueueRefineResult(bool success)
{
if (!m_pkStoneQueueEvent)
return;
if (success)
++m_iStoneQueueSuccessCount;
else
++m_iStoneQueueFailCount;
++m_iStoneQueueCurrentIndex;
if (m_iStoneQueueCurrentIndex >= static_cast<int>(m_vecStoneQueueSlots.size()))
{
ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Stone queue finished. Success: %d Fail: %d"), m_iStoneQueueSuccessCount, m_iStoneQueueFailCount);
CancelStoneQueue(false);
return;
}
SendStoneQueueState();
}
bool CHARACTER::ProcessStoneQueueTick()
{
if (!IsPC() || IsDead())
{
CancelStoneQueue();
return false;
}
if (m_iStoneQueueCurrentIndex >= static_cast<int>(m_vecStoneQueueSlots.size()))
{
CancelStoneQueue();
return false;
}
LPITEM scrollItem = GetInventoryItem(m_bStoneQueueScrollCell);
if (!scrollItem)
{
CancelStoneQueue();
return false;
}
const BYTE targetSlot = m_vecStoneQueueSlots[m_iStoneQueueCurrentIndex];
LPITEM targetItem = GetInventoryItem(targetSlot);
const int refineType = GetStoneQueueRefineType(scrollItem, targetItem);
if (!targetItem || targetItem->GetType() != ITEM_METIN || refineType < 0)
{
OnStoneQueueRefineResult(false);
return m_pkStoneQueueEvent != NULL;
}
SetRefineMode(m_bStoneQueueScrollCell);
SetRefineTime();
if (!DoRefineWithScroll(targetItem, refineType))
{
OnStoneQueueRefineResult(false);
return m_pkStoneQueueEvent != NULL;
}
return true;
}
int CHARACTER::GetSwitchbotActiveCount() const
{
int activeCount = 0;
for (size_t i = 0; i < m_switchbotSlots.size(); ++i)
{
if (m_switchbotSlots[i].active)
++activeCount;
}
return activeCount;
}
int CHARACTER::GetSwitchbotSpeedSeconds() const
{
const int speedIndexMax = sizeof(kSwitchbotSpeedSeconds) / sizeof(kSwitchbotSpeedSeconds[0]);
const int safeIndex = MINMAX(0, m_iSwitchbotSpeedIndex, speedIndexMax - 1);
return kSwitchbotSpeedSeconds[safeIndex];
}
void CHARACTER::SendSwitchbotState()
{
int switchItemCount = 0;
for (int i = 0; i < INVENTORY_MAX_NUM; ++i)
{
LPITEM item = GetInventoryItem(i);
if (!IsSwitchbotScroll(item))
continue;
switchItemCount += item->GetCount();
}
const int activeCount = GetSwitchbotActiveCount();
const int etaSeconds = activeCount > 0 ? (switchItemCount * GetSwitchbotSpeedSeconds()) / activeCount : 0;
ChatPacket(CHAT_TYPE_COMMAND, "SwitchbotState %d %d %d %d", m_iSwitchbotSpeedIndex, activeCount, switchItemCount, etaSeconds);
for (size_t i = 0; i < m_switchbotSlots.size(); ++i)
{
const TSwitchbotSlotState& slot = m_switchbotSlots[i];
ChatPacket(
CHAT_TYPE_COMMAND,
"SwitchbotSlot %d %d %d %d %d %d",
static_cast<int>(i),
slot.active ? 1 : 0,
slot.itemCell,
slot.attrType,
slot.minValue,
slot.attempts);
}
}
void CHARACTER::SetSwitchbotSpeed(int speedIndex)
{
const int speedIndexMax = sizeof(kSwitchbotSpeedSeconds) / sizeof(kSwitchbotSpeedSeconds[0]);
m_iSwitchbotSpeedIndex = MINMAX(0, speedIndex, speedIndexMax - 1);
if (m_pkSwitchbotEvent)
{
event_cancel(&m_pkSwitchbotEvent);
if (GetSwitchbotActiveCount() > 0)
{
char_event_info* info = AllocEventInfo<char_event_info>();
info->ch = this;
m_pkSwitchbotEvent = event_create(switchbot_event, info, PASSES_PER_SEC(GetSwitchbotSpeedSeconds()));
}
}
SendSwitchbotState();
}
bool CHARACTER::StartSwitchbotSlot(int switchbotSlot, BYTE itemCell, BYTE attrType, short minValue)
{
if (switchbotSlot < 0 || switchbotSlot >= kSwitchbotSlotMax)
return false;
LPITEM item = GetInventoryItem(itemCell);
if (!IsSwitchbotEligibleItem(item))
return false;
if (attrType == 0 || minValue <= 0)
return false;
TSwitchbotSlotState& slot = m_switchbotSlots[switchbotSlot];
slot.active = true;
slot.itemCell = itemCell;
slot.attrType = attrType;
slot.minValue = minValue;
slot.attempts = 0;
if (!m_pkSwitchbotEvent)
{
char_event_info* info = AllocEventInfo<char_event_info>();
info->ch = this;
m_pkSwitchbotEvent = event_create(switchbot_event, info, PASSES_PER_SEC(GetSwitchbotSpeedSeconds()));
}
SendSwitchbotState();
return true;
}
void CHARACTER::StopSwitchbotSlot(int switchbotSlot, bool clearConfig)
{
if (switchbotSlot < 0 || switchbotSlot >= kSwitchbotSlotMax)
return;
TSwitchbotSlotState& slot = m_switchbotSlots[switchbotSlot];
slot.active = false;
if (clearConfig)
{
slot.itemCell = 0;
slot.attrType = 0;
slot.minValue = 0;
slot.attempts = 0;
}
if (GetSwitchbotActiveCount() == 0)
event_cancel(&m_pkSwitchbotEvent);
SendSwitchbotState();
}
void CHARACTER::StopAllSwitchbotSlots(bool clearConfig)
{
for (size_t i = 0; i < m_switchbotSlots.size(); ++i)
{
m_switchbotSlots[i].active = false;
if (clearConfig)
{
m_switchbotSlots[i].itemCell = 0;
m_switchbotSlots[i].attrType = 0;
m_switchbotSlots[i].minValue = 0;
m_switchbotSlots[i].attempts = 0;
}
}
event_cancel(&m_pkSwitchbotEvent);
SendSwitchbotState();
}
bool CHARACTER::ProcessSwitchbotTick()
{
if (!IsPC() || IsDead())
{
StopAllSwitchbotSlots();
return false;
}
bool stateChanged = false;
for (size_t i = 0; i < m_switchbotSlots.size(); ++i)
{
TSwitchbotSlotState& slot = m_switchbotSlots[i];
if (!slot.active)
continue;
LPITEM targetItem = GetInventoryItem(slot.itemCell);
if (!IsSwitchbotEligibleItem(targetItem))
{
slot.active = false;
stateChanged = true;
continue;
}
if (IsSwitchbotTargetReached(targetItem, slot.attrType, slot.minValue))
{
slot.active = false;
stateChanged = true;
ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Switchbot slot %d finished."), static_cast<int>(i) + 1);
continue;
}
int switchItemCell = -1;
for (int cell = 0; cell < INVENTORY_MAX_NUM; ++cell)
{
LPITEM switchItem = GetInventoryItem(cell);
if (!IsSwitchbotScroll(switchItem))
continue;
switchItemCell = cell;
break;
}
if (switchItemCell < 0)
{
ChatPacket(CHAT_TYPE_INFO, LC_TEXT("No switch items left."));
StopAllSwitchbotSlots();
return false;
}
if (!UseItem(TItemPos(INVENTORY, switchItemCell), TItemPos(INVENTORY, slot.itemCell)))
{
slot.active = false;
stateChanged = true;
continue;
}
++slot.attempts;
stateChanged = true;
}
if (GetSwitchbotActiveCount() == 0)
event_cancel(&m_pkSwitchbotEvent);
if (stateChanged)
SendSwitchbotState();
return m_pkSwitchbotEvent != NULL;
}
bool CHARACTER::IsGuardNPC() const
{
return IsNPC() && (GetRaceNum() == 11000 || GetRaceNum() == 11002 || GetRaceNum() == 11004);

View File

@@ -1,6 +1,7 @@
#ifndef __INC_METIN_II_CHAR_H__
#define __INC_METIN_II_CHAR_H__
#include <array>
#include <unordered_map>
#include "common/stl.h"
@@ -749,6 +750,30 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider
void SetBlockMode(BYTE bFlag);
void SetBlockModeForce(BYTE bFlag);
bool IsBlockMode(BYTE bFlag) const { return (m_pointsInstant.bBlockMode & bFlag)?true:false; }
void SendAutoPickupState();
void RefreshAutoPickup();
void StartAutoPickupEvent();
void StopAutoPickupEvent();
bool IsAutoPickupEnabled() const;
int GetAutoPickupFilterMode() const;
int GetAutoPickupFilterMask() const;
bool HasAutoPickupVip() const;
int GetAutoPickupRadius() const;
bool ShouldAutoPickupItem(LPITEM item) const;
int GetStoneQueueMax() const;
void SendStoneQueueState();
void StartStoneQueue(const std::vector<BYTE>& slots, BYTE scrollCell);
void CancelStoneQueue(bool resetResults = true);
void OnStoneQueueRefineResult(bool success);
bool ProcessStoneQueueTick();
int GetSwitchbotActiveCount() const;
int GetSwitchbotSpeedSeconds() const;
void SendSwitchbotState();
void SetSwitchbotSpeed(int speedIndex);
bool StartSwitchbotSlot(int switchbotSlot, BYTE itemCell, BYTE attrType, short minValue);
void StopSwitchbotSlot(int switchbotSlot, bool clearConfig = false);
void StopAllSwitchbotSlots(bool clearConfig = false);
bool ProcessSwitchbotTick();
bool IsPolymorphed() const { return m_dwPolymorphRace>0; }
bool IsPolyMaintainStat() const { return m_bPolyMaintainStat; } // 이전 스텟을 유지하는 폴리모프.
@@ -1169,6 +1194,21 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider
int m_iRefineAdditionalCell;
bool m_bUnderRefine;
DWORD m_dwRefineNPCVID;
std::vector<BYTE> m_vecStoneQueueSlots;
BYTE m_bStoneQueueScrollCell;
int m_iStoneQueueCurrentIndex;
int m_iStoneQueueSuccessCount;
int m_iStoneQueueFailCount;
struct TSwitchbotSlotState
{
bool active;
BYTE itemCell;
BYTE attrType;
short minValue;
int attempts;
};
std::array<TSwitchbotSlotState, 5> m_switchbotSlots;
int m_iSwitchbotSpeedIndex;
public:
////////////////////////////////////////////////////////////////////////////////////////
@@ -1774,6 +1814,9 @@ class CHARACTER : public CEntity, public CFSM, public CHorseRider
LPEVENT m_pkWarpEvent;
LPEVENT m_pkCheckSpeedHackEvent;
LPEVENT m_pkDestroyWhenIdleEvent;
LPEVENT m_pkAutoPickupEvent;
LPEVENT m_pkStoneQueueEvent;
LPEVENT m_pkSwitchbotEvent;
LPEVENT m_pkPetSystemUpdateEvent;
bool IsWarping() const { return m_pkWarpEvent ? true : false; }

View File

@@ -46,6 +46,7 @@
namespace
{
constexpr int kRefineNpcMaxDistance = 2000;
const int kAutoPickupTick = passes_per_sec / 4;
constexpr DWORD kPickupPatternWindowMs = 8 * 60 * 1000;
constexpr int kPickupPatternMinSamples = 12;
constexpr int kPickupPatternMaxIntervalSpreadMs = 180;
@@ -102,6 +103,117 @@ namespace
return true;
}
enum EAutoPickupFilterBits
{
AUTO_PICKUP_FILTER_WEAPON = 1 << 0,
AUTO_PICKUP_FILTER_ARMOR = 1 << 1,
AUTO_PICKUP_FILTER_YANG = 1 << 2,
AUTO_PICKUP_FILTER_STONE = 1 << 3,
AUTO_PICKUP_FILTER_MATERIAL = 1 << 4,
};
int GetAutoPickupFilterBit(LPITEM item)
{
if (!item)
return 0;
switch (item->GetType())
{
case ITEM_WEAPON:
return AUTO_PICKUP_FILTER_WEAPON;
case ITEM_ARMOR:
return AUTO_PICKUP_FILTER_ARMOR;
case ITEM_ELK:
return AUTO_PICKUP_FILTER_YANG;
case ITEM_METIN:
return AUTO_PICKUP_FILTER_STONE;
case ITEM_MATERIAL:
return AUTO_PICKUP_FILTER_MATERIAL;
case ITEM_RESOURCE:
if (item->GetSubType() == RESOURCE_STONE || item->GetSubType() == RESOURCE_METIN)
return AUTO_PICKUP_FILTER_STONE;
return AUTO_PICKUP_FILTER_MATERIAL;
}
return 0;
}
struct AutoPickupScan
{
LPCHARACTER ch;
LPITEM bestItem;
int bestDistance;
int radius;
AutoPickupScan(LPCHARACTER character, int maxDistance)
: ch(character), bestItem(NULL), bestDistance(maxDistance + 1), radius(maxDistance)
{
}
void operator () (LPENTITY entity)
{
if (!entity || !entity->IsType(ENTITY_ITEM))
return;
LPITEM item = static_cast<LPITEM>(entity);
if (!item || !item->GetSectree() || !item->IsOwnership(ch))
return;
if (!ch->ShouldAutoPickupItem(item))
return;
const int distance = DISTANCE_APPROX(item->GetX() - ch->GetX(), item->GetY() - ch->GetY());
if (distance > radius || distance >= bestDistance)
return;
bestDistance = distance;
bestItem = item;
}
};
EVENTFUNC(autopickup_event)
{
char_event_info* info = dynamic_cast<char_event_info*>(event->info);
if (!info)
{
sys_err("autopickup_event> <Factor> Null pointer");
return 0;
}
LPCHARACTER ch = info->ch;
if (!ch)
return 0;
if (!ch->GetSectree() || !ch->IsPC() || ch->IsDead())
{
ch->m_pkAutoPickupEvent = NULL;
return 0;
}
if (!ch->IsAutoPickupEnabled())
{
ch->m_pkAutoPickupEvent = NULL;
return 0;
}
if (!ch->CanHandleItem() || ch->IsObserverMode())
return MAX(1, kAutoPickupTick);
AutoPickupScan scan(ch, ch->GetAutoPickupRadius());
ch->GetSectree()->ForEachAround(scan);
if (scan.bestItem)
ch->PickupItem(scan.bestItem->GetVID());
return MAX(1, kAutoPickupTick);
}
void ResetPickupPatternWindow(LPCHARACTER ch, DWORD now, long x, long y)
{
ch->m_dwPickupPatternWindowStart = now;
@@ -918,6 +1030,7 @@ void NotifyRefineSuccess(LPCHARACTER ch, LPITEM item, const char* way, int iType
if (NULL != ch && item != NULL)
{
ch->ChatPacket(CHAT_TYPE_COMMAND, "RefineSuceeded %d", iType);
ch->OnStoneQueueRefineResult(true);
LogManager::instance().RefineLog(ch->GetPlayerID(), item->GetName(), item->GetID(), item->GetRefineLevel(), 1, way);
}
@@ -928,6 +1041,7 @@ void NotifyRefineFail(LPCHARACTER ch, LPITEM item, const char* way, int iType, i
if (NULL != ch && NULL != item)
{
ch->ChatPacket(CHAT_TYPE_COMMAND, "RefineFailed %d", iType);
ch->OnStoneQueueRefineResult(false);
LogManager::instance().RefineLog(ch->GetPlayerID(), item->GetName(), item->GetID(), item->GetRefineLevel(), success, way);
}
@@ -6222,6 +6336,30 @@ bool CHARACTER::PickupItem(DWORD dwVID)
return false;
}
bool CHARACTER::ShouldAutoPickupItem(LPITEM item) const
{
const int mask = GetAutoPickupFilterMask();
const int filterBit = GetAutoPickupFilterBit(item);
if (GetAutoPickupFilterMode() == 0)
return filterBit != 0 && (mask & filterBit) != 0;
if (filterBit == 0)
return true;
return (mask & filterBit) == 0;
}
void CHARACTER::StartAutoPickupEvent()
{
if (m_pkAutoPickupEvent || !IsPC() || !IsAutoPickupEnabled())
return;
char_event_info* info = AllocEventInfo<char_event_info>();
info->ch = this;
m_pkAutoPickupEvent = event_create(autopickup_event, info, MAX(1, kAutoPickupTick));
}
bool CHARACTER::SwapItem(BYTE bCell, BYTE bDestCell)
{
if (!CanHandleItem())
@@ -6568,7 +6706,7 @@ void CHARACTER::BuffOnAttr_ValueChange(BYTE bType, BYTE bOldValue, BYTE bNewValu
break;
case POINT_COSTUME_ATTR_BONUS:
{
static BYTE abSlot[] = { WEAR_COSTUME_BODY, WEAR_COSTUME_HAIR };
static BYTE abSlot[] = { WEAR_COSTUME_BODY, WEAR_COSTUME_HAIR, WEAR_COSTUME_SASH };
static std::vector <BYTE> vec_slots (abSlot, abSlot + _countof(abSlot));
pBuff = M2_NEW CBuffOnAttributes(this, bType, &vec_slots);
}

View File

@@ -208,6 +208,11 @@ ACMD(do_get_mob_count);
ACMD(do_dice);
ACMD(do_special_item);
ACMD(do_biolog_submit);
ACMD(do_teleport_system);
ACMD(do_autopickup);
ACMD(do_stone_queue);
ACMD(do_switchbot);
ACMD(do_sash);
ACMD(do_click_mall);
@@ -465,6 +470,11 @@ struct command_info cmd_info[] =
{ "inventory", do_inventory, 0, POS_DEAD, GM_LOW_WIZARD },
{ "cube", do_cube, 0, POS_DEAD, GM_PLAYER },
{ "biolog_submit", do_biolog_submit, 0, POS_DEAD, GM_PLAYER },
{ "teleport_system", do_teleport_system, 0, POS_DEAD, GM_PLAYER },
{ "autopickup", do_autopickup, 0, POS_DEAD, GM_PLAYER },
{ "stone_queue", do_stone_queue, 0, POS_DEAD, GM_PLAYER },
{ "switchbot", do_switchbot, 0, POS_DEAD, GM_PLAYER },
{ "sash", do_sash, 0, POS_DEAD, GM_PLAYER },
{ "siege", do_siege, 0, POS_DEAD, GM_LOW_WIZARD },
{ "temp", do_temp, 0, POS_DEAD, GM_IMPLEMENTOR },
{ "frog", do_frog, 0, POS_DEAD, GM_HIGH_WIZARD },

View File

@@ -1,5 +1,6 @@
#include "stdafx.h"
#include <sodium.h>
#include <set>
#include "utils.h"
#include "config.h"
@@ -1866,6 +1867,418 @@ ACMD(do_biolog_submit)
quest::CQuestManager::instance().QuestButton(ch->GetPlayerID(), questIndex);
}
ACMD(do_teleport_system)
{
char arg1[256];
char arg2[256];
two_arguments(argument, arg1, sizeof(arg1), arg2, sizeof(arg2));
int action = 0;
if (!strcmp(arg1, "save"))
action = 1;
else if (!strcmp(arg1, "saved"))
action = 2;
else if (!strcmp(arg1, "preset"))
action = 3;
else
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Unknown teleport action."));
return;
}
int arg = 0;
str_to_number(arg, arg2);
if (arg <= 0)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid teleport argument."));
return;
}
quest::PC* pPC = quest::CQuestManager::instance().GetPC(ch->GetPlayerID());
if (!pPC)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("퀘스트를 로드하는 중입니다. 잠시만 기다려 주십시오."));
return;
}
const std::string questName = "teleport_system";
const DWORD questIndex = quest::CQuestManager::instance().GetQuestIndexByName(questName);
if (questIndex == 0)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Teleport quest could not be found."));
return;
}
pPC->SetFlag(questName + ".remote_action", action, true);
pPC->SetFlag(questName + ".remote_arg", arg, true);
quest::CQuestManager::instance().QuestButton(ch->GetPlayerID(), questIndex);
}
ACMD(do_autopickup)
{
char arg1[256];
char arg2[256];
argument = one_argument(argument, arg1, sizeof(arg1));
one_argument(argument, arg2, sizeof(arg2));
if (!*arg1 || !strcmp(arg1, "sync"))
{
ch->SendAutoPickupState();
return;
}
if (!strcmp(arg1, "enable"))
{
int enabled = 0;
str_to_number(enabled, arg2);
ch->SetQuestFlag("autopickup.enabled", enabled > 0 ? 1 : 0);
ch->RefreshAutoPickup();
return;
}
if (!strcmp(arg1, "mode"))
{
int mode = 0;
if (!strcmp(arg2, "blacklist"))
mode = 1;
else if (strcmp(arg2, "whitelist"))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Unknown autopickup mode."));
return;
}
ch->SetQuestFlag("autopickup.mode", mode);
ch->SendAutoPickupState();
return;
}
if (!strcmp(arg1, "mask"))
{
int mask = 0;
str_to_number(mask, arg2);
ch->SetQuestFlag("autopickup.mask", MAX(0, MIN(31, mask)));
ch->SendAutoPickupState();
return;
}
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Usage: /autopickup [sync|enable|mode|mask]"));
}
ACMD(do_stone_queue)
{
char arg1[256];
argument = one_argument(argument, arg1, sizeof(arg1));
if (!*arg1 || !strcmp(arg1, "sync"))
{
ch->SendStoneQueueState();
return;
}
if (!strcmp(arg1, "cancel"))
{
ch->CancelStoneQueue();
return;
}
if (strcmp(arg1, "start"))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Usage: /stone_queue [sync|cancel|start]"));
return;
}
char scrollArg[256];
argument = one_argument(argument, scrollArg, sizeof(scrollArg));
int scrollCell = -1;
str_to_number(scrollCell, scrollArg);
if (scrollCell < 0 || scrollCell >= INVENTORY_MAX_NUM)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid stone queue scroll slot."));
return;
}
LPITEM scrollItem = ch->GetInventoryItem(scrollCell);
if (!scrollItem || scrollItem->GetType() != ITEM_USE || scrollItem->GetSubType() != USE_TUNING)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid stone queue scroll item."));
return;
}
std::vector<BYTE> slots;
std::set<int> uniqueSlots;
char slotArg[256];
while (*argument)
{
argument = one_argument(argument, slotArg, sizeof(slotArg));
if (!*slotArg)
break;
int slot = -1;
str_to_number(slot, slotArg);
if (slot < 0 || slot >= INVENTORY_MAX_NUM)
continue;
if (uniqueSlots.find(slot) != uniqueSlots.end())
continue;
LPITEM targetItem = ch->GetInventoryItem(slot);
if (!targetItem || targetItem->GetType() != ITEM_METIN)
continue;
if (slots.size() >= static_cast<size_t>(ch->GetStoneQueueMax()))
break;
uniqueSlots.insert(slot);
slots.push_back(static_cast<BYTE>(slot));
}
if (slots.empty())
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Stone queue is empty."));
return;
}
ch->StartStoneQueue(slots, static_cast<BYTE>(scrollCell));
}
ACMD(do_switchbot)
{
char action[256];
argument = one_argument(argument, action, sizeof(action));
if (!*action || !strcmp(action, "sync"))
{
ch->SendSwitchbotState();
return;
}
if (!strcmp(action, "stop_all"))
{
ch->StopAllSwitchbotSlots();
return;
}
if (!strcmp(action, "speed"))
{
char speedArg[256];
argument = one_argument(argument, speedArg, sizeof(speedArg));
int speedIndex = 1;
str_to_number(speedIndex, speedArg);
ch->SetSwitchbotSpeed(speedIndex);
return;
}
char slotArg[256];
argument = one_argument(argument, slotArg, sizeof(slotArg));
int switchbotSlot = -1;
str_to_number(switchbotSlot, slotArg);
if (switchbotSlot < 0 || switchbotSlot >= 5)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid switchbot slot."));
return;
}
if (!strcmp(action, "stop"))
{
ch->StopSwitchbotSlot(switchbotSlot);
return;
}
if (!strcmp(action, "clear"))
{
ch->StopSwitchbotSlot(switchbotSlot, true);
return;
}
if (strcmp(action, "start"))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Usage: /switchbot [sync|speed|start|stop|clear|stop_all]"));
return;
}
char itemArg[256];
char attrArg[256];
char valueArg[256];
argument = one_argument(argument, itemArg, sizeof(itemArg));
argument = one_argument(argument, attrArg, sizeof(attrArg));
argument = one_argument(argument, valueArg, sizeof(valueArg));
int itemCell = -1;
int attrType = 0;
int minValue = 0;
str_to_number(itemCell, itemArg);
str_to_number(attrType, attrArg);
str_to_number(minValue, valueArg);
if (itemCell < 0 || itemCell >= INVENTORY_MAX_NUM || attrType <= 0 || minValue <= 0)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid switchbot config."));
return;
}
if (!ch->StartSwitchbotSlot(switchbotSlot, static_cast<BYTE>(itemCell), static_cast<BYTE>(attrType), static_cast<short>(minValue)))
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Unable to start switchbot slot."));
}
static bool FN_is_sash_item(LPITEM item)
{
return item && item->GetType() == ITEM_COSTUME && item->GetSubType() == COSTUME_SASH;
}
static bool FN_is_sash_absorb_source(LPITEM item)
{
if (!item)
return false;
if (item->GetType() == ITEM_WEAPON)
return item->GetSubType() != WEAPON_ARROW;
if (item->GetType() != ITEM_ARMOR)
return false;
switch (item->GetSubType())
{
case ARMOR_BODY:
case ARMOR_HEAD:
case ARMOR_SHIELD:
case ARMOR_WRIST:
case ARMOR_FOOTS:
case ARMOR_NECK:
case ARMOR_EAR:
return true;
}
return false;
}
static void FN_set_sash_attrs_from_source(LPITEM sash, LPITEM source, int absorbPct)
{
struct SAttrEntry
{
BYTE type;
short value;
};
std::vector<SAttrEntry> attrs;
const TItemTable* proto = source->GetProto();
if (!proto)
return;
for (int i = 0; i < ITEM_APPLY_MAX_NUM; ++i)
{
const TItemApply& apply = proto->aApplies[i];
if (apply.bType == APPLY_NONE || apply.lValue == 0)
continue;
long scaled = (apply.lValue * absorbPct) / 100;
if (scaled == 0)
scaled = (apply.lValue > 0) ? 1 : -1;
attrs.push_back({ apply.bType, static_cast<short>(scaled) });
if (attrs.size() >= ITEM_ATTRIBUTE_MAX_NUM)
break;
}
for (int i = 0; i < source->GetAttributeCount() && attrs.size() < ITEM_ATTRIBUTE_MAX_NUM; ++i)
{
const TPlayerItemAttribute& attr = source->GetAttribute(i);
if (!attr.bType || !attr.sValue)
continue;
bool duplicated = false;
for (size_t j = 0; j < attrs.size(); ++j)
{
if (attrs[j].type == attr.bType)
{
duplicated = true;
break;
}
}
if (duplicated)
continue;
long scaled = (attr.sValue * absorbPct) / 100;
if (scaled == 0)
scaled = (attr.sValue > 0) ? 1 : -1;
attrs.push_back({ attr.bType, static_cast<short>(scaled) });
}
sash->ClearAttribute();
for (size_t i = 0; i < attrs.size(); ++i)
sash->SetForceAttribute(static_cast<int>(i), attrs[i].type, attrs[i].value);
}
ACMD(do_sash)
{
char action[256];
argument = one_argument(argument, action, sizeof(action));
if (strcmp(action, "absorb"))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Usage: /sash absorb <sash_slot> <item_slot>"));
return;
}
char sashArg[256];
char sourceArg[256];
argument = one_argument(argument, sashArg, sizeof(sashArg));
argument = one_argument(argument, sourceArg, sizeof(sourceArg));
int sashCell = -1;
int sourceCell = -1;
str_to_number(sashCell, sashArg);
str_to_number(sourceCell, sourceArg);
if (sashCell < 0 || sashCell >= INVENTORY_MAX_NUM || sourceCell < 0 || sourceCell >= INVENTORY_MAX_NUM || sashCell == sourceCell)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Invalid sash slots."));
return;
}
LPITEM sash = ch->GetInventoryItem(sashCell);
LPITEM source = ch->GetInventoryItem(sourceCell);
if (!FN_is_sash_item(sash))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("The selected item is not a sash."));
return;
}
if (!FN_is_sash_absorb_source(source))
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("This item cannot be absorbed by a sash."));
return;
}
if (sash->IsEquipped() || source->IsEquipped())
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Unequip items before absorbing bonuses."));
return;
}
if (sash->GetSocket(0) != 0)
{
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("This sash already has absorbed bonuses."));
return;
}
int absorbPct = MINMAX(1, static_cast<int>(sash->GetValue(0)), 100);
FN_set_sash_attrs_from_source(sash, source, absorbPct);
sash->SetSocket(0, source->GetVnum());
sash->SetSocket(1, absorbPct);
sash->SetSocket(2, static_cast<int32_t>(source->GetID()));
source->SetCount(0);
ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("The sash absorbed bonuses successfully."));
}
ACMD(do_in_game_mall)
{
if (LC_IsYMIR() == true || LC_IsKorea() == true)

View File

@@ -552,6 +552,7 @@ void CInputLogin::Entergame(LPDESC d, const char * data)
ch->StartSaveEvent();
ch->StartRecoveryEvent();
ch->StartCheckSpeedHackEvent();
ch->RefreshAutoPickup();
CPVPManager::instance().Connect(ch);
CPVPManager::instance().SendList(d);

View File

@@ -523,6 +523,8 @@ int CItem::FindEquipCell(LPCHARACTER ch, int iCandidateCell)
return WEAR_COSTUME_BODY;
else if (GetSubType() == COSTUME_HAIR)
return WEAR_COSTUME_HAIR;
else if (GetSubType() == COSTUME_SASH)
return WEAR_COSTUME_SASH;
}
else if (GetType() == ITEM_RING)
{
@@ -781,6 +783,11 @@ void CItem::ModifyPoints(bool bAdd)
// [NOTE] 갑옷은 아이템 vnum을 보내고 헤어는 shape(value3)값을 보내는 이유는.. 기존 시스템이 그렇게 되어있음...
toSetValue = (true == bAdd) ? this->GetValue(3) : 0;
}
else if (GetSubType() == COSTUME_SASH)
{
toSetPart = PART_ACCE;
toSetValue = (true == bAdd) ? this->GetVnum() : 0;
}
if (PART_MAX_NUM != toSetPart)
{

View File

@@ -583,6 +583,7 @@ enum ECharacterEquipmentPart
CHR_EQUIPPART_WEAPON,
CHR_EQUIPPART_HEAD,
CHR_EQUIPPART_HAIR,
CHR_EQUIPPART_ACCE,
CHR_EQUIPPART_NUM,
};