Refactor production request handling: implement queuing mechanism and improve production application logic
This commit is contained in:
+6
-1
@@ -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
|
||||
|
||||
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
|
||||
@@ -345,6 +349,7 @@ func _enable_interface_sounds() -> void:
|
||||
func create_new_universe() -> void:
|
||||
var previous: Owniverse = owniverse
|
||||
owniverse = Owniverse.new()
|
||||
owniverse.production_applied.connect(_on_owniverse_production_applied)
|
||||
var inited_result: Dictionary = init_enitities()
|
||||
locked_items = inited_result.locked_items
|
||||
|
||||
|
||||
@@ -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))
|
||||
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")
|
||||
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 = []
|
||||
actions.action_pressed.connect(func ( _page , amount ) : emitted.append(amount))
|
||||
actions._on_action_button_pressed(2)
|
||||
|
||||
@@ -38,13 +38,14 @@ func run():
|
||||
sliced.evolute_frame(0.2, self)
|
||||
check(sliced.frame_busy, "Heavy transaction yields before publishing")
|
||||
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")
|
||||
sliced.evolute_frame(0.01, self)
|
||||
var requested: Dictionary = sliced.frame_clock.duplicate()
|
||||
sliced.speed = 3
|
||||
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.production.produce_hs("H", 1)
|
||||
direct.speed = 3
|
||||
@@ -54,7 +55,7 @@ func run():
|
||||
sliced.evolute_frame(0.189, self)
|
||||
var second_clock: Dictionary = Clock.advance(Clock.advance(requested, 0.001, 3), 0.189, 3)
|
||||
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)
|
||||
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")
|
||||
@@ -77,7 +78,7 @@ func run():
|
||||
expected.state = published.duplicate(true)
|
||||
resumed.restore_frame_checkpoint(checkpoint)
|
||||
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.production.produce_hs("H", 123)
|
||||
expected.speed = 3
|
||||
|
||||
@@ -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.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")
|
||||
|
||||
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]))
|
||||
dispose(world)
|
||||
quit((1 if failures else 0))
|
||||
|
||||
@@ -58,6 +58,13 @@ func _set_action_values(values: Array, displayed: Array) -> void:
|
||||
action_labels[ind].text = text
|
||||
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:
|
||||
if ((E.lt(value, 1) or (not E.finite(value))) or E.ne(value, E.floor_value(value))):
|
||||
|
||||
@@ -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 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 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 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.ge(assigned, total):
|
||||
break
|
||||
taken[share.id] = E.add(taken[share.id], 1)
|
||||
assigned = E.add(assigned, 1)
|
||||
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,13 +182,13 @@ 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:
|
||||
if what == NOTIFICATION_PREDELETE:
|
||||
_production_queue_cancelled = true
|
||||
if frame_budget != null:
|
||||
frame_budget.cancelled = true
|
||||
|
||||
var speed: int :
|
||||
|
||||
Reference in New Issue
Block a user