diff --git a/3rdParty/sfml/CMakeLists.txt b/3rdParty/sfml/CMakeLists.txt new file mode 100644 index 000000000..0129477cd --- /dev/null +++ b/3rdParty/sfml/CMakeLists.txt @@ -0,0 +1,12 @@ +include(functions/FetchContent_ExcludeFromAll_backport) + +set(SFML_BUILD_WINDOW OFF) +set(SFML_BUILD_GRAPHICS OFF) +set(SFML_BUILD_AUDIO OFF) + +include(FetchContent) +FetchContent_Declare_ExcludeFromAll(sfml + GIT_REPOSITORY https://github.com/SFML/SFML + GIT_TAG 016bea9491ccafc3529019fe1d403885a8b3a6ae +) +FetchContent_MakeAvailable_ExcludeFromAll(sfml) \ No newline at end of file diff --git a/CMake/Definitions.cmake b/CMake/Definitions.cmake index 6be176e0d..249ea7232 100644 --- a/CMake/Definitions.cmake +++ b/CMake/Definitions.cmake @@ -19,6 +19,7 @@ foreach( DEVILUTIONX_RESAMPLER_SDL DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT SCREEN_READER_INTEGRATION + DAPI_SERVER UNPACKED_MPQS UNPACKED_SAVES DEVILUTIONX_WINDOWS_NO_WCHAR diff --git a/CMake/Dependencies.cmake b/CMake/Dependencies.cmake index a574210b2..febb2d143 100644 --- a/CMake/Dependencies.cmake +++ b/CMake/Dependencies.cmake @@ -340,3 +340,10 @@ if(GPERF) find_package(Gperftools REQUIRED) message("INFO: ${GPERFTOOLS_LIBRARIES}") endif() + +if(DAPI_SERVER) + find_package(SFML 3.0 COMPONENTS Network CONFIG QUIET) + if(NOT SFML_FOUND) + add_subdirectory(3rdParty/sfml) + endif() +endif() diff --git a/CMake/VcPkgManifestFeatures.cmake b/CMake/VcPkgManifestFeatures.cmake index e22e866d6..0a8ed48c8 100644 --- a/CMake/VcPkgManifestFeatures.cmake +++ b/CMake/VcPkgManifestFeatures.cmake @@ -12,6 +12,9 @@ endif() if(USE_GETTEXT_FROM_VCPKG) list(APPEND VCPKG_MANIFEST_FEATURES "translations") endif() +if(DAPI_SERVER) + list(APPEND VCPKG_MANIFEST_FEATURES "dapi") +endif() if(BUILD_TESTING) list(APPEND VCPKG_MANIFEST_FEATURES "tests") endif() diff --git a/CMakeLists.txt b/CMakeLists.txt index 00d4a8566..0a40ea826 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,8 @@ cmake_dependent_option(PACKET_ENCRYPTION "Encrypt network packets" ON "NOT NONET if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg.cmake$") option(USE_GETTEXT_FROM_VCPKG "Add vcpkg dependency for gettext[tools] for compiling translations" OFF) endif() +option(DAPI_SERVER "Build with DAPI server component for gameplay automation" OFF) +mark_as_advanced(DAPI_SERVER) option(BUILD_TESTING "Build tests." ON) # These must be included after the options above but before the `project` call. diff --git a/CMakeSettings.json b/CMakeSettings.json index 56456356e..9a13ba0e0 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -39,6 +39,28 @@ } ] }, + { + "name": "x64-Debug-DAPI", + "generator": "Ninja", + "configurationType": "Debug", + "buildRoot": "${workspaceRoot}\\build\\${name}", + "installRoot": "${env.USERPROFILE}\\CMakeBuilds\\${workspaceHash}\\install\\${name}", + "inheritEnvironments": [ "msvc_x64" ], + "intelliSenseMode": "windows-msvc-x64", + "enableClangTidyCodeAnalysis": true, + "variables": [ + { + "name": "DISCORD_INTEGRATION", + "value": "False", + "type": "BOOL" + }, + { + "name": "DAPI_SERVER", + "value": "True", + "type": "BOOL" + } + ] + }, { "name": "x64-Debug-SDL1", "generator": "Ninja", @@ -149,4 +171,4 @@ ] } ] -} +} \ No newline at end of file diff --git a/Packaging/windows/CMakePresets.json b/Packaging/windows/CMakePresets.json index c59a2c30c..55b092cfa 100644 --- a/Packaging/windows/CMakePresets.json +++ b/Packaging/windows/CMakePresets.json @@ -36,6 +36,10 @@ "DISABLE_LTO": { "type": "BOOL", "value": "ON" + }, + "DAPI_SERVER": { + "type": "BOOL", + "value": "ON" } } }, diff --git a/Source/CMakeLists.txt b/Source/CMakeLists.txt index 2179dadf2..6ec56817f 100644 --- a/Source/CMakeLists.txt +++ b/Source/CMakeLists.txt @@ -872,6 +872,17 @@ if(DISCORD_INTEGRATION) ) endif() +if(DAPI_SERVER) + list(APPEND libdevilutionx_SRCS + dapi/Server.cpp + dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp + dapi/Backend/Messages/command.proto + dapi/Backend/Messages/data.proto + dapi/Backend/Messages/game.proto + dapi/Backend/Messages/init.proto + dapi/Backend/Messages/message.proto) +endif() + if(SCREEN_READER_INTEGRATION) list(APPEND libdevilutionx_SRCS utils/screen_reader.cpp @@ -990,6 +1001,10 @@ if(DISCORD_INTEGRATION) target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk) endif() +if(DAPI_SERVER) + target_link_libraries(libdevilutionx PRIVATE SFML::Network) +endif() + if(SCREEN_READER_INTEGRATION) if(WIN32) target_compile_definitions(libdevilutionx PRIVATE Tolk) @@ -1048,3 +1063,28 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang") target_link_libraries(libdevilutionx PUBLIC c++fs) endif() endif() + +if(DAPI_SERVER) + find_package(Protobuf REQUIRED) + + target_link_libraries(libdevilutionx PUBLIC protobuf::libprotobuf-lite) + find_package(absl REQUIRED) + set(PROTO_BINARY_DIR "${CMAKE_BINARY_DIR}/generated") + + file(MAKE_DIRECTORY ${PROTO_BINARY_DIR}) + + target_include_directories(libdevilutionx PRIVATE ${Protobuf_INCLUDE_DIRS}) + + # Generate the protobuf files into the 'generated' directory within the build tree + protobuf_generate( + TARGET libdevilutionx + IMPORT_DIRS "${CMAKE_CURRENT_SOURCE_DIR}/dapi/Backend/Messages" + PROTOC_OUT_DIR "${PROTO_BINARY_DIR}" + ) + + # Make sure the generated protobuf files are correctly included in the build + target_include_directories(libdevilutionx PUBLIC "$") + include_directories("${PROTO_BINARY_DIR}/dapi/Backend/Messages") + + target_include_directories(libdevilutionx PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/dapi/Backend/DAPIBackendCore") +endif() diff --git a/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp b/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp new file mode 100644 index 000000000..a37fb6d63 --- /dev/null +++ b/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp @@ -0,0 +1,214 @@ +#include + +#include "DAPIProtoClient.h" + +namespace DAPI { +DAPIProtoClient::DAPIProtoClient() + : mt(static_cast(std::chrono::system_clock::now().time_since_epoch().count())) +{ + udpbound = false; + connectionPort = 1025; +} + +void DAPIProtoClient::checkForConnection() +{ + if (isConnected()) + return; + + sf::Packet packet; + udpSocket.setBlocking(false); + if (!udpbound) { + if (udpSocket.bind(1024, sf::IpAddress::Any) != sf::Socket::Status::Done) + return; + udpbound = true; + } + + std::optional sender = sf::IpAddress::Any; + auto port = udpSocket.getLocalPort(); + if (udpSocket.receive(packet, sender, port) != sf::Socket::Status::Done) + return; + + auto size = packet.getDataSize(); + std::unique_ptr packetContents(new char[size]); + memcpy(packetContents.get(), packet.getData(), size); + + auto currentMessage = std::make_unique(); + currentMessage->ParseFromArray(packetContents.get(), static_cast(size)); + + if (!currentMessage->has_initbroadcast()) + return; + + auto reply = std::make_unique(); + auto initResponse = reply->mutable_initresponse(); + + initResponse->set_port(static_cast(connectionPort)); + + packet.clear(); + size = reply->ByteSizeLong(); + std::unique_ptr buffer(new char[size]); + + reply->SerializeToArray(&buffer[0], static_cast(size)); + packet.append(buffer.get(), size); + + udpSocket.send(packet, sender.value(), port); + udpSocket.unbind(); + udpbound = false; + + tcpListener.accept(tcpSocket); + return; +} + +void DAPIProtoClient::lookForServer() +{ + if (isConnected()) + return; + + sf::Packet packet; + auto broadcastMessage = std::make_unique(); + broadcastMessage->mutable_initbroadcast(); + + auto size = broadcastMessage->ByteSizeLong(); + std::unique_ptr buffer(new char[size]); + + broadcastMessage->SerializeToArray(&buffer[0], size); + packet.append(buffer.get(), size); + + std::optional server = sf::IpAddress::Broadcast; + unsigned short port = 1024; + + udpSocket.send(packet, server.value(), port); + server = sf::IpAddress::Any; + udpSocket.setBlocking(false); + // Sleep to give backend a chance to send the packet. + { + using namespace std::chrono_literals; + std::this_thread::sleep_for(2s); + } + if (udpSocket.receive(packet, server, port) == sf::Socket::Status::Done) { + size = packet.getDataSize(); + std::unique_ptr replyBuffer(new char[size]); + memcpy(replyBuffer.get(), packet.getData(), size); + + auto currentMessage = std::make_unique(); + currentMessage->ParseFromArray(replyBuffer.get(), size); + + if (!currentMessage->has_initresponse()) + return; + + connectionPort = static_cast(currentMessage->initresponse().port()); + + tcpSocket.connect(server.value(), connectionPort); + if (!tcpSocket.getRemoteAddress().has_value()) + fprintf(stderr, "%s", "Connection failed.\n"); + } +} + +void DAPIProtoClient::transmitMessages() +{ + // Check that we are connected to a game server. + if (!isConnected()) + return; + + std::unique_ptr currentMessage; + sf::Packet packet; + // Loop until the message queue is empty. + while (!messageQueue.empty()) { + packet.clear(); + currentMessage = std::move(messageQueue.front()); + messageQueue.pop_front(); + auto size = currentMessage->ByteSizeLong(); + if (size > 0) { + std::unique_ptr buffer(new char[size]); + currentMessage->SerializeToArray(&buffer[0], size); + packet.append(buffer.get(), size); + } + if (tcpSocket.send(packet) != sf::Socket::Status::Done) { + // Error sending message. + fprintf(stderr, "Failed to send a Message. Disconnecting.\n"); + disconnect(); + } + } + + // Finished with queue, send the EndOfQueue message. + currentMessage = std::make_unique(); + currentMessage->mutable_endofqueue(); + packet.clear(); + auto size = currentMessage->ByteSizeLong(); + std::unique_ptr buffer(new char[size]); + currentMessage->SerializeToArray(&buffer[0], size); + packet.append(buffer.get(), size); + if (tcpSocket.send(packet) != sf::Socket::Status::Done) { + // Error sending EndOfQueue + fprintf(stderr, "Failed to send end of queue message. Disconnecting.\n"); + disconnect(); + } +} + +void DAPIProtoClient::receiveMessages() +{ + // Check that we are connected to a game server or client. + if (!isConnected()) + return; + + std::unique_ptr currentMessage; + sf::Packet packet; + // Loop until the end of queue message is received. + while (true) { + packet.clear(); + currentMessage = std::make_unique(); + if (tcpSocket.receive(packet) != sf::Socket::Status::Done) { + fprintf(stderr, "Failed to receive message. Disconnecting.\n"); + disconnect(); + return; + } + auto size = packet.getDataSize(); + std::unique_ptr packetContents(new char[size]); + memcpy(packetContents.get(), packet.getData(), size); + currentMessage->ParseFromArray(packetContents.get(), packet.getDataSize()); + if (currentMessage->has_endofqueue()) + return; + messageQueue.push_back(std::move(currentMessage)); + } +} + +void DAPIProtoClient::disconnect() +{ + if (!isConnected()) + return; + tcpSocket.disconnect(); +} + +void DAPIProtoClient::initListen() +{ + tcpListener.setBlocking(true); + while (tcpListener.listen(connectionPort) != sf::Socket::Status::Done) + connectionPort = static_cast(getRandomInteger(1025, 49151)); +} + +void DAPIProtoClient::stopListen() +{ + tcpListener.close(); +} + +void DAPIProtoClient::queueMessage(std::unique_ptr newMessage) +{ + messageQueue.push_back(std::move(newMessage)); +} + +std::unique_ptr DAPIProtoClient::getNextMessage() +{ + auto nextMessage = std::move(messageQueue.front()); + messageQueue.pop_front(); + return nextMessage; +} + +bool DAPIProtoClient::isConnected() const +{ + return tcpSocket.getRemoteAddress().has_value(); +} + +int DAPIProtoClient::messageQueueSize() const +{ + return messageQueue.size(); +} +} // namespace DAPI diff --git a/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.h b/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.h new file mode 100644 index 000000000..8ba4fc82a --- /dev/null +++ b/Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.h @@ -0,0 +1,45 @@ +#pragma once + +#include +#include + +#include + +#include "message.pb.h" + +namespace DAPI { +struct DAPIProtoClient { + DAPIProtoClient(); + + void checkForConnection(); + void lookForServer(); + void transmitMessages(); + void receiveMessages(); + void disconnect(); + void initListen(); + void stopListen(); + + void queueMessage(std::unique_ptr newMessage); + std::unique_ptr getNextMessage(); + + bool isConnected() const; + int messageQueueSize() const; + +private: + sf::UdpSocket udpSocket; + sf::TcpSocket tcpSocket; + sf::TcpListener tcpListener; + sf::SocketSelector socketSelector; + std::deque> messageQueue; + std::mt19937 mt; + + int getRandomInteger(int min, int max) + { + std::uniform_int_distribution randomNumber(min, max); + return randomNumber(mt); + } + + unsigned short connectionPort; + bool udpbound; +}; +} // namespace DAPI diff --git a/Source/dapi/Backend/Messages/command.proto b/Source/dapi/Backend/Messages/command.proto new file mode 100644 index 000000000..c9c365624 --- /dev/null +++ b/Source/dapi/Backend/Messages/command.proto @@ -0,0 +1,185 @@ +syntax = "proto3"; +option optimize_for = LITE_RUNTIME; + +package dapi.commands; + +message SetFPS { + uint32 FPS = 1; +} + +message CancelQText { + +} + +message Move { + uint32 type = 1; + uint32 targetX = 2; + uint32 targetY = 3; +} + +message Talk { + uint32 targetX = 1; + uint32 targetY = 2; +} + +message SelectStoreOption { + uint32 option = 1; +} + +message BuyItem { + uint32 ID = 1; +} + +message SellItem { + uint32 ID = 1; +} + +message RechargeItem { + uint32 ID = 1; +} + +message RepairItem { + uint32 ID = 1; +} + +message AttackMonster { + uint32 index = 1; +} + +message AttackXY { + sint32 x = 1; + sint32 y = 2; +} + +message OperateObject { + uint32 index = 1; +} + +message UseBeltItem { + uint32 slot = 1; +} + +message ToggleCharacterSheet { + +} + +message IncreaseStat { + uint32 stat = 1; +} + +message GetItem { + uint32 ID = 1; +} + +message SetSpell { + sint32 spellID = 1; + sint32 spellType = 2; +} + +message CastMonster { + uint32 index = 1; +} + +message CastXY { + sint32 x = 1; + sint32 y = 2; +} + +message ToggleInventory { + +} + +message PutInCursor { + uint32 ID = 1; +} + +message PutCursorItem { + sint32 target = 1; +} + +message DropCursorItem { + +} + +message UseItem { + uint32 ID = 1; +} + +message IdentifyStoreItem { + uint32 ID = 1; +} + +message DisarmTrap { + uint32 index = 1; +} + +message SkillRepair { + uint32 ID = 1; +} + +message SkillRecharge { + uint32 ID = 1; +} + +message ToggleMenu { + +} + +message SaveGame { + +} + +message Quit { + +} + +message ClearCursor { + +} + +message IdentifyItem { + uint32 ID = 1; +} + +message SendChat { + string message = 1; +} + +message Command { + oneof command { + Move move = 1; + Talk talk = 2; + SelectStoreOption option = 3; + BuyItem buyItem = 4; + SellItem sellItem = 5; + RechargeItem rechargeItem = 6; + RepairItem repairItem = 7; + AttackMonster attackMonster = 8; + AttackXY attackXY = 9; + OperateObject operateObject = 10; + UseBeltItem useBeltItem = 11; + ToggleCharacterSheet toggleCharacterSheet = 12; + IncreaseStat increaseStat = 13; + GetItem getItem = 14; + SetSpell setSpell = 15; + CastMonster castMonster = 16; + CastXY castXY = 17; + ToggleInventory toggleInventory = 18; + PutInCursor putInCursor = 19; + PutCursorItem putCursorItem = 20; + DropCursorItem dropCursorItem = 21; + UseItem useItem = 22; + IdentifyStoreItem identifyStoreItem = 23; + CancelQText cancelQText = 24; + SetFPS setFPS = 25; + DisarmTrap disarmTrap = 26; + SkillRepair skillRepair = 27; + SkillRecharge skillRecharge = 28; + ToggleMenu toggleMenu = 29; + SaveGame saveGame = 30; + Quit quit = 31; + ClearCursor clearCursor = 32; + IdentifyItem identifyItem = 33; + SendChat sendChat = 34; + } +} diff --git a/Source/dapi/Backend/Messages/data.proto b/Source/dapi/Backend/Messages/data.proto new file mode 100644 index 000000000..08965ba1c --- /dev/null +++ b/Source/dapi/Backend/Messages/data.proto @@ -0,0 +1,179 @@ +syntax = "proto3"; +option optimize_for = LITE_RUNTIME; + +package dapi.data; + +message QuestData { + uint32 id = 1; + uint32 state = 2; +} + +message PortalData { + uint32 x = 1; + uint32 y = 2; + uint32 player = 3; +} + +message MissileData { + sint32 type = 1; + uint32 x = 2; + uint32 y = 3; + sint32 xvel = 4; + sint32 yvel = 5; + sint32 sx = 6; + sint32 sy = 7; +} + +message ObjectData { + uint32 x = 1; + uint32 y = 2; + sint32 type = 3; + sint32 shrineType = 4; + bool solid = 5; + sint32 doorState = 6; + bool selectable = 7; + uint32 index = 8; + bool trapped = 9; +} + +message MonsterData { + uint32 index = 1; + sint32 x = 2; + sint32 y = 3; + sint32 futx = 4; + sint32 futy = 5; + string name = 6; + sint32 type = 7; + sint32 kills = 8; + sint32 mode = 9; + bool unique = 10; +} + +message TriggerData { + uint32 lvl = 1; + sint32 x = 2; + sint32 y = 3; + sint32 type = 4; +} + +message TileData { + sint32 type = 1; + bool solid = 2; + sint32 x = 3; + sint32 y = 4; + bool stopMissile = 5; +} + +message TownerData { + uint32 ID = 1; + uint32 _ttype = 2; + sint32 _tx = 3; + sint32 _ty = 4; + string _tName = 5; +} + +message ItemData { + uint32 ID = 1; + sint32 _itype = 2; + sint32 _ix = 3; + sint32 _iy = 4; + + bool _iIdentified = 5; + uint32 _iMagical = 6; + string _iName = 7; + string _iIName = 8; + uint32 _iClass = 9; + sint32 _iCurs = 10; + sint32 _iValue = 11; + sint32 _iMinDam = 12; + sint32 _iMaxDam = 13; + sint32 _iAC = 14; + sint32 _iFlags = 15; + sint32 _iMiscId = 16; + sint32 _iSpell = 17; + sint32 _iCharges = 18; + sint32 _iMaxCharges = 19; + sint32 _iDurability = 20; + sint32 _iMaxDur = 21; + sint32 _iPLDam = 22; + sint32 _iPLToHit = 23; + sint32 _iPLAC = 24; + sint32 _iPLStr = 25; + sint32 _iPLMag = 26; + sint32 _iPLDex = 27; + sint32 _iPLVit = 28; + sint32 _iPLFR = 29; + sint32 _iPLLR = 30; + sint32 _iPLMR = 31; + sint32 _iPLMana = 32; + sint32 _iPLHP = 33; + sint32 _iPLDamMod = 34; + sint32 _iPLGetHit = 35; + sint32 _iPLLight = 36; + sint32 _iSplLvlAdd = 37; + sint32 _iFMinDam = 38; + sint32 _iFMaxDam = 39; + sint32 _iLMinDam = 40; + sint32 _iLMaxDam = 41; + sint32 _iPrePower = 42; + sint32 _iSufPower = 43; + sint32 _iMinStr = 44; + sint32 _iMinMag = 45; + sint32 _iMinDex = 46; + bool _iStatFlag = 47; + sint32 IDidx = 48; +} + +message PlayerData { + sint32 _pmode = 1; + sint32 pnum = 2; + sint32 plrlevel = 3; + sint32 _px = 4; + sint32 _py = 5; + sint32 _pfutx = 6; + sint32 _pfuty = 7; + sint32 _pdir = 8; + sint32 _pRSpell = 9; + uint32 _pRsplType = 10; + repeated uint32 _pSplLvl = 11; + uint64 _pMemSpells = 12; + uint64 _pAblSpells = 13; + uint64 _pScrlSpells = 14; + string _pName = 15; + uint32 _pClass = 16; + uint32 _pStrength = 17; + uint32 _pBaseStr = 18; + uint32 _pMagic = 19; + uint32 _pBaseMag = 20; + uint32 _pDexterity = 21; + uint32 _pBaseDex = 22; + uint32 _pVitality = 23; + uint32 _pBaseVit = 24; + uint32 _pStatPts = 25; + uint32 _pDamageMod = 26; + uint32 _pHitPoints = 27; + uint32 _pMaxHP = 28; + sint32 _pMana = 29; + uint32 _pMaxMana = 30; + uint32 _pLevel = 31; + uint32 _pExperience = 32; + uint32 _pArmorClass = 33; + uint32 _pMagResist = 34; + uint32 _pFireResist = 35; + uint32 _pLightResist = 36; + uint32 _pGold = 37; + repeated sint32 InvBody = 38; + repeated sint32 InvList = 39; + repeated sint32 InvGrid = 40; + repeated sint32 SpdList = 41; + sint32 HoldItem = 42; + uint32 _pIAC = 43; + uint32 _pIMinDam = 44; + uint32 _pIMaxDam = 45; + uint32 _pIBonusDam = 46; + uint32 _pIBonusToHit = 47; + uint32 _pIBonusAC = 48; + uint32 _pIBonusDamMod = 49; + int32 _pISplLvlAdd = 50; + bool pManaShield = 51; +} diff --git a/Source/dapi/Backend/Messages/game.proto b/Source/dapi/Backend/Messages/game.proto new file mode 100644 index 000000000..7f3fe301e --- /dev/null +++ b/Source/dapi/Backend/Messages/game.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; +option optimize_for = LITE_RUNTIME; +import "data.proto"; + +package dapi.game; + +message FrameUpdate { + uint32 player = 1; + sint32 stextflag = 2; + sint32 pauseMode = 3; + bool menuOpen = 4; + uint32 cursor = 5; + bool chrflag = 6; + bool invflag = 7; + bool qtextflag = 8; + string qtext = 9; + uint32 currlevel = 10; + bool setlevel = 11; + uint32 fps = 12; + uint32 gameMode = 13; + uint32 gnDifficulty = 14; + uint32 connectedTo = 15; + uint32 stashGold = 16; + + repeated dapi.data.TileData dPiece = 17; + repeated dapi.data.PlayerData playerData = 18; + repeated dapi.data.ItemData itemData = 19; + repeated uint32 groundItemID = 20; + repeated dapi.data.TownerData townerData = 21; + repeated uint32 storeOption = 22; + repeated uint32 storeItems = 23; + repeated uint32 stashItems = 24; + repeated dapi.data.TriggerData triggerData = 25; + repeated dapi.data.MonsterData monsterData = 26; + repeated dapi.data.ObjectData objectData = 27; + repeated dapi.data.MissileData missileData = 28; + repeated dapi.data.PortalData portalData = 29; + repeated dapi.data.QuestData questData = 30; + repeated string chatMessages = 31; +} diff --git a/Source/dapi/Backend/Messages/init.proto b/Source/dapi/Backend/Messages/init.proto new file mode 100644 index 000000000..816bb331a --- /dev/null +++ b/Source/dapi/Backend/Messages/init.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +option optimize_for = LITE_RUNTIME; + +package dapi.init; + +message ClientBroadcast { + +} + +message ServerResponse { + uint32 port = 1; +} diff --git a/Source/dapi/Backend/Messages/message.proto b/Source/dapi/Backend/Messages/message.proto new file mode 100644 index 000000000..77d1efd96 --- /dev/null +++ b/Source/dapi/Backend/Messages/message.proto @@ -0,0 +1,26 @@ +syntax = "proto3"; +option optimize_for = LITE_RUNTIME; +import "init.proto"; +import "game.proto"; +import "command.proto"; + +package dapi.message; + +// Empty message to intidate end of queue +message EndofQueue { + +} + +// Wrapper used to distinguish which message is which +message Message{ + oneof msg { + dapi.init.ClientBroadcast initBroadcast = 1; + dapi.init.ServerResponse initResponse = 2; + + dapi.game.FrameUpdate frameUpdate = 3; + + dapi.commands.Command command = 4; + + EndofQueue endOfQueue = 5; + } +} diff --git a/Source/dapi/GameData.h b/Source/dapi/GameData.h new file mode 100644 index 000000000..00cb09ae8 --- /dev/null +++ b/Source/dapi/GameData.h @@ -0,0 +1,47 @@ +#pragma once + +#include +#include + +#include "Item.h" +#include "Player.h" +#include "Towner.h" +#include "Trigger.h" + +namespace DAPI { +enum struct StoreOption { + TALK, + IDENTIFYANITEM, + EXIT, + HEAL, + BUYITEMS, + WIRTPEEK, + BUYBASIC, + BUYPREMIUM, + SELL, + REPAIR, + RECHARGE, + BACK, + ACCESSSTORAGE +}; + +struct GameData { + bool menuOpen; + int pcurs; + bool chrflag; + bool invflag; + bool qtextflag; + int currlevel; + int stashGold; + size_t lastLogSize; + + std::map playerList; + std::vector itemList; + std::vector groundItems; + std::vector stashItems; + std::map townerList; + std::vector storeList; + std::vector storeItems; + std::vector triggerList; +}; +} // namespace DAPI diff --git a/Source/dapi/Item.h b/Source/dapi/Item.h new file mode 100644 index 000000000..87f7c4e24 --- /dev/null +++ b/Source/dapi/Item.h @@ -0,0 +1,71 @@ +#pragma once + +#include "../items.h" + +namespace DAPI { +struct ItemData { + bool compare(devilution::Item &item) + { + return item._iSeed == _iSeed && item._iCreateInfo == _iCreateInfo && item.IDidx == IDidx; + } + + int ID; + + uint32_t _iSeed; + int _iCreateInfo; + int _itype; + int _ix; + int _iy; + + uint32_t _iIdentified; + char _iMagical; + char _iName[64]; + char _iIName[64]; + char _iClass; + int _iCurs; + int _ivalue; + int _iMinDam; + int _iMaxDam; + int _iAC; + int _iFlags; + int _iMiscId; + int _iSpell; + + int _iCharges; + int _iMaxCharges; + + int _iDurability; + int _iMaxDur; + + int _iPLDam; + int _iPLToHit; + int _iPLAC; + int _iPLStr; + int _iPLMag; + int _iPLDex; + int _iPLVit; + int _iPLFR; + int _iPLLR; + int _iPLMR; + int _iPLMana; + int _iPLHP; + int _iPLDamMod; + int _iPLGetHit; + int _iPLLight; + char _iSplLvlAdd; + + int _iFMinDam; + int _iFMaxDam; + int _iLMinDam; + int _iLMaxDam; + + char _iPrePower; + char _iSufPower; + + char _iMinStr; + char _iMinMag; + char _iMinDex; + uint32_t _iStatFlag; + int IDidx; +}; +} // namespace DAPI diff --git a/Source/dapi/Player.h b/Source/dapi/Player.h new file mode 100644 index 000000000..8fb946227 --- /dev/null +++ b/Source/dapi/Player.h @@ -0,0 +1,75 @@ +#pragma once +#include "Item.h" +#include + +namespace DAPI { +const int NUM_INVLOC = 7; +const int MAXINV = 40; +const int MAXSPD = 8; + +struct PlayerData { + int _pmode; + int pnum; + int plrlevel; + int _px; + int _py; + int _pfutx; + int _pfuty; + int _pdir; + + int _pRSpell; + char _pRSplType; + + char _pSplLvl[64]; + uint64_t _pMemSpells; + uint64_t _pAblSpells; + uint64_t _pScrlSpells; + + char _pName[32]; + char _pClass; + + int _pStrength; + int _pBaseStr; + int _pMagic; + int _pBaseMag; + int _pDexterity; + int _pBaseDex; + int _pVitality; + int _pBaseVit; + + int _pStatPts; + + int _pDamageMod; + + int _pHitPoints; + int _pMaxHP; + int _pMana; + int _pMaxMana; + char _pLevel; + int _pExperience; + + char _pArmorClass; + + char _pMagResist; + char _pFireResist; + char _pLightResist; + + int _pGold; + + std::map InvBody; + int InvList[MAXINV]; + int InvGrid[MAXINV]; + std::map SpdList; + int HoldItem; + + int _pIMinDam; + int _pIMaxDam; + int _pIBonusDam; + int _pIAC; + int _pIBonusToHit; + int _pIBonusAC; + int _pIBonusDamMod; + char _pISplLvlAdd; + bool pManaShield; +}; +} // namespace DAPI diff --git a/Source/dapi/Server.cpp b/Source/dapi/Server.cpp new file mode 100644 index 000000000..e8aa4992d --- /dev/null +++ b/Source/dapi/Server.cpp @@ -0,0 +1,2534 @@ +#include + +#include "Server.h" +#include "qol/stash.h" + +namespace DAPI { +Server::Server() + : FPS(20) +{ + output.open("output.csv"); + data = std::make_unique(); + for (int x = -8; x < 9; x++) { + switch (x) { + case 8: + panelScreenCheck[std::make_pair(x, 3)] = true; + break; + case 7: + for (int y = 2; y < 5; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 6: + for (int y = 1; y < 6; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 5: + for (int y = 0; y < 7; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 4: + for (int y = -1; y < 8; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 3: + for (int y = -2; y < 9; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 2: + for (int y = -3; y < 8; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 1: + for (int y = -4; y < 7; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case 0: + for (int y = -5; y < 6; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -1: + for (int y = -6; y < 5; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -2: + for (int y = -7; y < 4; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -3: + for (int y = -8; y < 3; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -4: + for (int y = -7; y < 2; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -5: + for (int y = -6; y < 1; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -6: + for (int y = -5; y < 0; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -7: + for (int y = -4; y < -1; y++) + panelScreenCheck[std::make_pair(x, y)] = true; + break; + case -8: + panelScreenCheck[std::make_pair(x, -3)] = true; + break; + } + } +} + +void Server::update() +{ + if (isConnected()) { + updateGameData(); + protoClient.transmitMessages(); + protoClient.receiveMessages(); + processMessages(); + } else { + checkForConnections(); + } +} + +bool Server::isConnected() const +{ + return protoClient.isConnected(); +} + +void Server::processMessages() +{ + bool issuedCommand = false; + while (protoClient.messageQueueSize()) { + if (devilution::MonstersData.size() != 138) + issuedCommand = false; + auto message = protoClient.getNextMessage(); + if (message.get() == nullptr) + return; + if (message->has_endofqueue()) + return; + + if (message->has_command() && !issuedCommand) { + auto command = message->command(); + if (command.has_move() && this->OKToAct()) { + auto moveMessage = command.move(); + this->move(moveMessage.targetx(), moveMessage.targety()); + } else if (command.has_talk() && this->OKToAct()) { + auto talkMessage = command.talk(); + this->talk(talkMessage.targetx(), talkMessage.targety()); + } else if (command.has_option() && devilution::ActiveStore != devilution::TalkID::None) { + auto option = command.option(); + this->selectStoreOption(static_cast(command.option().option())); + } else if (command.has_buyitem()) { + auto buyItem = command.buyitem(); + this->buyItem(buyItem.id()); + } else if (command.has_sellitem()) { + auto sellItem = command.sellitem(); + this->sellItem(sellItem.id()); + } else if (command.has_rechargeitem()) { + auto rechargeItem = command.rechargeitem(); + this->rechargeItem(rechargeItem.id()); + } else if (command.has_repairitem()) { + auto repairItem = command.repairitem(); + this->repairItem(repairItem.id()); + } else if (command.has_attackmonster()) { + auto attackMonster = command.attackmonster(); + this->attackMonster(attackMonster.index()); + } else if (command.has_attackxy()) { + auto attackXY = command.attackxy(); + this->attackXY(attackXY.x(), attackXY.y()); + } else if (command.has_operateobject()) { + auto operateObject = command.operateobject(); + this->operateObject(operateObject.index()); + } else if (command.has_usebeltitem()) { + auto useBeltItem = command.usebeltitem(); + this->useBeltItem(useBeltItem.slot()); + } else if (command.has_togglecharactersheet()) { + auto toggleCharacterSheet = command.togglecharactersheet(); + this->toggleCharacterScreen(); + } else if (command.has_increasestat()) { + auto increaseStat = command.increasestat(); + this->increaseStat(static_cast(increaseStat.stat())); + } else if (command.has_getitem()) { + auto getItem = command.getitem(); + this->getItem(getItem.id()); + } else if (command.has_setspell()) { + auto setSpell = command.setspell(); + this->setSpell(setSpell.spellid(), static_cast(setSpell.spelltype())); + } else if (command.has_castmonster()) { + auto castMonster = command.castmonster(); + this->castSpell(castMonster.index()); + } else if (command.has_toggleinventory()) { + this->toggleInventory(); + } else if (command.has_putincursor()) { + auto putInCursor = command.putincursor(); + this->putInCursor(putInCursor.id()); + } else if (command.has_putcursoritem()) { + auto putCursorItem = command.putcursoritem(); + this->putCursorItem(putCursorItem.target()); + } else if (command.has_dropcursoritem()) { + this->dropCursorItem(); + } else if (command.has_useitem()) { + auto useItem = command.useitem(); + this->useItem(useItem.id()); + } else if (command.has_identifystoreitem()) { + auto identifyStoreItem = command.identifystoreitem(); + this->identifyStoreItem(identifyStoreItem.id()); + } else if (command.has_castxy()) { + auto castXY = command.castxy(); + this->castSpell(castXY.x(), castXY.y()); + } else if (command.has_cancelqtext()) { + this->cancelQText(); + } else if (command.has_disarmtrap()) { + auto disarmTrap = command.disarmtrap(); + this->disarmTrap(disarmTrap.index()); + } else if (command.has_skillrepair()) { + auto skillRepair = command.skillrepair(); + this->skillRepair(skillRepair.id()); + } else if (command.has_skillrecharge()) { + auto skillRecharge = command.skillrecharge(); + this->skillRecharge(skillRecharge.id()); + } else if (command.has_togglemenu()) { + this->toggleMenu(); + } else if (command.has_savegame()) { + this->saveGame(); + } else if (command.has_quit()) { + this->quit(); + } else if (command.has_clearcursor()) { + this->clearCursor(); + } else if (command.has_identifyitem()) { + auto identifyItem = command.identifyitem(); + this->identifyItem(identifyItem.id()); + } else if (command.has_sendchat()) { + auto sendChat = command.sendchat(); + this->sendChat(sendChat.message()); + } + issuedCommand = true; + if (command.has_setfps()) { + auto setfps = command.setfps(); + this->setFPS(setfps.fps()); + issuedCommand = false; + } + } + } +} + +void Server::checkForConnections() +{ + if (isConnected()) + return; + if (!listening) { + protoClient.initListen(); + listening = false; + } + protoClient.checkForConnection(); + if (!protoClient.isConnected()) + return; + protoClient.stopListen(); +} + +void Server::updateGameData() +{ + std::vector itemsModified; + + auto fullFillItemInfo = [&](int itemID, devilution::Item *item) { + itemsModified.push_back(itemID); + + data->itemList[itemID].ID = itemID; + data->itemList[itemID]._iSeed = item->_iSeed; + data->itemList[itemID]._iCreateInfo = item->_iCreateInfo; + data->itemList[itemID]._itype = static_cast(item->_itype); + data->itemList[itemID]._ix = item->position.x; + data->itemList[itemID]._iy = item->position.y; + + data->itemList[itemID]._iIdentified = item->_iIdentified; + data->itemList[itemID]._iMagical = item->_iMagical; + strcpy(data->itemList[itemID]._iName, item->_iName); + if (data->itemList[itemID]._iIdentified) { + strcpy(data->itemList[itemID]._iIName, item->_iIName); + + data->itemList[itemID]._iFlags = static_cast(item->_iFlags); + data->itemList[itemID]._iPrePower = item->_iPrePower; + data->itemList[itemID]._iSufPower = item->_iSufPower; + data->itemList[itemID]._iPLDam = item->_iPLDam; + data->itemList[itemID]._iPLToHit = item->_iPLToHit; + data->itemList[itemID]._iPLAC = item->_iPLAC; + data->itemList[itemID]._iPLStr = item->_iPLStr; + data->itemList[itemID]._iPLMag = item->_iPLMag; + data->itemList[itemID]._iPLDex = item->_iPLDex; + data->itemList[itemID]._iPLVit = item->_iPLVit; + data->itemList[itemID]._iPLFR = item->_iPLFR; + data->itemList[itemID]._iPLLR = item->_iPLLR; + data->itemList[itemID]._iPLMR = item->_iPLMR; + data->itemList[itemID]._iPLMana = item->_iPLMana; + data->itemList[itemID]._iPLHP = item->_iPLHP; + data->itemList[itemID]._iPLDamMod = item->_iPLDamMod; + data->itemList[itemID]._iPLGetHit = item->_iPLGetHit; + data->itemList[itemID]._iPLLight = item->_iPLLight; + data->itemList[itemID]._iSplLvlAdd = item->_iSplLvlAdd; + data->itemList[itemID]._iFMinDam = item->_iFMinDam; + data->itemList[itemID]._iFMaxDam = item->_iFMaxDam; + data->itemList[itemID]._iLMinDam = item->_iLMinDam; + data->itemList[itemID]._iLMaxDam = item->_iLMaxDam; + } else { + strcpy(data->itemList[itemID]._iName, item->_iName); + + data->itemList[itemID]._iFlags = -1; + data->itemList[itemID]._iPrePower = -1; + data->itemList[itemID]._iSufPower = -1; + data->itemList[itemID]._iPLDam = -1; + data->itemList[itemID]._iPLToHit = -1; + data->itemList[itemID]._iPLAC = -1; + data->itemList[itemID]._iPLStr = -1; + data->itemList[itemID]._iPLMag = -1; + data->itemList[itemID]._iPLDex = -1; + data->itemList[itemID]._iPLVit = -1; + data->itemList[itemID]._iPLFR = -1; + data->itemList[itemID]._iPLLR = -1; + data->itemList[itemID]._iPLMR = -1; + data->itemList[itemID]._iPLMana = -1; + data->itemList[itemID]._iPLHP = -1; + data->itemList[itemID]._iPLDamMod = -1; + data->itemList[itemID]._iPLGetHit = -1; + data->itemList[itemID]._iPLLight = -1; + data->itemList[itemID]._iSplLvlAdd = -1; + data->itemList[itemID]._iFMinDam = -1; + data->itemList[itemID]._iFMaxDam = -1; + data->itemList[itemID]._iLMinDam = -1; + data->itemList[itemID]._iLMaxDam = -1; + } + data->itemList[itemID]._iClass = item->_iClass; + data->itemList[itemID]._iCurs = item->_iCurs; + if (item->_itype == devilution::ItemType::Gold) + data->itemList[itemID]._ivalue = item->_ivalue; + else + data->itemList[itemID]._ivalue = -1; + data->itemList[itemID]._iMinDam = item->_iMinDam; + data->itemList[itemID]._iMaxDam = item->_iMaxDam; + data->itemList[itemID]._iAC = item->_iAC; + data->itemList[itemID]._iMiscId = item->_iMiscId; + data->itemList[itemID]._iSpell = static_cast(item->_iSpell); + + data->itemList[itemID]._iCharges = item->_iCharges; + data->itemList[itemID]._iMaxCharges = item->_iMaxCharges; + + data->itemList[itemID]._iDurability = item->_iDurability; + data->itemList[itemID]._iMaxDur = item->_iMaxDur; + + data->itemList[itemID]._iMinStr = item->_iMinStr; + data->itemList[itemID]._iMinMag = item->_iMinMag; + data->itemList[itemID]._iMinDex = item->_iMinDex; + data->itemList[itemID]._iStatFlag = item->_iStatFlag; + data->itemList[itemID].IDidx = item->IDidx; + }; + + auto partialFillItemInfo = [&](int itemID, devilution::Item *item) { + itemsModified.push_back(itemID); + + data->itemList[itemID].ID = itemID; + data->itemList[itemID]._iSeed = item->_iSeed; + data->itemList[itemID]._iCreateInfo = item->_iCreateInfo; + data->itemList[itemID]._itype = static_cast(item->_itype); + data->itemList[itemID]._ix = item->position.x; + data->itemList[itemID]._iy = item->position.y; + + data->itemList[itemID]._iIdentified = item->_iIdentified; + data->itemList[itemID]._iMagical = item->_iMagical; + strcpy(data->itemList[itemID]._iName, item->_iName); + if (data->itemList[itemID]._iIdentified) + strcpy(data->itemList[itemID]._iIName, item->_iIName); + else + strcpy(data->itemList[itemID]._iName, item->_iName); + data->itemList[itemID]._iFlags = -1; + data->itemList[itemID]._iPrePower = -1; + data->itemList[itemID]._iSufPower = -1; + data->itemList[itemID]._iPLDam = -1; + data->itemList[itemID]._iPLToHit = -1; + data->itemList[itemID]._iPLAC = -1; + data->itemList[itemID]._iPLStr = -1; + data->itemList[itemID]._iPLMag = -1; + data->itemList[itemID]._iPLDex = -1; + data->itemList[itemID]._iPLVit = -1; + data->itemList[itemID]._iPLFR = -1; + data->itemList[itemID]._iPLLR = -1; + data->itemList[itemID]._iPLMR = -1; + data->itemList[itemID]._iPLMana = -1; + data->itemList[itemID]._iPLHP = -1; + data->itemList[itemID]._iPLDamMod = -1; + data->itemList[itemID]._iPLGetHit = -1; + data->itemList[itemID]._iPLLight = -1; + data->itemList[itemID]._iSplLvlAdd = -1; + data->itemList[itemID]._iFMinDam = -1; + data->itemList[itemID]._iFMaxDam = -1; + data->itemList[itemID]._iLMinDam = -1; + data->itemList[itemID]._iLMaxDam = -1; + data->itemList[itemID]._iClass = item->_iClass; + data->itemList[itemID]._iCurs = item->_iCurs; + if (item->_itype == devilution::ItemType::Gold) + data->itemList[itemID]._ivalue = item->_ivalue; + else + data->itemList[itemID]._ivalue = -1; + data->itemList[itemID]._iMinDam = -1; + data->itemList[itemID]._iMaxDam = -1; + data->itemList[itemID]._iAC = -1; + data->itemList[itemID]._iMiscId = item->_iMiscId; + data->itemList[itemID]._iSpell = static_cast(item->_iSpell); + + data->itemList[itemID]._iCharges = -1; + data->itemList[itemID]._iMaxCharges = -1; + + data->itemList[itemID]._iDurability = -1; + data->itemList[itemID]._iMaxDur = -1; + + data->itemList[itemID]._iMinStr = -1; + data->itemList[itemID]._iMinMag = -1; + data->itemList[itemID]._iMinDex = -1; + data->itemList[itemID]._iStatFlag = -1; + data->itemList[itemID].IDidx = item->IDidx; + }; + + auto isOnScreen = [&](int x, int y) { + int dx = devilution::Players[devilution::MyPlayerId].position.tile.x - x; + int dy = devilution::Players[devilution::MyPlayerId].position.tile.y - y; + if (!devilution::CharFlag && !devilution::invflag) { + if (dy > 0) { + if (dx < 1 && abs(dx) + abs(dy) < 11) { + return true; + } else if (dx > 0 && abs(dx) + abs(dy) < 12) { + return true; + } + } else { + if ((dx > -1 || dy == 0) && abs(dx) + abs(dy) < 11) { + return true; + } else if ((dx < 0 && dy != 0) && abs(dx) + abs(dy) < 12) { + return true; + } + } + } else if ((devilution::CharFlag && !devilution::invflag) || (!devilution::CharFlag && devilution::invflag)) { + return panelScreenCheck[std::make_pair(dx, dy)]; + } + return false; + }; + + auto message = std::make_unique(); + auto update = message->mutable_frameupdate(); + + update->set_connectedto(1); + + for (auto chatLogLine = devilution::ChatLogLines.size() - (devilution::MessageCounter - data->lastLogSize); chatLogLine < devilution::ChatLogLines.size(); chatLogLine++) { + std::stringstream message; + for (auto &textLine : devilution::ChatLogLines[chatLogLine].colors) { + if (devilution::HasAnyOf(textLine.color, devilution::UiFlags::ColorWhitegold) || devilution::HasAnyOf(textLine.color, devilution::UiFlags::ColorBlue)) + message << textLine.text << ": "; + if (devilution::HasAnyOf(textLine.color, devilution::UiFlags::ColorWhite)) + message << textLine.text; + } + if (message.str().size()) { + auto chatMessage = update->add_chatmessages(); + *chatMessage = message.str(); + } + } + data->lastLogSize = devilution::MessageCounter; + + update->set_player(devilution::MyPlayerId); + + update->set_stextflag(static_cast(devilution::ActiveStore)); + update->set_pausemode(devilution::PauseMode); + if (devilution::sgpCurrentMenu != nullptr) + data->menuOpen = true; + else + data->menuOpen = false; + update->set_menuopen(static_cast(data->menuOpen)); + update->set_cursor(devilution::pcurs); + data->chrflag = devilution::CharFlag; + update->set_chrflag(devilution::CharFlag); + data->invflag = devilution::invflag; + update->set_invflag(devilution::invflag); + data->qtextflag = devilution::qtextflag; + update->set_qtextflag(devilution::qtextflag); + if (!devilution::setlevel) + data->currlevel = static_cast(devilution::currlevel); + else + data->currlevel = static_cast(devilution::setlvlnum); + update->set_currlevel(data->currlevel); + update->set_setlevel(devilution::setlevel); + if (devilution::qtextflag) { + std::stringstream qtextss; + for (auto &line : devilution::TextLines) { + if (qtextss.str().size()) + qtextss << " "; + qtextss << line; + } + update->set_qtext(qtextss.str().c_str()); + } else + update->set_qtext(""); + update->set_fps(FPS); + if (!devilution::gbIsMultiplayer) + update->set_gamemode(0); + else + update->set_gamemode(1); + + update->set_gndifficulty(devilution::sgGameInitInfo.nDifficulty); + + for (int x = 0; x < 112; x++) { + for (int y = 0; y < 112; y++) { + if (isOnScreen(x, y)) { + auto dpiece = update->add_dpiece(); + dpiece->set_type(devilution::dPiece[x][y]); + dpiece->set_solid(HasAnyOf(devilution::SOLData[dpiece->type()], devilution::TileProperties::Solid)); + dpiece->set_x(x); + dpiece->set_y(y); + dpiece->set_stopmissile(HasAnyOf(devilution::SOLData[dpiece->type()], devilution::TileProperties::BlockMissile)); + } + } + } + + for (int i = 0; i < devilution::numtrigs; i++) { + if (isOnScreen(devilution::trigs[i].position.x, devilution::trigs[i].position.y)) { + auto trigger = update->add_triggerdata(); + trigger->set_lvl(devilution::trigs[i]._tlvl); + trigger->set_x(devilution::trigs[i].position.x); + trigger->set_y(devilution::trigs[i].position.y); + // Adding 0x402 to the message stored in the trigger to translate to what the front end expects. + // The front end uses what Diablo 1.09 uses internally. + trigger->set_type(static_cast(devilution::trigs[i]._tmsg) + 0x402); + } + } + + for (int i = 0; i < MAXQUESTS; i++) { + auto quest = update->add_questdata(); + quest->set_id(i); + if (devilution::Quests[i]._qactive == 2) + quest->set_state(devilution::Quests[i]._qactive); + else + quest->set_state(0); + + if (devilution::currlevel == devilution::Quests[i]._qlevel && devilution::Quests[i]._qslvl != 0 && devilution::Quests[i]._qactive != devilution::quest_state::QUEST_NOTAVAIL) { + auto trigger = update->add_triggerdata(); + trigger->set_lvl(devilution::Quests[i]._qslvl); + trigger->set_x(devilution::Quests[i].position.x); + trigger->set_y(devilution::Quests[i].position.y); + trigger->set_type(1029); + } + } + + data->storeList.clear(); + // Check for qtextflag added for DevilutionX so that store options are not transmitted + // while qtext is up. + if (!devilution::qtextflag && devilution::ActiveStore != devilution::TalkID::None) { + for (int i = 0; i < 24; i++) { + if (devilution::TextLine[i].isSelectable()) { + if (!strcmp(devilution::TextLine[i].text.c_str(), "Talk to Cain") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Farnham") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Pepin") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Gillian") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Ogden") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Griswold") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Adria") || !strcmp(devilution::TextLine[i].text.c_str(), "Talk to Wirt")) + data->storeList.push_back(StoreOption::TALK); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Identify an item")) + data->storeList.push_back(StoreOption::IDENTIFYANITEM); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Say goodbye") || !strcmp(devilution::TextLine[i].text.c_str(), "Say Goodbye") || !strcmp(devilution::TextLine[i].text.c_str(), "Leave Healer's home") || !strcmp(devilution::TextLine[i].text.c_str(), "Leave the shop") || !strcmp(devilution::TextLine[i].text.c_str(), "Leave the shack") || !strcmp(devilution::TextLine[i].text.c_str(), "Leave the tavern") || !strcmp(devilution::TextLine[i].text.c_str(), "Leave")) + data->storeList.push_back(StoreOption::EXIT); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Receive healing")) + data->storeList.push_back(StoreOption::HEAL); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Buy items")) + data->storeList.push_back(StoreOption::BUYITEMS); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "What have you got?")) + data->storeList.push_back(StoreOption::WIRTPEEK); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Buy basic items")) + data->storeList.push_back(StoreOption::BUYBASIC); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Buy premium items")) + data->storeList.push_back(StoreOption::BUYPREMIUM); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Sell items")) + data->storeList.push_back(StoreOption::SELL); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Repair items")) + data->storeList.push_back(StoreOption::REPAIR); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Recharge staves")) + data->storeList.push_back(StoreOption::RECHARGE); + else if (!strcmp(devilution::TextLine[i].text.c_str(), "Access Storage")) + data->storeList.push_back(StoreOption::ACCESSSTORAGE); + } + } + + switch (devilution::ActiveStore) { + case devilution::TalkID::HealerBuy: + case devilution::TalkID::StorytellerIdentifyShow: + case devilution::TalkID::NoMoney: + case devilution::TalkID::NoRoom: + case devilution::TalkID::SmithBuy: + case devilution::TalkID::StorytellerIdentify: + case devilution::TalkID::SmithPremiumBuy: + case devilution::TalkID::SmithRepair: + case devilution::TalkID::SmithSell: + case devilution::TalkID::WitchBuy: + case devilution::TalkID::WitchRecharge: + case devilution::TalkID::WitchSell: + case devilution::TalkID::Gossip: + data->storeList.push_back(StoreOption::BACK); + break; + default: + break; + } + } + + for (auto &option : data->storeList) + update->add_storeoption(static_cast(option)); + + data->groundItems.clear(); + + for (size_t i = 0; i < (devilution::gbIsMultiplayer ? 4 : 1); /* devilution::gbActivePlayers;*/ i++) { + auto playerData = update->add_playerdata(); + + data->playerList[i].InvBody.clear(); + + data->playerList[i].pnum = i; + playerData->set_pnum(i); + + for (int j = 0; j < MAXINV; j++) + data->playerList[i].InvList[j] = -1; + + if (devilution::MyPlayerId == i && devilution::Players.size() >= i + 1) { + memcpy(data->playerList[i]._pName, devilution::Players[i]._pName, 32); + playerData->set__pname(data->playerList[i]._pName); + + data->playerList[i]._pmode = devilution::Players[i]._pmode; + data->playerList[i].plrlevel = devilution::Players[i].plrlevel; + data->playerList[i]._px = devilution::Players[i].position.tile.x; + data->playerList[i]._py = devilution::Players[i].position.tile.y; + data->playerList[i]._pfutx = devilution::Players[i].position.future.x; + data->playerList[i]._pfuty = devilution::Players[i].position.future.y; + data->playerList[i]._pdir = static_cast(devilution::Players[i]._pdir); + + data->playerList[i]._pRSpell = static_cast(devilution::Players[i]._pRSpell); + data->playerList[i]._pRSplType = static_cast(devilution::Players[i]._pRSplType); + + memcpy(data->playerList[i]._pSplLvl, devilution::Players[i]._pSplLvl, sizeof(data->playerList[i]._pSplLvl)); + data->playerList[i]._pMemSpells = devilution::Players[i]._pMemSpells; + data->playerList[i]._pAblSpells = devilution::Players[i]._pAblSpells; + data->playerList[i]._pScrlSpells = devilution::Players[i]._pScrlSpells; + + data->playerList[i]._pClass = static_cast(devilution::Players[i]._pClass); + + data->playerList[i]._pStrength = devilution::Players[i]._pStrength; + data->playerList[i]._pBaseStr = devilution::Players[i]._pBaseStr; + data->playerList[i]._pMagic = devilution::Players[i]._pMagic; + data->playerList[i]._pBaseMag = devilution::Players[i]._pBaseMag; + data->playerList[i]._pDexterity = devilution::Players[i]._pDexterity; + data->playerList[i]._pBaseDex = devilution::Players[i]._pBaseDex; + data->playerList[i]._pVitality = devilution::Players[i]._pVitality; + data->playerList[i]._pBaseVit = devilution::Players[i]._pBaseVit; + + data->playerList[i]._pStatPts = devilution::Players[i]._pStatPts; + + data->playerList[i]._pDamageMod = devilution::Players[i]._pDamageMod; + + data->playerList[i]._pHitPoints = devilution::Players[i]._pHitPoints; + data->playerList[i]._pMaxHP = devilution::Players[i]._pMaxHP; + data->playerList[i]._pMana = devilution::Players[i]._pMana; + data->playerList[i]._pMaxMana = devilution::Players[i]._pMaxMana; + data->playerList[i]._pLevel = devilution::Players[i].getCharacterLevel(); + data->playerList[i]._pExperience = devilution::Players[i]._pExperience; + + data->playerList[i]._pArmorClass = devilution::Players[i]._pArmorClass; + + data->playerList[i]._pMagResist = devilution::Players[i]._pMagResist; + data->playerList[i]._pFireResist = devilution::Players[i]._pFireResist; + data->playerList[i]._pLightResist = devilution::Players[i]._pLghtResist; + + data->playerList[i]._pGold = devilution::Players[i]._pGold; + + for (int j = 0; j < NUM_INVLOC; j++) { + if (devilution::Players[i].InvBody[j]._itype == devilution::ItemType::None) { + data->playerList[i].InvBody[j] = -1; + continue; + } + + size_t itemID = data->itemList.size(); + for (size_t k = 0; k < data->itemList.size(); k++) { + if (data->itemList[k].compare(devilution::Players[i].InvBody[j])) { + itemID = k; + break; + } + } + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + fullFillItemInfo(itemID, &devilution::Players[i].InvBody[j]); + data->playerList[i].InvBody[j] = itemID; + } + bool used[40] = { false }; + for (int j = 0; j < MAXINV; j++) { + auto index = devilution::Players[i].InvGrid[j]; + if (index != 0) { + size_t itemID = data->itemList.size(); + for (size_t k = 0; k < data->itemList.size(); k++) { + if (data->itemList[k].compare(devilution::Players[i].InvList[abs(index) - 1])) { + itemID = k; + break; + } + } + data->playerList[i].InvGrid[j] = itemID; + if (!used[abs(index) - 1]) { + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + fullFillItemInfo(itemID, &devilution::Players[i].InvList[abs(index) - 1]); + used[abs(index) - 1] = true; + data->playerList[i].InvList[abs(index) - 1] = itemID; + } + } else + data->playerList[i].InvGrid[j] = -1; + } + for (int j = 0; j < MAXSPD; j++) { + if (devilution::Players[i].SpdList[j]._itype == devilution::ItemType::None) { + data->playerList[i].SpdList[j] = -1; + continue; + } + + size_t itemID = data->itemList.size(); + for (size_t k = 0; k < data->itemList.size(); k++) { + if (data->itemList[k].compare(devilution::Players[i].SpdList[j])) { + itemID = k; + break; + } + } + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + fullFillItemInfo(itemID, &devilution::Players[i].SpdList[j]); + data->playerList[i].SpdList[j] = itemID; + } + if (devilution::pcurs < 12) + data->playerList[i].HoldItem = -1; + else { + size_t itemID = data->itemList.size(); + for (size_t j = 0; j < data->itemList.size(); j++) { + if (data->itemList[j].compare(devilution::Players[i].HoldItem)) { + itemID = j; + break; + } + } + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + partialFillItemInfo(itemID, &devilution::Players[i].HoldItem); + data->playerList[i].HoldItem = itemID; + } + + data->playerList[i]._pIMinDam = devilution::Players[i]._pIMinDam; + data->playerList[i]._pIMaxDam = devilution::Players[i]._pIMaxDam; + data->playerList[i]._pIBonusDam = devilution::Players[i]._pIBonusDam; + data->playerList[i]._pIAC = devilution::Players[i]._pIAC; + data->playerList[i]._pIBonusToHit = devilution::Players[i]._pIBonusToHit; + data->playerList[i]._pIBonusAC = devilution::Players[i]._pIBonusAC; + data->playerList[i]._pIBonusDamMod = devilution::Players[i]._pIBonusDamMod; + data->playerList[i]._pISplLvlAdd = devilution::Players[i]._pISplLvlAdd; + data->playerList[i].pManaShield = devilution::Players[i].pManaShield; + } else if (devilution::Players.size() >= i + 1 && devilution::Players[i].plractive && devilution::Players[i].isOnActiveLevel() && devilution::IsTileLit(devilution::Point { devilution::Players[i].position.tile.x, devilution::Players[i].position.tile.y })) { + memcpy(data->playerList[i]._pName, devilution::Players[i]._pName, 32); + playerData->set__pname(data->playerList[i]._pName); + + data->playerList[i]._pmode = devilution::Players[i]._pmode; + data->playerList[i].plrlevel = devilution::Players[i].plrlevel; + data->playerList[i]._px = devilution::Players[i].position.tile.x; + data->playerList[i]._py = devilution::Players[i].position.tile.y; + data->playerList[i]._pfutx = devilution::Players[i].position.future.x; + data->playerList[i]._pfuty = devilution::Players[i].position.future.y; + data->playerList[i]._pdir = static_cast(devilution::Players[i]._pdir); + + data->playerList[i]._pRSpell = -1; + data->playerList[i]._pRSplType = 4; + + memset(data->playerList[i]._pSplLvl, 0, 64); + data->playerList[i]._pMemSpells = -1; + data->playerList[i]._pAblSpells = -1; + data->playerList[i]._pScrlSpells = -1; + + data->playerList[i]._pClass = static_cast(devilution::Players[i]._pClass); + + data->playerList[i]._pStrength = -1; + data->playerList[i]._pBaseStr = -1; + data->playerList[i]._pMagic = -1; + data->playerList[i]._pBaseMag = -1; + data->playerList[i]._pDexterity = -1; + data->playerList[i]._pBaseDex = -1; + data->playerList[i]._pVitality = -1; + data->playerList[i]._pBaseVit = -1; + + data->playerList[i]._pStatPts = -1; + + data->playerList[i]._pDamageMod = -1; + + data->playerList[i]._pHitPoints = devilution::Players[i]._pHitPoints; + data->playerList[i]._pMaxHP = devilution::Players[i]._pMaxHP; + data->playerList[i]._pMana = -1; + data->playerList[i]._pMaxMana = -1; + data->playerList[i]._pLevel = devilution::Players[i].getCharacterLevel(); + data->playerList[i]._pExperience = -1; + + data->playerList[i]._pArmorClass = -1; + + data->playerList[i]._pMagResist = -1; + data->playerList[i]._pFireResist = -1; + data->playerList[i]._pLightResist = -1; + + data->playerList[i]._pGold = -1; + + for (int j = 0; j < NUM_INVLOC; j++) + data->playerList[i].InvBody[j] = -1; + for (int j = 0; j < MAXINV; j++) + data->playerList[i].InvGrid[j] = -1; + for (int j = 0; j < MAXSPD; j++) + data->playerList[i].SpdList[j] = -1; + data->playerList[i].HoldItem = -1; + + data->playerList[i]._pIMinDam = -1; + data->playerList[i]._pIMaxDam = -1; + data->playerList[i]._pIBonusDam = -1; + data->playerList[i]._pIAC = -1; + data->playerList[i]._pIBonusToHit = -1; + data->playerList[i]._pIBonusAC = -1; + data->playerList[i]._pIBonusDamMod = -1; + data->playerList[i]._pISplLvlAdd = -1; + data->playerList[i].pManaShield = devilution::Players[i].pManaShield; + } else { + memset(data->playerList[i]._pName, 0, 32); + playerData->set__pname(data->playerList[i]._pName); + + data->playerList[i]._pmode = 0; + data->playerList[i].plrlevel = -1; + data->playerList[i]._px = -1; + data->playerList[i]._py = -1; + data->playerList[i]._pfutx = -1; + data->playerList[i]._pfuty = -1; + data->playerList[i]._pdir = -1; + + data->playerList[i]._pRSpell = -1; + data->playerList[i]._pRSplType = -1; + + memset(data->playerList[i]._pSplLvl, 0, 64); + data->playerList[i]._pMemSpells = -1; + data->playerList[i]._pAblSpells = -1; + data->playerList[i]._pScrlSpells = -1; + + data->playerList[i]._pClass = -1; + + data->playerList[i]._pStrength = -1; + data->playerList[i]._pBaseStr = -1; + data->playerList[i]._pMagic = -1; + data->playerList[i]._pBaseMag = -1; + data->playerList[i]._pDexterity = -1; + data->playerList[i]._pBaseDex = -1; + data->playerList[i]._pVitality = -1; + data->playerList[i]._pBaseVit = -1; + + data->playerList[i]._pStatPts = -1; + + data->playerList[i]._pDamageMod = -1; + + data->playerList[i]._pHitPoints = -1; + data->playerList[i]._pMaxHP = -1; + data->playerList[i]._pMana = -1; + data->playerList[i]._pMaxMana = -1; + data->playerList[i]._pLevel = devilution::Players[i].getCharacterLevel(); + data->playerList[i]._pExperience = -1; + + data->playerList[i]._pArmorClass = -1; + + data->playerList[i]._pMagResist = -1; + data->playerList[i]._pFireResist = -1; + data->playerList[i]._pLightResist = -1; + data->playerList[i]._pIBonusToHit = -1; + + data->playerList[i]._pGold = -1; + for (int j = 0; j < NUM_INVLOC; j++) + data->playerList[i].InvBody[j] = -1; + for (int j = 0; j < MAXINV; j++) + data->playerList[i].InvGrid[j] = -1; + for (int j = 0; j < MAXSPD; j++) + data->playerList[i].SpdList[j] = -1; + data->playerList[i].HoldItem = -1; + + data->playerList[i]._pIMinDam = -1; + data->playerList[i]._pIMaxDam = -1; + data->playerList[i]._pIBonusDam = -1; + data->playerList[i]._pIAC = -1; + data->playerList[i]._pIBonusToHit = -1; + data->playerList[i]._pIBonusAC = -1; + data->playerList[i]._pIBonusDamMod = -1; + data->playerList[i]._pISplLvlAdd = -1; + data->playerList[i].pManaShield = false; + } + + playerData->set__pmode(data->playerList[i]._pmode); + playerData->set_plrlevel(data->playerList[i].plrlevel); + playerData->set__px(data->playerList[i]._px); + playerData->set__py(data->playerList[i]._py); + playerData->set__pfutx(data->playerList[i]._pfutx); + playerData->set__pfuty(data->playerList[i]._pfuty); + playerData->set__pdir(data->playerList[i]._pdir); + + playerData->set__prspell(data->playerList[i]._pRSpell); + playerData->set__prspltype(data->playerList[i]._pRSplType); + + for (int j = 0; j < 64; j++) + playerData->add__pspllvl(data->playerList[i]._pSplLvl[j]); + playerData->set__pmemspells(data->playerList[i]._pMemSpells); + playerData->set__pablspells(data->playerList[i]._pAblSpells); + playerData->set__pscrlspells(data->playerList[i]._pScrlSpells); + + playerData->set__pclass(data->playerList[i]._pClass); + + playerData->set__pstrength(data->playerList[i]._pStrength); + playerData->set__pbasestr(data->playerList[i]._pBaseStr); + playerData->set__pmagic(data->playerList[i]._pMagic); + playerData->set__pbasemag(data->playerList[i]._pBaseMag); + playerData->set__pdexterity(data->playerList[i]._pDexterity); + playerData->set__pbasedex(data->playerList[i]._pBaseDex); + playerData->set__pvitality(data->playerList[i]._pVitality); + playerData->set__pbasevit(data->playerList[i]._pBaseVit); + + playerData->set__pstatpts(data->playerList[i]._pStatPts); + + playerData->set__pdamagemod(data->playerList[i]._pDamageMod); + + playerData->set__phitpoints(data->playerList[i]._pHitPoints); + playerData->set__pmaxhp(data->playerList[i]._pMaxHP); + playerData->set__pmana(data->playerList[i]._pMana); + playerData->set__pmaxmana(data->playerList[i]._pMaxMana); + playerData->set__plevel(data->playerList[i]._pLevel); + playerData->set__pexperience(data->playerList[i]._pExperience); + + playerData->set__parmorclass(data->playerList[i]._pArmorClass); + + playerData->set__pmagresist(data->playerList[i]._pMagResist); + playerData->set__pfireresist(data->playerList[i]._pFireResist); + playerData->set__plightresist(data->playerList[i]._pLightResist); + + playerData->set__pgold(data->playerList[i]._pGold); + for (int j = 0; j < NUM_INVLOC; j++) + playerData->add_invbody(data->playerList[i].InvBody[j]); + for (int j = 0; j < MAXINV; j++) + playerData->add_invlist(data->playerList[i].InvList[j]); + for (int j = 0; j < MAXINV; j++) + playerData->add_invgrid(data->playerList[i].InvGrid[j]); + for (int j = 0; j < MAXSPD; j++) + playerData->add_spdlist(data->playerList[i].SpdList[j]); + playerData->set_holditem(data->playerList[i].HoldItem); + + playerData->set__pimindam(data->playerList[i]._pIMinDam); + playerData->set__pimaxdam(data->playerList[i]._pIMaxDam); + playerData->set__pibonusdam(data->playerList[i]._pIBonusDam); + playerData->set__piac(data->playerList[i]._pIAC); + playerData->set__pibonustohit(data->playerList[i]._pIBonusToHit); + playerData->set__pibonusac(data->playerList[i]._pIBonusAC); + playerData->set__pibonusdammod(data->playerList[i]._pIBonusDamMod); + playerData->set__pispllvladd(data->playerList[i]._pISplLvlAdd); + playerData->set_pmanashield(data->playerList[i].pManaShield); + } + + data->stashItems.clear(); + for (auto &stashItem : devilution::Stash.stashList) { + if (stashItem.isEmpty()) { + continue; + } + size_t itemID = data->itemList.size(); + for (size_t k = 0; k < data->itemList.size(); k++) { + if (data->itemList[k].compare(stashItem)) { + itemID = k; + break; + } + } + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + fullFillItemInfo(itemID, &stashItem); + data->stashItems.push_back(itemID); + update->add_stashitems(itemID); + } + + data->stashGold = devilution::Stash.gold; + update->set_stashgold(devilution::Stash.gold); + + auto emptyFillItemInfo = [&](int itemID, devilution::Item *item) { + itemsModified.push_back(itemID); + + data->itemList[itemID].ID = itemID; + data->itemList[itemID]._iSeed = item->_iSeed; + data->itemList[itemID]._iCreateInfo = item->_iCreateInfo; + data->itemList[itemID]._itype = static_cast(item->_itype); + data->itemList[itemID]._ix = -1; + data->itemList[itemID]._iy = -1; + + data->itemList[itemID]._iIdentified = -1; + data->itemList[itemID]._iMagical = -1; + strcpy(data->itemList[itemID]._iName, ""); + strcpy(data->itemList[itemID]._iIName, ""); + data->itemList[itemID]._iFlags = -1; + data->itemList[itemID]._iPrePower = -1; + data->itemList[itemID]._iSufPower = -1; + data->itemList[itemID]._iPLDam = -1; + data->itemList[itemID]._iPLToHit = -1; + data->itemList[itemID]._iPLAC = -1; + data->itemList[itemID]._iPLStr = -1; + data->itemList[itemID]._iPLMag = -1; + data->itemList[itemID]._iPLDex = -1; + data->itemList[itemID]._iPLVit = -1; + data->itemList[itemID]._iPLFR = -1; + data->itemList[itemID]._iPLLR = -1; + data->itemList[itemID]._iPLMR = -1; + data->itemList[itemID]._iPLMana = -1; + data->itemList[itemID]._iPLHP = -1; + data->itemList[itemID]._iPLDamMod = -1; + data->itemList[itemID]._iPLGetHit = -1; + data->itemList[itemID]._iPLLight = -1; + data->itemList[itemID]._iSplLvlAdd = -1; + data->itemList[itemID]._iFMinDam = -1; + data->itemList[itemID]._iFMaxDam = -1; + data->itemList[itemID]._iLMinDam = -1; + data->itemList[itemID]._iLMaxDam = -1; + data->itemList[itemID]._iClass = -1; + data->itemList[itemID]._ivalue = -1; + data->itemList[itemID]._iMinDam = -1; + data->itemList[itemID]._iMaxDam = -1; + data->itemList[itemID]._iAC = -1; + data->itemList[itemID]._iMiscId = -1; + data->itemList[itemID]._iSpell = -1; + + data->itemList[itemID]._iCharges = -1; + data->itemList[itemID]._iMaxCharges = -1; + + data->itemList[itemID]._iDurability = -1; + data->itemList[itemID]._iMaxDur = -1; + + data->itemList[itemID]._iMinStr = -1; + data->itemList[itemID]._iMinMag = -1; + data->itemList[itemID]._iMinDex = -1; + data->itemList[itemID]._iStatFlag = -1; + data->itemList[itemID].IDidx = item->IDidx; + }; + + for (int i = 0; i < devilution::ActiveItemCount; i++) { + size_t itemID = static_cast(data->itemList.size()); + for (size_t j = 0; j < data->itemList.size(); j++) { + if (data->itemList[j].compare(devilution::Items[devilution::ActiveItems[i]])) { + itemID = j; + break; + } + } + + if (devilution::dItem[devilution::Items[devilution::ActiveItems[i]].position.x][devilution::Items[devilution::ActiveItems[i]].position.y] != static_cast(devilution::ActiveItems[i] + 1)) + continue; + if (itemID == data->itemList.size()) + data->itemList.push_back(ItemData {}); + int dx = devilution::Players[devilution::MyPlayerId].position.tile.x - devilution::Items[devilution::ActiveItems[i]].position.x; + int dy = devilution::Players[devilution::MyPlayerId].position.tile.y - devilution::Items[devilution::ActiveItems[i]].position.y; + if (dy > 0) { + if (dx < 1 && abs(dx) + abs(dy) < 11) { + partialFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + data->groundItems.push_back(itemID); + } else if (dx > 0 && abs(dx) + abs(dy) < 12) { + partialFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + data->groundItems.push_back(itemID); + } else + emptyFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + } else { + if ((dx > -1 || dy == 0) && abs(dx) + abs(dy) < 11) { + partialFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + data->groundItems.push_back(itemID); + } else if ((dx < 0 && dy != 0) && abs(dx) + abs(dy) < 12) { + partialFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + data->groundItems.push_back(itemID); + } else + emptyFillItemInfo(itemID, &devilution::Items[devilution::ActiveItems[i]]); + } + } + + data->storeItems.clear(); + int storeLoopMax = 0; + devilution::Item *currentItem; + bool useiValue = false; + bool shiftValue = false; + switch (devilution::ActiveStore) { + case devilution::TalkID::StorytellerIdentify: + case devilution::TalkID::WitchSell: + case devilution::TalkID::WitchRecharge: + case devilution::TalkID::SmithSell: + case devilution::TalkID::SmithRepair: + storeLoopMax = devilution::CurrentItemIndex; + currentItem = &devilution::PlayerItems[0]; + useiValue = true; + break; + case devilution::TalkID::WitchBuy: + storeLoopMax = devilution::WitchItems.size(); + currentItem = &devilution::WitchItems[0]; + break; + case devilution::TalkID::SmithBuy: + storeLoopMax = devilution::SmithItems.size(); + currentItem = &devilution::SmithItems[0]; + useiValue = true; + break; + case devilution::TalkID::HealerBuy: + storeLoopMax = devilution::HealerItems.size(); + currentItem = &devilution::HealerItems[0]; + useiValue = true; + break; + case devilution::TalkID::SmithPremiumBuy: + storeLoopMax = devilution::PremiumItems.size(); + currentItem = &devilution::PremiumItems[0]; + break; + case devilution::TalkID::BoyBuy: + storeLoopMax = 1; + currentItem = &devilution::BoyItem; + shiftValue = true; + break; + default: + break; + } + for (int i = 0; i < storeLoopMax; i++) { + if (currentItem->_itype != devilution::ItemType::None) { + int itemID = static_cast(data->itemList.size()); + + for (auto &item : data->itemList) { + if (item.compare(*currentItem)) { + itemID = item.ID; + break; + } + } + if (itemID == static_cast(data->itemList.size())) { + data->itemList.push_back(ItemData {}); + fullFillItemInfo(itemID, currentItem); + } + if (useiValue) + data->itemList[itemID]._ivalue = currentItem->_ivalue; + else if (shiftValue) + data->itemList[itemID]._ivalue = currentItem->_iIvalue + (currentItem->_iIvalue >> 1); + else + data->itemList[itemID]._ivalue = currentItem->_iIvalue; + if (data->itemList[itemID]._ivalue != 0) { + data->storeItems.push_back(itemID); + update->add_storeitems(itemID); + } + } + currentItem++; + } + + if (devilution::currlevel != 0) { + for (auto &[_, townerData] : data->townerList) { + strcpy(townerData._tName, ""); + townerData._tx = -1; + townerData._ty = -1; + } + } else { + for (auto &towner : devilution::Towners) + for (auto i = 0; i < devilution::Towners.size(); i++) { + auto townerID = i; + auto &towner = data->townerList[townerID]; + towner.ID = static_cast(townerID); + if (isOnScreen(devilution::Towners[i].position.x, devilution::Towners[i].position.y)) { + towner._ttype = devilution::Towners[i]._ttype; + towner._tx = devilution::Towners[i].position.x; + towner._ty = devilution::Towners[i].position.y; + // might rework this and just change the type in data. + if (devilution::Towners[i].name.size() < 31) { + memcpy(towner._tName, devilution::Towners[i].name.data(), devilution::Towners[i].name.size()); + towner._tName[devilution::Towners[i].name.size()] = '\0'; + } + // strcpy(towner._tName, devilution::Towners[i].name); old code but with devilution subbed in for reference. + } else { + towner._ttype = devilution::Towners[i]._ttype; + towner._tx = -1; + towner._ty = -1; + strcpy(towner._tName, ""); + } + } + } + + for (auto &[_, townie] : data->townerList) { + auto townerData = update->add_townerdata(); + townerData->set_id(townie.ID); + if (townie._tx != -1) + townerData->set__ttype(static_cast(townie._ttype)); + else + townerData->set__ttype(-1); + townerData->set__tx(townie._tx); + townerData->set__ty(townie._ty); + townerData->set__tname(townie._tName); + } + + for (auto &itemID : itemsModified) + // for (auto& item : data->itemList) + { + auto &item = data->itemList[itemID]; + auto itemData = update->add_itemdata(); + itemData->set_id(item.ID); + itemData->set__itype(item._itype); + itemData->set__ix(item._ix); + itemData->set__iy(item._iy); + itemData->set__iidentified(item._iIdentified); + itemData->set__imagical(item._iMagical); + itemData->set__iname(item._iName); + itemData->set__iiname(item._iIName); + itemData->set__iclass(item._iClass); + itemData->set__icurs(item._iCurs); + itemData->set__ivalue(item._ivalue); + itemData->set__imindam(item._iMinDam); + itemData->set__imaxdam(item._iMaxDam); + itemData->set__iac(item._iAC); + itemData->set__iflags(item._iFlags); + itemData->set__imiscid(item._iMiscId); + itemData->set__ispell(item._iSpell); + itemData->set__icharges(item._iCharges); + itemData->set__imaxcharges(item._iMaxCharges); + itemData->set__idurability(item._iDurability); + itemData->set__imaxdur(item._iMaxDur); + itemData->set__ipldam(item._iPLDam); + itemData->set__ipltohit(item._iPLToHit); + itemData->set__iplac(item._iPLAC); + itemData->set__iplstr(item._iPLStr); + itemData->set__iplmag(item._iPLMag); + itemData->set__ipldex(item._iPLDex); + itemData->set__iplvit(item._iPLVit); + itemData->set__iplfr(item._iPLFR); + itemData->set__ipllr(item._iPLLR); + itemData->set__iplmr(item._iPLMR); + itemData->set__iplmana(item._iPLMana); + itemData->set__iplhp(item._iPLHP); + itemData->set__ipldammod(item._iPLDamMod); + itemData->set__iplgethit(item._iPLGetHit); + itemData->set__ipllight(item._iPLLight); + itemData->set__ispllvladd(item._iSplLvlAdd); + itemData->set__ifmindam(item._iFMinDam); + itemData->set__ifmaxdam(item._iFMaxDam); + itemData->set__ilmindam(item._iLMinDam); + itemData->set__ilmaxdam(item._iLMaxDam); + itemData->set__iprepower(item._iPrePower); + itemData->set__isufpower(item._iSufPower); + itemData->set__iminstr(item._iMinStr); + itemData->set__iminmag(item._iMinMag); + itemData->set__imindex(item._iMinDex); + itemData->set__istatflag(item._iStatFlag); + itemData->set_ididx(item.IDidx); + } + for (auto &itemID : data->groundItems) + update->add_grounditemid(itemID); + + for (size_t i = 0; i < devilution::ActiveMonsterCount; i++) { + if (isOnScreen(devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.x, devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.y) && devilution::HasAnyOf(devilution::dFlags[devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.x][devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.y], devilution::DungeonFlag::Lit) && !(devilution::Monsters[devilution::ActiveMonsters[i]].flags & 0x01)) { + auto m = update->add_monsterdata(); + m->set_index(devilution::ActiveMonsters[i]); + m->set_x(devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.x); + m->set_y(devilution::Monsters[devilution::ActiveMonsters[i]].position.tile.y); + m->set_futx(devilution::Monsters[devilution::ActiveMonsters[i]].position.future.x); + m->set_futy(devilution::Monsters[devilution::ActiveMonsters[i]].position.future.y); + m->set_type(devilution::Monsters[devilution::ActiveMonsters[i]].type().type); + std::string monsterName = std::string(devilution::Monsters[devilution::ActiveMonsters[i]].name()); + m->set_name(monsterName.c_str()); + m->set_mode(static_cast(devilution::Monsters[devilution::ActiveMonsters[i]].mode)); + m->set_unique(devilution::Monsters[devilution::ActiveMonsters[i]].isUnique()); + } + } + + auto fillObject = [&](int index, devilution::Object &ob) { + auto o = update->add_objectdata(); + o->set_type(ob._otype); + o->set_x(ob.position.x); + o->set_y(ob.position.y); + o->set_shrinetype(-1); + o->set_solid(ob._oSolidFlag); + o->set_doorstate(-1); + o->set_selectable(ob.selectionRegion != devilution::SelectionRegion::None ? true : false); + o->set_index(index); + switch (static_cast(ob._otype)) { + case devilution::_object_id::OBJ_BARRELEX: + o->set_type(static_cast(devilution::_object_id::OBJ_BARREL)); + break; + case devilution::_object_id::OBJ_SHRINEL: + case devilution::_object_id::OBJ_SHRINER: + if (ob.selectionRegion != devilution::SelectionRegion::None) + o->set_shrinetype(ob._oVar1); + break; + case devilution::_object_id::OBJ_L1LDOOR: + case devilution::_object_id::OBJ_L1RDOOR: + case devilution::_object_id::OBJ_L2LDOOR: + case devilution::_object_id::OBJ_L2RDOOR: + case devilution::_object_id::OBJ_L3LDOOR: + case devilution::_object_id::OBJ_L3RDOOR: + o->set_doorstate(ob._oVar4); + break; + default: + break; + } + if (devilution::Players[devilution::MyPlayerId]._pClass == devilution::HeroClass::Rogue) + o->set_trapped(ob._oTrapFlag); + else + o->set_trapped(false); + }; + + auto fillMissile = [&](devilution::Missile &ms) { + auto m = update->add_missiledata(); + m->set_type(static_cast(ms._mitype)); + m->set_x(ms.position.tile.x); + m->set_y(ms.position.tile.y); + m->set_xvel(ms.position.velocity.deltaX); + m->set_yvel(ms.position.velocity.deltaY); + switch (ms.sourceType()) { + case devilution::MissileSource::Monster: + if (isOnScreen(ms.sourceMonster()->position.tile.x, ms.sourceMonster()->position.tile.y)) { + m->set_sx(ms.sourceMonster()->position.tile.x); + m->set_sy(ms.sourceMonster()->position.tile.y); + } else { + m->set_sx(-1); + m->set_sy(-1); + } + break; + case devilution::MissileSource::Player: + if (isOnScreen(ms.sourcePlayer()->position.tile.x, ms.sourcePlayer()->position.tile.y)) { + m->set_sx(ms.sourcePlayer()->position.tile.x); + m->set_sy(ms.sourcePlayer()->position.tile.y); + } else { + m->set_sx(-1); + m->set_sy(-1); + } + break; + default: + m->set_sx(-1); + m->set_sy(-1); + } + }; + + if (devilution::Players[devilution::MyPlayerId].plrlevel != 0) { + for (int i = 0; i < devilution::ActiveObjectCount; i++) { + if (isOnScreen(devilution::Objects[devilution::ActiveObjects[i]].position.x, devilution::Objects[devilution::ActiveObjects[i]].position.y) && devilution::dObject[devilution::Objects[devilution::ActiveObjects[i]].position.x][devilution::Objects[devilution::ActiveObjects[i]].position.y] == devilution::ActiveObjects[i] + 1) { + fillObject(devilution::ActiveObjects[i], devilution::Objects[devilution::ActiveObjects[i]]); + } + } + + for (auto &missile : devilution::Missiles) { + if (isOnScreen(missile.position.tile.x, missile.position.tile.y)) + fillMissile(missile); + } + } + + for (int i = 0; i < MAXPORTAL; i++) { + if (devilution::Portals[i].open && (devilution::Portals[i].level == devilution::Players[devilution::MyPlayerId].plrlevel) && isOnScreen(devilution::Portals[i].position.x, devilution::Portals[i].position.y)) { + auto tp = update->add_portaldata(); + tp->set_x(devilution::Portals[i].position.x); + tp->set_y(devilution::Portals[i].position.y); + tp->set_player(i); + } else if (devilution::Portals[i].open && 0 == devilution::Players[devilution::MyPlayerId].plrlevel && isOnScreen(devilution::PortalTownPosition[i].x, devilution::PortalTownPosition[i].y)) { + auto tp = update->add_portaldata(); + tp->set_x(devilution::PortalTownPosition[i].x); + tp->set_y(devilution::PortalTownPosition[i].y); + tp->set_player(i); + } + } + + protoClient.queueMessage(std::move(message)); +} + +bool Server::isOnScreen(int x, int y) +{ + bool returnValue = false; + int dx = data->playerList[devilution::MyPlayerId]._px - x; + int dy = data->playerList[devilution::MyPlayerId]._py - y; + if (!devilution::CharFlag) { + if (dy > 0) { + if (dx < 1 && abs(dx) + abs(dy) < 11) + returnValue = true; + else if (dx > 0 && abs(dx) + abs(dy) < 12) + returnValue = true; + } else { + if ((dx > -1 || dy == 0) && abs(dx) + abs(dy) < 11) + returnValue = true; + else if ((dx < 0 && dy != 0) && abs(dx) + abs(dy) < 12) + returnValue = true; + } + } else if (devilution::CharFlag) { + returnValue = panelScreenCheck[std::make_pair(dx, dy)]; + } + return returnValue; +} + +bool Server::OKToAct() +{ + return devilution::ActiveStore == devilution::TalkID::None && devilution::pcurs == 1 && !data->qtextflag; +} + +void Server::move(int x, int y) +{ + devilution::NetSendCmdLoc(devilution::MyPlayerId, true, devilution::_cmd_id::CMD_WALKXY, devilution::Point { x, y }); +} + +void Server::talk(int x, int y) +{ + int index; + for (index = 0; index < devilution::Towners.size(); index++) { + if (devilution::Towners[index].position.x == x && devilution::Towners[index].position.y == y) + break; + } + if (index != devilution::Towners.size()) + devilution::NetSendCmdLocParam1(true, devilution::_cmd_id::CMD_TALKXY, devilution::Point { x, y }, index); +} + +void Server::selectStoreOption(StoreOption option) +{ + // Need to check for qtextflag in DevilutionX, for some reason we can access stores when + // the shop keeper is giving us quest text. Doesn't happen in 1.09. + if (devilution::qtextflag) + return; + + switch (option) { + case StoreOption::TALK: + if (devilution::ActiveStore == devilution::TalkID::Witch) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::OldTextLine = 12; + devilution::TownerId = devilution::_talker_id::TOWN_WITCH; + devilution::OldActiveStore = devilution::TalkID::Witch; + devilution::StartStore(devilution::TalkID::Gossip); + } + break; + case StoreOption::IDENTIFYANITEM: + if (devilution::ActiveStore == devilution::TalkID::Storyteller) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::StorytellerIdentify); + } + break; + case StoreOption::EXIT: + switch (devilution::ActiveStore) { + case devilution::TalkID::Barmaid: + case devilution::TalkID::Boy: + case devilution::TalkID::BoyBuy: + case devilution::TalkID::Drunk: + case devilution::TalkID::Healer: + case devilution::TalkID::Smith: + case devilution::TalkID::Storyteller: + case devilution::TalkID::Tavern: + case devilution::TalkID::Witch: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::ActiveStore = devilution::TalkID::None; + default: + break; + } + break; + case StoreOption::BUYITEMS: + switch (devilution::ActiveStore) { + case devilution::TalkID::Witch: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::WitchBuy); + break; + case devilution::TalkID::Healer: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::HealerBuy); + break; + default: + break; + } + break; + case StoreOption::BUYBASIC: + if (devilution::ActiveStore == devilution::TalkID::Smith) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::SmithBuy); + } + break; + case StoreOption::BUYPREMIUM: + if (devilution::ActiveStore == devilution::TalkID::Smith) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::SmithPremiumBuy); + } + break; + case StoreOption::SELL: + switch (devilution::ActiveStore) { + case devilution::TalkID::Smith: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::SmithSell); + break; + case devilution::TalkID::Witch: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::WitchSell); + break; + default: + break; + } + break; + case StoreOption::REPAIR: + if (devilution::ActiveStore == devilution::TalkID::Smith) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::SmithRepair); + } + break; + case StoreOption::RECHARGE: + if (devilution::ActiveStore == devilution::TalkID::Witch) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::WitchRecharge); + } + break; + case StoreOption::WIRTPEEK: + if (devilution::ActiveStore == devilution::TalkID::Boy) { + if (devilution::PlayerCanAfford(50)) { + devilution::TakePlrsMoney(50); + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::BoyBuy); + } else { + devilution::OldActiveStore = devilution::TalkID::Boy; + devilution::OldTextLine = 18; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::StartStore(devilution::TalkID::NoMoney); + } + } + break; + case StoreOption::BACK: + switch (devilution::ActiveStore) { + case devilution::TalkID::SmithRepair: + case devilution::TalkID::SmithSell: + case devilution::TalkID::SmithBuy: + case devilution::TalkID::SmithPremiumBuy: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::Smith); + break; + case devilution::TalkID::WitchBuy: + case devilution::TalkID::WitchSell: + case devilution::TalkID::WitchRecharge: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::Witch); + break; + case devilution::TalkID::HealerBuy: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::Healer); + break; + case devilution::TalkID::StorytellerIdentify: + devilution::PlaySFX(devilution::SfxID::MenuSelect); + devilution::StartStore(devilution::TalkID::Storyteller); + break; + default: + break; + } + break; + case StoreOption::ACCESSSTORAGE: + if (devilution::ActiveStore != devilution::TalkID::Barmaid) + break; + devilution::ActiveStore = devilution::TalkID::None; + devilution::IsStashOpen = true; + devilution::Stash.RefreshItemStatFlags(); + devilution::invflag = true; + break; + default: + break; + } +} + +void Server::buyItem(int itemID) +{ + int idx; + + if (devilution::ActiveStore == devilution::TalkID::WitchBuy) { + + idx = -1; + + for (int i = 0; i < devilution::WitchItems.size(); i++) { + if (data->itemList[itemID].compare(devilution::WitchItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::OldActiveStore = devilution::ActiveStore; + + if (!devilution::PlayerCanAfford(devilution::WitchItems[idx]._iIvalue)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else if (!devilution::StoreAutoPlace(devilution::WitchItems[idx], false)) { + devilution::StartStore(devilution::TalkID::NoRoom); + return; + } else { + if (idx < 3) + devilution::WitchItems[idx]._iSeed = devilution::AdvanceRndSeed(); + + devilution::TakePlrsMoney(devilution::WitchItems[idx]._iIvalue); + devilution::StoreAutoPlace(devilution::WitchItems[idx], true); + + if (idx >= 3) { + devilution::WitchItems.erase(devilution::WitchItems.begin() + idx); + } + + devilution::CalcPlrInv(*devilution::MyPlayer, true); + } + devilution::StartStore(devilution::OldActiveStore); + } else if (devilution::ActiveStore == devilution::TalkID::SmithBuy) { + idx = -1; + + for (int i = 0; i < devilution::SmithItems.size(); i++) { + if (data->itemList[itemID].compare(devilution::SmithItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::OldActiveStore = devilution::TalkID::SmithBuy; + + if (!devilution::PlayerCanAfford(devilution::SmithItems[idx]._iIvalue)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else if (!devilution::StoreAutoPlace(devilution::SmithItems[idx], false)) { + devilution::StartStore(devilution::TalkID::NoRoom); + } else { + devilution::TakePlrsMoney(devilution::SmithItems[idx]._iIvalue); + if (devilution::SmithItems[idx]._iMagical == devilution::item_quality::ITEM_QUALITY_NORMAL) + devilution::SmithItems[idx]._iIdentified = false; + devilution::StoreAutoPlace(devilution::SmithItems[idx], true); + devilution::SmithItems.erase(devilution::SmithItems.begin() + idx); + devilution::CalcPlrInv(*devilution::MyPlayer, true); + devilution::StartStore(devilution::OldActiveStore); + } + } else if (devilution::ActiveStore == devilution::TalkID::SmithPremiumBuy) { + int idx = -1; + for (int i = 0; i < devilution::PremiumItems.size(); i++) { + if (data->itemList[itemID].compare(devilution::PremiumItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::OldActiveStore = devilution::TalkID::SmithBuy; + + if (!devilution::PlayerCanAfford(devilution::PremiumItems[idx]._iIvalue)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else if (!devilution::StoreAutoPlace(devilution::PremiumItems[idx], false)) { + devilution::StartStore(devilution::TalkID::NoRoom); + return; + } else { + devilution::TakePlrsMoney(devilution::PremiumItems[idx]._iIvalue); + if (devilution::PremiumItems[idx]._iMagical == devilution::item_quality::ITEM_QUALITY_NORMAL) + devilution::PremiumItems[idx]._iIdentified = false; + devilution::StoreAutoPlace(devilution::PremiumItems[idx], true); + devilution::ReplacePremium(*devilution::MyPlayer, idx); + devilution::CalcPlrInv(*devilution::MyPlayer, true); + devilution::StartStore(devilution::OldActiveStore); + } + } else if (devilution::ActiveStore == devilution::TalkID::HealerBuy) { + idx = -1; + + for (int i = 0; i < devilution::HealerItems.size(); i++) { + if (data->itemList[itemID].compare(devilution::HealerItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::OldActiveStore = devilution::TalkID::HealerBuy; + + if (!devilution::PlayerCanAfford(devilution::HealerItems[idx]._iIvalue)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else if (!devilution::StoreAutoPlace(devilution::HealerItems[idx], false)) { + devilution::StartStore(devilution::TalkID::NoRoom); + return; + } else { + if (!devilution::gbIsMultiplayer) { + if (idx < 2) + devilution::HealerItems[idx]._iSeed = devilution::AdvanceRndSeed(); + } else { + if (idx < 3) + devilution::HealerItems[idx]._iSeed = devilution::AdvanceRndSeed(); + } + + devilution::TakePlrsMoney(devilution::HealerItems[idx]._iIvalue); + if (devilution::HealerItems[idx]._iMagical == devilution::item_quality::ITEM_QUALITY_NORMAL) + devilution::HealerItems[idx]._iIdentified = false; + devilution::StoreAutoPlace(devilution::HealerItems[idx], true); + + if (!devilution::gbIsMultiplayer) { + if (idx < 2) + return; + } else { + if (idx < 3) + return; + } + devilution::HealerItems.erase(devilution::HealerItems.begin() + idx); + CalcPlrInv(*devilution::MyPlayer, true); + devilution::StartStore(devilution::OldActiveStore); + } + } else if (devilution::ActiveStore == devilution::TalkID::BoyBuy) { + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldActiveStore = devilution::TalkID::BoyBuy; + devilution::OldScrollPos = devilution::ScrollPos; + devilution::OldTextLine = 10; + + int price = devilution::BoyItem._iIvalue; + if (devilution::gbIsHellfire) + price -= devilution::BoyItem._iIvalue / 4; + else + price += devilution::BoyItem._iIvalue / 2; + + if (!devilution::PlayerCanAfford(price)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else if (!devilution::StoreAutoPlace(devilution::BoyItem, false)) { + devilution::StartStore(devilution::TalkID::NoRoom); + return; + } else { + devilution::TakePlrsMoney(price); + devilution::StoreAutoPlace(devilution::BoyItem, true); + devilution::BoyItem.clear(); + devilution::OldActiveStore = devilution::TalkID::Boy; + devilution::CalcPlrInv(*devilution::MyPlayer, true); + devilution::OldTextLine = 12; + devilution::StartStore(devilution::OldActiveStore); + } + } +} + +void Server::sellItem(int itemID) +{ + int idx; + + if (devilution::ActiveStore != devilution::TalkID::WitchSell && devilution::ActiveStore != devilution::TalkID::SmithSell) + return; + + idx = -1; + + for (int i = 0; i < 48; i++) { + if (data->itemList[itemID].compare(devilution::PlayerItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldActiveStore = devilution::ActiveStore; + devilution::OldScrollPos = devilution::ScrollPos; + + if (!devilution::StoreGoldFit(devilution::PlayerItems[idx])) { + devilution::StartStore(devilution::TalkID::NoRoom); + return; + } + + devilution::Player &myPlayer = *devilution::MyPlayer; + + if (devilution::PlayerItemIndexes[idx] >= 0) + myPlayer.RemoveInvItem(devilution::PlayerItemIndexes[idx]); + else + myPlayer.RemoveSpdBarItem(-(devilution::PlayerItemIndexes[idx] + 1)); + + const int cost = devilution::PlayerItems[idx]._iIvalue; + devilution::CurrentItemIndex--; + if (idx != devilution::CurrentItemIndex) { + while (idx < devilution::CurrentItemIndex) { + devilution::PlayerItems[idx] = devilution::PlayerItems[idx + 1]; + devilution::PlayerItemIndexes[idx] = devilution::PlayerItemIndexes[idx + 1]; + idx++; + } + } + + devilution::AddGoldToInventory(myPlayer, cost); + + myPlayer._pGold += cost; + devilution::StartStore(devilution::OldActiveStore); +} + +void Server::rechargeItem(int itemID) +{ + int idx; + + if (devilution::ActiveStore != devilution::TalkID::WitchRecharge) + return; + + idx = -1; + + for (int i = 0; i < 20; i++) { + if (data->itemList[itemID].compare(devilution::PlayerItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldActiveStore = devilution::TalkID::WitchRecharge; + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + + int price = devilution::PlayerItems[idx]._iIvalue; + + if (!devilution::PlayerCanAfford(price)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else { + devilution::PlayerItems[idx]._iCharges = devilution::PlayerItems[idx]._iMaxCharges; + + devilution::Player &myPlayer = *devilution::MyPlayer; + + int8_t i = devilution::PlayerItemIndexes[idx]; + if (i < 0) { + myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._iCharges = myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._iMaxCharges; + devilution::NetSendCmdChItem(true, devilution::inv_body_loc::INVLOC_HAND_LEFT); + } else { + myPlayer.InvList[i]._iCharges = myPlayer.InvList[i]._iMaxCharges; + devilution::NetSyncInvItem(myPlayer, i); + } + + devilution::TakePlrsMoney(price); + devilution::CalcPlrInv(myPlayer, true); + devilution::StartStore(devilution::OldActiveStore); + } +} + +void Server::repairItem(int itemID) +{ + int idx; + + if (devilution::ActiveStore != devilution::TalkID::SmithRepair) + return; + + idx = -1; + + for (int i = 0; i < 20; i++) { + if (data->itemList[itemID].compare(devilution::PlayerItems[i])) { + idx = i; + break; + } + } + + if (idx == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldActiveStore = devilution::TalkID::SmithRepair; + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + + int price = devilution::PlayerItems[idx]._iIvalue; + + if (!devilution::PlayerCanAfford(price)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } + + devilution::PlayerItems[idx]._iDurability = devilution::PlayerItems[idx]._iMaxDur; + + int8_t i = devilution::PlayerItemIndexes[idx]; + + devilution::Player &myPlayer = *devilution::MyPlayer; + + if (i < 0) { + if (i == -1) + myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HEAD]._iDurability = myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HEAD]._iMaxDur; + if (i == -2) + myPlayer.InvBody[devilution::inv_body_loc::INVLOC_CHEST]._iDurability = myPlayer.InvBody[devilution::inv_body_loc::INVLOC_CHEST]._iMaxDur; + if (i == -3) + myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._iDurability = myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._iMaxDur; + if (i == -4) + myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_RIGHT]._iDurability = myPlayer.InvBody[devilution::inv_body_loc::INVLOC_HAND_RIGHT]._iMaxDur; + devilution::TakePlrsMoney(price); + devilution::StartStore(devilution::OldActiveStore); + return; + } + + myPlayer.InvList[i]._iDurability = myPlayer.InvList[i]._iMaxDur; + devilution::TakePlrsMoney(price); + devilution::StartStore(devilution::OldActiveStore); +} + +void Server::attackMonster(int index) +{ + if (index < 0 || 199 < index) + return; + + if (!OKToAct()) + return; + + if (!isOnScreen(devilution::Monsters[index].position.tile.x, devilution::Monsters[index].position.tile.y)) + return; + + if (devilution::M_Talker(devilution::Monsters[index])) + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ATTACKID, index); + else if (devilution::Players[devilution::MyPlayerId].InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._itype == devilution::ItemType::Bow || devilution::Players[devilution::MyPlayerId].InvBody[devilution::inv_body_loc::INVLOC_HAND_RIGHT]._itype == devilution::ItemType::Bow) + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_RATTACKID, index); + else + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ATTACKID, index); +} + +void Server::attackXY(int x, int y) +{ + if (!isOnScreen(x, y)) + return; + + if (!OKToAct()) + return; + + if (devilution::Players[devilution::MyPlayerId].InvBody[devilution::inv_body_loc::INVLOC_HAND_LEFT]._itype == devilution::ItemType::Bow || devilution::Players[devilution::MyPlayerId].InvBody[devilution::inv_body_loc::INVLOC_HAND_RIGHT]._itype == devilution::ItemType::Bow) + devilution::NetSendCmdLoc(devilution::MyPlayerId, true, devilution::_cmd_id::CMD_RATTACKXY, devilution::Point { x, y }); + else + devilution::NetSendCmdLoc(devilution::MyPlayerId, true, devilution::_cmd_id::CMD_SATTACKXY, devilution::Point { x, y }); +} + +void Server::operateObject(int index) +{ + if (index < 0 || 126 < index) + return; + + if (!isOnScreen(devilution::Objects[index].position.x, devilution::Objects[index].position.y)) + return; + + if (!OKToAct()) + return; + + bool found = false; + for (int i = 0; i < devilution::ActiveObjectCount; i++) { + if (devilution::ActiveObjects[i] == index) { + found = true; + break; + } + } + + if (!found) + return; + + devilution::NetSendCmdLoc(devilution::MyPlayerId, true, devilution::_cmd_id::CMD_OPOBJXY, devilution::Point { devilution::Objects[index].position.x, devilution::Objects[index].position.y }); +} + +void Server::useBeltItem(int slot) +{ + if (slot < 0 || 7 < slot) + return; + + if (devilution::Players[devilution::MyPlayerId].SpdList[slot]._itype == devilution::ItemType::None) + return; + + int cii = slot + 47; + devilution::UseInvItem(cii); +} + +void Server::toggleCharacterScreen() +{ + if (!OKToAct()) + return; + + devilution::CharFlag = !devilution::CharFlag; + devilution::IsStashOpen = false; +} + +void Server::increaseStat(CommandType commandType) +{ + int maxValue = 0; + + if (devilution::Players[devilution::MyPlayerId]._pStatPts == 0) + return; + + switch (commandType) { + case CommandType::ADDSTR: + switch (static_cast(devilution::Players[devilution::MyPlayerId]._pClass)) { + case devilution::HeroClass::Warrior: + maxValue = 250; + break; + case devilution::HeroClass::Rogue: + maxValue = 55; + break; + case devilution::HeroClass::Sorcerer: + maxValue = 45; + break; + default: + break; + } + if (devilution::Players[devilution::MyPlayerId]._pBaseStr < maxValue) { + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ADDSTR, 1); + devilution::Players[devilution::MyPlayerId]._pStatPts -= 1; + } + break; + case CommandType::ADDMAG: + switch (static_cast(devilution::Players[devilution::MyPlayerId]._pClass)) { + case devilution::HeroClass::Warrior: + maxValue = 50; + break; + case devilution::HeroClass::Rogue: + maxValue = 70; + break; + case devilution::HeroClass::Sorcerer: + maxValue = 250; + break; + default: + break; + } + if (devilution::Players[devilution::MyPlayerId]._pBaseMag < maxValue) { + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ADDMAG, 1); + devilution::Players[devilution::MyPlayerId]._pStatPts -= 1; + } + break; + case CommandType::ADDDEX: + switch (static_cast(devilution::Players[devilution::MyPlayerId]._pClass)) { + case devilution::HeroClass::Warrior: + maxValue = 60; + break; + case devilution::HeroClass::Rogue: + maxValue = 250; + break; + case devilution::HeroClass::Sorcerer: + maxValue = 85; + break; + default: + break; + } + if (devilution::Players[devilution::MyPlayerId]._pBaseDex < maxValue) { + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ADDDEX, 1); + devilution::Players[devilution::MyPlayerId]._pStatPts -= 1; + } + break; + case CommandType::ADDVIT: + switch (static_cast(devilution::Players[devilution::MyPlayerId]._pClass)) { + case devilution::HeroClass::Warrior: + maxValue = 100; + break; + default: + maxValue = 80; + break; + } + if (devilution::Players[devilution::MyPlayerId]._pBaseVit < maxValue) { + devilution::NetSendCmdParam1(true, devilution::_cmd_id::CMD_ADDVIT, 1); + devilution::Players[devilution::MyPlayerId]._pStatPts -= 1; + } + break; + default: + break; + } +} + +void Server::getItem(int itemID) +{ + if (!OKToAct()) + return; + + bool found = false; + + for (size_t i = 0; i < data->groundItems.size(); i++) { + if (data->groundItems[i] == itemID) + found = true; + if (found) + break; + } + + if (!found) + return; + + auto itemData = data->itemList[itemID]; + + if (!isOnScreen(itemData._ix, itemData._iy)) + return; + + int index = -1; + for (int i = 0; i < devilution::ActiveItemCount; i++) { + if (itemData.compare(devilution::Items[devilution::ActiveItems[i]])) + index = devilution::ActiveItems[i]; + + if (index != -1) + break; + } + + if (index == -1) + return; + + if (devilution::invflag) + devilution::NetSendCmdLocParam1(true, devilution::_cmd_id::CMD_GOTOGETITEM, devilution::Point { itemData._ix, itemData._iy }, index); + else + devilution::NetSendCmdLocParam1(true, devilution::_cmd_id::CMD_GOTOAGETITEM, devilution::Point { itemData._ix, itemData._iy }, index); +} + +void Server::setSpell(int spellID, devilution::SpellType spellType) +{ + if (spellID == -1) + return; + + switch (spellType) { + case devilution::SpellType::Skill: + if (!(devilution::Players[devilution::MyPlayerId]._pAblSpells & (1 << (spellID - 1)))) + return; + break; + case devilution::SpellType::Spell: + if (!(devilution::Players[devilution::MyPlayerId]._pMemSpells & (1 << (spellID - 1)))) + return; + break; + case devilution::SpellType::Scroll: + if (!(devilution::Players[devilution::MyPlayerId]._pScrlSpells & (1 << (spellID - 1)))) + return; + break; + case devilution::SpellType::Charges: + if ((devilution::Players[devilution::MyPlayerId].InvBody[4]._iSpell != static_cast(spellID) && devilution::Players[devilution::MyPlayerId].InvBody[5]._iSpell != static_cast(spellID)) || (devilution::Players[devilution::MyPlayerId].InvBody[4]._iCharges == 0 && devilution::Players[devilution::MyPlayerId].InvBody[5]._iCharges == 0)) + return; + break; + case devilution::SpellType::Invalid: + default: + return; + break; + } + + devilution::Players[devilution::MyPlayerId]._pRSpell = static_cast(spellID); + devilution::Players[devilution::MyPlayerId]._pRSplType = static_cast(spellType); + //*force_redraw = 255; // TODO: Is this line needed in devilutionX? If so, what is it? +} + +void Server::castSpell(int index) +{ + if (!OKToAct()) + return; + + devilution::pcursmonst = index; + devilution::PlayerUnderCursor = nullptr; + devilution::cursPosition = devilution::Point { devilution::Monsters[index].position.tile.x, devilution::Monsters[index].position.tile.y }; + + devilution::CheckPlrSpell(false); +} + +void Server::toggleInventory() +{ + if (!OKToAct()) + return; + + devilution::invflag = !devilution::invflag; + if (!devilution::invflag) + devilution::IsStashOpen = false; +} + +void Server::putInCursor(size_t itemID) +{ + if (!OKToAct()) + return; + + if (data->itemList.size() <= itemID) + return; + + auto &item = data->itemList[itemID]; + int mx, my; + + mx = 0; + my = 0; + for (int i = 0; i < 105; i++) { + if (i < 7) { + if (item.compare(devilution::Players[devilution::MyPlayerId].InvBody[i]) && devilution::Players[devilution::MyPlayerId].InvBody[i]._itype != devilution::ItemType::None) { + if (!devilution::invflag) + return; + + // Switch statement is left here because left and right ring are reversed in DevilutionX + switch (static_cast(i)) { + case EquipSlot::HEAD: + mx = devilution::InvRect[0].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[0].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::LEFTRING: + mx = devilution::InvRect[1].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[1].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::RIGHTRING: + mx = devilution::InvRect[2].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[2].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::AMULET: + mx = devilution::InvRect[3].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[3].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::LEFTHAND: + mx = devilution::InvRect[4].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[4].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::RIGHTHAND: + mx = devilution::InvRect[5].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[5].position.y + 1 + devilution::GetRightPanel().position.y; + break; + case EquipSlot::BODY: + mx = devilution::InvRect[6].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[6].position.y + 1 + devilution::GetRightPanel().position.y; + break; + default: + break; + } + break; + } + } else if (i < 47) { + if (item.compare(devilution::Players[devilution::MyPlayerId].InvList[i - 7]) && devilution::Players[devilution::MyPlayerId].InvList[i - 7]._itype != devilution::ItemType::None && i - 7 < devilution::Players[devilution::MyPlayerId]._pNumInv) { + if (!devilution::invflag) + return; + + for (int rect_index = 0; rect_index < 40; rect_index++) { + if (devilution::Players[devilution::MyPlayerId].InvGrid[rect_index] == i - 6) { + int index = rect_index + 7; + mx = devilution::InvRect[index].position.x + 1 + devilution::GetRightPanel().position.x; + my = devilution::InvRect[index].position.y + 1 + devilution::GetRightPanel().position.y; + break; + } + } + break; + } + } else if (i < 55) { + if (item.compare(devilution::Players[devilution::MyPlayerId].SpdList[i - 47]) && devilution::Players[devilution::MyPlayerId].SpdList[i - 47]._itype != devilution::ItemType::None) { + mx = devilution::InvRect[i].position.x + 1 + devilution::GetMainPanel().position.x; + my = devilution::InvRect[i].position.y + 1 + devilution::GetMainPanel().position.y; + break; + } + } else { + int j = 55; + for (auto point : devilution::StashGridRange) { + if (j != i) { + continue; + } + const devilution::StashStruct::StashCell itemId = devilution::Stash.GetItemIdAtPosition(point); + if (itemId == devilution::StashStruct::EmptyCell) { + continue; + } + devilution::Item &stashItem = devilution::Stash.stashList[itemId]; + if (stashItem.isEmpty()) { + continue; + } + if (item.compare(stashItem)) { + auto cursorPosition = devilution::GetStashSlotCoord(point); + devilution::CheckStashItem(point); + return; + } + j++; + } + } + } + if (mx != 0 && my != 0) + devilution::CheckInvCut(*devilution::MyPlayer, devilution::Point { mx, my }, false, false); +} + +void Server::putCursorItem(int location) +{ + if (devilution::MyPlayer->HoldItem.isEmpty()) + return; + + EquipSlot equipLocation = static_cast(location); + + if (!data->invflag) + return; + + int invRectIndex = location; + devilution::Point cursorPosition; + devilution::Displacement panelOffset; + devilution::Displacement hotPixelCellOffset; + if (equipLocation == EquipSlot::LEFTRING) { + invRectIndex = 1; + } else if (equipLocation == EquipSlot::RIGHTRING) { + invRectIndex = 2; + } + if (equipLocation < EquipSlot::BELT1) { + panelOffset = devilution::GetRightPanel().position - devilution::Point { 0, 0 }; + } else if (EquipSlot::BELT8 < equipLocation) { + panelOffset = devilution::GetLeftPanel().position - devilution::Point { 0, 0 }; + } + else { + panelOffset = devilution::GetMainPanel().position - devilution::Point { 0, 0 }; + } + if (EquipSlot::INV1 <= equipLocation && equipLocation <= EquipSlot::INV40) { + const devilution::Size itemSize = devilution::GetInventorySize(devilution::MyPlayer->HoldItem); + if (itemSize.height <= 1 && itemSize.width <= 1) { + hotPixelCellOffset = devilution::Displacement { 1, 1 }; + } + hotPixelCellOffset = { (itemSize.width - 1) / 2 + 19, (itemSize.height - 1) / 2 + 19 }; + } else { + hotPixelCellOffset = devilution::Displacement { 1, 1 }; + } + if (equipLocation < EquipSlot::STASH1) { + cursorPosition = devilution::InvRect[invRectIndex].position + panelOffset + hotPixelCellOffset; + devilution::CheckInvPaste(*devilution::MyPlayer, cursorPosition); + } else { + int i = static_cast(EquipSlot::STASH1); + for (auto point : devilution::StashGridRange) { + if (static_cast(i) != equipLocation) { + i++; + } else { + cursorPosition = devilution::GetStashSlotCoord(point); + break; + } + } + devilution::CheckStashItem(cursorPosition); + //cursorPosition = devilution::StashGridRange[invRectIndex] + panelOffset + hotPixelCellOffset; + } +} + +void Server::dropCursorItem() +{ + if (!isOnScreen(devilution::Players[devilution::MyPlayerId].position.tile.x, devilution::Players[devilution::MyPlayerId].position.tile.y)) + return; + else if (!devilution::MyPlayer->HoldItem.isEmpty()) { + if (!devilution::TryOpenDungeonWithMouse()) { + const devilution::Point currentPosition = devilution::MyPlayer->position.tile; + std::optional itemTile = FindAdjacentPositionForItem(currentPosition, GetDirection(currentPosition, devilution::MyPlayer->position.tile)); + if (itemTile) { + NetSendCmdPItem(true, devilution::_cmd_id::CMD_PUTITEM, *itemTile, devilution::MyPlayer->HoldItem); + devilution::NewCursor(devilution::cursor_id::CURSOR_HAND); + } + } + } +} + +void Server::useItem(size_t itemID) +{ + if (!OKToAct()) + return; + + if (data->itemList.size() <= itemID) + return; + + auto itemData = data->itemList[itemID]; + for (int i = 0; i < 8; i++) { + if (devilution::Players[devilution::MyPlayerId].SpdList[i]._itype != devilution::ItemType::None && itemData.compare(devilution::Players[devilution::MyPlayerId].SpdList[i])) { + useBeltItem(i); + return; + } + } + + for (int i = 0; i < devilution::Players[devilution::MyPlayerId]._pNumInv; i++) { + if (devilution::Players[devilution::MyPlayerId].InvList[i]._itype != devilution::ItemType::None && itemData.compare(devilution::Players[devilution::MyPlayerId].InvList[i])) { + devilution::UseInvItem(i + 7); + return; + } + } +} + +void Server::identifyStoreItem(int itemID) +{ + if (devilution::ActiveStore != devilution::TalkID::StorytellerIdentify) + return; + + int id = -1; + + for (int i = 0; i < 20; i++) { + if (data->itemList[itemID].compare(devilution::PlayerItems[i])) { + id = i; + break; + } + } + + if (id == -1) + return; + + devilution::PlaySFX(devilution::SfxID::MenuSelect); + + devilution::OldActiveStore = devilution::TalkID::StorytellerIdentify; + devilution::OldTextLine = devilution::CurrentTextLine; + devilution::OldScrollPos = devilution::ScrollPos; + + if (!devilution::PlayerCanAfford(devilution::PlayerItems[id]._iIvalue)) { + devilution::StartStore(devilution::TalkID::NoMoney); + return; + } else { + devilution::Player &myPlayer = *devilution::MyPlayer; + + int idx = devilution::PlayerItemIndexes[id]; + if (idx < 0) { + if (idx == -1) + myPlayer.InvBody[devilution::INVLOC_HEAD]._iIdentified = true; + if (idx == -2) + myPlayer.InvBody[devilution::INVLOC_CHEST]._iIdentified = true; + if (idx == -3) + myPlayer.InvBody[devilution::INVLOC_HAND_LEFT]._iIdentified = true; + if (idx == -4) + myPlayer.InvBody[devilution::INVLOC_HAND_RIGHT]._iIdentified = true; + if (idx == -5) + myPlayer.InvBody[devilution::INVLOC_RING_LEFT]._iIdentified = true; + if (idx == -6) + myPlayer.InvBody[devilution::INVLOC_RING_RIGHT]._iIdentified = true; + if (idx == -7) + myPlayer.InvBody[devilution::INVLOC_AMULET]._iIdentified = true; + } else { + myPlayer.InvList[idx]._iIdentified = true; + } + devilution::PlayerItems[id]._iIdentified = true; + devilution::TakePlrsMoney(devilution::PlayerItems[id]._iIvalue); + devilution::CalcPlrInv(myPlayer, true); + devilution::StartStore(devilution::OldActiveStore); + } +} + +void Server::castSpell(int x, int y) +{ + if (!OKToAct()) + return; + + if (!isOnScreen(x, y)) + return; + + devilution::pcursmonst = -1; + devilution::PlayerUnderCursor = nullptr; + devilution::cursPosition = devilution::Point { x, y }; + + devilution::CheckPlrSpell(false); +} + +void Server::cancelQText() +{ + if (!data->qtextflag) + return; + + devilution::qtextflag = false; + data->qtextflag = false; + devilution::stream_stop(); +} + +void Server::setFPS(int newFPS) +{ + FPS = newFPS; + devilution::sgGameInitInfo.nTickRate = newFPS; + devilution::gnTickDelay = 1000 / devilution::sgGameInitInfo.nTickRate; +} + +void Server::disarmTrap(int index) +{ + if (devilution::pcurs != devilution::cursor_id::CURSOR_DISARM) + return; + + if (static_cast(data->playerList[devilution::MyPlayerId]._pClass) != devilution::HeroClass::Rogue) + return; + + devilution::NetSendCmdLoc(devilution::MyPlayerId, true, devilution::_cmd_id::CMD_DISARMXY, devilution::Point { devilution::Objects[index].position.x, devilution::Objects[index].position.y }); +} + +void Server::skillRepair(int itemID) +{ + if (devilution::pcurs != devilution::cursor_id::CURSOR_REPAIR) + return; + + if (static_cast(data->playerList[devilution::MyPlayerId]._pClass) != devilution::HeroClass::Warrior) + return; + + if (!data->invflag) + return; + + for (int i = 0; i < 7; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvBody[i])) { + devilution::DoRepair(*devilution::MyPlayer, i); + return; + } + } + for (int i = 0; i < MAXINV; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvList[i])) { + devilution::DoRepair(*devilution::MyPlayer, i + 7); + return; + } + } +} + +void Server::skillRecharge(int itemID) +{ + if (devilution::pcurs != devilution::cursor_id::CURSOR_RECHARGE) + return; + + if (static_cast(data->playerList[devilution::MyPlayerId]._pClass) != devilution::HeroClass::Sorcerer) + return; + + for (int i = 0; i < 7; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvBody[i])) { + devilution::DoRecharge(*devilution::MyPlayer, i); + return; + } + } + for (int i = 0; i < MAXINV; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvList[i])) { + devilution::DoRecharge(*devilution::MyPlayer, i + 7); + return; + } + } +} + +void Server::toggleMenu() +{ + devilution::qtextflag = false; + if (!devilution::gmenu_is_active()) + devilution::gamemenu_on(); + else + devilution::gamemenu_off(); + return; +} + +void Server::saveGame() +{ + if (devilution::gbIsMultiplayer || !devilution::gmenu_is_active()) + return; + + devilution::gmenu_presskeys(SDLK_DOWN); + devilution::gmenu_presskeys(SDLK_KP_ENTER); + return; +} + +void Server::quit() +{ + if (!devilution::gmenu_is_active()) + return; + + devilution::gmenu_presskeys(SDLK_UP); + devilution::gmenu_presskeys(SDLK_KP_ENTER); + return; +} + +void Server::clearCursor() +{ + if (devilution::pcurs == devilution::cursor_id::CURSOR_REPAIR || devilution::pcurs == devilution::cursor_id::CURSOR_DISARM || devilution::pcurs == devilution::cursor_id::CURSOR_RECHARGE || devilution::pcurs == devilution::cursor_id::CURSOR_IDENTIFY) + devilution::NewCursor(devilution::cursor_id::CURSOR_HAND); + + return; +} + +void Server::identifyItem(int itemID) +{ + if (devilution::pcurs != devilution::cursor_id::CURSOR_IDENTIFY) + return; + + if (!data->invflag) + return; + + for (int i = 0; i < 7; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvBody[i])) { + devilution::CheckIdentify(*devilution::MyPlayer, i); + return; + } + } + for (int i = 0; i < MAXINV; i++) { + if (data->itemList[itemID].compare(devilution::Players[devilution::MyPlayerId].InvList[i])) { + devilution::CheckIdentify(*devilution::MyPlayer, i + 7); + return; + } + } +} + +void Server::sendChat(std::string message) +{ + if (!devilution::gbIsMultiplayer) + return; + + if (79 < message.length()) + message = message.substr(0, 79); + + char charMsg[MAX_SEND_STR_LEN]; + devilution::CopyUtf8(charMsg, message, sizeof(charMsg)); + devilution::NetSendCmdString(0xFFFFFF, charMsg); +} +} // namespace DAPI diff --git a/Source/dapi/Server.h b/Source/dapi/Server.h new file mode 100644 index 000000000..b698987da --- /dev/null +++ b/Source/dapi/Server.h @@ -0,0 +1,361 @@ +#pragma once + +#include +#include +#include + +#include "Backend/DAPIBackendCore/DAPIProtoClient.h" +#include "GameData.h" + +#include "control.h" +#include "cursor.h" +#include "diablo.h" +#include "engine/random.hpp" +#include "gamemenu.h" +#include "gmenu.h" +#include "inv.h" +#include "levels/gendung.h" +#include "minitext.h" +#include "missiles.h" +#include "msg.h" +#include "multi.h" +#include "objects.h" +#include "player.h" +#include "portal.h" +#include "qol/chatlog.h" +#include "spelldat.h" +#include "stores.h" +#include "towners.h" + +namespace DAPI { +enum struct CommandType { + STAND, + WALKXY, + ACK_PLRINFO, + ADDSTR, + ADDMAG, + ADDDEX, + ADDVIT, + SBSPELL, + GETITEM, + AGETITEM, + PUTITEM, + RESPAWNITEM, + ATTACKXY, + RATTACKXY, + SPELLXY, + TSPELLXY, + OPOBJXY, + DISARMXY, + ATTACKID, + ATTACKPID, + RATTACKID, + RATTACKPID, + SPELLID, + SPELLPID, + TSPELLID, + TSPELLPID, + RESURRECT, + OPOBJT, + KNOCKBACK, + TALKXY, + NEWLVL, + WARP, + CHEAT_EXPERIENCE, + CHEAT_SPELL_LEVEL, + DEBUG, + SYNCDATA, + MONSTDEATH, + MONSTDAMAGE, + PLRDEAD, + REQUESTGITEM, + REQUESTAGITEM, + GOTOGETITEM, + GOTOAGETITEM, + OPENDOOR, + CLOSEDOOR, + OPERATEOBJ, + PLROPOBJ, + BREAKOBJ, + CHANGEPLRITEMS, + DELPLRITEMS, + PLRDAMAGE, + PLRLEVEL, + DROPITEM, + PLAYER_JOINLEVEL, + SEND_PLRINFO, + SATTACKXY, + ACTIVATEPORTAL, + DEACTIVATEPORTAL, + DLEVEL_0, + DLEVEL_1, + DLEVEL_2, + DLEVEL_3, + DLEVEL_4, + DLEVEL_5, + DLEVEL_6, + DLEVEL_7, + DLEVEL_8, + DLEVEL_9, + DLEVEL_10, + DLEVEL_11, + DLEVEL_12, + DLEVEL_13, + DLEVEL_14, + DLEVEL_15, + DLEVEL_16, + DLEVEL_JUNK, + DLEVEL_END, + HEALOTHER, + STRING, + SETSTR, + SETMAG, + SETDEX, + SETVIT, + RETOWN, + SPELLXYD, + ITEMEXTRA, + SYNCPUTITEM, + KILLGOLEM, + SYNCQUEST, + ENDSHIELD, + AWAKEGOLEM, + NOVA, + SETSHIELD, + REMSHIELD, + FAKE_SETID, + FAKE_DROPID, + TOGGLECHARACTER, + SETSPELL, + TOGGLEINVENTORY, + PUTINCURSOR, + PUTCURSORITEM, + IDENTIFYSTOREITEM, + NUM_CMDS, +}; + +enum struct EquipSlot { + HEAD = 0, + RIGHTRING = 1, + LEFTRING = 2, + AMULET = 3, + LEFTHAND = 4, + RIGHTHAND = 5, + BODY = 6, + INV1 = 7, + INV2 = 8, + INV3 = 9, + INV4 = 10, + INV5 = 11, + INV6 = 12, + INV7 = 13, + INV8 = 14, + INV9 = 15, + INV10 = 16, + INV11 = 17, + INV12 = 18, + INV13 = 19, + INV14 = 20, + INV15 = 21, + INV16 = 22, + INV17 = 23, + INV18 = 24, + INV19 = 25, + INV20 = 26, + INV21 = 27, + INV22 = 28, + INV23 = 29, + INV24 = 30, + INV25 = 31, + INV26 = 32, + INV27 = 33, + INV28 = 34, + INV29 = 35, + INV30 = 36, + INV31 = 37, + INV32 = 38, + INV33 = 39, + INV34 = 40, + INV35 = 41, + INV36 = 42, + INV37 = 43, + INV38 = 44, + INV39 = 45, + INV40 = 46, + BELT1 = 47, + BELT2 = 48, + BELT3 = 49, + BELT4 = 50, + BELT5 = 51, + BELT6 = 52, + BELT7 = 53, + BELT8 = 54, + STASH1 = 55, + STASH2 = 56, + STASH3 = 57, + STASH4 = 58, + STASH5 = 59, + STASH6 = 60, + STASH7 = 61, + STASH8 = 62, + STASH9 = 63, + STASH10 = 64, + STASH11 = 65, + STASH12 = 66, + STASH13 = 67, + STASH14 = 68, + STASH15 = 69, + STASH16 = 70, + STASH17 = 71, + STASH18 = 72, + STASH19 = 73, + STASH20 = 74, + STASH21 = 75, + STASH22 = 76, + STASH23 = 77, + STASH24 = 78, + STASH25 = 79, + STASH26 = 80, + STASH27 = 81, + STASH28 = 82, + STASH29 = 83, + STASH30 = 84, + STASH31 = 85, + STASH32 = 86, + STASH33 = 87, + STASH34 = 88, + STASH35 = 89, + STASH36 = 90, + STASH37 = 91, + STASH38 = 92, + STASH39 = 93, + STASH40 = 94, + STASH41 = 95, + STASH42 = 96, + STASH43 = 97, + STASH44 = 98, + STASH45 = 99, + STASH46 = 100, + STASH47 = 101, + STASH48 = 102, + STASH49 = 103, + STASH50 = 104, + STASH51 = 105, + STASH52 = 106, + STASH53 = 107, + STASH54 = 108, + STASH55 = 109, + STASH56 = 110, + STASH57 = 111, + STASH58 = 112, + STASH59 = 113, + STASH60 = 114, + STASH61 = 115, + STASH62 = 116, + STASH63 = 117, + STASH64 = 118, + STASH65 = 119, + STASH66 = 120, + STASH67 = 121, + STASH68 = 122, + STASH69 = 123, + STASH70 = 124, + STASH71 = 125, + STASH72 = 126, + STASH73 = 127, + STASH74 = 128, + STASH75 = 129, + STASH76 = 130, + STASH77 = 131, + STASH78 = 132, + STASH79 = 133, + STASH80 = 134, + STASH81 = 135, + STASH82 = 136, + STASH83 = 137, + STASH84 = 138, + STASH85 = 139, + STASH86 = 140, + STASH87 = 141, + STASH88 = 142, + STASH89 = 143, + STASH90 = 144, + STASH91 = 145, + STASH92 = 146, + STASH93 = 147, + STASH94 = 148, + STASH95 = 149, + STASH96 = 150, + STASH97 = 151, + STASH98 = 152, + STASH99 = 153, + STASH100 = 154 +}; + +enum struct Backend { + Vanilla109, + DevilutionX +}; + +struct Server { + Server(); + + Server(const Server &other) = delete; + Server(Server &&other) = delete; + + void update(); + bool isConnected() const; + + int FPS; + std::ofstream output; + +private: + void processMessages(); + void checkForConnections(); + void updateGameData(); + bool isOnScreen(int x, int y); + bool OKToAct(); + void move(int x, int y); + void talk(int x, int y); + void selectStoreOption(StoreOption option); + void buyItem(int itemID); + void sellItem(int itemID); + void rechargeItem(int itemID); + void repairItem(int itemID); + void attackMonster(int index); + void attackXY(int x, int y); + void operateObject(int index); + void useBeltItem(int slot); + void toggleCharacterScreen(); + void increaseStat(CommandType commandType); + void getItem(int itemID); + void setSpell(int spellID, devilution::SpellType spellType); + void castSpell(int index); + void toggleInventory(); + void putInCursor(size_t itemID); + void putCursorItem(int location); + void dropCursorItem(); + void useItem(size_t itemID); + void identifyStoreItem(int itemID); + void castSpell(int x, int y); + void cancelQText(); + void setFPS(int newFPS); + void disarmTrap(int index); + void skillRepair(int itemID); + void skillRecharge(int itemID); + void toggleMenu(); + void saveGame(); + void quit(); + void clearCursor(); + void identifyItem(int itemID); + void sendChat(std::string message); + + bool listening = false; + + std::unique_ptr data; + + std::map, bool> panelScreenCheck; + + DAPIProtoClient protoClient; +}; +} // namespace DAPI diff --git a/Source/dapi/Towner.h b/Source/dapi/Towner.h new file mode 100644 index 000000000..2a2266d0f --- /dev/null +++ b/Source/dapi/Towner.h @@ -0,0 +1,14 @@ +#pragma once + +#include "../towners.h" + +namespace DAPI { + +struct TownerData { + int ID; + devilution::_talker_id _ttype; + int _tx; + int _ty; + char _tName[32]; +}; +} // namespace DAPI diff --git a/Source/dapi/Trigger.h b/Source/dapi/Trigger.h new file mode 100644 index 000000000..63f6c7802 --- /dev/null +++ b/Source/dapi/Trigger.h @@ -0,0 +1,14 @@ +#pragma once +#include "../levels/trigs.h" + +namespace DAPI { +struct TriggerData { + bool compare(devilution::TriggerStruct &other) { return (other._tlvl == lvl && other._tmsg == type && other.position.x == x && other.position.y == y); } + + int ID; + int x; + int y; + int lvl; + int type; +}; +} // namespace DAPI diff --git a/Source/diablo.cpp b/Source/diablo.cpp index b14da41cb..997d47838 100644 --- a/Source/diablo.cpp +++ b/Source/diablo.cpp @@ -122,6 +122,10 @@ #include "controls/touch/renderers.h" #endif +#ifdef DAPI_SERVER +#include "dapi/Server.h" +#endif + #ifdef __vita__ #include "platform/vita/touch.h" #endif @@ -146,6 +150,10 @@ clicktype sgbMouseDown; uint16_t gnTickDelay = 50; char gszProductName[64] = "DevilutionX vUnknown"; +#ifdef DAPI_SERVER +DAPI::Server dapiServer; +#endif + #ifdef _DEBUG bool DebugDisableNetworkTimeout = false; std::vector DebugCmdsFromCommandLine; @@ -313,23 +321,6 @@ void LeftMouseCmd(bool bShift) } } -bool TryOpenDungeonWithMouse() -{ - if (leveltype != DTYPE_TOWN) - return false; - - const Item &holdItem = MyPlayer->HoldItem; - if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) - OpenHive(); - else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) - OpenGrave(); - else - return false; - - NewCursor(CURSOR_HAND); - return true; -} - void LeftMouseDown(uint16_t modState) { LastPlayerAction = PlayerActionType::None; @@ -1511,8 +1502,15 @@ void UpdateMonsterLights() void GameLogic() { if (!ProcessInput()) { +#ifdef DAPI_SERVER + if (gmenu_is_active()) + dapiServer.update(); // For game menu commands +#endif return; } +#ifdef DAPI_SERVER + dapiServer.update(); +#endif if (gbProcessPlayers) { gGameLogicStep = GameLogicStep::ProcessPlayers; ProcessPlayers(); @@ -3495,4 +3493,21 @@ void PrintScreen(SDL_Keycode vkey) ReleaseKey(vkey); } +bool TryOpenDungeonWithMouse() +{ + if (leveltype != DTYPE_TOWN) + return false; + + const Item &holdItem = MyPlayer->HoldItem; + if (holdItem.IDidx == IDI_RUNEBOMB && OpensHive(cursPosition)) + OpenHive(); + else if (holdItem.IDidx == IDI_MAPOFDOOM && OpensGrave(cursPosition)) + OpenGrave(); + else + return false; + + NewCursor(CURSOR_HAND); + return true; +} + } // namespace devilution diff --git a/Source/diablo.h b/Source/diablo.h index ad28ebb82..fde9e9a77 100644 --- a/Source/diablo.h +++ b/Source/diablo.h @@ -102,6 +102,7 @@ void DisableInputEventHandler(const SDL_Event &event, uint16_t modState); tl::expected LoadGameLevel(bool firstflag, lvl_entry lvldir); bool IsDiabloAlive(bool playSFX); void PrintScreen(SDL_Keycode vkey); +bool TryOpenDungeonWithMouse(); /** * @param bStartup Process additional ticks before returning diff --git a/Source/inv.cpp b/Source/inv.cpp index fc97994c9..51d594d69 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -558,68 +558,6 @@ item_equip_type GetItemEquipType(const Player &player, int slot, item_equip_type return ILOC_UNEQUIPABLE; } -void CheckInvPaste(Player &player, Point cursorPosition) -{ - const Size itemSize = GetInventorySize(player.HoldItem); - - const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); - if (slot == NUM_XY_SLOTS) - return; - - const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); - const item_equip_type location = GetItemEquipType(player, slot, desiredLocation); - - if (location == ILOC_BELT) { - if (!CanBePlacedOnBelt(player, player.HoldItem)) return; - } else if (location != ILOC_UNEQUIPABLE) { - if (desiredLocation != location) return; - } - - if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { - if (!player.CanUseItem(player.HoldItem)) { - player.Say(HeroSpeech::ICantUseThisYet); - return; - } - if (player._pmode > PM_WALK_SIDEWAYS) - return; - } - - if (&player == MyPlayer) { - PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); - } - - // Select the parameters that go into - // ChangeEquipment and add it to post switch - switch (location) { - case ILOC_HELM: - case ILOC_RING: - case ILOC_AMULET: - case ILOC_ARMOR: - ChangeBodyEquipment(player, slot, location); - break; - case ILOC_ONEHAND: - ChangeEquippedItem(player, slot); - break; - case ILOC_TWOHAND: - ChangeTwoHandItem(player); - break; - case ILOC_UNEQUIPABLE: - if (!ChangeInvItem(player, slot, itemSize)) return; - break; - case ILOC_BELT: - ChangeBeltItem(player, slot); - break; - case ILOC_NONE: - case ILOC_INVALID: - break; - } - - CalcPlrInv(player, true); - if (&player == MyPlayer) { - NewCursor(player.HoldItem); - } -} - inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot) { assert(slot <= SLOTXY_CHEST); @@ -746,283 +684,74 @@ bool CouldFitItemInInventory(const Player &player, const Item &item, int itemInd return static_cast(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); } -void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) +void TryCombineNaKrulNotes(Player &player, Item ¬eItem) { - if (player._pmode > PM_WALK_SIDEWAYS) { + const int idx = noteItem.IDidx; + const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; + + if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { return; } - CloseGoldDrop(); + for (const _item_indexes note : notes) { + if (idx != note && !HasInventoryItemWithId(player, note)) { + return; // the player doesn't have all notes + } + } - std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); + MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - if (!maybeSlot) { - // not on an inventory slot rectangle - return; + for (const _item_indexes note : notes) { + if (idx != note) { + RemoveInventoryItemById(player, note); + } } - const inv_xy_slot r = *maybeSlot; + const Point position = noteItem.position; // copy the position to restore it after re-initialising the item + noteItem = {}; + GetItemAttrs(noteItem, IDI_FULLNOTE, 16); + SetupItem(noteItem); + noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table +} - Item &holdItem = player.HoldItem; - holdItem.clear(); +void CheckQuestItem(Player &player, Item &questItem) +{ + const Player &myPlayer = *MyPlayer; - bool attemptedMove = false; - bool automaticallyMoved = false; - SfxID successSound = SfxID::None; - HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else + if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE + && (questItem.IDidx == IDI_OPTAMULET + || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { + Quests[Q_BLIND]._qactive = QUEST_DONE; + NetSendCmdQuest(true, Quests[Q_BLIND]); + } - if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { - const inv_body_loc invloc = MapSlotToInvBodyLoc(r); - if (!player.InvBody[invloc].isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; - RemoveEquipment(player, invloc, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = player.InvBody[invloc]; - RemoveEquipment(player, invloc, false); - } - } + if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { + player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP + Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; + NetSendCmdQuest(true, Quests[Q_MUSHROOM]); } - if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { - const unsigned ig = r - SLOTXY_INV_FIRST; - const int iv = std::abs(player.InvGrid[ig]) - 1; - if (iv >= 0) { - if (automaticMove) { - attemptedMove = true; - if (CanBePlacedOnBelt(player, player.InvList[iv])) { - automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveInvItem(iv, false); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else if (CanEquip(player.InvList[iv])) { - failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails + if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { + Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ANVIL]); + } + if (Quests[Q_ANVIL]._qlog) { + myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); + } + } - /* - * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be - * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except - * we don't want to leave the item on the hand if the equip attempt failed. We would end up - * generating wasteful network messages if we did the lift first. Instead we work out whatever slot - * needs to be unequipped (if any): - */ - int invloc = NUM_INVLOC; - switch (player.GetItemLocation(player.InvList[iv])) { - case ILOC_ARMOR: - invloc = INVLOC_CHEST; - break; - case ILOC_HELM: - invloc = INVLOC_HEAD; - break; - case ILOC_AMULET: - invloc = INVLOC_AMULET; - break; - case ILOC_ONEHAND: - if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() - && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass - || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { - // The left hand is not empty and we're either trying to equip the same type of item or - // it's holding a two handed weapon, so it must be unequipped - invloc = INVLOC_HAND_LEFT; - } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { - // The right hand is not empty and we're trying to equip the same type of item, so we need - // to unequip that item - invloc = INVLOC_HAND_RIGHT; - } - // otherwise one hand is empty (and we can let the auto-equip code put the target item into - // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip - // a shield (in which case the attempt will fail). - break; - case ILOC_TWOHAND: - // Moving a two-hand item from inventory to InvBody requires emptying both hands. - if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { - // If the right hand is empty then we can simply try equipping this item in the left hand, - // we'll let the common code take care of unequipping anything held there. - invloc = INVLOC_HAND_LEFT; - } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { - // We have an item in the right hand but nothing in the left, so let the common code - // take care of unequipping whatever is held in the right hand. The auto-equip code - // picks the most appropriate location for the item type (which in this case will be - // the left hand), invloc isn't used there. - invloc = INVLOC_HAND_RIGHT; - } else { - // Both hands are holding items, we must unequip one of the items and check that there's - // space for the other before trying to auto-equip - inv_body_loc mainHand = INVLOC_HAND_LEFT; - inv_body_loc offHand = INVLOC_HAND_RIGHT; - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - // No space to move right hand item to inventory, can we move the left instead? - std::swap(mainHand, offHand); - if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { - break; - } - } - if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { - // No space for the main hand item. Move the other item back to the off hand and abort. - player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; - player.RemoveInvItem(player._pNumInv - 1, false); - break; - } - RemoveEquipment(player, offHand, false); - invloc = mainHand; - } - break; - default: - // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. - failedSpeech = HeroSpeech::ICantDoThat; - break; - } - // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip - if (invloc != NUM_INVLOC - && !player.InvBody[invloc].isEmpty() - && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { - holdItem = player.InvBody[invloc].pop(); - } - automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); - if (automaticallyMoved) { - successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; - player.RemoveInvItem(iv, false); + if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { + myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); + } - // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv - if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { - holdItem.clear(); - } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. - } else if (!holdItem.isEmpty()) { - // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... - player.InvBody[invloc] = holdItem.pop(); - } - } - } else { - holdItem = player.InvList[iv]; - player.RemoveInvItem(iv, false); - } - } - } - - if (r >= SLOTXY_BELT_FIRST) { - const Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; - if (!beltItem.isEmpty()) { - if (automaticMove) { - attemptedMove = true; - automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); - if (automaticallyMoved) { - successSound = SfxID::GrabItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } else { - failedSpeech = HeroSpeech::IHaveNoRoom; - } - } else { - holdItem = beltItem; - player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); - } - } - } - - if (!holdItem.isEmpty()) { - if (holdItem._itype == ItemType::Gold) { - player._pGold = CalculateGold(player); - } - - CalcPlrInv(player, true); - holdItem._iStatFlag = player.CanUseItem(holdItem); - - if (&player == MyPlayer) { - PlaySFX(SfxID::GrabItem); - NewCursor(holdItem); - } - if (dropItem) { - TryDropItem(); - } - } else if (automaticMove) { - if (automaticallyMoved) { - CalcPlrInv(player, true); - } - if (attemptedMove && &player == MyPlayer) { - if (automaticallyMoved) { - PlaySFX(successSound); - } else { - player.SaySpecific(failedSpeech); - } - } - } -} - -void TryCombineNaKrulNotes(Player &player, Item ¬eItem) -{ - const int idx = noteItem.IDidx; - const _item_indexes notes[] = { IDI_NOTE1, IDI_NOTE2, IDI_NOTE3 }; - - if (IsNoneOf(idx, IDI_NOTE1, IDI_NOTE2, IDI_NOTE3)) { - return; - } - - for (const _item_indexes note : notes) { - if (idx != note && !HasInventoryItemWithId(player, note)) { - return; // the player doesn't have all notes - } - } - - MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10); - - for (const _item_indexes note : notes) { - if (idx != note) { - RemoveInventoryItemById(player, note); - } - } - - const Point position = noteItem.position; // copy the position to restore it after re-initialising the item - noteItem = {}; - GetItemAttrs(noteItem, IDI_FULLNOTE, 16); - SetupItem(noteItem); - noteItem.position = position; // this ensures CleanupItem removes the entry in the dropped items lookup table -} - -void CheckQuestItem(Player &player, Item &questItem) -{ - const Player &myPlayer = *MyPlayer; - - if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE - && (questItem.IDidx == IDI_OPTAMULET - || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) { - Quests[Q_BLIND]._qactive = QUEST_DONE; - NetSendCmdQuest(true, Quests[Q_BLIND]); - } - - if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) { - player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP - Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED; - NetSendCmdQuest(true, Quests[Q_MUSHROOM]); - } - - if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ANVIL]._qactive == QUEST_INIT) { - Quests[Q_ANVIL]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ANVIL]); - } - if (Quests[Q_ANVIL]._qlog) { - myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10); - } - } - - if (questItem.IDidx == IDI_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) { - myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30); - } - - if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { - if (Quests[Q_ROCK]._qactive == QUEST_INIT) { - Quests[Q_ROCK]._qactive = QUEST_ACTIVE; - NetSendCmdQuest(true, Quests[Q_ROCK]); - } - if (Quests[Q_ROCK]._qlog) { - myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); + if (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) { + if (Quests[Q_ROCK]._qactive == QUEST_INIT) { + Quests[Q_ROCK]._qactive = QUEST_ACTIVE; + NetSendCmdQuest(true, Quests[Q_ROCK]); + } + if (Quests[Q_ROCK]._qlog) { + myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10); } } @@ -2276,4 +2005,275 @@ Size GetInventorySize(const Item &item) return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; } +void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) +{ + if (player._pmode > PM_WALK_SIDEWAYS) { + return; + } + + CloseGoldDrop(); + + std::optional maybeSlot = FindSlotUnderCursor(cursorPosition); + + if (!maybeSlot) { + // not on an inventory slot rectangle + return; + } + + inv_xy_slot r = *maybeSlot; + + Item &holdItem = player.HoldItem; + holdItem.clear(); + + bool attemptedMove = false; + bool automaticallyMoved = false; + SfxID successSound = SfxID::None; + HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else + + if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { + inv_body_loc invloc = MapSlotToInvBodyLoc(r); + if (!player.InvBody[invloc].isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, player.InvBody[invloc]); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvBody[invloc]._iCurs]]; + RemoveEquipment(player, invloc, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = player.InvBody[invloc]; + RemoveEquipment(player, invloc, false); + } + } + } + + if (r >= SLOTXY_INV_FIRST && r <= SLOTXY_INV_LAST) { + unsigned ig = r - SLOTXY_INV_FIRST; + int iv = std::abs(player.InvGrid[ig]) - 1; + if (iv >= 0) { + if (automaticMove) { + attemptedMove = true; + if (CanBePlacedOnBelt(player, player.InvList[iv])) { + automaticallyMoved = AutoPlaceItemInBelt(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveInvItem(iv, false); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else if (CanEquip(player.InvList[iv])) { + failedSpeech = HeroSpeech::IHaveNoRoom; // Default to saying "I have no room" if auto-equip fails + + /* + * If the player shift-clicks an item in the inventory we want to swap it with whatever item may be + * equipped in the target slot. Lifting the item to the hand unconditionally would be ideal, except + * we don't want to leave the item on the hand if the equip attempt failed. We would end up + * generating wasteful network messages if we did the lift first. Instead we work out whatever slot + * needs to be unequipped (if any): + */ + int invloc = NUM_INVLOC; + switch (player.GetItemLocation(player.InvList[iv])) { + case ILOC_ARMOR: + invloc = INVLOC_CHEST; + break; + case ILOC_HELM: + invloc = INVLOC_HEAD; + break; + case ILOC_AMULET: + invloc = INVLOC_AMULET; + break; + case ILOC_ONEHAND: + if (!player.InvBody[INVLOC_HAND_LEFT].isEmpty() + && (player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_LEFT]._iClass + || player.GetItemLocation(player.InvBody[INVLOC_HAND_LEFT]) == ILOC_TWOHAND)) { + // The left hand is not empty and we're either trying to equip the same type of item or + // it's holding a two handed weapon, so it must be unequipped + invloc = INVLOC_HAND_LEFT; + } else if (!player.InvBody[INVLOC_HAND_RIGHT].isEmpty() && player.InvList[iv]._iClass == player.InvBody[INVLOC_HAND_RIGHT]._iClass) { + // The right hand is not empty and we're trying to equip the same type of item, so we need + // to unequip that item + invloc = INVLOC_HAND_RIGHT; + } + // otherwise one hand is empty (and we can let the auto-equip code put the target item into + // that hand) or we're playing a bard with two swords equipped and we're trying to auto-equip + // a shield (in which case the attempt will fail). + break; + case ILOC_TWOHAND: + // Moving a two-hand item from inventory to InvBody requires emptying both hands. + if (player.InvBody[INVLOC_HAND_RIGHT].isEmpty()) { + // If the right hand is empty then we can simply try equipping this item in the left hand, + // we'll let the common code take care of unequipping anything held there. + invloc = INVLOC_HAND_LEFT; + } else if (player.InvBody[INVLOC_HAND_LEFT].isEmpty()) { + // We have an item in the right hand but nothing in the left, so let the common code + // take care of unequipping whatever is held in the right hand. The auto-equip code + // picks the most appropriate location for the item type (which in this case will be + // the left hand), invloc isn't used there. + invloc = INVLOC_HAND_RIGHT; + } else { + // Both hands are holding items, we must unequip one of the items and check that there's + // space for the other before trying to auto-equip + inv_body_loc mainHand = INVLOC_HAND_LEFT; + inv_body_loc offHand = INVLOC_HAND_RIGHT; + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + // No space to move right hand item to inventory, can we move the left instead? + std::swap(mainHand, offHand); + if (!AutoPlaceItemInInventory(player, player.InvBody[offHand])) { + break; + } + } + if (!CouldFitItemInInventory(player, player.InvBody[mainHand], iv)) { + // No space for the main hand item. Move the other item back to the off hand and abort. + player.InvBody[offHand] = player.InvList[player._pNumInv - 1]; + player.RemoveInvItem(player._pNumInv - 1, false); + break; + } + RemoveEquipment(player, offHand, false); + invloc = mainHand; + } + break; + default: + // If the player is trying to equip a ring we want to say "I can't do that" if they don't already have a ring slot free. + failedSpeech = HeroSpeech::ICantDoThat; + break; + } + // Then empty the identified InvBody slot (invloc) and hand over to AutoEquip + if (invloc != NUM_INVLOC + && !player.InvBody[invloc].isEmpty() + && CouldFitItemInInventory(player, player.InvBody[invloc], iv)) { + holdItem = player.InvBody[invloc].pop(); + } + automaticallyMoved = AutoEquip(player, player.InvList[iv], true, &player == MyPlayer); + if (automaticallyMoved) { + successSound = ItemInvSnds[ItemCAnimTbl[player.InvList[iv]._iCurs]]; + player.RemoveInvItem(iv, false); + + // If we're holding an item at this point we just lifted it from a body slot to make room for the original item, so we need to put it into the inv + if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { + holdItem.clear(); + } // there should never be a situation where holdItem is not empty but we fail to place it into the inventory given the checks earlier... leave it on the hand in this case. + } else if (!holdItem.isEmpty()) { + // We somehow failed to equip the item in the slot we already checked should hold it? Better put this item back... + player.InvBody[invloc] = holdItem.pop(); + } + } + } else { + holdItem = player.InvList[iv]; + player.RemoveInvItem(iv, false); + } + } + } + + if (r >= SLOTXY_BELT_FIRST) { + Item &beltItem = player.SpdList[r - SLOTXY_BELT_FIRST]; + if (!beltItem.isEmpty()) { + if (automaticMove) { + attemptedMove = true; + automaticallyMoved = AutoPlaceItemInInventory(player, beltItem); + if (automaticallyMoved) { + successSound = SfxID::GrabItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } else { + failedSpeech = HeroSpeech::IHaveNoRoom; + } + } else { + holdItem = beltItem; + player.RemoveSpdBarItem(r - SLOTXY_BELT_FIRST); + } + } + } + + if (!holdItem.isEmpty()) { + if (holdItem._itype == ItemType::Gold) { + player._pGold = CalculateGold(player); + } + + CalcPlrInv(player, true); + holdItem._iStatFlag = player.CanUseItem(holdItem); + + if (&player == MyPlayer) { + PlaySFX(SfxID::GrabItem); + NewCursor(holdItem); + } + if (dropItem) { + TryDropItem(); + } + } else if (automaticMove) { + if (automaticallyMoved) { + CalcPlrInv(player, true); + } + if (attemptedMove && &player == MyPlayer) { + if (automaticallyMoved) { + PlaySFX(successSound); + } else { + player.SaySpecific(failedSpeech); + } + } + } +} + +void CheckInvPaste(Player &player, Point cursorPosition) +{ + const Size itemSize = GetInventorySize(player.HoldItem); + + const int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); + if (slot == NUM_XY_SLOTS) + return; + + const item_equip_type desiredLocation = player.GetItemLocation(player.HoldItem); + const item_equip_type location = GetItemEquipType(player, slot, desiredLocation); + + if (location == ILOC_BELT) { + if (!CanBePlacedOnBelt(player, player.HoldItem)) return; + } else if (location != ILOC_UNEQUIPABLE) { + if (desiredLocation != location) return; + } + + if (IsNoneOf(location, ILOC_UNEQUIPABLE, ILOC_BELT)) { + if (!player.CanUseItem(player.HoldItem)) { + player.Say(HeroSpeech::ICantUseThisYet); + return; + } + if (player._pmode > PM_WALK_SIDEWAYS) + return; + } + + if (&player == MyPlayer) { + PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); + } + + // Select the parameters that go into + // ChangeEquipment and add it to post switch + switch (location) { + case ILOC_HELM: + case ILOC_RING: + case ILOC_AMULET: + case ILOC_ARMOR: + ChangeBodyEquipment(player, slot, location); + break; + case ILOC_ONEHAND: + ChangeEquippedItem(player, slot); + break; + case ILOC_TWOHAND: + ChangeTwoHandItem(player); + break; + case ILOC_UNEQUIPABLE: + if (!ChangeInvItem(player, slot, itemSize)) return; + break; + case ILOC_BELT: + ChangeBeltItem(player, slot); + break; + case ILOC_NONE: + case ILOC_INVALID: + break; + } + + CalcPlrInv(player, true); + if (&player == MyPlayer) { + NewCursor(player.HoldItem); + } +} + } // namespace devilution diff --git a/Source/inv.h b/Source/inv.h index 4a1d56c70..f5ba70726 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -389,6 +389,10 @@ inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id) */ void ConsumeScroll(Player &player); +void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem); + +void CheckInvPaste(Player &player, Point cursorPosition); + /* data */ } // namespace devilution diff --git a/Source/minitext.cpp b/Source/minitext.cpp index 075279377..45f52026d 100644 --- a/Source/minitext.cpp +++ b/Source/minitext.cpp @@ -26,6 +26,8 @@ namespace devilution { bool qtextflag; +std::vector TextLines; + namespace { /** Vertical speed of the scrolling text in ms/px */ @@ -38,8 +40,6 @@ OptionalOwnedClxSpriteList pTextBoxCels; /** Pixels for a line of text and the empty space under it. */ const int LineHeight = 38; -std::vector TextLines; - void LoadText(std::string_view text) { TextLines.clear(); diff --git a/Source/minitext.h b/Source/minitext.h index 2dfd293e4..684431daf 100644 --- a/Source/minitext.h +++ b/Source/minitext.h @@ -13,6 +13,8 @@ namespace devilution { /** Specify if the quest dialog window is being shown */ extern bool qtextflag; +extern std::vector TextLines; + /** * @brief Free the resources used by the quest dialog window */ diff --git a/Source/portal.cpp b/Source/portal.cpp index 4d02adda5..0efd52f71 100644 --- a/Source/portal.cpp +++ b/Source/portal.cpp @@ -16,11 +16,6 @@ namespace devilution { /** In-game state of portals. */ Portal Portals[MAXPORTAL]; -namespace { - -/** Current portal number (a portal array index). */ -size_t portalindex; - /** Coordinate of each player's portal in town. */ Point PortalTownPosition[MAXPORTAL] = { { 57, 40 }, @@ -29,6 +24,11 @@ Point PortalTownPosition[MAXPORTAL] = { { 63, 40 }, }; +namespace { + +/** Current portal number (a portal array index). */ +size_t portalindex; + } // namespace void InitPortals() diff --git a/Source/portal.h b/Source/portal.h index 30e05f6a8..3bd0981aa 100644 --- a/Source/portal.h +++ b/Source/portal.h @@ -25,6 +25,8 @@ struct Portal { extern Portal Portals[MAXPORTAL]; +extern Point PortalTownPosition[MAXPORTAL]; + void InitPortals(); void SetPortalStats(int i, bool o, Point position, int lvl, dungeon_type lvltype, bool isSetLevel); void AddPortalMissile(const Player &player, Point position, bool sync); diff --git a/Source/qol/chatlog.cpp b/Source/qol/chatlog.cpp index c73a25df5..dade53f80 100644 --- a/Source/qol/chatlog.cpp +++ b/Source/qol/chatlog.cpp @@ -28,23 +28,13 @@ namespace devilution { -namespace { - -struct ColoredText { - std::string text; - UiFlags color; -}; +std::vector ChatLogLines; +unsigned int MessageCounter = 0; -struct MultiColoredText { - std::string text; - std::vector colors; -}; +namespace { bool UnreadFlag = false; size_t SkipLines; -unsigned int MessageCounter = 0; - -std::vector ChatLogLines; constexpr int PaddingTop = 32; constexpr int PaddingLeft = 32; diff --git a/Source/qol/chatlog.h b/Source/qol/chatlog.h index 5291684f5..e73990024 100644 --- a/Source/qol/chatlog.h +++ b/Source/qol/chatlog.h @@ -11,7 +11,19 @@ namespace devilution { +struct ColoredText { + std::string text; + UiFlags color; +}; + +struct MultiColoredText { + std::string text; + std::vector colors; +}; + extern bool ChatLogFlag; +extern std::vector ChatLogLines; +extern unsigned int MessageCounter; void ToggleChatLog(); void AddMessageToChatLog(std::string_view message, Player *player = nullptr, UiFlags flags = UiFlags::ColorWhite); diff --git a/Source/stores.cpp b/Source/stores.cpp index bd5cb919f..1ec994448 100644 --- a/Source/stores.cpp +++ b/Source/stores.cpp @@ -54,65 +54,39 @@ StaticVector WitchItems; int BoyItemLevel; Item BoyItem; -namespace { +/** Text lines */ +STextStruct TextLine[NumStoreLines]; /** The current towner being interacted with */ _talker_id TownerId; -/** Is the current dialog full size */ -bool IsTextFullSize; - -/** Number of text lines in the current dialog */ -int NumTextLines; /** Remember currently selected text line from TextLine while displaying a dialog */ int OldTextLine; + +/** Remember current store while displaying a dialog */ +TalkID OldActiveStore; + +/** Remember last scroll position */ +int OldScrollPos; +/** Scroll position */ +int ScrollPos; + /** Currently selected text line from TextLine */ int CurrentTextLine; -struct STextStruct { - enum Type : uint8_t { - Label, - Divider, - Selectable, - }; - - std::string text; - int _sval; - int y; - UiFlags flags; - Type type; - uint8_t _sx; - uint8_t _syoff; - int cursId; - bool cursIndent; - - [[nodiscard]] bool isDivider() const - { - return type == Divider; - } - [[nodiscard]] bool isSelectable() const - { - return type == Selectable; - } +namespace { - [[nodiscard]] bool hasText() const - { - return !text.empty(); - } -}; +/** Is the current dialog full size */ +bool IsTextFullSize; -/** Text lines */ -STextStruct TextLine[NumStoreLines]; +/** Number of text lines in the current dialog */ +int NumTextLines; /** Whether to render the player's gold amount in the top left */ bool RenderGold; /** Does the current panel have a scrollbar */ bool HasScrollbar; -/** Remember last scroll position */ -int OldScrollPos; -/** Scroll position */ -int ScrollPos; /** Next scroll position */ int NextScrollPos; /** Previous scroll position */ @@ -122,9 +96,6 @@ int8_t CountdownScrollUp; /** Countdown for the push state of the scroll down button */ int8_t CountdownScrollDown; -/** Remember current store while displaying a dialog */ -TalkID OldActiveStore; - /** Temporary item used to hold the item being traded */ Item TempItem; @@ -337,25 +308,6 @@ void PrintStoreItem(const Item &item, int l, UiFlags flags, bool cursIndent = fa AddSText(40, l++, productLine, flags, false, -1, cursIndent); } -bool StoreAutoPlace(Item &item, bool persistItem) -{ - Player &player = *MyPlayer; - - if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) { - return true; - } - - if (AutoPlaceItemInBelt(player, item, persistItem, true)) { - return true; - } - - if (persistItem) { - return AutoPlaceItemInInventory(player, item, true); - } - - return CanFitItemInInventory(player, item); -} - void ScrollVendorStore(std::span itemData, int storeLimit, int idx, int selling = true) { ClearSText(5, 21); @@ -400,17 +352,6 @@ void ScrollSmithBuy(int idx) ScrollVendorStore(SmithItems, static_cast(SmithItems.size()), idx); } -uint32_t TotalPlayerGold() -{ - return MyPlayer->_pGold + Stash.gold; -} - -// TODO: Change `_iIvalue` to be unsigned instead of passing `int` here. -bool PlayerCanAfford(int price) -{ - return TotalPlayerGold() >= static_cast(price); -} - void StartSmithBuy() { IsTextFullSize = true; @@ -1365,20 +1306,6 @@ void SmithPremiumBuyEnter() StartStore(TalkID::Confirm); } -bool StoreGoldFit(Item &item) -{ - const int cost = item._iIvalue; - - const Size itemSize = GetInventorySize(item); - const int itemRoomForGold = itemSize.width * itemSize.height * MaxGold; - - if (cost <= itemRoomForGold) { - return true; - } - - return cost <= itemRoomForGold + RoomForGold(); -} - /** * @brief Sells an item from the player's inventory or belt. */ @@ -2740,4 +2667,48 @@ bool IsPlayerInStore() return ActiveStore != TalkID::None; } +uint32_t TotalPlayerGold() +{ + return MyPlayer->_pGold + Stash.gold; +} + +// TODO: Change `_iIvalue` to be unsigned instead of passing `int` here. +bool PlayerCanAfford(int price) +{ + return TotalPlayerGold() >= static_cast(price); +} + +bool StoreAutoPlace(Item &item, bool persistItem) +{ + Player &player = *MyPlayer; + + if (AutoEquipEnabled(player, item) && AutoEquip(player, item, persistItem, true)) { + return true; + } + + if (AutoPlaceItemInBelt(player, item, persistItem, true)) { + return true; + } + + if (persistItem) { + return AutoPlaceItemInInventory(player, item, true); + } + + return CanFitItemInInventory(player, item); +} + +bool StoreGoldFit(Item &item) +{ + int cost = item._iIvalue; + + Size itemSize = GetInventorySize(item); + int itemRoomForGold = itemSize.width * itemSize.height * MaxGold; + + if (cost <= itemRoomForGold) { + return true; + } + + return cost <= itemRoomForGold + RoomForGold(); +} + } // namespace devilution diff --git a/Source/stores.h b/Source/stores.h index fc38e00db..723e0182d 100644 --- a/Source/stores.h +++ b/Source/stores.h @@ -13,6 +13,7 @@ #include "engine/clx_sprite.hpp" #include "engine/surface.hpp" #include "game_mode.hpp" +#include "towners.h" #include "utils/attributes.h" #include "utils/static_vector.hpp" @@ -35,6 +36,41 @@ constexpr int NumWitchPinnedItems = 3; constexpr int NumStoreLines = 104; +struct STextStruct { + enum Type : uint8_t { + Label, + Divider, + Selectable, + }; + + std::string text; + int _sval; + int y; + UiFlags flags; + Type type; + uint8_t _sx; + uint8_t _syoff; + int cursId; + bool cursIndent; + + [[nodiscard]] bool isDivider() const + { + return type == Divider; + } + [[nodiscard]] bool isSelectable() const + { + return type == Selectable; + } + + [[nodiscard]] bool hasText() const + { + return !text.empty(); + } +}; + +/** Text lines */ +extern STextStruct TextLine[NumStoreLines]; + enum class TalkID : uint8_t { None, Smith, @@ -92,6 +128,23 @@ extern int BoyItemLevel; /** Current item sold by Wirt */ extern Item BoyItem; +/** The current towner being interacted with */ +extern _talker_id TownerId; + +/** Remember currently selected text line from TextLine while displaying a dialog */ +extern int OldTextLine; + +/** Remember current store while displaying a dialog */ +extern TalkID OldActiveStore; + +/** Remember last scroll position */ +extern int OldScrollPos; +/** Scroll position */ +extern int ScrollPos; + +/** Currently selected text line from TextLine */ +extern int CurrentTextLine; + void AddStoreHoldRepair(Item *itm, int8_t i); /** Clears premium items sold by Griswold and Wirt. */ @@ -118,5 +171,9 @@ void StoreEnter(); void CheckStoreBtn(); void ReleaseStoreBtn(); bool IsPlayerInStore(); +uint32_t TotalPlayerGold(); +bool PlayerCanAfford(int price); +bool StoreAutoPlace(Item &item, bool persistItem); +bool StoreGoldFit(Item &item); } // namespace devilution diff --git a/vcpkg.json b/vcpkg.json index eecfd4895..32c0c9510 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -30,6 +30,10 @@ } ] }, + "dapi": { + "description": "Build DAPI server for gameplay automation", + "dependencies": [ "protobuf", "sfml" ] + }, "tests": { "description": "Build tests", "dependencies": [ "gtest", "benchmark" ]