Skip to content

Commit

Permalink
[Inventory] Fixed a bug when splitting item stacks
Browse files Browse the repository at this point in the history
  • Loading branch information
peter-kish committed May 13, 2024
1 parent e34dfc9 commit 54a77f6
Show file tree
Hide file tree
Showing 6 changed files with 398 additions and 35 deletions.
5 changes: 5 additions & 0 deletions addons/gloot/core/constraints/constraint_manager.gd
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@ func _init(inventory_: Inventory) -> void:

func _on_item_added(item: InventoryItem) -> void:
assert(_enforce_constraints(item), "Failed to enforce constraints!")

# Enforcing constraints can result in the item being removed from the inventory
# (e.g. when it's merged with another item stack)
if !is_instance_valid(item.get_inventory()) || item.is_queued_for_deletion():
item = null

if _weight_constraint != null:
_weight_constraint._on_item_added(item)
Expand Down
44 changes: 15 additions & 29 deletions addons/gloot/core/constraints/grid_constraint.gd
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ signal size_changed
const Verify = preload("res://addons/gloot/core/verify.gd")
const GridConstraint = preload("res://addons/gloot/core/constraints/grid_constraint.gd")
const StacksConstraint = preload("res://addons/gloot/core/constraints/stacks_constraint.gd")
const ItemMap = preload("res://addons/gloot/core/constraints/item_map.gd")
const QuadTree = preload("res://addons/gloot/core/constraints/quadtree.gd")

# TODO: Replace KEY_WIDTH and KEY_HEIGHT with KEY_SIZE
const KEY_WIDTH: String = "width"
Expand All @@ -16,8 +16,8 @@ const KEY_POSITIVE_ROTATION: String = "positive_rotation"
const KEY_GRID_POSITION: String = "grid_position"
const DEFAULT_SIZE: Vector2i = Vector2i(10, 10)

var _item_map := ItemMap.new(Vector2i.ZERO)
var _swap_position := Vector2i.ZERO
var _quad_tree := QuadTree.new(size)

@export var size: Vector2i = DEFAULT_SIZE :
set(new_size):
Expand All @@ -30,36 +30,32 @@ var _swap_position := Vector2i.ZERO
if _bounds_broken():
size = old_size
if size != old_size:
_refresh_item_map()
_refresh_quad_tree()
size_changed.emit()


func _refresh_item_map() -> void:
_item_map.resize(size)
_fill_item_map()


func _fill_item_map() -> void:
func _refresh_quad_tree() -> void:
_quad_tree = QuadTree.new(size)
for item in inventory.get_items():
_item_map.fill_rect(get_item_rect(item), item)
_quad_tree.add(get_item_rect(item), item)


func _on_inventory_set() -> void:
_refresh_item_map()
_refresh_quad_tree()


func _on_item_added(item: InventoryItem) -> void:
if item == null:
return
_item_map.fill_rect(get_item_rect(item), item)
_quad_tree.add(get_item_rect(item), item)


func _on_item_removed(item: InventoryItem) -> void:
_item_map.clear_rect(get_item_rect(item))
_quad_tree.remove(item)


func _on_item_modified(item: InventoryItem) -> void:
_refresh_item_map()
_refresh_quad_tree()


func _on_pre_item_swap(item1: InventoryItem, item2: InventoryItem) -> bool:
Expand Down Expand Up @@ -261,10 +257,10 @@ func create_and_add_item_at(prototype_id: String, position: Vector2i) -> Invento

func get_item_at(position: Vector2i) -> InventoryItem:
assert(inventory != null, "Inventory not set!")

if !_item_map.contains(position):
var first = _quad_tree.get_first(position)
if first == null:
return null
return _item_map.get_field(position)
return first.metadata


func get_items_under(rect: Rect2i) -> Array[InventoryItem]:
Expand Down Expand Up @@ -362,12 +358,7 @@ func rect_free(rect: Rect2i, exception: InventoryItem = null) -> bool:
if rect.position.y + rect.size.y > size.y:
return false

for i in range(rect.position.x, rect.position.x + rect.size.x):
for j in range(rect.position.y, rect.position.y + rect.size.y):
var field = _item_map.get_field(Vector2i(i, j))
if field != null && field != exception:
return false
return true
return _quad_tree.get_first(rect, exception) == null


# TODO: Check if this is needed after adding find_free_space
Expand Down Expand Up @@ -419,8 +410,6 @@ func _sort_if_needed() -> void:
func get_space_for(item: InventoryItem) -> ItemCount:
var occupied_rects: Array[Rect2i]
var item_size = get_item_size(item)
if item_size == Vector2i.ONE:
return ItemCount.new(_item_map.free_fields)

var free_space := find_free_space(item_size, occupied_rects)
while free_space.success:
Expand All @@ -430,10 +419,7 @@ func get_space_for(item: InventoryItem) -> ItemCount:


func has_space_for(item: InventoryItem) -> bool:
var item_size = get_item_size(item)
if item_size == Vector2i.ONE:
return _item_map.free_fields > 0

var item_size = get_item_size(item)
return find_free_space(item_size).success


Expand Down
233 changes: 233 additions & 0 deletions addons/gloot/core/constraints/quadtree.gd
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@

class QtRect:
var rect: Rect2i
var metadata: Variant


func _init(rect_: Rect2i, metadata_: Variant) -> void:
rect = rect_
metadata = metadata_


func _to_string() -> String:
return "[R: %s, M: %s]" % [str(rect), str(metadata)]


class QtNode:
var quadrants: Array[QtNode] = [null, null, null, null]
var quadrant_count: int = 0
var qt_rects: Array[QtRect]
var rect: Rect2i


func _init(r: Rect2i) -> void:
rect = r


func _to_string() -> String:
return "[R: %s]" % str(rect)


