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
+6 -1
View File
@@ -315,7 +315,11 @@ func _page_action_pressed(page_id: int, action_amount) -> void:
var item_id: String = g_active_items[page_id].item_id var item_id: String = g_active_items[page_id].item_id
if E.gt(action_amount, 0): if E.gt(action_amount, 0):
owniverse.request_production(item_id, action_amount) if owniverse.request_production(item_id, action_amount):
page_actions[page_id].mark_pending(action_amount)
func _on_owniverse_production_applied(_item_id: String) -> void:
active_actions_dirty = true
#endregion #endregion
@@ -345,6 +349,7 @@ func _enable_interface_sounds() -> void:
func create_new_universe() -> void: func create_new_universe() -> void:
var previous: Owniverse = owniverse var previous: Owniverse = owniverse
owniverse = Owniverse.new() owniverse = Owniverse.new()
owniverse.production_applied.connect(_on_owniverse_production_applied)
var inited_result: Dictionary = init_enitities() var inited_result: Dictionary = init_enitities()
locked_items = inited_result.locked_items locked_items = inited_result.locked_items
+4
View File
@@ -38,6 +38,10 @@ func run() -> void:
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5)) actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted") check(E.eq(actions.action_values.values(), [10, 100, 123, 0, 0]), "Unproducible small requests omitted")
check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained") check(E.eq(actions.action_labels[2].text, "120"), "Recipe preview displayed while original request retained")
actions.mark_pending(123)
check(actions.action_labels[2].text == "…" and actions.action_labels[2].get_parent().get_node("TextureButton").disabled, "Queued action is visibly pending and cannot be duplicated")
actions.set_actions("step", 123, func ( request ) : return E.multiply(E.floor_value(E.divide(request, 5)), 5))
check(actions.action_labels[2].text == "120" and not actions.action_labels[2].get_parent().get_node("TextureButton").disabled, "Action refresh clears pending state")
var emitted: Array = [] var emitted: Array = []
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount)) actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
actions._on_action_button_pressed(2) actions._on_action_button_pressed(2)
+5 -4
View File
@@ -38,13 +38,14 @@ func run():
sliced.evolute_frame(0.2, self) sliced.evolute_frame(0.2, self)
check(sliced.frame_busy, "Heavy transaction yields before publishing") check(sliced.frame_busy, "Heavy transaction yields before publishing")
check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction") check(E.eq(sliced.state.time.year, 0), "Published clock stays at completed transaction")
sliced.request_production("H", 1, true) check(sliced.request_production("H", 1, true), "Production requested during a frame is queued")
check(sliced.frame_checkpoint().productions.size() == 1, "Queued production remains part of the save checkpoint")
check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit") check(E.eq(sliced.state.amount.H, initial_h), "Player action waits for transaction commit")
sliced.evolute_frame(0.01, self) sliced.evolute_frame(0.01, self)
var requested: Dictionary = sliced.frame_clock.duplicate() var requested: Dictionary = sliced.frame_clock.duplicate()
sliced.speed = 3 sliced.speed = 3
check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction") check(sliced.speed == 3 and sliced.state.speed == 6, "Speed change is visible and cannot mutate an unfinished transaction")
while sliced.frame_busy: await process_frame while sliced.frame_busy or sliced.production_queue_busy: await process_frame
direct.evolute(0.2) direct.evolute(0.2)
direct.production.produce_hs("H", 1) direct.production.produce_hs("H", 1)
direct.speed = 3 direct.speed = 3
@@ -54,7 +55,7 @@ func run():
sliced.evolute_frame(0.189, self) sliced.evolute_frame(0.189, self)
var second_clock: Dictionary = Clock.advance(Clock.advance(requested, 0.001, 3), 0.189, 3) var second_clock: Dictionary = Clock.advance(Clock.advance(requested, 0.001, 3), 0.189, 3)
var active: Dictionary = sliced.frame_active_clock.duplicate() var active: Dictionary = sliced.frame_active_clock.duplicate()
while sliced.frame_busy: await process_frame while sliced.frame_busy or sliced.production_queue_busy: await process_frame
direct.evolution.evolute_to_datetime(active.datetime, active) direct.evolution.evolute_to_datetime(active.datetime, active)
check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock") check(E.eq(direct.state, sliced.state), "Bounded frame transaction matches its exact target clock")
check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock") check(E.eq(sliced.frame_clock, second_clock), "All elapsed time during a busy frame remains in the requested clock")
@@ -77,7 +78,7 @@ func run():
expected.state = published.duplicate(true) expected.state = published.duplicate(true)
resumed.restore_frame_checkpoint(checkpoint) resumed.restore_frame_checkpoint(checkpoint)
resumed.evolute_frame(0.0, self) resumed.evolute_frame(0.0, self)
while resumed.frame_busy: await process_frame while resumed.frame_busy or resumed.production_queue_busy: await process_frame
expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock) expected.evolution.evolute_to_datetime(expected_clock.datetime, expected_clock)
expected.production.produce_hs("H", 123) expected.production.produce_hs("H", 123)
expected.speed = 3 expected.speed = 3
+8
View File
@@ -72,6 +72,14 @@ func run() -> void:
check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding") check((E.eq(world.state.amount.H, 6) and E.eq(world.state.amount.S, 4)), "Exact cost and binding")
check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget") check(E.eq(world.state.auto_resources.H, 6), "Exact automatic budget")
check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated") check((E.eq(old_batches[0].amount, 10) and E.eq(world.state.batches.H[0].amount, 6)), "Consumed batches isolated")
for member in world.catalog.members("Star"): world.state.amount[member] = 0
var star_stocks: Array = ["1234567890123456789", "876543210987654321", "3141592653589793238", "2718281828459045235", "1414213562373095048", "1732050807568877293", "2236067977499789696", "2449489742783178098", "3162277660168379331"]
for i in star_stocks.size(): world.state.amount[world.catalog.members("Star")[i]] = E.of(star_stocks[i])
var scd_request: Variant = E.of("1e15")
var scd_created: Variant = world.production.produce("SCD", scd_request)
check(E.eq(scd_created, scd_request), "Large SCD production distributes a whole star quota exactly")
check(E.eq(world.state.amount.SCD, scd_request), "Large SCD creation is committed")
print(("Transaction: %d checks, %d failures" % [checks, failures])) print(("Transaction: %d checks, %d failures" % [checks, failures]))
dispose(world) dispose(world)
quit((1 if failures else 0)) quit((1 if failures else 0))
+7
View File
@@ -58,6 +58,13 @@ func _set_action_values(values: Array, displayed: Array) -> void:
action_labels[ind].text = text action_labels[ind].text = text
action_labels[ind].get_parent().get_node("TextureButton").disabled = E.le(values[ind], 0) action_labels[ind].get_parent().get_node("TextureButton").disabled = E.le(values[ind], 0)
func mark_pending(amount: Variant) -> void:
for ind in range(5):
if not E.eq(action_values[ind], amount): continue
action_labels[ind].text = "…"
action_labels[ind].get_parent().get_node("TextureButton").disabled = true
return
func _format_action_value(value: Variant) -> String: func _format_action_value(value: Variant) -> String:
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))): if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
+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: func take_available(target: Dictionary, id: String, amount: Variant) -> Dictionary:
var quantities: Dictionary = {} var quantities: Dictionary = {}
for member in world.catalog.members(id): for member in world.catalog.members(id):
quantities[member] = target.amount.get(member, 0) quantities[member] = E.maximum(0, target.amount.get(member, 0))
var distribution: Dictionary = QuantityMath.normalize_distribution(quantities)
var taken: Dictionary = {} var taken: Dictionary = {}
var total: Variant = E.minimum(amount, available(target, id)) var stock_total: Variant = 0
var discrete: bool = (not distribution.is_empty()) for quantity in quantities.values(): stock_total = E.add(stock_total, quantity)
for member in distribution: var total: Variant = E.minimum(amount, stock_total)
discrete = (discrete and is_discrete(member)) var discrete: bool = true
if discrete: for member in quantities:
total = E.floor_value(total) discrete = discrete and is_discrete(member)
var shares: Array = [] if discrete: total = E.floor_value(total)
var assigned: Variant = 0 if E.le(total, 0) or E.le(stock_total, 0): return taken
for member in distribution: if E.eq(total, stock_total):
var expected: Variant = E.multiply(total, distribution[member]) taken = quantities
taken[member] = (E.floor_value(expected) if discrete else expected) elif discrete:
assigned = E.add(assigned, taken[member]) var shares: Array = []
shares.append({"id": member, "rest": E.subtract(expected, taken[member])}) var assigned: Variant = 0
if discrete: for member in quantities:
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 expected: Variant = E.mul_div(total, quantities[member], stock_total)
for share : Dictionary in shares: var whole: Variant = E.floor_value(expected)
if E.ge(assigned, total): taken[member] = whole
break assigned = E.add(assigned, whole)
taken[share.id] = E.add(taken[share.id], 1) shares.append({"id": member, "rest": E.subtract(expected, whole)})
assigned = E.add(assigned, 1) 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: for member in taken:
target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member])) target.amount[member] = E.maximum(0, E.subtract(target.amount.get(member, 0), taken[member]))
return taken return taken
+43 -12
View File
@@ -1,5 +1,6 @@
extends Node extends Node
class_name Owniverse class_name Owniverse
signal production_applied(id: String)
const E = preload("res://world/owniverse/math/exact.gd") const E = preload("res://world/owniverse/math/exact.gd")
## Owns the state and modules. Rules live in the modules below. ## Owns the state and modules. Rules live in the modules below.
const SimulationTime = preload("res://world/owniverse/simulation_time.gd") const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
@@ -97,10 +98,13 @@ var frame_clock: Dictionary = {}
var frame_budget: RefCounted var frame_budget: RefCounted
var _requested_speed: int = 0 var _requested_speed: int = 0
var _queued_productions: Array = [] var _queued_productions: Array = []
var production_queue_busy: bool = false
var _production_queue_cancelled: bool = false
var frame_active_clock: Dictionary = {} var frame_active_clock: Dictionary = {}
var _resume_clock: Dictionary = {} var _resume_clock: Dictionary = {}
var frame_elapsed_seconds: float = 0.0 var frame_elapsed_seconds: float = 0.0
const frame_step_seconds: float = 0.2 const frame_step_seconds: float = 0.2
const production_slice_usec: int = 4000
## Keep the requested clock advancing while a transaction spans render frames. ## Keep the requested clock advancing while a transaction spans render frames.
func evolute_frame(delta: float, tree: SceneTree) -> void: 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() if frame_clock.is_empty(): frame_clock = state.time.duplicate()
frame_clock = SimulationTime.advance(frame_clock, delta, speed) frame_clock = SimulationTime.advance(frame_clock, delta, speed)
frame_elapsed_seconds += delta frame_elapsed_seconds += delta
if frame_busy: return if frame_busy or production_queue_busy: return
var next_clock: Dictionary = _resume_clock 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(): if next_clock.is_empty():
# Cadence limits starts; each transaction consumes the whole pending interval. # Cadence limits starts; each transaction consumes the whole pending interval.
if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return if frame_elapsed_seconds + 1e-12 < frame_step_seconds: return
@@ -121,13 +128,37 @@ func evolute_frame(delta: float, tree: SceneTree) -> void:
_resume_clock = {} _resume_clock = {}
evolution.run_frame(frame_active_clock, frame_budget) evolution.run_frame(frame_active_clock, frame_budget)
func request_production(id: String, amount: Variant, resource: bool = false) -> void: func request_production(id: String, amount: Variant, resource: bool = false) -> bool:
if frame_busy: if frame_busy or production_queue_busy:
_queued_productions.append({"id": id, "amount": amount, "resource": resource}) _queued_productions.append({"id": id, "amount": amount, "resource": resource})
elif resource: return true
production.produce_hs(id, amount) _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: 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. ## Persist work requests, never the partially calculated transaction.
func frame_checkpoint() -> Dictionary: func frame_checkpoint() -> Dictionary:
@@ -151,14 +182,14 @@ func finish_frame() -> void:
if _requested_speed != 0: if _requested_speed != 0:
speed = _requested_speed speed = _requested_speed
_requested_speed = 0 _requested_speed = 0
var productions: Array = _queued_productions if not _queued_productions.is_empty():
_queued_productions = [] _start_production_drain(frame_budget.tree)
for command in productions:
request_production(command.id, command.amount, command.resource)
func _notification(what: int) -> void: func _notification(what: int) -> void:
if what == NOTIFICATION_PREDELETE and frame_budget != null: if what == NOTIFICATION_PREDELETE:
frame_budget.cancelled = true _production_queue_cancelled = true
if frame_budget != null:
frame_budget.cancelled = true
var speed: int : var speed: int :
get: get: