Browse Source

Merge 21e0b2fc6f into 5a08031caf

pull/7983/merge
NiteKat 4 days ago committed by GitHub
parent
commit
581b30556c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 12
      3rdParty/sfml/CMakeLists.txt
  2. 1
      CMake/Definitions.cmake
  3. 7
      CMake/Dependencies.cmake
  4. 3
      CMake/VcPkgManifestFeatures.cmake
  5. 2
      CMakeLists.txt
  6. 24
      CMakeSettings.json
  7. 4
      Packaging/windows/CMakePresets.json
  8. 40
      Source/CMakeLists.txt
  9. 214
      Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp
  10. 45
      Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.h
  11. 185
      Source/dapi/Backend/Messages/command.proto
  12. 179
      Source/dapi/Backend/Messages/data.proto
  13. 40
      Source/dapi/Backend/Messages/game.proto
  14. 12
      Source/dapi/Backend/Messages/init.proto
  15. 26
      Source/dapi/Backend/Messages/message.proto
  16. 47
      Source/dapi/GameData.h
  17. 71
      Source/dapi/Item.h
  18. 75
      Source/dapi/Player.h
  19. 2534
      Source/dapi/Server.cpp
  20. 361
      Source/dapi/Server.h
  21. 14
      Source/dapi/Towner.h
  22. 14
      Source/dapi/Trigger.h
  23. 49
      Source/diablo.cpp
  24. 1
      Source/diablo.h
  25. 648
      Source/inv.cpp
  26. 4
      Source/inv.h
  27. 4
      Source/minitext.cpp
  28. 2
      Source/minitext.h
  29. 10
      Source/portal.cpp
  30. 2
      Source/portal.h
  31. 16
      Source/qol/chatlog.cpp
  32. 12
      Source/qol/chatlog.h
  33. 149
      Source/stores.cpp
  34. 57
      Source/stores.h
  35. 4
      vcpkg.json

12
3rdParty/sfml/CMakeLists.txt vendored

@ -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)

1
CMake/Definitions.cmake

@ -19,6 +19,7 @@ foreach(
DEVILUTIONX_RESAMPLER_SDL DEVILUTIONX_RESAMPLER_SDL
DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT DEVILUTIONX_PALETTE_TRANSPARENCY_BLACK_16_LUT
SCREEN_READER_INTEGRATION SCREEN_READER_INTEGRATION
DAPI_SERVER
UNPACKED_MPQS UNPACKED_MPQS
UNPACKED_SAVES UNPACKED_SAVES
DEVILUTIONX_WINDOWS_NO_WCHAR DEVILUTIONX_WINDOWS_NO_WCHAR

7
CMake/Dependencies.cmake

@ -340,3 +340,10 @@ if(GPERF)
find_package(Gperftools REQUIRED) find_package(Gperftools REQUIRED)
message("INFO: ${GPERFTOOLS_LIBRARIES}") message("INFO: ${GPERFTOOLS_LIBRARIES}")
endif() endif()
if(DAPI_SERVER)
find_package(SFML 3.0 COMPONENTS Network CONFIG QUIET)
if(NOT SFML_FOUND)
add_subdirectory(3rdParty/sfml)
endif()
endif()

3
CMake/VcPkgManifestFeatures.cmake

@ -12,6 +12,9 @@ endif()
if(USE_GETTEXT_FROM_VCPKG) if(USE_GETTEXT_FROM_VCPKG)
list(APPEND VCPKG_MANIFEST_FEATURES "translations") list(APPEND VCPKG_MANIFEST_FEATURES "translations")
endif() endif()
if(DAPI_SERVER)
list(APPEND VCPKG_MANIFEST_FEATURES "dapi")
endif()
if(BUILD_TESTING) if(BUILD_TESTING)
list(APPEND VCPKG_MANIFEST_FEATURES "tests") list(APPEND VCPKG_MANIFEST_FEATURES "tests")
endif() endif()

2
CMakeLists.txt

@ -38,6 +38,8 @@ cmake_dependent_option(PACKET_ENCRYPTION "Encrypt network packets" ON "NOT NONET
if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg.cmake$") if(CMAKE_TOOLCHAIN_FILE MATCHES "vcpkg.cmake$")
option(USE_GETTEXT_FROM_VCPKG "Add vcpkg dependency for gettext[tools] for compiling translations" OFF) option(USE_GETTEXT_FROM_VCPKG "Add vcpkg dependency for gettext[tools] for compiling translations" OFF)
endif() endif()
option(DAPI_SERVER "Build with DAPI server component for gameplay automation" OFF)
mark_as_advanced(DAPI_SERVER)
option(BUILD_TESTING "Build tests." ON) option(BUILD_TESTING "Build tests." ON)
# These must be included after the options above but before the `project` call. # These must be included after the options above but before the `project` call.

24
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", "name": "x64-Debug-SDL1",
"generator": "Ninja", "generator": "Ninja",
@ -149,4 +171,4 @@
] ]
} }
] ]
} }

