diff --git a/D2Hackit/Modules/autoBuy/AutoBuy.cpp b/D2Hackit/Modules/autoBuy/AutoBuy.cpp index ac929c7..a5a234e 100644 --- a/D2Hackit/Modules/autoBuy/AutoBuy.cpp +++ b/D2Hackit/Modules/autoBuy/AutoBuy.cpp @@ -11,6 +11,9 @@ BOOL CALLBACK FindStuffToRefillCallback(LPCITEM item, LPARAM lParam); AutoBuy::AutoBuy() { + memset(&merchantNpc, 0, sizeof(GAMEUNIT)); + merchantName = ""; + this->isAutomaticallyRefillTP = GetPrivateProfileInt("AutoBuy", "AutomaticallyRefillTP", 1, CONFIG_PATH); this->refillTpAtCharge = GetPrivateProfileInt("AutoBuy", "RefillTpAtCharge", 35, CONFIG_PATH); } @@ -26,7 +29,7 @@ struct D2GS_NPC_BUY }; #pragma pack(pop) -bool AutoBuy::BuyItemInQuantity(DWORD dwItemID) const +bool AutoBuy::BuyItemInQuantity(DWORD dwItemID) { if (!me->IsUIOpened(UI_NPCSHOP)) { @@ -64,10 +67,12 @@ bool AutoBuy::BuyItemInQuantity(DWORD dwItemID) const packet.BuyType = 0x80000000; packet.Cost = 0; + + currentState = State::PurchaseWaitforitem; return server->GameSendPacketToServer((uint8_t*)&packet, sizeof(packet)); } -bool AutoBuy::Start(int quantity, const std::string& itemCode) +bool AutoBuy::Start(int quantity, const std::string& itemCode, bool isAutomaticMode) { if (currentState != State::Uninitialized) { @@ -80,8 +85,39 @@ bool AutoBuy::Start(int quantity, const std::string& itemCode) this->itemQuantityToBuy = max(0, quantity); this->itemCodeToBuy = itemCode; this->hasAlreadyRestockedTps = false; + this->isAutomaticMode = isAutomaticMode; + + if (!me->IsUIOpened(UI_NPCSHOP)) + { + merchantNpc.dwUnitType = UNIT_TYPE_MONSTER; + merchantNpc.dwUnitID = FindMerchant(); + + if (merchantNpc.dwUnitID == 0) + { + server->GamePrintString("˙c:AutoBuy˙c0: Failed to find merchant"); + currentState = State::Uninitialized; + return false; + } + + currentState = State::NpcListingitems; + if (!me->StartNpcSession(&merchantNpc, NPC_TRADE)) + { + server->GamePrintString("˙c:AutoBuy˙c0: Failed to start npc session"); + currentState = State::Uninitialized; + return false; + } + } + else + { + if (isAutomaticMode) + { + currentState = State::CloseMerchantUiAndRestart; + me->CloseAllUIs(); + return true; + } - BuyOurStuff(); + BuyOurStuff(); + } return true; } @@ -104,6 +140,11 @@ void AutoBuy::Stop() void AutoBuy::BuyOurStuff() { + if (isAutomaticMode) + { + itemQuantityToBuy = me->GetNumberOfFreeStorageSlots(STORAGE_INVENTORY); + } + if (itemQuantityToBuy > 0) { auto customItemIdIter = merchantItems.find(itemCodeToBuy); @@ -116,7 +157,10 @@ void AutoBuy::BuyOurStuff() } } - this->Stop(); + if (!isAutomaticMode) + { + this->Stop(); + } } void AutoBuy::RestockScrolls() @@ -140,6 +184,11 @@ void AutoBuy::RestockScrolls() // Called when NPC is listing items up for gamble, before NPC_SESSION message void AutoBuy::OnNpcItemList(const ITEM& merchantItem) { + if (!me->IsUIOpened(UI_NPCSHOP)) + { + return; + } + const auto itemCode = std::string(merchantItem.szItemCode); merchantItems[itemCode] = merchantItem.dwItemID; @@ -154,6 +203,7 @@ void AutoBuy::OnNPCShopScreenOpened() { merchantItems.clear(); this->hasAlreadyRestockedTps = false; + } void AutoBuy::ProcessInventoryItem(const ITEM* item) @@ -187,3 +237,161 @@ BOOL CALLBACK FindStuffToRefillCallback(LPCITEM item, LPARAM lParam) return TRUE; } + +void AutoBuy::OnNpcSession(int success) +{ + if (currentState != State::NpcListingitems) + { + return; + } + + if (!success) + { + me->RedrawClient(FALSE); + me->MoveToUnit(&merchantNpc, TRUE); + server->GamePrintInfo("˙c:AutoBuy˙c0: NPC request failed"); + currentState = State::Uninitialized; // try again + + if (isAutomaticMode) + { + currentState = State::Uninitialized; + Start(1, itemCodeToBuy, isAutomaticMode); + } + + return; + } + + if (isAutomaticMode) + { + currentState = State::PurchaseNextitem; + BuyOurStuff(); + } +} + +void AutoBuy::OnItemToStorageFromStore(ITEM& item) +{ + if (currentState != State::PurchaseWaitforitem) + { + return; + } + + if (!WillItemFit(item.dwItemID)) + { + currentState = State::CloseMerchantUiAndRunAutostock; + me->CloseAllUIs(); + } +} + +void AutoBuy::OnUIClosed() +{ + if (currentState == State::CloseMerchantUiAndRestart) + { + currentState = State::Uninitialized; + Start(1, itemCodeToBuy, isAutomaticMode); + return; + } + + if (currentState == State::CloseMerchantUiAndRunAutostock) + { + currentState = State::RunAutostocker; + server->GameCommandLine("as start chat"); + return; + } +} + +bool AutoBuy::OnAutostockerMessage(const std::string_view& message) +{ + if (currentState != State::RunAutostocker) + { + return false; + } + + if (message == "Autostocker Ended") + { + me->CloseAllUIs(); + + if (isAutomaticMode) + { + currentState = State::Uninitialized; + Start(1, itemCodeToBuy, true); + } + } + + return true; +} + +bool AutoBuy::WillItemFit(DWORD dwItemId) +{ + char itemCode[4]; + + if (!server->GetItemCode(dwItemId, itemCode, 4)) + { + server->GameErrorf("WillItemFit: Failed to get item code (ID = %04X), attempting retry", dwItemId); + Sleep(1000); + + if (!server->GetItemCode(dwItemId, itemCode, 4)) + { + server->GameErrorf("WillItemFit: Failed to get item code (ID = %04X)", dwItemId); + return false; + } + } + + return me->FindFirstStorageSpace(STORAGE_INVENTORY, server->GetItemSize(itemCode), NULL) == true; +} + +DWORD AutoBuy::FindMerchant() +{ + GAMEUNIT gameUnit; + + if (merchantName.length() == 0) + { + const char* merchantNames[] = + { + "charsi", + "gheed", + "akara", + "elzix", + "lysander", + "fara", + "drognan", + "alkor", + "asheara", + "ormus", + "hratli", + "jamella", + "halbu", + "larzuk", + "malah", + "anya", + }; + + DWORD closestNpcID = 0; + DWORD closestDistance = 0; + + for (int i = 0; i < sizeof(merchantNames) / sizeof(merchantNames[0]); i++) + { + if (server->FindUnitByName(merchantNames[i], UNIT_TYPE_MONSTER, &gameUnit)) + { + MAPPOS npcPos = server->GetUnitPosition(&gameUnit); + DWORD distance = me->GetDistanceFrom(npcPos.x, npcPos.y); + + if (closestNpcID == 0 || closestDistance > distance) + { + closestNpcID = gameUnit.dwUnitID; + closestDistance = distance; + } + } + } + + return closestNpcID; + } + else + { + if (server->FindUnitByName(merchantName.c_str(), UNIT_TYPE_MONSTER, &gameUnit)) + { + return gameUnit.dwUnitID; + } + } + + return 0; +} diff --git a/D2Hackit/Modules/autoBuy/AutoBuy.h b/D2Hackit/Modules/autoBuy/AutoBuy.h index bc2c708..7a6f10a 100644 --- a/D2Hackit/Modules/autoBuy/AutoBuy.h +++ b/D2Hackit/Modules/autoBuy/AutoBuy.h @@ -6,7 +6,17 @@ enum class State { Uninitialized = 0, + CloseMerchantUiAndRestart, WaitForNPC, + + OpenMerchantUi, + + NpcListingitems, + NpcDonelistingitems, + PurchaseNextitem, + PurchaseWaitforitem, + CloseMerchantUiAndRunAutostock, + RunAutostocker, }; @@ -15,26 +25,38 @@ class AutoBuy public: AutoBuy(); - bool Start(int quantity, const std::string& itemCode); + bool Start(int quantity, const std::string& itemCode, bool isAutomaticMode); void Stop(); void OnNPCShopScreenOpened(); void ProcessInventoryItem(const ITEM *item); void OnNpcItemList(const ITEM &merchantItem); + void OnUIClosed(); + void OnNpcSession(int success); + void OnItemToStorageFromStore(ITEM& item); + + bool OnAutostockerMessage(const std::string_view& message); private: void BuyOurStuff(); void RestockScrolls(); - bool BuyItemInQuantity(DWORD dwItemID) const; + bool BuyItemInQuantity(DWORD dwItemID); + + DWORD FindMerchant(); + bool WillItemFit(DWORD dwItemId); bool isAutomaticallyRefillTP = 0; int refillTpAtCharge = 0; int numTPTomesToRefill = 0; bool hasAlreadyRestockedTps = false; + bool isAutomaticMode = false; int itemQuantityToBuy = 0; std::string itemCodeToBuy = ""; State currentState = State::Uninitialized; std::unordered_map merchantItems; + + std::string merchantName; + GAMEUNIT merchantNpc; }; diff --git a/D2Hackit/Modules/autoBuy/main.cpp b/D2Hackit/Modules/autoBuy/main.cpp index 15fd6d5..6f1b2a0 100644 --- a/D2Hackit/Modules/autoBuy/main.cpp +++ b/D2Hackit/Modules/autoBuy/main.cpp @@ -22,11 +22,44 @@ BOOL PRIVATE Start(char** argv, int argc) auto quantity = atoi(argv[2]); auto itemCode = std::string(argv[3]); - autoBuy.Start(quantity, itemCode); + autoBuy.Start(quantity, itemCode, false); return TRUE; } + +BOOL PRIVATE Auto(char** argv, int argc) +{ + if (argc != 3) + { + return FALSE; + } + + auto itemCode = std::string(argv[2]); + + autoBuy.Start(1, itemCode, true); + + return TRUE; +} + +BOOL PRIVATE Gempacks(char** argv, int argc) +{ + autoBuy.Start(1, "6gk", true); + return TRUE; +} + +VOID EXPORT OnThisPlayerMessage(UINT nMessage, WPARAM wParam, LPARAM lParam) +{ + if (nMessage == PM_UICLOSED) + { + autoBuy.OnUIClosed(); + } + if (nMessage == PM_NPCSESSION) + { + autoBuy.OnNpcSession(wParam); + } +} + bool isTrading = false; DWORD EXPORT OnGameTimerTick() { @@ -79,6 +112,50 @@ DWORD EXPORT OnGamePacketBeforeReceived(BYTE* aPacket, DWORD aLen) return aLen; } +void EXPORT OnGamePacketAfterReceived(BYTE* aPacket, DWORD aLen) +{ + if (aPacket[0] == 0x9c) + { + ITEM currentItem; + + if (!server->ParseItem(aPacket, aLen, currentItem)) + { + return; + } + + if (currentItem.iAction == ITEM_ACTION_TO_STORAGE) + { + autoBuy.OnItemToStorageFromStore(currentItem); + } + } + + return; +} + +DWORD EXPORT OnGamePacketBeforeSent(BYTE* aPacket, DWORD aLen) +{ + if (aPacket[0] == 0x15 && aPacket[1] == 0x01) + { + char* chatMessage = (char*)(aPacket + 3); + + // Sneaky message passing, doesn't send autostocker chat messages out + if (strncmp(chatMessage, "˙c:Autostocker˙c0:", 18) == 0) + { + const auto message = std::string_view(chatMessage + 19); + if (!autoBuy.OnAutostockerMessage(message)) + { + return aLen; + } + else + { + return 0; + } + } + } + + return aLen; +} + MODULECOMMANDSTRUCT ModuleCommands[]= { { @@ -92,6 +169,16 @@ MODULECOMMANDSTRUCT ModuleCommands[]= Start, "Usage: Start ", }, + { + "Auto", + Auto, + "Usage: Auto ", + }, + { + "Gempacks", + Gempacks, + "Usage: Gempacks", + }, {NULL} };