func is_empty() -> bool:
return (quadrant_count == 0) && qt_rects.is_empty()


func get_first_under_rect(test_rect: Rect2i, exception_metadata: Variant = null) -> QtRect:
for qtr in qt_rects:
if exception_metadata != null && qtr.metadata == exception_metadata:
continue
if qtr.rect.intersects(test_rect):
return qtr

for quadrant in quadrants:
if quadrant == null:
continue
if !quadrant.rect.intersects(test_rect):
continue
var first = quadrant.get_first_under_rect(test_rect, exception_metadata)
if first != null:
return first

return null


func get_first_containing_point(point: Vector2i, exception_metadata: Variant = null) -> QtRect:
for qtr in qt_rects:
if exception_metadata != null && qtr.metadata == exception_metadata:
continue
if qtr.rect.has_point(point):
return qtr

for quadrant in quadrants:
if quadrant == null:
continue
if !quadrant.rect.has_point(point):
continue
var first = quadrant.get_first_containing_point(point, exception_metadata)
if first != null:
return first

return null


func get_all_under_rect(test_rect: Rect2i, exception_metadata: Variant = null) -> Array[QtRect]:
var result: Array[QtRect]

for qtr in qt_rects:
if exception_metadata != null && qtr.metadata == exception_metadata:
continue
if qtr.rect.intersects(test_rect):
result.append(qtr)

for quadrant in quadrants:
if quadrant == null:
continue
if !quadrant.rect.intersects(test_rect):
continue
result.append_array(quadrant.get_all_under_rect(test_rect, exception_metadata))

return result


func get_all_containing_point(point: Vector2i, exception_metadata: Variant = null) -> Array[QtRect]:
var result: Array[QtRect]

for qtr in qt_rects:
if exception_metadata != null && qtr.metadata == exception_metadata:
continue
if qtr.rect.has_point(point):
result.append(qtr)

for quadrant in quadrants:
if quadrant == null:
continue
if !quadrant.rect.has_point(point):
continue
result.append_array(quadrant.get_all_containing_point(point, exception_metadata))

return result


func add(qt_rect: QtRect) -> void:
if !_can_subdivide(rect.size):
qt_rects.append(qt_rect)
return

if is_empty():
qt_rects.append(qt_rect)
return

var quadrant_rects := _get_quadrant_rects(rect)
for i in quadrant_rects.size():
var quadrant_rect := quadrant_rects[i]
if !quadrant_rect.intersects(qt_rect.rect):
continue
if quadrants[i] == null:
quadrants[i] = QtNode.new(quadrant_rect)
quadrant_count += 1
while !qt_rects.is_empty():
var qtr = qt_rects.pop_back()

add(qtr)
quadrants[i].add(qt_rect)


func remove(metadata: Variant) -> bool:
# TODO: Optimize with a Rect2i
var result = false
for i in range(qt_rects.size() - 1, -1, -1):
if qt_rects[i].metadata == metadata:
qt_rects.remove_at(i)
result = true

for i in range(quadrants.size()):
if quadrants[i] == null:
continue
if quadrants[i].remove(metadata):
result = true
if quadrants[i].is_empty():
quadrants[i] = null
quadrant_count -= 1

_collapse()

return result


func _collapse() -> void:
if quadrant_count == 0:
return
var collapsing_into: QtRect = null
for i in quadrants.size():
if quadrants[i] == null:
continue
if quadrants[i].quadrant_count != 0:
return
for qtr in quadrants[i].qt_rects:
if collapsing_into != null && collapsing_into != qtr:
return
collapsing_into = qtr

for i in quadrants.size():
quadrants[i] = null
quadrant_count = 0
qt_rects.append(collapsing_into)


static func _can_subdivide(size: Vector2i) -> bool:
return size.x > 1 && size.y > 1


# +----+---+
# | 0 | 1 |
# | | |
# +----+---+ (the first quadrant is rounded up when the size is odd)
# | 2 | 3 |
# +----+---+
static func _get_quadrant_rects(rect: Rect2i) -> Array[Rect2i]:
var q0w := roundi(float(rect.size.x) / 2.0)
var q0h := roundi(float(rect.size.y) / 2.0)
var q0 := Rect2i(rect.position, Vector2i(q0w, q0h))
var q3 := Rect2i(rect.position + q0.size, rect.size - q0.size)
var q1 := Rect2i(Vector2i(q3.position.x, q0.position.y), Vector2i(q3.size.x, q0.size.y))
var q2 := Rect2i(Vector2i(q0.position.x, q3.position.y), Vector2i(q0.size.x, q3.size.y))
return [q0, q1, q2, q3]


var _root: QtNode
var _size: Vector2i


func _init(size: Vector2) -> void:
_size = size
_root = QtNode.new(Rect2i(Vector2i.ZERO, _size))


func get_first(at: Variant, exception_metadata: Variant = null) -> QtRect:
assert(at is Rect2i || at is Vector2i)
if at is Rect2i:
return _root.get_first_under_rect(at, exception_metadata)
if at is Vector2i:
return _root.get_first_containing_point(at, exception_metadata)
return null


func get_all(at: Variant, exception_metadata: Variant = null) -> Array[QtRect]:
assert(at is Rect2i || at is Vector2i)
if at is Rect2i:
return _root.get_all_under_rect(at, exception_metadata)
if at is Vector2i:
return _root.get_all_containing_point(at, exception_metadata)
return []


func add(rect: Rect2i, metadata: Variant) -> void:
_root.add(QtRect.new(rect, metadata))


func remove(metadata: Variant) -> bool:
return _root.remove(metadata)


func is_empty() -> bool:
return _root.is_empty()
Loading

0 comments on commit 54a77f6

Please sign in to comment.