4
Packaging/windows/CMakePresets.json

@ -36,6 +36,10 @@
"DISABLE_LTO": { "DISABLE_LTO": {
"type": "BOOL", "type": "BOOL",
"value": "ON" "value": "ON"
},
"DAPI_SERVER": {
"type": "BOOL",
"value": "ON"
} }
} }
}, },

40
Source/CMakeLists.txt

@ -872,6 +872,17 @@ if(DISCORD_INTEGRATION)
) )
endif() 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) if(SCREEN_READER_INTEGRATION)
list(APPEND libdevilutionx_SRCS list(APPEND libdevilutionx_SRCS
utils/screen_reader.cpp utils/screen_reader.cpp
@ -990,6 +1001,10 @@ if(DISCORD_INTEGRATION)
target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk) target_link_libraries(libdevilutionx PRIVATE discord discord_game_sdk)
endif() endif()
if(DAPI_SERVER)
target_link_libraries(libdevilutionx PRIVATE SFML::Network)
endif()
if(SCREEN_READER_INTEGRATION) if(SCREEN_READER_INTEGRATION)
if(WIN32) if(WIN32)
target_compile_definitions(libdevilutionx PRIVATE Tolk) target_compile_definitions(libdevilutionx PRIVATE Tolk)
@ -1048,3 +1063,28 @@ elseif(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
target_link_libraries(libdevilutionx PUBLIC c++fs) target_link_libraries(libdevilutionx PUBLIC c++fs)
endif() endif()
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 "$<BUILD_INTERFACE:${PROTO_BINARY_DIR}>")
include_directories("${PROTO_BINARY_DIR}/dapi/Backend/Messages")
target_include_directories(libdevilutionx PUBLIC "${CMAKE_CURRENT_SOURCE_DIR}/dapi/Backend/DAPIBackendCore")
endif()

214
Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.cpp

@ -0,0 +1,214 @@
#include <thread>
#include "DAPIProtoClient.h"
namespace DAPI {
DAPIProtoClient::DAPIProtoClient()
: mt(static_cast<unsigned int>(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<sf::IpAddress> 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<char[]> packetContents(new char[size]);
memcpy(packetContents.get(), packet.getData(), size);
auto currentMessage = std::make_unique<dapi::message::Message>();
currentMessage->ParseFromArray(packetContents.get(), static_cast<int>(size));
if (!currentMessage->has_initbroadcast())
return;
auto reply = std::make_unique<dapi::message::Message>();
auto initResponse = reply->mutable_initresponse();
initResponse->set_port(static_cast<int>(connectionPort));
packet.clear();
size = reply->ByteSizeLong();
std::unique_ptr<char[]> buffer(new char[size]);
reply->SerializeToArray(&buffer[0], static_cast<int>(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<dapi::message::Message>();
broadcastMessage->mutable_initbroadcast();
auto size = broadcastMessage->ByteSizeLong();
std::unique_ptr<char[]> buffer(new char[size]);
broadcastMessage->SerializeToArray(&buffer[0], size);
packet.append(buffer.get(), size);
std::optional<sf::IpAddress> 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<char[]> replyBuffer(new char[size]);
memcpy(replyBuffer.get(), packet.getData(), size);
auto currentMessage = std::make_unique<dapi::message::Message>();
currentMessage->ParseFromArray(replyBuffer.get(), size);
if (!currentMessage->has_initresponse())
return;
connectionPort = static_cast<unsigned short>(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<dapi::message::Message> 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<char[]> 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<dapi::message::Message>();
currentMessage->mutable_endofqueue();
packet.clear();
auto size = currentMessage->ByteSizeLong();
std::unique_ptr<char[]> 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<dapi::message::Message> currentMessage;
sf::Packet packet;
// Loop until the end of queue message is received.
while (true) {
packet.clear();
currentMessage = std::make_unique<dapi::message::Message>();
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<char[]> 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<unsigned short>(getRandomInteger(1025, 49151));
}
void DAPIProtoClient::stopListen()
{
tcpListener.close();
}
void DAPIProtoClient::queueMessage(std::unique_ptr<dapi::message::Message> newMessage)
{
messageQueue.push_back(std::move(newMessage));
}
std::unique_ptr<dapi::message::Message> 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

45
Source/dapi/Backend/DAPIBackendCore/DAPIProtoClient.h

@ -0,0 +1,45 @@
#pragma once
#include <deque>
#include <random>
#include <SFML/Network.hpp>
#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<dapi::message::Message> newMessage);
std::unique_ptr<dapi::message::Message> getNextMessage();
bool isConnected() const;
int messageQueueSize() const;
private:
sf::UdpSocket udpSocket;
sf::TcpSocket tcpSocket;
sf::TcpListener tcpListener;
sf::SocketSelector socketSelector;
std::deque<std::unique_ptr<dapi::message::Message>> messageQueue;
std::mt19937 mt;
int getRandomInteger(int min, int max)
{
std::uniform_int_distribution<int> randomNumber(min, max);
return randomNumber(mt);
}
unsigned short connectionPort;
bool udpbound;
};
} // namespace DAPI

185
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;
}
}

179
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;
}

40
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;
}

12
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;
}

