diff --git a/.bettercodehub.yml b/.bettercodehub.yml deleted file mode 100644 index 70e508efa..000000000 --- a/.bettercodehub.yml +++ /dev/null @@ -1,6 +0,0 @@ -exclude: -- /Packaging/.* -- /3rdParty/.* -languages: -- cpp -component_depth: 2 diff --git a/.github/workflows/Windows_MSVC_x64.yml b/.github/workflows/Windows_MSVC_x64.yml index 32fd06c50..49e76d0fc 100644 --- a/.github/workflows/Windows_MSVC_x64.yml +++ b/.github/workflows/Windows_MSVC_x64.yml @@ -31,9 +31,9 @@ jobs: uses: lukka/get-cmake@latest - name: Restore or setup vcpkg - uses: lukka/run-vcpkg@v11 + uses: lukka/run-vcpkg@v11.1 with: - vcpkgGitCommitId: '78b61582c9e093fda56a01ebb654be15a0033897' + vcpkgGitCommitId: '927bc12e31148b0d44ae9d174b96c20e3bcf08eb' - name: Fetch test data run: | diff --git a/Packaging/nix/devilutionx.metainfo.xml b/Packaging/nix/devilutionx.metainfo.xml index f4ee23893..20d820e63 100644 --- a/Packaging/nix/devilutionx.metainfo.xml +++ b/Packaging/nix/devilutionx.metainfo.xml @@ -64,6 +64,23 @@ + + +

This is a primarily bugfix release, which includes the following updates:

+
    +
  • Resolve various gameplay and graphical issues
  • +
  • Revamped settings menu for better organization
  • +
  • Rectification of crashes identified in version 1.5.0
  • +
  • Reduced RAM usage for improved performance
  • +
  • Updates to PVP arenas
  • +
  • Increased reliability in multiplayer functionality
  • +
  • Improved translations
  • +
  • Fixed gameplay recording playback issues
  • +
+

Please visit the full changelog for more detailed notes

+
+ https://github.com/diasurgical/devilutionX/releases/tag/1.5.1 +

This release includes a lot of features, such as:

