Refactor production request handling: implement queuing mechanism and improve production application logic
This commit is contained in:
@@ -63,29 +63,41 @@ func expand_requirements(source: Dictionary, requirements: Dictionary) -> Dictio
|
||||
func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
|
||||
var quantities: Dictionary = {}
|
||||
for member in world.catalog.members(id):
|
||||
quantities[member] = target.amount.get(member, 0)
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
quantities[member] = E.maximum(0, target.amount.get(member, 0))
|
||||
var taken: Dictionary = {}
|
||||
var total: Variant = E.minimum(amount, available(target, id))
|
||||
var discrete: bool = (not distribution.is_empty())
|
||||
for member in distribution:
|
||||
discrete = (discrete and is_discrete(member))
|
||||
if discrete:
|
||||
total = E.floor_value(total)
|
||||
var shares: Array = []
|
||||
var assigned: Variant = 0
|
||||
for member in distribution:
|
||||
var expected: Variant = E.multiply(total, distribution[member])
|
||||
taken[member] = (E.floor_value(expected) if discrete else expected)
|
||||
assigned = E.add(assigned, taken[member])
|
||||
shares.append({"id": member, "rest": E.subtract(expected, taken[member])})
|
||||
if discrete:
|
||||
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return (E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id))))
|
||||
for share : Dictionary in shares:
|
||||
if E.ge(assigned, total):
|
||||
break
|
||||
taken[share.id] = E.add(taken[share.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
var stock_total: Variant = 0
|
||||
for quantity in quantities.values(): stock_total = E.add(stock_total, quantity)
|
||||
var total: Variant = E.minimum(amount, stock_total)
|
||||
var discrete: bool = true
|
||||
for member in quantities:
|
||||
discrete = discrete and is_discrete(member)
|
||||
if discrete: total = E.floor_value(total)
|
||||
if E.le(total, 0) or E.le(stock_total, 0): return taken
|
||||
if E.eq(total, stock_total):
|
||||
taken = quantities
|
||||
elif discrete:
|
||||
var shares: Array = []
|
||||
var assigned: Variant = 0
|
||||
for member in quantities:
|
||||
var expected: Variant = E.mul_div(total, quantities[member], stock_total)
|
||||
var whole: Variant = E.floor_value(expected)
|
||||
taken[member] = whole
|
||||
assigned = E.add(assigned, whole)
|
||||
shares.append({"id": member, "rest": E.subtract(expected, whole)})
|
||||
shares.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: return E.gt(a.rest, b.rest) if E.ne(a.rest, b.rest) else E.lt(str(a.id), str(b.id)))
|
||||
var missing: Variant = E.subtract(total, assigned)
|
||||
for share: Dictionary in shares:
|
||||
if E.le(missing, 0): break
|
||||
var member: String = share.id
|
||||
if E.lt(taken[member], quantities[member]):
|
||||
taken[member] = E.add(taken[member], 1)
|
||||
missing = E.subtract(missing, 1)
|
||||
assert(E.eq(missing, 0), "Discrete resource allocation must exhaust its requested amount")
|
||||
else:
|
||||
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
|
||||
for member in distribution:
|
||||
taken[member] = E.multiply(total, distribution[member])
|
||||
|
||||
for member in taken:
|
||||
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
|
||||
return taken
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
extends Node
|
||||
class_name Owniverse
|
||||
signal production_applied(id: String)
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
## Owns the state and modules. Rules live in the modules below.
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
@@ -97,10 +98,13 @@ var frame_clock: Dictionary = {}
|
||||
var frame_budget: RefCounted
|
||||
var _requested_speed: int = 0
|
||||
var _queued_productions: Array = []
|
||||
var production_queue_busy: bool = false
|
||||
var _production_queue_cancelled: bool = false
|
||||
var frame_active_clock: Dictionary = {}
|
||||
var _resume_clock: Dictionary = {}
|
||||
var frame_elapsed_seconds: float = 0.0
|
||||
const frame_step_seconds: float = 0.2
|
||||
const production_slice_usec: int = 4000
|
||||
|
||||
## Keep the requested clock advancing while a transaction spans render frames.
|
||||
func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||
@@ -108,8 +112,11 @@ func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||
if frame_clock.is_empty(): frame_clock = state.time.duplicate()
|
||||
frame_clock = SimulationTime.advance(frame_clock, delta, speed)
|
||||
frame_elapsed_seconds += delta
|
||||
if frame_busy: return
|
||||
if frame_busy or production_queue_busy: return
|
||||
var next_clock: Dictionary = _resume_clock
|
||||
if next_clock.is_empty() and not _queued_productions.is_empty():
|
||||
_start_production_drain(tree)
|
||||
return
|
||||
if next_clock.is_empty():
|
||||
# Cadence limits starts; each transaction consumes the whole pending interval.
|
||||
if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return
|
||||
@@ -121,13 +128,37 @@ func evolute_frame(delta: float, tree: SceneTree) -> void:
|
||||
_resume_clock = {}
|
||||
evolution.run_frame(frame_active_clock, frame_budget)
|
||||
|
||||
func request_production(id: String, amount: Variant, resource: bool = false) -> void:
|
||||
if frame_busy:
|
||||
func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
|
||||
if frame_busy or production_queue_busy:
|
||||
_queued_productions.append({"id": id, "amount": amount, "resource": resource})
|
||||
elif resource:
|
||||
production.produce_hs(id, amount)
|
||||
return true
|
||||
_apply_production({"id": id, "amount": amount, "resource": resource})
|
||||
return false
|
||||
|
||||
func _apply_production(command: Dictionary) -> void:
|
||||
if command.resource:
|
||||
production.produce_hs(command.id, command.amount)
|
||||
else:
|
||||
production.produce(id, amount)
|
||||
production.produce(command.id, command.amount)
|
||||
production_applied.emit(command.id)
|
||||
|
||||
func _start_production_drain(tree: SceneTree) -> void:
|
||||
if production_queue_busy or _queued_productions.is_empty(): return
|
||||
production_queue_busy = true
|
||||
_production_queue_cancelled = false
|
||||
_drain_productions(tree)
|
||||
|
||||
func _drain_productions(tree: SceneTree) -> void:
|
||||
var keep_alive: Owniverse = self
|
||||
while not _queued_productions.is_empty():
|
||||
var slice_started: int = Time.get_ticks_usec()
|
||||
while not _queued_productions.is_empty() and Time.get_ticks_usec() - slice_started < production_slice_usec:
|
||||
var command: Dictionary = _queued_productions.pop_front()
|
||||
_apply_production(command)
|
||||
if _queued_productions.is_empty(): break
|
||||
await tree.process_frame
|
||||
if keep_alive._production_queue_cancelled: return
|
||||
production_queue_busy = false
|
||||
|
||||
## Persist work requests, never the partially calculated transaction.
|
||||
func frame_checkpoint() -> Dictionary:
|
||||
@@ -151,14 +182,14 @@ func finish_frame() -> void:
|
||||
if _requested_speed != 0:
|
||||
speed = _requested_speed
|
||||
_requested_speed = 0
|
||||
var productions: Array = _queued_productions
|
||||
_queued_productions = []
|
||||
for command in productions:
|
||||
request_production(command.id, command.amount, command.resource)
|
||||
if not _queued_productions.is_empty():
|
||||
_start_production_drain(frame_budget.tree)
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_PREDELETE and frame_budget != null:
|
||||
frame_budget.cancelled = true
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_production_queue_cancelled = true
|
||||
if frame_budget != null:
|
||||
frame_budget.cancelled = true
|
||||
|
||||
var speed: int :
|
||||
get:
|
||||
|
||||
Reference in New Issue
Block a user