26
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;
}
}

47
Source/dapi/GameData.h

@ -0,0 +1,47 @@
#pragma once
#include <map>
#include <vector>
#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<int, PlayerData> playerList;
std::vector<ItemData> itemList;
std::vector<int> groundItems;
std::vector<int> stashItems;
std::map<int, TownerData> townerList;
std::vector<StoreOption> storeList;
std::vector<int> storeItems;
std::vector<TriggerData> triggerList;
};
} // namespace DAPI

71
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

75
Source/dapi/Player.h

@ -0,0 +1,75 @@
#pragma once
#include "Item.h"
#include <map>
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<int, int> InvBody;
int InvList[MAXINV];
int InvGrid[MAXINV];
std::map<int, int> SpdList;
int HoldItem;
int _pIMinDam;
int _pIMaxDam;
int _pIBonusDam;
int _pIAC;
int _pIBonusToHit;
int _pIBonusAC;
int _pIBonusDamMod;
char _pISplLvlAdd;
bool pManaShield;
};
} // namespace DAPI

2534
Source/dapi/Server.cpp

File diff suppressed because it is too large Load Diff

361
Source/dapi/Server.h

@ -0,0 +1,361 @@
#pragma once
#include <fstream>
#include <memory>
#include <sstream>
#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<GameData> data;
std::map<std::pair<int, int>, bool> panelScreenCheck;
DAPIProtoClient protoClient;
};
} // namespace DAPI

14
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

14
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

49
Source/diablo.cpp

