Refactor production request handling: implement queuing mechanism and improve production application logic

This commit is contained in:
Kirill
2026-10-08 22:19:29 +03:00
parent 02c9843e1b
commit 00b6192d19
7 changed files with 107 additions and 39 deletions
+34 -22
View File
@@ -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
+43 -12
View File
@@ -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: