Files
d2warehouse/d2warehouse/stash.py

128 lines
4.1 KiB
Python

# Copyright 2023 <omicron.me@protonmail.com>
# Copyright 2023 <andreasruden91@gmail.com>
#
# This file is part of d2warehouse.
#
# d2warehouse is free software: you can redistribute it and/or modify it under the
# terms of the GNU General Public License as published by the Free Software
# Foundation, either version 3 of the License, or (at your option) any later
# version.
#
# d2warehouse is distributed in the hope that it will be useful, but WITHOUT ANY
# WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
# A PARTICULAR PURPOSE. See the GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along with
# Mercator. If not, see <https://www.gnu.org/licenses/>.
from d2warehouse.item import Item
from d2warehouse.fileformat import STASH_TAB_MAGIC, ITEM_DATA_MAGIC
from struct import pack
class StashError(RuntimeError):
pass
class StashFullError(StashError):
pass
class Stash:
def __init__(self) -> None:
self.tabs: list[StashTab] = []
def raw(self) -> bytes:
return b"".join(tab.raw() for tab in self.tabs)
def add(self, item: Item) -> None:
for tab in self.tabs:
try:
tab.add(item)
return
except StashFullError:
pass
raise StashFullError(
"Could not locate an open spot in the stash to add the item"
)
class StashTab:
def __init__(self) -> None:
self.gold: int = 0
self.version: int = 99
self.item_data: bytes = b""
self.items: list[Item] = []
def raw(self) -> bytes:
"""Get the computed raw representation of the stash"""
item_raw = b"".join(item.raw() for item in self.items)
raw_length = len(item_raw) + 0x44
return (
STASH_TAB_MAGIC
+ pack("I", 1)
+ pack("I", self.version)
+ pack("I", self.gold)
+ pack("I", raw_length)
+ b"\x00" * 44
+ ITEM_DATA_MAGIC
+ pack("H", len(self.items))
+ item_raw
)
def remove(self, item: Item) -> None:
for idx, current_item in enumerate(self.items):
if item is current_item:
self.items.pop(idx)
return
raise StashError("Item not found in stash")
def add(self, item: Item) -> None:
width, height = item.size()
dest = self._find_destination(width, height)
if not dest:
raise StashFullError(
"Could not locate an open spot in the stash to add the item"
)
x, y = dest
item.set_position(x, y)
self.items.append(item)
@staticmethod
def _is_free_location(
slots: list[list[bool]], x: int, y: int, width: int, height: int
) -> bool:
"""Check if a given location can fit an item of a given width and height"""
if x + width > len(slots):
return False
if y + height > len(slots[0]):
return False
for i in range(width):
for j in range(height):
if slots[x + i][y + j]:
return False
return True
def _find_destination(self, width: int, height: int) -> tuple[int, int] | None:
"""Find a free destination to store an item of given width and height"""
slots = self._build_slot_matrix()
stash_width = len(slots)
stash_height = len(slots[0])
for y in range(stash_width):
for x in range(stash_height):
if self._is_free_location(slots, x, y, width, height):
return x, y
return None
def _build_slot_matrix(self, rows: int = 10, cols: int = 10) -> list[list[bool]]:
"""Build a matrix of filled and empty slots for the stash tab."""
slots = []
for i in range(cols):
slots.append([False] * rows)
for item in self.items:
x, y = item.pos_x, item.pos_y
width, height = item.size()
for n in range(width):
for m in range(height):
slots[x + n][y + m] = True
return slots