@ -122,6 +122,10 @@
#include "controls/touch/renderers.h" #include "controls/touch/renderers.h"
#endif #endif
#ifdef DAPI_SERVER
#include "dapi/Server.h"
#endif
#ifdef __vita__ #ifdef __vita__
#include "platform/vita/touch.h" #include "platform/vita/touch.h"
#endif #endif
@ -146,6 +150,10 @@ clicktype sgbMouseDown;
uint16_t gnTickDelay = 50; uint16_t gnTickDelay = 50;
char gszProductName[64] = "DevilutionX vUnknown"; char gszProductName[64] = "DevilutionX vUnknown";
#ifdef DAPI_SERVER
DAPI::Server dapiServer;
#endif
#ifdef _DEBUG #ifdef _DEBUG
bool DebugDisableNetworkTimeout = false; bool DebugDisableNetworkTimeout = false;
std::vector<std::string> DebugCmdsFromCommandLine; std::vector<std::string> 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) void LeftMouseDown(uint16_t modState)
{ {
LastPlayerAction = PlayerActionType::None; LastPlayerAction = PlayerActionType::None;
@ -1511,8 +1502,15 @@ void UpdateMonsterLights()
void GameLogic() void GameLogic()
{ {
if (!ProcessInput()) { if (!ProcessInput()) {
#ifdef DAPI_SERVER
if (gmenu_is_active())
dapiServer.update(); // For game menu commands
#endif
return; return;
} }
#ifdef DAPI_SERVER
dapiServer.update();
#endif
if (gbProcessPlayers) { if (gbProcessPlayers) {
gGameLogicStep = GameLogicStep::ProcessPlayers; gGameLogicStep = GameLogicStep::ProcessPlayers;
ProcessPlayers(); ProcessPlayers();
@ -3495,4 +3493,21 @@ void PrintScreen(SDL_Keycode vkey)
ReleaseKey(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 } // namespace devilution

1
Source/diablo.h

@ -102,6 +102,7 @@ void DisableInputEventHandler(const SDL_Event &event, uint16_t modState);
tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir); tl::expected<void, std::string> LoadGameLevel(bool firstflag, lvl_entry lvldir);
bool IsDiabloAlive(bool playSFX); bool IsDiabloAlive(bool playSFX);
void PrintScreen(SDL_Keycode vkey); void PrintScreen(SDL_Keycode vkey);
bool TryOpenDungeonWithMouse();
/** /**
* @param bStartup Process additional ticks before returning * @param bStartup Process additional ticks before returning

648
Source/inv.cpp

@ -558,68 +558,6 @@ item_equip_type GetItemEquipType(const Player &player, int slot, item_equip_type
return ILOC_UNEQUIPABLE; 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) inv_body_loc MapSlotToInvBodyLoc(inv_xy_slot slot)
{ {
assert(slot <= SLOTXY_CHEST); assert(slot <= SLOTXY_CHEST);
@ -746,283 +684,74 @@ bool CouldFitItemInInventory(const Player &player, const Item &item, int itemInd
return static_cast<bool>(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore)); return static_cast<bool>(FindSlotForItem(player, GetInventorySize(item), itemIndexToIgnore));
} }
void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem) void TryCombineNaKrulNotes(Player &player, Item &noteItem)
{ {
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; return;
} }
CloseGoldDrop(); for (const _item_indexes note : notes) {
if (idx != note && !HasInventoryItemWithId(player, note)) {
return; // the player doesn't have all notes
}
}
std::optional<inv_xy_slot> maybeSlot = FindSlotUnderCursor(cursorPosition); MyPlayer->Say(HeroSpeech::JustWhatIWasLookingFor, 10);
if (!maybeSlot) { for (const _item_indexes note : notes) {
// not on an inventory slot rectangle if (idx != note) {
return; 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; void CheckQuestItem(Player &player, Item &questItem)
holdItem.clear(); {
const Player &myPlayer = *MyPlayer;
bool attemptedMove = false; if (Quests[Q_BLIND]._qactive == QUEST_ACTIVE
bool automaticallyMoved = false; && (questItem.IDidx == IDI_OPTAMULET
SfxID successSound = SfxID::None; || (Quests[Q_BLIND].IsAvailable() && questItem.position == (SetPiece.position.megaToWorld() + Displacement { 5, 5 })))) {
HeroSpeech failedSpeech = HeroSpeech::ICantDoThat; // Default message if the player attempts to automove an item that can't go anywhere else Quests[Q_BLIND]._qactive = QUEST_DONE;
NetSendCmdQuest(true, Quests[Q_BLIND]);
}
if (r >= SLOTXY_HEAD && r <= SLOTXY_CHEST) { if (questItem.IDidx == IDI_MUSHROOM && Quests[Q_MUSHROOM]._qactive == QUEST_ACTIVE && Quests[Q_MUSHROOM]._qvar1 == QS_MUSHSPAWNED) {
const inv_body_loc invloc = MapSlotToInvBodyLoc(r); player.Say(HeroSpeech::NowThatsOneBigMushroom, 10); // BUGFIX: Voice for this quest might be wrong in MP
if (!player.InvBody[invloc].isEmpty()) { Quests[Q_MUSHROOM]._qvar1 = QS_MUSHPICKED;
if (automaticMove) { NetSendCmdQuest(true, Quests[Q_MUSHROOM]);
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) { if (questItem.IDidx == IDI_ANVIL && Quests[Q_ANVIL]._qactive != QUEST_NOTAVAIL) {
const unsigned ig = r - SLOTXY_INV_FIRST; if (Quests[Q_ANVIL]._qactive == QUEST_INIT) {
const int iv = std::abs(player.InvGrid[ig]) - 1; Quests[Q_ANVIL]._qactive = QUEST_ACTIVE;
if (iv >= 0) { NetSendCmdQuest(true, Quests[Q_ANVIL]);
if (automaticMove) { }
attemptedMove = true; if (Quests[Q_ANVIL]._qlog) {
if (CanBePlacedOnBelt(player, player.InvList[iv])) { myPlayer.Say(HeroSpeech::INeedToGetThisToGriswold, 10);
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_GLDNELIX && Quests[Q_VEIL]._qactive != QUEST_NOTAVAIL) {
* If the player shift-clicks an item in the inventory we want to swap it with whatever item may be myPlayer.Say(HeroSpeech::INeedToGetThisToLachdanan, 30);
* 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 (questItem.IDidx == IDI_ROCK && Quests[Q_ROCK]._qactive != QUEST_NOTAVAIL) {
if (!holdItem.isEmpty() && AutoPlaceItemInInventory(player, holdItem)) { if (Quests[Q_ROCK]._qactive == QUEST_INIT) {
holdItem.clear(); Quests[Q_ROCK]._qactive = QUEST_ACTIVE;
} // 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. NetSendCmdQuest(true, Quests[Q_ROCK]);
} 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... if (Quests[Q_ROCK]._qlog) {
player.InvBody[invloc] = holdItem.pop(); myPlayer.Say(HeroSpeech::ThisMustBeWhatGriswoldWanted, 10);
}
}
} 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 &noteItem)
{
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);
} }
} }
@ -2276,4 +2005,275 @@ Size GetInventorySize(const Item &item)
return { size.width / InventorySlotSizeInPixels.width, size.height / InventorySlotSizeInPixels.height }; 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<inv_xy_slot> 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 } // namespace devilution

4
Source/inv.h

@ -389,6 +389,10 @@ inline bool RemoveInventoryOrBeltItemById(Player &player, _item_indexes id)
*/ */
void ConsumeScroll(Player &player); void ConsumeScroll(Player &player);
void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool dropItem);
void CheckInvPaste(Player &player, Point cursorPosition);
/* data */ /* data */
} // namespace devilution } // namespace devilution

4
Source/minitext.cpp

@ -26,6 +26,8 @@ namespace devilution {
bool qtextflag; bool qtextflag;
std::vector<std::string> TextLines;
namespace { namespace {
/** Vertical speed of the scrolling text in ms/px */ /** 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. */ /** Pixels for a line of text and the empty space under it. */
const int LineHeight = 38; const int LineHeight = 38;
std::vector<std::string> TextLines;
void LoadText(std::string_view text) void LoadText(std::string_view text)
{ {
TextLines.clear(); TextLines.clear();

2
Source/minitext.h

@ -13,6 +13,8 @@ namespace devilution {
/** Specify if the quest dialog window is being shown */ /** Specify if the quest dialog window is being shown */
extern bool qtextflag; extern bool qtextflag;
extern std::vector<std::string> TextLines;
/** /**
* @brief Free the resources used by the quest dialog window * @brief Free the resources used by the quest dialog window
*/ */

10
Source/portal.cpp

@ -16,11 +16,6 @@ namespace devilution {
/** In-game state of portals. */ /** In-game state of portals. */
Portal Portals[MAXPORTAL]; Portal Portals[MAXPORTAL];
namespace {
/** Current portal number (a portal array index). */
size_t portalindex;
/** Coordinate of each player's portal in town. */ /** Coordinate of each player's portal in town. */
Point PortalTownPosition[MAXPORTAL] = { Point PortalTownPosition[MAXPORTAL] = {
{ 57, 40 }, { 57, 40 },
@ -29,6 +24,11 @@ Point PortalTownPosition[MAXPORTAL] = {
{ 63, 40 }, { 63, 40 },
}; };
namespace {
/** Current portal number (a portal array index). */
size_t portalindex;
} // namespace } // namespace
void InitPortals() void InitPortals()

2
Source/portal.h

@ -25,6 +25,8 @@ struct Portal {
extern Portal Portals[MAXPORTAL]; extern Portal Portals[MAXPORTAL];
extern Point PortalTownPosition[MAXPORTAL];
void InitPortals(); void InitPortals();
void SetPortalStats(int i, bool o, Point position, int lvl, dungeon_type lvltype, bool isSetLevel); void SetPortalStats(int i, bool o, Point position, int lvl, dungeon_type lvltype, bool isSetLevel);
void AddPortalMissile(const Player &player, Point position, bool sync); void AddPortalMissile(const Player &player, Point position, bool sync);

16
Source/qol/chatlog.cpp

@ -28,23 +28,13 @@
namespace devilution { namespace devilution {
namespace { std::vector<MultiColoredText> ChatLogLines;
unsigned int MessageCounter = 0;
struct ColoredText {
std::string text;
UiFlags color;
};
struct MultiColoredText { namespace {
std::string text;
std::vector<ColoredText> colors;
};
bool UnreadFlag = false; bool UnreadFlag = false;
size_t SkipLines; size_t SkipLines;
unsigned int MessageCounter = 0;
std::vector<MultiColoredText> ChatLogLines;
constexpr int PaddingTop = 32; constexpr int PaddingTop = 32;
constexpr int PaddingLeft = 32; constexpr int PaddingLeft = 32;

12
Source/qol/chatlog.h

@ -11,7 +11,19 @@
namespace devilution { namespace devilution {
struct ColoredText {
std::string text;
UiFlags color;
};
struct MultiColoredText {
std::string text;
std::vector<ColoredText> colors;
};
extern bool ChatLogFlag; extern bool ChatLogFlag;
extern std::vector<MultiColoredText> ChatLogLines;
extern unsigned int MessageCounter;
void ToggleChatLog(); void ToggleChatLog();
void AddMessageToChatLog(std::string_view message, Player *player = nullptr, UiFlags flags = UiFlags::ColorWhite); void AddMessageToChatLog(std::string_view message, Player *player = nullptr, UiFlags flags = UiFlags::ColorWhite);

149
Source/stores.cpp

@ -54,65 +54,39 @@ StaticVector<Item, NumWitchItemsHf> WitchItems;
int BoyItemLevel; int BoyItemLevel;
Item BoyItem; Item BoyItem;
namespace { /** Text lines */
STextStruct TextLine[NumStoreLines];
/** The current towner being interacted with */ /** The current towner being interacted with */
_talker_id TownerId; _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 */ /** Remember currently selected text line from TextLine while displaying a dialog */
int OldTextLine; 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 */ /** Currently selected text line from TextLine */
int CurrentTextLine; int CurrentTextLine;
struct STextStruct { namespace {
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 /** Is the current dialog full size */
{ bool IsTextFullSize;
return !text.empty();
}
};
/** Text lines */ /** Number of text lines in the current dialog */
STextStruct TextLine[NumStoreLines]; int NumTextLines;
/** Whether to render the player's gold amount in the top left */ /** Whether to render the player's gold amount in the top left */
bool RenderGold; bool RenderGold;
/** Does the current panel have a scrollbar */ /** Does the current panel have a scrollbar */
bool HasScrollbar; bool HasScrollbar;
/** Remember last scroll position */
int OldScrollPos;
/** Scroll position */
int ScrollPos;
/** Next scroll position */ /** Next scroll position */
int NextScrollPos; int NextScrollPos;
/** Previous scroll position */ /** Previous scroll position */
@ -122,9 +96,6 @@ int8_t CountdownScrollUp;
/** Countdown for the push state of the scroll down button */ /** Countdown for the push state of the scroll down button */
int8_t CountdownScrollDown; int8_t CountdownScrollDown;
/** Remember current store while displaying a dialog */
TalkID OldActiveStore;
/** Temporary item used to hold the item being traded */ /** Temporary item used to hold the item being traded */
Item TempItem; 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); 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<Item> itemData, int storeLimit, int idx, int selling = true) void ScrollVendorStore(std::span<Item> itemData, int storeLimit, int idx, int selling = true)
{ {
ClearSText(5, 21); ClearSText(5, 21);
@ -400,17 +352,6 @@ void ScrollSmithBuy(int idx)
ScrollVendorStore(SmithItems, static_cast<int>(SmithItems.size()), idx); ScrollVendorStore(SmithItems, static_cast<int>(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<uint32_t>(price);
}
void StartSmithBuy() void StartSmithBuy()
{ {
IsTextFullSize = true; IsTextFullSize = true;
@ -1365,20 +1306,6 @@ void SmithPremiumBuyEnter()
StartStore(TalkID::Confirm); 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. * @brief Sells an item from the player's inventory or belt.
*/ */
@ -2740,4 +2667,48 @@ bool IsPlayerInStore()
return ActiveStore != TalkID::None; 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<uint32_t>(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 } // namespace devilution

57
Source/stores.h

@ -13,6 +13,7 @@
#include "engine/clx_sprite.hpp" #include "engine/clx_sprite.hpp"
#include "engine/surface.hpp" #include "engine/surface.hpp"
#include "game_mode.hpp" #include "game_mode.hpp"
#include "towners.h"
#include "utils/attributes.h" #include "utils/attributes.h"
#include "utils/static_vector.hpp" #include "utils/static_vector.hpp"
@ -35,6 +36,41 @@ constexpr int NumWitchPinnedItems = 3;
constexpr int NumStoreLines = 104; 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 { enum class TalkID : uint8_t {
None, None,
Smith, Smith,
@ -92,6 +128,23 @@ extern int BoyItemLevel;
/** Current item sold by Wirt */ /** Current item sold by Wirt */
extern Item BoyItem; 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); void AddStoreHoldRepair(Item *itm, int8_t i);
/** Clears premium items sold by Griswold and Wirt. */ /** Clears premium items sold by Griswold and Wirt. */
@ -118,5 +171,9 @@ void StoreEnter();
void CheckStoreBtn(); void CheckStoreBtn();
void ReleaseStoreBtn(); void ReleaseStoreBtn();
bool IsPlayerInStore(); bool IsPlayerInStore();
uint32_t TotalPlayerGold();
bool PlayerCanAfford(int price);
bool StoreAutoPlace(Item &item, bool persistItem);
bool StoreGoldFit(Item &item);
} // namespace devilution } // namespace devilution

4
vcpkg.json

@ -30,6 +30,10 @@
} }
] ]
}, },
"dapi": {
"description": "Build DAPI server for gameplay automation",
"dependencies": [ "protobuf", "sfml" ]
},
"tests": { "tests": {
"description": "Build tests", "description": "Build tests",
"dependencies": [ "gtest", "benchmark" ] "dependencies": [ "gtest", "benchmark" ]

Loading…
Cancel
Save