Files
owniverse/world/owniverse/object_batches.gd
T

195 lines
12 KiB
GDScript

extends "res://world/owniverse/simulation_module.gd"
const E = preload("res://world/owniverse/math/exact.gd")
const CivilizationState = preload("res://world/owniverse/civilization_state.gd")
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
var batches: Array = target.batches.get(id, [])
if batches.is_empty():
return
# The common case for short-lived stellar events is a single cohort. Avoid
# allocating the shares array and sorting it when no redistribution is
# needed.
if batches.size() == 1:
var only: Dictionary = batches[0]
var expected: Variant = E.multiply(only.amount, E.maximum(0, factor))
only.amount = E.round_value(expected)
only.withdraw_remainder = E.minimum(only.get("withdraw_remainder", 0), only.amount)
return
var total: Variant = 0
var assigned: Variant = 0
var shares: Array = []
for batch : Dictionary in batches:
var expected: Variant = E.multiply(batch.amount, E.maximum(0, factor))
total = E.add(total, expected)
batch.amount = E.floor_value(expected)
assigned = E.add(assigned, batch.amount)
shares.append({"batch": batch, "rest": E.subtract(expected, batch.amount)})
shares.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.gt(a.rest, b.rest))
for share : Dictionary in shares:
if E.lt(assigned, E.round_value(total)):
share.batch.amount = E.add(share.batch.amount, 1)
assigned = E.add(assigned, 1)
share.batch.withdraw_remainder = E.minimum(share.batch.get("withdraw_remainder", 0), share.batch.amount)
func append_batch(target: Dictionary, id: String, amount: Variant, created_from: Variant, created_to: Variant, completed: Dictionary = { }, completed_before: Variant = - INF) -> Variant:
if ((E.le(amount, 0) or (not world.catalog.entities.has(id))) or E.le(world.catalog.entities[id].lifespan, 0)):
return NAN
if E.lt(created_to, created_from):
return NAN
if (not target.batches.has(id)):
target.batches[id] = []
if (not target.has("batch_origins")):
target.batch_origins = {}
var batches: Array = target.batches[id]
var lifespan: Variant = CivilizationState.effective_span(world, target, id, Global.CivilizationRules)
if E.eq(world.catalog.entities[id].group_as, "Star"):
lifespan = E.divide(lifespan, WorldParameters.effective(target, "Bur"))
var batch_duration: Variant = (E.divide(lifespan, Global.batch_count) if E.gt(lifespan, 10) else lifespan)
if E.le(batch_duration, 0):
return NAN
if (not target.batch_origins.has(id)):
target.batch_origins[id] = (batches[0].get("created_from", created_from) if (not batches.is_empty()) else created_from)
var origin: Variant = target.batch_origins[id]
var cursor: Variant = created_from
var duration: Variant = E.subtract(created_to, created_from)
var remaining_amount: Variant = amount
var completed_amount: Variant = 0
var completed_from: Variant = INF
var completed_to: Variant = E.neg(INF)
# Subtract each completed window's lifetime production exactly.
var remaining_object_years: Variant = (E.multiply(amount, E.maximum(0, E.subtract(completed_before, E.multiply((E.add(created_from, created_to)), E.of("0.5"))))) if E.finite(completed_before) else NAN)
# Advance an exact window index rather than repeatedly finding a boundary.
var window_index: Variant = E.floor_value(E.divide((E.subtract(cursor, origin)), batch_duration))
if E.le(E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration)), cursor):
window_index = E.add(window_index, 1)
var batches_by_start: Dictionary = {}
var skip_empty_expired: bool = E.gt(duration, 0) and E.finite(completed_before) and E.eq(amount, E.floor_value(amount))
for existing : Dictionary in batches:
var start: Variant = existing.get("created_from", E.neg(INF))
if (not batches_by_start.has(str(start))):
batches_by_start[str(start)] = []
batches_by_start[str(start)].append(existing)
while true:
# Whole allocation changes only when its cumulative quota reaches the
# next integer. Skip expired empty windows without changing the span
# passed to the next generation or the lifetime production integral.
if skip_empty_expired:
var threshold = E.add(created_from, E.mul_div(duration, E.add(E.subtract(amount, remaining_amount), 1), amount))
var next_nonempty = E.subtract(E.ceil_value(E.divide(E.subtract(threshold, origin), batch_duration)), 1)
var expired_end = E.floor_value(E.divide(E.subtract(E.subtract(completed_before, lifespan), origin), batch_duration))
var skip_to = E.minimum(next_nonempty, expired_end)
if E.gt(skip_to, window_index):
completed_from = E.minimum(completed_from, E.add(E.add(origin, E.multiply(window_index, batch_duration)), lifespan))
completed_to = E.maximum(completed_to, E.add(E.add(origin, E.multiply(skip_to, batch_duration)), lifespan))
window_index = skip_to
cursor = E.add(origin, E.multiply(window_index, batch_duration))
var window_start: Variant = E.add(origin, E.multiply(window_index, batch_duration))
var window_end: Variant = E.add(origin, E.multiply((E.add(window_index, 1)), batch_duration))
if E.le(window_end, cursor):
push_error("Batch window does not advance: " + id)
return remaining_object_years
var part_end: Variant = E.minimum(created_to, window_end)
var part_amount: Variant = remaining_amount
if (E.gt(duration, 0) and E.lt(part_end, created_to)):
part_amount = E.subtract(E.floor_value(E.mul_div(amount, (E.subtract(part_end, created_from)), duration)), (E.subtract(amount, remaining_amount)))
if E.le(E.add(window_end, lifespan), completed_before):
# Retain the original integer allocation and lifetime integral, but
# do not allocate thousands of already-dead batch dictionaries.
completed_amount = E.add(completed_amount, part_amount)
completed_from = E.minimum(completed_from, E.add(window_start, lifespan))
completed_to = E.maximum(completed_to, E.add(window_end, lifespan))
remaining_object_years = E.subtract(remaining_object_years, E.multiply(part_amount, expired_years(completed_before, E.add(window_start, lifespan), E.add(window_end, lifespan))))
else:
var batch: Dictionary = {}
for existing : Dictionary in batches_by_start.get(str(window_start), []):
if ((E.eq(existing.get("created_from", E.neg(INF)), window_start) and E.eq(existing.start_year, E.add(window_start, lifespan))) and E.eq(existing.end_year, E.add(window_end, lifespan))):
batch = existing
break
if batch.is_empty():
batch = {"created_from": window_start, "created_to": window_end, "start_year": E.add(window_start, lifespan), "end_year": E.add(window_end, lifespan), "amount": 0}
batches.append(batch)
if (not batches_by_start.has(str(window_start))):
batches_by_start[str(window_start)] = []
batches_by_start[str(window_start)].append(batch)
batch.amount = E.add(batch.amount, part_amount)
remaining_amount = E.subtract(remaining_amount, part_amount)
if (E.eq(duration, 0) or E.ge(part_end, created_to)):
break
cursor = window_end
window_index = E.add(window_index, 1)
batches.sort_custom(func ( a : Dictionary , b : Dictionary ) -> bool : return E.lt(a.created_from, b.created_from))
if E.gt(completed_amount, 0):
if (not completed.has(id)): completed[id] = []
completed[id].append({"amount": completed_amount, "from": completed_from, "to": completed_to})
return remaining_object_years
func withdraw_batches(target: Dictionary, id: String, to_year: Variant) -> Array:
var expired: Array = []
var remaining: Array = []
for batch : Dictionary in target.batches.get(id, []):
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
var end: Variant = batch.end_year
var amount: Variant = batch.amount
if E.le(amount, 0):
continue
if (E.lt(to_year, start) or ((E.eq(to_year, start) and E.gt(end, start)))):
remaining.append(batch)
continue
var consumed: Variant = amount
var until: Variant = E.minimum(to_year, end)
if E.lt(to_year, end):
var carry: Variant = batch.get("withdraw_remainder", 0)
var expected: Variant = E.add(carry, E.mul_div((E.subtract(amount, carry)), (E.subtract(to_year, start)), (E.subtract(end, start))))
consumed = E.floor_value(expected)
batch.withdraw_remainder = E.subtract(expected, consumed)
batch.amount = E.maximum(0, E.subtract(amount, consumed))
batch.processed_until = to_year
remaining.append(batch)
if E.gt(consumed, 0):
expired.append({"amount": consumed, "from": start, "to": until})
target.batches[id] = remaining
return expired
func object_years(source: Dictionary, id: String, quantity: Variant, from_year: Variant, to_year: Variant) -> Variant:
var result: Variant = E.multiply(quantity, SimulationTime.elapsed_years(from_year, to_year))
for batch : Dictionary in source.batches.get(id, []):
var start: Variant = E.maximum(batch.start_year, batch.get("processed_until", batch.start_year))
var end: Variant = batch.end_year
result = E.subtract(result, E.multiply(batch.amount, (E.subtract(expired_years(to_year, start, end), expired_years(from_year, start, end)))))
return E.maximum(0, result)
func expired_years(year: Variant, start: Variant, end: Variant) -> Variant:
if E.le(year, start):
return 0
if E.le(end, start):
return E.subtract(year, start)
if E.ge(year, end):
return E.add(E.subtract(year, end), E.multiply((E.subtract(end, start)), E.of("0.5")))
return E.mul_div((E.subtract(year, start)), (E.subtract(year, start)), (E.multiply(2, (E.subtract(end, start)))))
func consume_from_batches(entity: OwniverseEntity, current_year: Variant) -> Variant:
var total: Variant = 0
var physical: Dictionary = world.ledger.physical_amounts(world.state)
for part : Dictionary in withdraw_batches(world.state, entity.id, current_year):
total = E.add(total, part.amount)
var fraction: Variant = (E.clamp_value(E.divide(total, physical[entity.id]), 0, 1) if E.gt(physical.get(entity.id, 0), 0) else 0)
world.state.amount[entity.id] = E.multiply(world.state.amount.get(entity.id, 0), (E.subtract(1, fraction)))
for owner_id in world.state.binds:
if world.state.binds[owner_id].has(entity.id):
world.state.binds[owner_id][entity.id] = E.multiply(world.state.binds[owner_id][entity.id], E.subtract(1, fraction))
world.ledger.round_object_allocations(world.state)
world.ledger.refresh_groups(world.state)
world.presentation.refresh(world.state, world.catalog)
return total
func add_entity_to_batch(entity_id: String, amount: Variant, current_year: Variant) -> void:
append_batch(world.state, entity_id, amount, current_year, current_year)