diff --git a/Source/automap.cpp b/Source/automap.cpp index 0fb67dbd7..cce3d7643 100644 --- a/Source/automap.cpp +++ b/Source/automap.cpp @@ -116,16 +116,28 @@ void DrawDiamond(const Surface &out, Point center, uint8_t color) void DrawMapVerticalDoor(const Surface &out, Point center, uint8_t colorBright, uint8_t colorDim) { - DrawMapLineNE(out, { center.x + AmLine(8), center.y - AmLine(4) }, AmLine(4), colorDim); - DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim); - DrawDiamond(out, center, colorBright); + if (leveltype != DTYPE_CATACOMBS) { + DrawMapLineNE(out, { center.x + AmLine(8), center.y - AmLine(4) }, AmLine(4), colorDim); + DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim); + DrawDiamond(out, center, colorBright); + } else { + DrawMapLineNE(out, { center.x - AmLine(8), center.y + AmLine(4) }, AmLine(8), colorDim); + DrawMapLineNE(out, { center.x - AmLine(16), center.y + AmLine(8) }, AmLine(4), colorDim); + DrawDiamond(out, { center.x + AmLine(16), center.y - AmLine(8) }, colorBright); + } } void DrawMapHorizontalDoor(const Surface &out, Point center, uint8_t colorBright, uint8_t colorDim) { - DrawMapLineSE(out, { center.x - AmLine(16), center.y - AmLine(8) }, AmLine(4), colorDim); - DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim); - DrawDiamond(out, center, colorBright); + if (leveltype != DTYPE_CATACOMBS) { + DrawMapLineSE(out, { center.x - AmLine(16), center.y - AmLine(8) }, AmLine(4), colorDim); + DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim); + DrawDiamond(out, center, colorBright); + } else { + DrawMapLineSE(out, { center.x - AmLine(8), center.y - AmLine(4) }, AmLine(8), colorDim); + DrawMapLineSE(out, { center.x + AmLine(8), center.y + AmLine(4) }, AmLine(4), colorDim); + DrawDiamond(out, { center.x - AmLine(16), center.y - AmLine(8) }, colorBright); + } } void DrawDirt(const Surface &out, Point center, uint8_t color) @@ -897,6 +909,7 @@ void DrawAutomap(const Surface &out) Displacement myPlayerOffset = {}; if (myPlayer.isWalking()) myPlayerOffset = GetOffsetForWalking(myPlayer.AnimInfo, myPlayer._pdir, true); + myPlayerOffset += Displacement { -1, (leveltype != DTYPE_CAVES) ? TILE_HEIGHT - 1 : -1 }; int d = (AutoMapScale * 64) / 100; int cells = 2 * (gnScreenWidth / 2 / d) + 1; @@ -958,6 +971,8 @@ void DrawAutomap(const Surface &out) screen.y += AmLine(32); } + if (leveltype == DTYPE_CAVES) + myPlayerOffset.deltaY += TILE_HEIGHT; for (size_t playerId = 0; playerId < Players.size(); playerId++) { Player &player = Players[playerId]; if (player.isOnActiveLevel() && player.plractive && !player._pLvlChanging && (&player == MyPlayer || player.friendlyMode)) { @@ -965,6 +980,7 @@ void DrawAutomap(const Surface &out) } } + myPlayerOffset.deltaY -= TILE_HEIGHT / 2; if (AutoMapShowItems) SearchAutomapItem(out, myPlayerOffset, 8, [](Point position) { return dItem[position.x][position.y] != 0; }); #ifdef _DEBUG diff --git a/Source/controls/plrctrls.cpp b/Source/controls/plrctrls.cpp index fdcb89e91..72af03300 100644 --- a/Source/controls/plrctrls.cpp +++ b/Source/controls/plrctrls.cpp @@ -655,10 +655,10 @@ Point InvGetEquipSlotCoordFromInvSlot(const inv_xy_slot slot) Point GetSlotCoord(int slot) { if (slot >= SLOTXY_BELT_FIRST && slot <= SLOTXY_BELT_LAST) { - return GetPanelPosition(UiPanels::Main, InvRect[slot].position); + return GetPanelPosition(UiPanels::Main, InvRect[slot].Center()); } - return GetPanelPosition(UiPanels::Inventory, InvRect[slot].position); + return GetPanelPosition(UiPanels::Inventory, InvRect[slot].Center()); } /** @@ -734,25 +734,12 @@ void ResetInvCursorPosition() } else { mousePos = GetSlotCoord(Slot); } - - if (!MyPlayer->HoldItem.isEmpty()) { - mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX }; - } } else if (Slot >= SLOTXY_BELT_FIRST && Slot <= SLOTXY_BELT_LAST) { mousePos = GetSlotCoord(Slot); - if (!MyPlayer->HoldItem.isEmpty()) - mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX }; } else { mousePos = InvGetEquipSlotCoordFromInvSlot((inv_xy_slot)Slot); - if (!MyPlayer->HoldItem.isEmpty()) { - Size itemSize = GetInventorySize(MyPlayer->HoldItem); - mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX * itemSize.height }; - } } - mousePos.x += (InventorySlotSizeInPixels.width / 2); - mousePos.y -= (InventorySlotSizeInPixels.height / 2); - SetCursorPos(mousePos); } @@ -760,7 +747,6 @@ int FindClosestInventorySlot(Point mousePos) { int shortestDistance = std::numeric_limits::max(); int bestSlot = 0; - mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, INV_SLOT_HALF_SIZE_PX }; for (int i = 0; i < NUM_XY_SLOTS; i++) { int distance = mousePos.ManhattanDistance(GetSlotCoord(i)); @@ -777,7 +763,6 @@ Point FindClosestStashSlot(Point mousePos) { int shortestDistance = std::numeric_limits::max(); Point bestSlot = {}; - mousePos += Displacement { -INV_SLOT_HALF_SIZE_PX, -INV_SLOT_HALF_SIZE_PX }; for (Point point : PointsInRectangle(Rectangle { { 0, 0 }, Size { 10, 10 } })) { int distance = mousePos.ManhattanDistance(GetStashSlotCoord(point)); @@ -820,7 +805,7 @@ void InventoryMove(AxisDirection dir) const Item &heldItem = MyPlayer->HoldItem; const bool isHoldingItem = !heldItem.isEmpty(); - Size itemSize = GetInventorySize(heldItem); + Size itemSize = isHoldingItem ? GetInventorySize(heldItem) : Size { 1 }; // when item is on cursor (pcurs > 1), this is the real cursor XY if (dir.x == AxisDirectionX_LEFT) { @@ -949,7 +934,7 @@ void InventoryMove(AxisDirection dir) if (Slot == SLOTXY_HEAD || Slot == SLOTXY_CHEST) { Slot = SLOTXY_INV_ROW1_FIRST + 4; } else if (Slot == SLOTXY_RING_LEFT || Slot == SLOTXY_HAND_LEFT) { - Slot = SLOTXY_INV_ROW1_FIRST + 1; + Slot = SLOTXY_INV_ROW1_FIRST + (itemSize.width > 1 ? 0 : 1); } else if (Slot == SLOTXY_RING_RIGHT || Slot == SLOTXY_HAND_RIGHT || Slot == SLOTXY_AMULET) { Slot = SLOTXY_INV_ROW1_LAST - 1; } else if (Slot <= (SLOTXY_INV_ROW4_LAST - (itemSize.height * INV_ROW_SLOT_SIZE))) { @@ -1007,28 +992,27 @@ void InventoryMove(AxisDirection dir) } else { mousePos = GetSlotCoord(Slot); } - // move cursor to the center of the slot if not holding anything or top left is holding an object - if (isHoldingItem) { - if (Slot < SLOTXY_INV_FIRST) { - // The coordinates we get for body slots are based on the centre of the region relative to the hand cursor - // Need to adjust the position for items larger than 1x1 so they're aligned as expected - mousePos.x -= itemSize.width * INV_SLOT_HALF_SIZE_PX; - mousePos.y -= itemSize.height * INV_SLOT_HALF_SIZE_PX; - } - } else { - // get item under new slot if navigating on the inventory - if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_BELT_LAST) { + // If we're in the inventory we may need to move the cursor to an area that doesn't line up with the center of a cell + if (Slot >= SLOTXY_INV_FIRST && Slot <= SLOTXY_INV_LAST) { + if (!isHoldingItem) { + // If we're not holding an item int8_t itemInvId = GetItemIdOnSlot(Slot); - int itemSlot = FindFirstSlotOnItem(itemInvId); - if (itemSlot < 0) - itemSlot = Slot; - - // offset the cursor so it shows over the center of the item - mousePos = GetSlotCoord(itemSlot); - itemSize = GetItemSizeOnSlot(itemSlot); - mousePos.x += (itemSize.width * InventorySlotSizeInPixels.width) / 2; - mousePos.y += (itemSize.height * InventorySlotSizeInPixels.height) / 2; + if (itemInvId != 0) { + // but the cursor moved over an item + int itemSlot = FindFirstSlotOnItem(itemInvId); + if (itemSlot < 0) + itemSlot = Slot; + + // then we need to offset the cursor so it shows over the center of the item + mousePos = GetSlotCoord(itemSlot); + itemSize = GetItemSizeOnSlot(itemSlot); + } } + // At this point itemSize is either the size of the cell/item the hand cursor is over, or the size of the item we're currently holding. + // mousePos is the center of the top left cell of the item under the hand cursor, or the top left cell of the region that could fit the item we're holding. + // either way we need to offset the mouse position to account for items (we're holding or hovering over) with a dimension larger than a single cell. + mousePos.x += ((itemSize.width - 1) * InventorySlotSizeInPixels.width) / 2; + mousePos.y += ((itemSize.height - 1) * InventorySlotSizeInPixels.height) / 2; } if (mousePos == MousePosition) { @@ -1184,9 +1168,9 @@ void StashMove(AxisDirection dir) if (ActiveStashSlot != InvalidStashPoint) { Point mousePos = GetStashSlotCoord(ActiveStashSlot); - if (pcurs == CURSOR_HAND) { - mousePos += Displacement { INV_SLOT_HALF_SIZE_PX, INV_SLOT_HALF_SIZE_PX }; - } + // Stash coordinates are all the top left of the cell, so we need to shift the mouse to the center of the held item + // or the center of the cell if we have a hand cursor (itemSize will be 1x1 here so we can use the same calculation) + mousePos += Displacement { itemSize.width * INV_SLOT_HALF_SIZE_PX, itemSize.height * INV_SLOT_HALF_SIZE_PX }; SetCursorPos(mousePos); return; } @@ -1827,46 +1811,49 @@ void PerformPrimaryAction() } else if (GetRightPanel().contains(MousePosition) || GetMainPanel().contains(MousePosition)) { int inventorySlot = (Slot >= 0) ? Slot : FindClosestInventorySlot(MousePosition); - const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); - - // Find any item occupying a slot that is currently under the cursor - int8_t itemUnderCursor = [](int inventorySlot, Size cursorSizeInCells) { - if (inventorySlot < SLOTXY_INV_FIRST || inventorySlot > SLOTXY_INV_LAST) - return 0; - for (int x = 0; x < cursorSizeInCells.width; x++) { - for (int y = 0; y < cursorSizeInCells.height; y++) { - int slotUnderCursor = inventorySlot + x + y * INV_ROW_SLOT_SIZE; - if (slotUnderCursor > SLOTXY_INV_LAST) - continue; - int itemId = GetItemIdOnSlot(slotUnderCursor); - if (itemId != 0) - return itemId; + int jumpSlot = inventorySlot; // If the cursor is over an inventory slot we may need to adjust it due to pasting items of different sizes over each other + if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) { + const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); + + // Find any item occupying a slot that is currently under the cursor + int8_t itemUnderCursor = [](int inventorySlot, Size cursorSizeInCells) { + if (inventorySlot < SLOTXY_INV_FIRST || inventorySlot > SLOTXY_INV_LAST) + return 0; + for (int x = 0; x < cursorSizeInCells.width; x++) { + for (int y = 0; y < cursorSizeInCells.height; y++) { + int slotUnderCursor = inventorySlot + x + y * INV_ROW_SLOT_SIZE; + if (slotUnderCursor > SLOTXY_INV_LAST) + continue; + int itemId = GetItemIdOnSlot(slotUnderCursor); + if (itemId != 0) + return itemId; + } } - } - return 0; - }(inventorySlot, cursorSizeInCells); + return 0; + }(inventorySlot, cursorSizeInCells); - // The cursor will need to be shifted to - // this slot if the item is swapped or lifted - int jumpSlot = FindFirstSlotOnItem(itemUnderCursor); + // Capture the first slot of the first item (if any) under the cursor + if (itemUnderCursor > 0) + jumpSlot = FindFirstSlotOnItem(itemUnderCursor); + } CheckInvItem(); - // If we don't find the item in the same position as before, - // it suggests that the item was swapped or lifted - int newSlot = FindFirstSlotOnItem(itemUnderCursor); - if (jumpSlot >= 0 && jumpSlot != newSlot) { + if (inventorySlot >= SLOTXY_INV_FIRST && inventorySlot <= SLOTXY_INV_LAST) { Point mousePos = GetSlotCoord(jumpSlot); Slot = jumpSlot; + const Size newCursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? GetItemSizeOnSlot(jumpSlot) : GetInventorySize(MyPlayer->HoldItem); + mousePos.x += ((newCursorSizeInCells.width - 1) * InventorySlotSizeInPixels.width) / 2; + mousePos.y += ((newCursorSizeInCells.height - 1) * InventorySlotSizeInPixels.height) / 2; SetCursorPos(mousePos); } } else if (IsStashOpen && GetLeftPanel().contains(MousePosition)) { Point stashSlot = (ActiveStashSlot != InvalidStashPoint) ? ActiveStashSlot : FindClosestStashSlot(MousePosition); - const Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); + Size cursorSizeInCells = MyPlayer->HoldItem.isEmpty() ? Size { 1, 1 } : GetInventorySize(MyPlayer->HoldItem); // Find any item occupying a slot that is currently under the cursor StashStruct::StashCell itemUnderCursor = [](Point stashSlot, Size cursorSizeInCells) -> StashStruct::StashCell { - if (stashSlot != InvalidStashPoint) + if (stashSlot == InvalidStashPoint) return StashStruct::EmptyCell; for (Point slotUnderCursor : PointsInRectangle(Rectangle { stashSlot, cursorSizeInCells })) { if (slotUnderCursor.x >= 10 || slotUnderCursor.y >= 10) @@ -1878,20 +1865,26 @@ void PerformPrimaryAction() return StashStruct::EmptyCell; }(stashSlot, cursorSizeInCells); - // The cursor will need to be shifted to - // this slot if the item is swapped or lifted - Point jumpSlot = FindFirstStashSlotOnItem(itemUnderCursor); + Point jumpSlot = itemUnderCursor == StashStruct::EmptyCell ? stashSlot : FindFirstStashSlotOnItem(itemUnderCursor); CheckStashItem(MousePosition); - // If we don't find the item in the same position as before, - // it suggests that the item was swapped or lifted - Point newSlot = FindFirstStashSlotOnItem(itemUnderCursor); - if (jumpSlot != InvalidStashPoint && jumpSlot != newSlot) { - Point mousePos = GetStashSlotCoord(jumpSlot); - mousePos.y -= InventorySlotSizeInPixels.height; - ActiveStashSlot = jumpSlot; - SetCursorPos(mousePos); + Point mousePos = GetStashSlotCoord(jumpSlot); + ActiveStashSlot = jumpSlot; + if (MyPlayer->HoldItem.isEmpty()) { + // For inventory cut/paste we can combine the cases where we swap or simply paste items. Because stash movement is always cell based (there's no fast + // movement over large items) it looks better if we offset the hand cursor to the bottom right cell of the item we just placed. + ActiveStashSlot += Displacement { cursorSizeInCells - 1 }; // shift the active stash slot coordinates to account for items larger than 1x1 + // Then we displace the mouse position to the bottom right corner of the item, then shift it back half a cell to center it. + // Could also be written as (cursorSize - 1) * InventorySlotSize + HalfInventorySlotSize, same thing in the end. + mousePos += Displacement { cursorSizeInCells } * Displacement { InventorySlotSizeInPixels } - Displacement { InventorySlotSizeInPixels } / 2; + } else { + // If we've picked up an item then use the same logic as the inventory so that the cursor is offset to the center of where the old item location was + // (in this case jumpSlot was the top left cell of where it used to be in the grid, and we need to update the cursor size since we're now holding the item) + cursorSizeInCells = GetInventorySize(MyPlayer->HoldItem); + mousePos.x += ((cursorSizeInCells.width) * InventorySlotSizeInPixels.width) / 2; + mousePos.y += ((cursorSizeInCells.height) * InventorySlotSizeInPixels.height) / 2; } + SetCursorPos(mousePos); } return; } diff --git a/Source/engine/render/scrollrt.cpp b/Source/engine/render/scrollrt.cpp index 10ab6c042..71b122112 100644 --- a/Source/engine/render/scrollrt.cpp +++ b/Source/engine/render/scrollrt.cpp @@ -240,13 +240,15 @@ void DrawCursor(const Surface &out) // Copy the buffer before the item cursor and its 1px outline are drawn to a temporary buffer. const int outlineWidth = !MyPlayer->HoldItem.isEmpty() ? 1 : 0; + Displacement offset = !MyPlayer->HoldItem.isEmpty() ? Displacement { cursSize / 2 } : Displacement { 0 }; + Point cursPosition = MousePosition - offset; Rectangle &rect = cursor.rect; - rect.position.x = MousePosition.x - outlineWidth; + rect.position.x = cursPosition.x - outlineWidth; rect.size.width = cursSize.width + 2 * outlineWidth; Clip(rect.position.x, rect.size.width, out.w()); - rect.position.y = MousePosition.y - outlineWidth; + rect.position.y = cursPosition.y - outlineWidth; rect.size.height = cursSize.height + 2 * outlineWidth; Clip(rect.position.y, rect.size.height, out.h()); @@ -254,7 +256,7 @@ void DrawCursor(const Surface &out) return; BlitCursor(cursor.behindBuffer, rect.size.width, &out[rect.position], out.pitch(), rect.size.width, rect.size.height); - DrawSoftwareCursor(out, MousePosition + Displacement { 0, cursSize.height - 1 }, pcurs); + DrawSoftwareCursor(out, cursPosition + Displacement { 0, cursSize.height - 1 }, pcurs); } /** diff --git a/Source/error.cpp b/Source/error.cpp index 5368d7adf..785706d3a 100644 --- a/Source/error.cpp +++ b/Source/error.cpp @@ -22,19 +22,23 @@ namespace devilution { namespace { -std::deque DiabloMessages; +struct MessageEntry { + std::string text; + uint32_t duration; // Duration in milliseconds +}; + +std::deque DiabloMessages; +uint32_t msgStartTime = 0; std::vector TextLines; -uint32_t msgdelay; int ErrorWindowHeight = 54; const int LineHeight = 12; const int LineWidth = 418; void InitNextLines() { - msgdelay = GetMillisecondsSinceStartup(); TextLines.clear(); - const std::string paragraphs = WordWrapString(DiabloMessages.front(), LineWidth, GameFont12, 1); + const std::string paragraphs = WordWrapString(DiabloMessages.front().text, LineWidth, GameFont12, 1); size_t previous = 0; while (true) { @@ -53,7 +57,7 @@ void InitNextLines() /** Maps from error_id to error message. */ const char *const MsgStrings[] = { "", - N_("No automap available in town"), + N_("Game saved"), N_("No multiplayer functions in demo"), N_("Direct Sound Creation Failed"), N_("Not available in shareware version"), @@ -109,22 +113,25 @@ const char *const MsgStrings[] = { N_(/* TRANSLATORS: Shrine Text. Keep atmospheric. :) */ "That which can break will."), }; -void InitDiabloMsg(diablo_message e) +void InitDiabloMsg(diablo_message e, uint32_t duration /*= 3500*/) { - InitDiabloMsg(LanguageTranslate(MsgStrings[e])); + InitDiabloMsg(LanguageTranslate(MsgStrings[e]), duration); } -void InitDiabloMsg(std::string_view msg) +void InitDiabloMsg(std::string_view msg, uint32_t duration /*= 3500*/) { if (DiabloMessages.size() >= MAX_SEND_STR_LEN) return; - if (c_find(DiabloMessages, msg) != DiabloMessages.end()) + if (c_find_if(DiabloMessages, [&msg](const MessageEntry &entry) { return entry.text == msg; }) + != DiabloMessages.end()) return; - DiabloMessages.push_back(std::string(msg)); - if (DiabloMessages.size() == 1) + DiabloMessages.push_back({ std::string(msg), duration }); + if (DiabloMessages.size() == 1) { InitNextLines(); + msgStartTime = SDL_GetTicks(); + } } bool IsDiabloMsgAvailable() @@ -134,7 +141,13 @@ bool IsDiabloMsgAvailable() void CancelCurrentDiabloMsg() { - msgdelay = 0; + if (!DiabloMessages.empty()) { + DiabloMessages.pop_front(); + if (!DiabloMessages.empty()) { + InitNextLines(); + msgStartTime = SDL_GetTicks(); + } + } } void ClrDiabloMsg() @@ -173,13 +186,17 @@ void DrawDiabloMsg(const Surface &out) lineNumber += 1; } - if (msgdelay > 0 && msgdelay <= GetMillisecondsSinceStartup() - 3500) { - msgdelay = 0; - } - if (msgdelay == 0) { + // Calculate the time the current message has been displayed + uint32_t currentTime = SDL_GetTicks(); + uint32_t messageElapsedTime = currentTime - msgStartTime; + + // Check if the current message's duration has passed + if (!DiabloMessages.empty() && messageElapsedTime >= DiabloMessages.front().duration) { DiabloMessages.pop_front(); - if (!DiabloMessages.empty()) + if (!DiabloMessages.empty()) { InitNextLines(); + msgStartTime = currentTime; + } } } diff --git a/Source/error.h b/Source/error.h index bf0b56e57..b939a16a4 100644 --- a/Source/error.h +++ b/Source/error.h @@ -15,7 +15,7 @@ namespace devilution { enum diablo_message : uint8_t { EMSG_NONE, - EMSG_NO_AUTOMAP_IN_TOWN, + EMSG_GAME_SAVED, EMSG_NO_MULTIPLAYER_IN_DEMO, EMSG_DIRECT_SOUND_FAILED, EMSG_NOT_IN_SHAREWARE, @@ -71,8 +71,8 @@ enum diablo_message : uint8_t { EMSG_SHRINE_MURPHYS, }; -void InitDiabloMsg(diablo_message e); -void InitDiabloMsg(std::string_view msg); +void InitDiabloMsg(diablo_message e, uint32_t duration = 3500); +void InitDiabloMsg(std::string_view msg, uint32_t duration = 3500); bool IsDiabloMsgAvailable(); void CancelCurrentDiabloMsg(); void ClrDiabloMsg(); diff --git a/Source/gamemenu.cpp b/Source/gamemenu.cpp index 89fe16bb2..3237c414a 100644 --- a/Source/gamemenu.cpp +++ b/Source/gamemenu.cpp @@ -332,6 +332,7 @@ void gamemenu_save_game(bool /*bActivate*/) DrawAndBlit(); SaveGame(); ClrDiabloMsg(); + InitDiabloMsg(EMSG_GAME_SAVED, 1000); RedrawEverything(); NewCursor(CURSOR_HAND); if (CornerStone.activated) { diff --git a/Source/inv.cpp b/Source/inv.cpp index 8807e5931..972b67c72 100644 --- a/Source/inv.cpp +++ b/Source/inv.cpp @@ -45,21 +45,21 @@ bool invflag; * arranged as follows: * * @code{.unparsed} - * 00 01 - * 02 03 06 + * 00 00 + * 00 00 03 * - * 07 08 19 20 13 14 - * 09 10 21 22 15 16 - * 11 12 23 24 17 18 + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 + * 04 04 06 06 05 05 * - * 04 05 + * 01 02 * - * 25 26 27 28 29 30 31 32 33 34 - * 35 36 37 38 39 40 41 42 43 44 - * 45 46 47 48 49 50 51 52 53 54 - * 55 56 57 58 59 60 61 62 63 64 + * 07 08 09 10 11 12 13 14 15 16 + * 17 18 19 20 21 22 23 24 25 26 + * 27 28 29 30 31 32 33 34 35 36 + * 37 38 39 40 41 42 43 44 45 46 * - * 65 66 67 68 69 70 71 72 + * 47 48 49 50 51 52 53 54 * @endcode */ const Rectangle InvRect[] = { @@ -284,36 +284,43 @@ bool AutoEquip(Player &player, const Item &item, inv_body_loc bodyLocation, bool return true; } -int FindSlotUnderCursor(Point cursorPosition, Size itemSize) +int FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) { - int i = cursorPosition.x; - int j = cursorPosition.y; - - if (!IsHardwareCursor()) { - // offset the cursor position to match the hot pixel we'd use for a hardware cursor - i += itemSize.width * INV_SLOT_HALF_SIZE_PX; - j += itemSize.height * INV_SLOT_HALF_SIZE_PX; + Displacement panelOffset = Point { 0, 0 } - GetRightPanel().position; + for (int r = SLOTXY_EQUIPPED_FIRST; r <= SLOTXY_EQUIPPED_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) + return r; } - - for (int r = 0; r < NUM_XY_SLOTS; r++) { - int xo = GetRightPanel().position.x; - int yo = GetRightPanel().position.y; - if (r >= SLOTXY_BELT_FIRST) { - xo = GetMainPanel().position.x; - yo = GetMainPanel().position.y; + for (int r = SLOTXY_INV_FIRST; r <= SLOTXY_INV_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) { + // When trying to paste into the inventory we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. + if (itemSize.height <= 1 && itemSize.width <= 1) { + // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do + return r; + } + // Otherwise work out how far the central cell is from the top-left cell + Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; + // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. + if (itemSize.width % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { + // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left + hotPixelCellOffset.deltaX++; + } + if (itemSize.height % 2 == 0 && InvRect[r].contains(cursorPosition + panelOffset + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { + // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above + hotPixelCellOffset.deltaY++; + } + // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the inventory would otherwise put it out of bounds) + int hotPixelCell = r - SLOTXY_INV_FIRST; + int targetRow = std::clamp((hotPixelCell / InventorySizeInSlots.width) - hotPixelCellOffset.deltaY, 0, InventorySizeInSlots.height - itemSize.height); + int targetColumn = std::clamp((hotPixelCell % InventorySizeInSlots.width) - hotPixelCellOffset.deltaX, 0, InventorySizeInSlots.width - itemSize.width); + return SLOTXY_INV_FIRST + targetRow * InventorySizeInSlots.width + targetColumn; } + } - if (r == SLOTXY_INV_FIRST) { - if (itemSize.width % 2 == 0) - i -= INV_SLOT_HALF_SIZE_PX; - if (itemSize.height % 2 == 0) - j -= INV_SLOT_HALF_SIZE_PX; - } - if (InvRect[r].contains(i - xo, j - yo)) { + panelOffset = Point { 0, 0 } - GetMainPanel().position; + for (int r = SLOTXY_BELT_FIRST; r <= SLOTXY_BELT_LAST; r++) { + if (InvRect[r].contains(cursorPosition + panelOffset)) return r; - } - if (r == SLOTXY_INV_LAST && itemSize.height % 2 == 0) - j += INV_SLOT_HALF_SIZE_PX; } return NUM_XY_SLOTS; } @@ -322,7 +329,7 @@ void CheckInvPaste(Player &player, Point cursorPosition) { Size itemSize = GetInventorySize(player.HoldItem); - int slot = FindSlotUnderCursor(cursorPosition, itemSize); + int slot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); if (slot == NUM_XY_SLOTS) return; @@ -359,26 +366,25 @@ void CheckInvPaste(Player &player, Point cursorPosition) } } } else { - int yy = std::max(INV_ROW_SLOT_SIZE * ((ii / INV_ROW_SLOT_SIZE) - ((itemSize.height - 1) / 2)), 0); - for (int j = 0; j < itemSize.height; j++) { - if (yy >= InventoryGridCells) - return; - int xx = std::max((ii % INV_ROW_SLOT_SIZE) - ((itemSize.width - 1) / 2), 0); - for (int i = 0; i < itemSize.width; i++) { - if (xx >= INV_ROW_SLOT_SIZE) - return; - if (player.InvGrid[xx + yy] != 0) { - int8_t iv = std::abs(player.InvGrid[xx + yy]); + // check that the item we're pasting only overlaps one other item (or is going into empty space) + unsigned originCell = static_cast(slot - SLOTXY_INV_FIRST); + for (unsigned rowOffset = 0; rowOffset < static_cast(itemSize.height * InventorySizeInSlots.width); rowOffset += InventorySizeInSlots.width) { + for (unsigned columnOffset = 0; columnOffset < static_cast(itemSize.width); columnOffset++) { + unsigned testCell = originCell + rowOffset + columnOffset; + // FindTargetSlotUnderItemCursor returns the top left slot of the inventory region that fits the item, we can be confident this calculation is not going to read out of range. + assert(testCell < sizeof(player.InvGrid)); + if (player.InvGrid[testCell] != 0) { + int8_t iv = std::abs(player.InvGrid[testCell]); if (it != 0) { - if (it != iv) + if (it != iv) { + // Found two different items that would be displaced by the held item, can't paste the item here. return; + } } else { it = iv; } } - xx++; } - yy += INV_ROW_SLOT_SIZE; } } } else if (il == ILOC_BELT) { @@ -526,13 +532,8 @@ void CheckInvPaste(Player &player, Point cursorPosition) itemIndex = 0; } } - int ii = slot - SLOTXY_INV_FIRST; - // Calculate top-left position of item for InvGrid and then add item to InvGrid - - int xx = std::max(ii % INV_ROW_SLOT_SIZE - ((itemSize.width - 1) / 2), 0); - int yy = std::max(INV_ROW_SLOT_SIZE * (ii / INV_ROW_SLOT_SIZE - ((itemSize.height - 1) / 2)), 0); - AddItemToInvGrid(player, xx + yy, it, itemSize); + AddItemToInvGrid(player, slot - SLOTXY_INV_FIRST, it, itemSize); } break; case ILOC_BELT: { @@ -555,8 +556,6 @@ void CheckInvPaste(Player &player, Point cursorPosition) } CalcPlrInv(player, true); if (&player == MyPlayer) { - if (player.HoldItem.isEmpty() && !IsHardwareCursor()) - SetCursorPos(MousePosition + Displacement { itemSize * INV_SLOT_HALF_SIZE_PX }); NewCursor(player.HoldItem); } } @@ -820,11 +819,6 @@ void CheckInvCut(Player &player, Point cursorPosition, bool automaticMove, bool holdItem.clear(); } else { NewCursor(holdItem); - if (!IsHardwareCursor() && !dropItem) { - // For a hardware cursor, we set the "hot point" to the center of the item instead. - Size cursSize = GetInvItemSize(holdItem._iCurs + CURSOR_FIRSTITEM); - SetCursorPos(cursorPosition - Displacement(cursSize / 2)); - } } } } diff --git a/Source/inv.h b/Source/inv.h index 353d44427..ac3e98eb5 100644 --- a/Source/inv.h +++ b/Source/inv.h @@ -18,7 +18,8 @@ namespace devilution { #define INV_SLOT_SIZE_PX 28 #define INV_SLOT_HALF_SIZE_PX (INV_SLOT_SIZE_PX / 2) -#define INV_ROW_SLOT_SIZE 10 +constexpr Size InventorySizeInSlots { 10, 4 }; +#define INV_ROW_SLOT_SIZE InventorySizeInSlots.width constexpr Size InventorySlotSizeInPixels { INV_SLOT_SIZE_PX }; enum inv_item : int8_t { @@ -44,12 +45,14 @@ enum inv_item : int8_t { enum inv_xy_slot : uint8_t { // clang-format off SLOTXY_HEAD = 0, + SLOTXY_EQUIPPED_FIRST = SLOTXY_HEAD, SLOTXY_RING_LEFT = 1, SLOTXY_RING_RIGHT = 2, SLOTXY_AMULET = 3, SLOTXY_HAND_LEFT = 4, SLOTXY_HAND_RIGHT = 5, SLOTXY_CHEST = 6, + SLOTXY_EQUIPPED_LAST = SLOTXY_CHEST, // regular inventory SLOTXY_INV_FIRST = 7, @@ -80,7 +83,7 @@ enum item_color : uint8_t { }; extern bool invflag; -extern const Rectangle InvRect[73]; +extern const Rectangle InvRect[NUM_XY_SLOTS]; void InvDrawSlotBack(const Surface &out, Point targetPosition, Size size, item_quality itemQuality); /** diff --git a/Source/pack.cpp b/Source/pack.cpp index d7f34d649..74af8abaa 100644 --- a/Source/pack.cpp +++ b/Source/pack.cpp @@ -556,6 +556,28 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) for (int i = 0; i < NUM_INVLOC; i++) { if (!UnPackNetItem(player, packed.InvBody[i], player.InvBody[i])) return false; + if (player.InvBody[i].isEmpty()) + continue; + auto loc = static_cast(player.GetItemLocation(player.InvBody[i])); + switch (i) { + case INVLOC_HEAD: + ValidateField(loc, loc == ILOC_HELM); + break; + case INVLOC_RING_LEFT: + case INVLOC_RING_RIGHT: + ValidateField(loc, loc == ILOC_RING); + break; + case INVLOC_AMULET: + ValidateField(loc, loc == ILOC_AMULET); + break; + case INVLOC_HAND_LEFT: + case INVLOC_HAND_RIGHT: + ValidateField(loc, IsAnyOf(loc, ILOC_ONEHAND, ILOC_TWOHAND)); + break; + case INVLOC_CHEST: + ValidateField(loc, loc == ILOC_ARMOR); + break; + } } player._pNumInv = packed._pNumInv; @@ -568,8 +590,17 @@ bool UnPackNetPlayer(const PlayerNetPack &packed, Player &player) player.InvGrid[i] = packed.InvGrid[i]; for (int i = 0; i < MaxBeltItems; i++) { - if (!UnPackNetItem(player, packed.SpdList[i], player.SpdList[i])) + Item &item = player.SpdList[i]; + if (!UnPackNetItem(player, packed.SpdList[i], item)) return false; + if (item.isEmpty()) + continue; + Size beltItemSize = GetInventorySize(item); + int8_t beltItemType = static_cast(item._itype); + bool beltItemUsable = item.isUsable(); + ValidateFields(beltItemSize.width, beltItemSize.height, (beltItemSize == Size { 1, 1 })); + ValidateField(beltItemType, item._itype != ItemType::Gold); + ValidateField(beltItemUsable, beltItemUsable); } CalcPlrInv(player, false); diff --git a/Source/qol/stash.cpp b/Source/qol/stash.cpp index cef7bc53e..1a8d39c57 100644 --- a/Source/qol/stash.cpp +++ b/Source/qol/stash.cpp @@ -50,7 +50,8 @@ constexpr Rectangle StashButtonRect[] = { // clang-format on }; -constexpr PointsInRectangle StashGridRange { { { 0, 0 }, Size { 10, 10 } } }; +constexpr Size StashGridSize { 10, 10 }; +constexpr PointsInRectangle StashGridRange { { { 0, 0 }, StashGridSize } }; OptionalOwnedClxSpriteList StashPanelArt; OptionalOwnedClxSpriteList StashNavButtonArt; @@ -68,7 +69,7 @@ void AddItemToStashGrid(unsigned page, Point position, uint16_t stashListIndex, } } -Point FindSlotUnderCursor(Point cursorPosition) +std::optional FindTargetSlotUnderItemCursor(Point cursorPosition, Size itemSize) { for (auto point : StashGridRange) { Rectangle cell { @@ -77,11 +78,30 @@ Point FindSlotUnderCursor(Point cursorPosition) }; if (cell.contains(cursorPosition)) { + // When trying to paste into the stash we need to determine the top left cell of the nearest area that could fit the item, not the slot under the center/hot pixel. + if (itemSize.height <= 1 && itemSize.width <= 1) { + // top left cell of a 1x1 item is the same cell as the hot pixel, no work to do + return point; + } + // Otherwise work out how far the central cell is from the top-left cell + Displacement hotPixelCellOffset = { (itemSize.width - 1) / 2, (itemSize.height - 1) / 2 }; + // For even dimension items we need to work out if the cursor is in the left/right (or top/bottom) half of the central cell and adjust the offset so the item lands in the area most covered by the cursor. + if (itemSize.width % 2 == 0 && cell.contains(cursorPosition + Displacement { INV_SLOT_HALF_SIZE_PX, 0 })) { + // hot pixel was in the left half of the cell, so we want to increase the offset to preference the column to the left + hotPixelCellOffset.deltaX++; + } + if (itemSize.height % 2 == 0 && cell.contains(cursorPosition + Displacement { 0, INV_SLOT_HALF_SIZE_PX })) { + // hot pixel was in the top half of the cell, so we want to increase the offset to preference the row above + hotPixelCellOffset.deltaY++; + } + // Then work out the top left cell of the nearest area that could fit this item (as pasting on the edge of the stash would otherwise put it out of bounds) + point.y = std::clamp(point.y - hotPixelCellOffset.deltaY, 0, StashGridSize.height - itemSize.height); + point.x = std::clamp(point.x - hotPixelCellOffset.deltaX, 0, StashGridSize.width - itemSize.width); return point; } } - return InvalidStashPoint; + return {}; } bool IsItemAllowedInStash(const Item &item) @@ -93,14 +113,6 @@ void CheckStashPaste(Point cursorPosition) { Player &player = *MyPlayer; - const Size itemSize = GetInventorySize(player.HoldItem); - const Displacement hotPixelOffset = Displacement(itemSize * INV_SLOT_HALF_SIZE_PX); - if (IsHardwareCursor()) { - // It's more natural to select the top left cell of the region the sprite is overlapping when putting an item - // into an inventory grid, so compensate for the adjusted hot pixel of hardware cursors. - cursorPosition -= hotPixelOffset; - } - if (!IsItemAllowedInStash(player.HoldItem)) return; @@ -111,23 +123,17 @@ void CheckStashPaste(Point cursorPosition) player.HoldItem.clear(); PlaySFX(IS_GOLD); Stash.dirty = true; - if (!IsHardwareCursor()) { - // To make software cursors behave like hardware cursors we need to adjust the hand cursor position manually - SetCursorPos(cursorPosition + hotPixelOffset); - } NewCursor(CURSOR_HAND); return; } - // Make the hot pixel the center of the top-left cell of the item, this favors the cell which contains more of the - // item sprite - Point firstSlot = FindSlotUnderCursor(cursorPosition + Displacement(INV_SLOT_HALF_SIZE_PX)); - if (firstSlot == InvalidStashPoint) + const Size itemSize = GetInventorySize(player.HoldItem); + + std::optional targetSlot = FindTargetSlotUnderItemCursor(cursorPosition, itemSize); + if (!targetSlot) return; - if (firstSlot.x + itemSize.width > 10 || firstSlot.y + itemSize.height > 10) { - return; // Item does not fit - } + Point firstSlot = *targetSlot; // Check that no more than 1 item is replaced by the move StashStruct::StashCell stashIndex = StashStruct::EmptyCell; @@ -144,6 +150,7 @@ void CheckStashPaste(Point cursorPosition) PlaySFX(ItemInvSnds[ItemCAnimTbl[player.HoldItem._iCurs]]); + // Need to set the item anchor position to the bottom left so drawing code functions correctly. player.HoldItem.position = firstSlot + Displacement { 0, itemSize.height - 1 }; if (stashIndex == StashStruct::EmptyCell) { @@ -151,8 +158,9 @@ void CheckStashPaste(Point cursorPosition) // stashList will have at most 10 000 items, up to 65 535 are supported with uint16_t indexes stashIndex = static_cast(Stash.stashList.size() - 1); } else { - // remove item from stash grid + // swap the held item and whatever was in the stash at this position std::swap(Stash.stashList[stashIndex], player.HoldItem); + // then clear the space occupied by the old item for (auto &row : Stash.GetCurrentGrid()) { for (auto &itemId : row) { if (itemId - 1 == stashIndex) @@ -161,14 +169,11 @@ void CheckStashPaste(Point cursorPosition) } } + // Finally mark the area now occupied by the pasted item in the current page/grid. AddItemToStashGrid(Stash.GetPage(), firstSlot, stashIndex, itemSize); Stash.dirty = true; - if (player.HoldItem.isEmpty() && !IsHardwareCursor()) { - // To make software cursors behave like hardware cursors we need to adjust the hand cursor position manually - SetCursorPos(cursorPosition + hotPixelOffset); - } NewCursor(player.HoldItem); } @@ -243,11 +248,6 @@ void CheckStashCut(Point cursorPosition, bool automaticMove) holdItem.clear(); } else { NewCursor(holdItem); - if (!IsHardwareCursor()) { - // For a hardware cursor, we set the "hot point" to the center of the item instead. - Size cursSize = GetInvItemSize(holdItem._iCurs + CURSOR_FIRSTITEM); - SetCursorPos(cursorPosition - Displacement(cursSize / 2)); - } } } } diff --git a/Translations/sv.po b/Translations/sv.po index 58b308891..9f93f1a85 100644 --- a/Translations/sv.po +++ b/Translations/sv.po @@ -1087,7 +1087,7 @@ msgstr "" #: Source/cursor.cpp:220 msgid "Town Portal" -msgstr "Stadsportal" +msgstr "Byportal" #: Source/cursor.cpp:221 msgid "from {:s}" @@ -1481,7 +1481,7 @@ msgstr "fel: läste 0 bytes från servern" #: Source/error.cpp:55 msgid "No automap available in town" -msgstr "Ingen autokarta tillgänglig i staden" +msgstr "Ingen autokarta tillgänglig i byn" #: Source/error.cpp:56 msgid "No multiplayer functions in demo" @@ -1501,7 +1501,7 @@ msgstr "Inte tillräckligt med utrymme för att spara" #: Source/error.cpp:60 msgid "No Pause in town" -msgstr "Ingen Paus i staden" +msgstr "Ingen Paus i byn" #: Source/error.cpp:61 msgid "Copying to a hard disk is recommended" @@ -1745,7 +1745,7 @@ msgstr "Avsluta Spel" #: Source/gamemenu.cpp:51 msgid "Restart In Town" -msgstr "Starta Om i Staden" +msgstr "Starta Om i Byn" #: Source/gamemenu.cpp:63 msgid "Gamma" @@ -2063,7 +2063,7 @@ msgstr "Köttyxa" #: Source/itemdat.cpp:60 Source/itemdat.cpp:434 msgid "The Undead Crown" -msgstr "De Vandödas Krona" +msgstr "De Odödas Krona" #: Source/itemdat.cpp:61 Source/itemdat.cpp:435 msgid "Empyrean Band" @@ -2143,7 +2143,7 @@ msgstr "Identifiera-Skriftrulle" #: Source/itemdat.cpp:80 Source/itemdat.cpp:151 msgid "Scroll of Town Portal" -msgstr "Stadsportal-Skriftrulle" +msgstr "Byportal-Skriftrulle" #: Source/itemdat.cpp:81 Source/itemdat.cpp:440 msgid "Arkaine's Valor" @@ -2483,7 +2483,7 @@ msgstr "Huggare" #: Source/itemdat.cpp:175 msgid "Claymore" -msgstr "Höglandssvärd" +msgstr "Slagsvärd" #: Source/itemdat.cpp:176 msgid "Blade" @@ -2560,7 +2560,7 @@ msgstr "Spikklubba" #: Source/itemdat.cpp:194 msgid "Flail" -msgstr "Slaga" +msgstr "Stridsgissel" #: Source/itemdat.cpp:195 msgid "Maul" @@ -4413,7 +4413,7 @@ msgstr "extra PK mot demoner" #: Source/items.cpp:3754 msgid "extra AC vs undead" -msgstr "extra PK mot vandöda" +msgstr "extra PK mot ödöda" #: Source/items.cpp:3756 msgid "50% Mana moved to Health" @@ -4788,7 +4788,7 @@ msgstr "Likdemon" #: Source/monstdat.cpp:91 msgctxt "monster" msgid "Undead Balrog" -msgstr "Vandöd Balrog" +msgstr "Odöd Balrog" #: Source/monstdat.cpp:92 msgctxt "monster" @@ -5614,7 +5614,7 @@ msgstr "Demon" #: Source/monster.cpp:3386 msgid "Undead" -msgstr "Vandöd" +msgstr "Odöd" #: Source/monster.cpp:4648 msgid "Type: {:s} Kills: {:d}" @@ -6848,7 +6848,7 @@ msgstr "Magi" #: Source/panels/spell_list.cpp:172 msgid "Damages undead only" -msgstr "Skadar endast vandöda" +msgstr "Skadar endast odöda" #: Source/panels/spell_list.cpp:183 msgid "Scroll" @@ -7080,7 +7080,7 @@ msgstr "Eldmur" #: Source/spelldat.cpp:22 msgctxt "spell" msgid "Town Portal" -msgstr "Stadsportal" +msgstr "Byportal" #: Source/spelldat.cpp:23 msgctxt "spell" @@ -7683,7 +7683,7 @@ msgstr "" "\n" "Här tar historien en ännu mörkare vändning än vad jag trodde var möjligt! " "Vår forne kung har stigit upp från sin eviga sömn och styr nu en legion av " -"vandöda tjänare inne i Labyrinten. Hans kropp begravdes i en grift tre " +"odöda tjänare inne i Labyrinten. Hans kropp begravdes i en grift tre " "våningar under Katedralen. Snälla, kära mästare, släpp hans själ fri genom " "att förgöra hans förbannade nuvarande form..." @@ -7768,7 +7768,7 @@ msgid "" "all who still live here." msgstr "" "De döda som går bland de levande följer den förbannade Kungen. Han har " -"makten att väcka upp ännu fler krigare för de vandödas evigt växande armé. " +"makten att väcka upp ännu fler krigare för de odödas evigt växande armé. " "Om du inte stoppar hans styre kommer han säkerligen tåga över detta land och " "slå ned alla som ännu bor här." @@ -7782,7 +7782,7 @@ msgid "" msgstr "" "Lyssna, jag har affärer att sköta här. Jag säljer inte information, och jag " "bryr mig inte om någon Kung som varit död längre än jag levat. Men om du " -"behöver något att använda mot den här vandöda Kungen, så kanske jag kan " +"behöver något att använda mot den här odöda Kungen, så kanske jag kan " "hjälpa dig..." #. TRANSLATORS: Quest dialog spoken by The Skeleton King (Hostile) @@ -8142,7 +8142,7 @@ msgid "" "Please, do what you can or I don't know what we will do." msgstr "" "Jag har alltid försökt hålla stora förråd av mat och dryck i vår källare, " -"men om hela staden saknar färskvatten kommer till och med våra förråd sina " +"men om hela byn saknar färskvatten kommer till och med våra förråd sina " "snart.\n" "\n" "Snälla, gör vad du kan, annars vet jag inte vad vi ska ta oss till." @@ -10005,7 +10005,7 @@ msgid "" "little skeletons!" msgstr "" "Om du är ute efter ett bra vapen så se här. Ta ett vanligt trubbigt vapen, " -"som en stridsklubba. Funkar utmärkt mot de där vandöda fasorna där nere, och " +"som en stridsklubba. Funkar utmärkt mot de där odöda fasorna där nere, och " "det finns inget bättre för att spräcka smala små skelett!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) @@ -10030,7 +10030,7 @@ msgid "" msgstr "" "Titta på den här klingan, balansen. Ett svärd i rätt händer och mot rätt " "fiende, är alla vapens mästare. Dess skarpa egg har inte mycket att skära " -"eller hugga igenom mot de vandöda, men mot levande fiender kan ett svärd på " +"eller hugga igenom mot de odöda, men mot levande fiender kan ett svärd på " "riktigt skära deras kött!" #. TRANSLATORS: Neutral dialog spoken by Griswold (Gossip) @@ -10945,7 +10945,7 @@ msgid "" "the talk of an old sick woman, but anything seems possible these days." msgstr "" "Min mormor berättar ofta historier om mystiska krafter som bebor kyrkogården " -"utanför stade. Och du kanske vill höra en av dem. Hon sade att om man lämnar " +"utanför byn. Och du kanske vill höra en av dem. Hon sade att om man lämnar " "en lämplig gåva i kyrkogården, går in i katedralen för att be för de döda, " "och sedan kommer tillbaka, så skulle gåvan ändrats på något konstigt sätt. " "Jag vet inte om det bara är en sjuk gammal kvinnas prat, men allt verkar " @@ -11463,7 +11463,7 @@ msgstr "Krögaren Ogden" #: Source/towners.cpp:110 msgid "Wounded Townsman" -msgstr "Skadad Stadsbo" +msgstr "Skadad Bybo" #: Source/towners.cpp:132 msgid "Adria the Witch" @@ -11499,7 +11499,7 @@ msgstr "Celia" #: Source/towners.cpp:277 msgid "Slain Townsman" -msgstr "Dräpt Stadsbo" +msgstr "Dräpt Bybo" #: Source/trigs.cpp:343 msgid "Down to dungeon" @@ -11533,7 +11533,7 @@ msgstr "Upp till nivå {:d}" #: Source/trigs.cpp:416 Source/trigs.cpp:471 Source/trigs.cpp:523 #: Source/trigs.cpp:602 Source/trigs.cpp:619 Source/trigs.cpp:666 msgid "Up to town" -msgstr "Upp till staden" +msgstr "Upp till byn" #: Source/trigs.cpp:427 Source/trigs.cpp:505 Source/trigs.cpp:558 #: Source/trigs.cpp:583 Source/trigs.cpp:648 diff --git a/docs/CHANGELOG.md b/docs/CHANGELOG.md index 1c63c2e19..1ae15e992 100644 --- a/docs/CHANGELOG.md +++ b/docs/CHANGELOG.md @@ -15,41 +15,50 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Update the pvp arenas - Rename "Loopback" to "Offline" +#### Stability / Performance / System + +- Move hp/mana display and item graphics to gameplay options +- Validate properties when reloading items +- Demomode: Improve replay stability +- Update [Discord link](https://discord.gg/devilutionx) +- Display save game confirmation +- Reduce ram usage + #### Translations +- Update Simplified Chinese translation - Update French translation - Update German translation - Update Greek translation - Update Japanese translation - Update Korean translation - Update Portuguese translation +- Update Spanish translation +- Update Swedish translation - Update Ukrainian translation -#### Platforms - -- Android TV: Update banner to include app name - -#### Stability / Performance / System - -- Move hp/mana display and item graphics to gameplay options -- Validate properties when reloading items -- Demomode: Improve replay stability -- Update [Discord link](https://discord.gg/devilutionx) -- Reduce ram usage - ### Bugfixes #### Gameplay - Being able to enter Lazarus' chamber before opening the portal - Book requirements not updating -- Diablo: Incorrect level 4 layout when Magic Banner quest is active -- Halls of the Blind not being compleated by picking up the amulet +- Some monsters not walking +- Missiles not traveling the full distance at some angles +- Diablo: Incorrect level 4 layout when the Magic Banner quest is active +- Halls of the Blind not being completed by picking up the amulet +- Shareware: Bucklers not dropping +- Player animation stuttering + +#### Multiplayer + +- Potions dropped by Divine shrines not being synced #### Platforms -- Linux: Add sdl-image dependency for deb package +- Linux: Add sdl-image dependency for the deb package - Linux: Include discord dependency +- Xbox One: Missing assets #### Graphics / Audio @@ -57,14 +66,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Incorrect outlines at the right edge of the screen - NPC speech continuing after starting a new game - Correct various font rendering issues +- Hide the hit indicator when only one player is in the game +- Issues with flashing lights +- Floating number still appearing after death +- Misaligned automap #### Controls +- Inconsistencies with placing items in to the stash - Gamepad: Being stuck in dialogs - Gamepad: Unable to use some scrolls directly #### Stability / Performance / System +- Unable to playback new demo files - Various crashes ### Bugfixes for original Diablo bugs @@ -72,11 +87,24 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 #### Gameplay - Durability overflowing when reloading items +- Teleporting onto an occupied tile +- Right-click during dialogs casts spells #### Graphics / Audio +- Cursor jitter when interacting with the inventory - Broken lava tiles +#### Controls + +- Inconsistencies with placing items in to the inventory + +### Bugfixes for original Hellfire bugs + +#### Gameplay + +- Warping onto a solid tile + ## DevilutionX 1.5.0 ### Features @@ -399,7 +427,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 - Improve Romanian localization - Improve Russian localization ([optional dub](https://github.com/diasurgical/devilutionx-assets/releases/download/v2/ru.mpq) by Stream) - Improve Spanish localization - + #### Gameplay - Added a stash at Gillian's house diff --git a/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo b/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo index be134143f..8f1668e49 100644 Binary files a/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo and b/test/fixtures/timedemo/WarriorLevel1to2/demo_0.dmo differ diff --git a/test/pack_test.cpp b/test/pack_test.cpp index 77bb65262..882b61ec3 100644 --- a/test/pack_test.cpp +++ b/test/pack_test.cpp @@ -871,6 +871,7 @@ public: { Players.resize(2); MyPlayer = &Players[0]; + gbIsMultiplayer = true; PlayerPack testPack { 0, 0, -1, 9, 0, 2, 61, 24, 0, 0, "MP-Warrior", 0, 120, 25, 60, 60, 37, 0, 85670061, 3921, 13568, 13568, 3904, 3904, diff --git a/vcpkg.json b/vcpkg.json index 4eeef04be..c6a6010da 100644 --- a/vcpkg.json +++ b/vcpkg.json @@ -6,7 +6,7 @@ "bzip2", "simpleini" ], - "builtin-baseline": "78b61582c9e093fda56a01ebb654be15a0033897", + "builtin-baseline": "927bc12e31148b0d44ae9d174b96c20e3bcf08eb", "features": { "sdl1": { "description": "Use SDL1.2 instead of SDL2",