diff --git a/src/common/item_length.h b/src/common/item_length.h index 1c46b54..dd73779 100644 --- a/src/common/item_length.h +++ b/src/common/item_length.h @@ -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 diff --git a/src/common/length.h b/src/common/length.h index 99870ad..d6d215b 100644 --- a/src/common/length.h +++ b/src/common/length.h @@ -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, diff --git a/src/db/ProtoReader.cpp b/src/db/ProtoReader.cpp index 379c29c..33bbffa 100644 --- a/src/db/ProtoReader.cpp +++ b/src/db/ProtoReader.cpp @@ -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" }; diff --git a/src/game/char.cpp b/src/game/char.cpp index c70b5e7..43d68f2 100644 --- a/src/game/char.cpp +++ b/src/game/char.cpp @@ -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(event->info); + if (!info) + { + sys_err("stone_queue_event> 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(event->info); + if (!info) + { + sys_err("switchbot_event> 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& slots, BYTE scrollCell) +{ + CancelStoneQueue(); + m_vecStoneQueueSlots = slots; + m_bStoneQueueScrollCell = scrollCell; + char_event_info* info = AllocEventInfo(); + 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(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(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(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(); + 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(); + 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(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); diff --git a/src/game/char.h b/src/game/char.h index 46d8cda..a3dd2ef 100644 --- a/src/game/char.h +++ b/src/game/char.h @@ -1,6 +1,7 @@ #ifndef __INC_METIN_II_CHAR_H__ #define __INC_METIN_II_CHAR_H__ +#include #include #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& 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 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 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; } diff --git a/src/game/char_item.cpp b/src/game/char_item.cpp index 23c8ed9..b7eeb82 100644 --- a/src/game/char_item.cpp +++ b/src/game/char_item.cpp @@ -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(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(event->info); + if (!info) + { + sys_err("autopickup_event> 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(); + 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 vec_slots (abSlot, abSlot + _countof(abSlot)); pBuff = M2_NEW CBuffOnAttributes(this, bType, &vec_slots); } diff --git a/src/game/cmd.cpp b/src/game/cmd.cpp index 4dfae98..834fe66 100644 --- a/src/game/cmd.cpp +++ b/src/game/cmd.cpp @@ -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 }, diff --git a/src/game/cmd_general.cpp b/src/game/cmd_general.cpp index d0d6cd8..bc2fa6f 100644 --- a/src/game/cmd_general.cpp +++ b/src/game/cmd_general.cpp @@ -1,5 +1,6 @@ #include "stdafx.h" #include +#include #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 slots; + std::set 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(ch->GetStoneQueueMax())) + break; + + uniqueSlots.insert(slot); + slots.push_back(static_cast(slot)); + } + + if (slots.empty()) + { + ch->ChatPacket(CHAT_TYPE_INFO, LC_TEXT("Stone queue is empty.")); + return; + } + + ch->StartStoneQueue(slots, static_cast(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(itemCell), static_cast(attrType), static_cast(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 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(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(scaled) }); + } + + sash->ClearAttribute(); + for (size_t i = 0; i < attrs.size(); ++i) + sash->SetForceAttribute(static_cast(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 ")); + 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(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(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) diff --git a/src/game/input_login.cpp b/src/game/input_login.cpp index c6f7d35..82c51d0 100644 --- a/src/game/input_login.cpp +++ b/src/game/input_login.cpp @@ -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); diff --git a/src/game/item.cpp b/src/game/item.cpp index 7735d21..e337444 100644 --- a/src/game/item.cpp +++ b/src/game/item.cpp @@ -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) { diff --git a/src/game/packet_structs.h b/src/game/packet_structs.h index 23a5cf6..19a8414 100644 --- a/src/game/packet_structs.h +++ b/src/game/packet_structs.h @@ -583,6 +583,7 @@ enum ECharacterEquipmentPart CHR_EQUIPPART_WEAPON, CHR_EQUIPPART_HEAD, CHR_EQUIPPART_HAIR, + CHR_EQUIPPART_ACCE, CHR_EQUIPPART_NUM, };