Enhance simulation performance and accuracy by optimizing physical amount calculations and refining arithmetic operations

This commit is contained in:
Kirill
2026-10-01 20:30:53 +03:00
parent befb377774
commit f51eea6c0f
14 changed files with 155 additions and 55 deletions
+13 -2
View File
@@ -91,6 +91,8 @@ var g_counters: Dictionary[String, Counter] = {
var g_active_items: Dictionary = {} var g_active_items: Dictionary = {}
var pending_page: int = -1 var pending_page: int = -1
var active_actions_dirty: bool = true
var active_actions_elapsed: float = 0.0
var owniverse:Owniverse = Owniverse.new() var owniverse:Owniverse = Owniverse.new()
var owniverse_items: Dictionary[String, OwniverseItem] = {} var owniverse_items: Dictionary[String, OwniverseItem] = {}
@@ -174,6 +176,7 @@ func _control_page(item: OwniverseItem) -> void:
return return
g_active_items[item.item_page] = item g_active_items[item.item_page] = item
active_actions_dirty = true
if current_item != null and current_item.item_page == item.item_page: if current_item != null and current_item.item_page == item.item_page:
current_item.set_selection(false) current_item.set_selection(false)
@@ -338,6 +341,8 @@ func create_new_universe() -> void:
description_samples.clear() description_samples.clear()
description_rates.clear() description_rates.clear()
description_elapsed = 0.0 description_elapsed = 0.0
active_actions_dirty = true
active_actions_elapsed = 0.0
for label in g_item_descriptions: for label in g_item_descriptions:
label.text = "" label.text = ""
for item in owniverse_items.values(): for item in owniverse_items.values():
@@ -379,8 +384,14 @@ func _process(delta: float) -> void:
var actions = _check_counters(delta) var actions = _check_counters(delta)
_do_actions(actions) _do_actions(actions)
# Production limits are expensive (they walk the resource ledger). Refresh
_set_active_page_actions() # them when selection changes and at a modest cadence instead of every
# rendered frame. The simulation itself continues to run every frame.
active_actions_elapsed += delta
if active_actions_dirty or active_actions_elapsed >= 0.1:
active_actions_elapsed = 0.0
active_actions_dirty = false
_set_active_page_actions()
func _change_speed(delta: int) -> void: func _change_speed(delta: int) -> void:
+3 -2
View File
@@ -13,8 +13,9 @@ godot --headless --path Owniverse.GI --script res://tests/benchmark_saved_state.
На диагностической копии с 120 партиями прежняя рациональная арифметика после На диагностической копии с 120 партиями прежняя рациональная арифметика после
оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6. оптимизации пустых окон давала 90–99 мс на шаг 1/60 секунды на скорости 6.
После перехода на целые блоки и float-остаток повторный замер дал 58–62 мс. После перехода на целые блоки и устранения повторного построения физических
При шаге 0,1 секунды: 71–82 мс вместо 117–138 мс. Это замеры расчёта на данной количеств повторный замер дал 53–58 мс. При шаге 0,1 секунды: 65–67 мс вместо
117–138 мс. Это замеры расчёта на данной
копии, а не обещание FPS. Для сравнения старый JSON был преобразован только копии, а не обещание FPS. Для сравнения старый JSON был преобразован только
во временный тестовый файл; в загрузчик игры миграция не добавлена. во временный тестовый файл; в загрузчик игры миграция не добавлена.
Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`. Разреженные сценарии сравниваются с полным расчётом в `test_short_lived_events.gd`.
+6 -1
View File
@@ -62,11 +62,16 @@ func run():
check(E.bounded(count), str(count), "integer counts never quantized") check(E.bounded(count), str(count), "integer counts never quantized")
check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage") check(E.mul_div(E.of("1e27"), 15, 100), "150000000000000000000000000", "exact percentage")
check(E.mul_div(count, count, count), str(count), "fused product beyond storage range") check(E.mul_div(count, count, count), str(count), "fused product beyond storage range")
var limit = E.of("1e27")
check(E.floor_value(E.mul_div(E.subtract(limit, 1), E.subtract(limit, 3), E.subtract(limit, 2))), "999999999999999999999999997", "fused ratio immediately below whole boundary")
check(E.floor_value(E.divide(E.MIN_INT, -3)), "3074457345618258602", "MIN_INT division preserves quotient")
check(E.floor_value(E.divide(-10, 3)), "-4", "native negative fractional division")
check(E.mul_div(E.MAX_INT, 0, 7), "0", "native zero fused product")
check(E.subtract(E.add(count, 1), count), "1", "single unit at E27") check(E.subtract(E.add(count, 1), count), "1", "single unit at E27")
check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object") check(E.floor_value(E.divide(E.subtract(count, 1), count)), "0", "division must not round up a whole object")
check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate") check(E.fixed(E.add(count, E.of("0.125")), 3), "1000000000000000000000000001.125", "formatting without oversized intermediate")
check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary") check(E.scientific(E.of("9.999999e27"), 2), "1.00E+28", "display rounding at upper boundary")
check(E.eq(E.power(10, -100), E.of("1e-100")), "true", "negative power needs no huge intermediate") check(absf(E.approx(E.power(10, -100)) / E.approx(E.of("1e-100")) - 1.0) < 1e-15, "true", "negative power retains float precision without huge intermediate")
var largest = E.of("9999999999999999999999999999") var largest = E.of("9999999999999999999999999999")
check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip") check(E.eq(E.decode(E.encode(largest)), largest), "true", "upper range round trip")
check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid") check(E.valid_encoded(E.encode(largest)), "true", "upper range encoding valid")
+5
View File
@@ -41,6 +41,11 @@ Stage обращается непосредственно к `world.presentation
В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`. В `owniverse.gd` остались создание состояния и модулей, скорость и проверка входного delta перед запуском эволюции. Дублирующее поле new_state и закомментированная старая реализация удалены. Единственный источник текущего состояния — `world.state`.
Внутри одной фазы снимок физических количеств переиспользуется для сверки
этик, звёздного бюджета и бюджета структур. После списания партий или переходов
создаётся новый снимок. Операции над одним типом объекта используют
`physical_amount`, не пересчитывая полную карту вселенной.
### Порядок шага ### Порядок шага
1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость. 1. Определить обработанную десятую года. При продвижении времени восстановить недостающие партии жизни и проверить вместимость.
+6 -1
View File
@@ -216,7 +216,12 @@ func produce_newborn_lifetime(target: Dictionary, id: String, count: Variant, bo
object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year))) object_years = E.subtract(object_years, E.multiply(E.maximum(0, added), world.batches.expired_years(to_year, batch.start_year, batch.end_year)))
object_years = E.maximum(0, object_years) object_years = E.maximum(0, object_years)
for resource in products: for resource in products:
var output: Variant = E.multiply(E.multiply(object_years, products[resource]), WorldParameters.production(target, world.catalog.entities[id])) var coefficient: Variant = E.multiply(products[resource], WorldParameters.production(target, world.catalog.entities[id]))
# A giant cohort can accumulate more than E27 object-years. Clamp the
# multiplicand before multiplying so the exact-number backend never
# constructs an out-of-range intermediate value.
var safe_years: Variant = (E.minimum(object_years, E.divide(E.max_value(), coefficient)) if E.gt(coefficient, 0) else 0)
var output: Variant = E.multiply(safe_years, coefficient)
if E.contains(["H", "S"], resource): if E.contains(["H", "S"], resource):
output = E.minimum(output, target.energy) output = E.minimum(output, target.energy)
var actual: Variant = world.ledger.credit(target, resource, output) var actual: Variant = world.ledger.credit(target, resource, output)
+2 -2
View File
@@ -76,8 +76,8 @@ static func consume_untriggered(world, target: Dictionary, plan: Dictionary, pop
plan.to = E.maximum(plan.to, year) plan.to = E.maximum(plan.to, year)
return removed return removed
static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary) -> Dictionary: static func prepare(world, target: Dictionary, expired: Dictionary, year: Variant, budget: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
State.reconcile(world, target, rules) State.reconcile(world, target, rules, physical)
var remaining: Dictionary = target.ethics.duplicate(true) var remaining: Dictionary = target.ethics.duplicate(true)
var plans: Dictionary = {} var plans: Dictionary = {}
var colonies: Array = [] var colonies: Array = []
+22 -17
View File
@@ -10,10 +10,11 @@ static func is_space_civilization(id: String, rules: Dictionary) -> bool:
# Preserve any existing ethics. New populations start equally distributed; # Preserve any existing ethics. New populations start equally distributed;
# reductions (including blasts) are proportional and never leave negative counts. # reductions (including blasts) are proportional and never leave negative counts.
static func reconcile(world, target: Dictionary, rules: Dictionary) -> void: static func reconcile(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
if (not target.has("ethics")): if (not target.has("ethics")):
target.ethics = {} target.ethics = {}
var physical: Dictionary = world.ledger.physical_amounts(target) if physical.is_empty():
physical = world.ledger.physical_amounts(target)
for id in rules: for id in rules:
var count: Variant = physical.get(id, 0) var count: Variant = physical.get(id, 0)
var current: Dictionary = target.ethics.get(id, {}) var current: Dictionary = target.ethics.get(id, {})
@@ -40,8 +41,9 @@ static func detach_space_civilizations(target: Dictionary, rules: Dictionary) ->
# A shared budget per stellar family, including stars bound into structures. # A shared budget per stellar family, including stars bound into structures.
# Reservations affect capacity, not the physical stock or its production. # Reservations affect capacity, not the physical stock or its production.
static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary: static func stellar_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
var physical: Dictionary = world.ledger.physical_amounts(target) if physical.is_empty():
physical = world.ledger.physical_amounts(target)
var capacity: Dictionary = {} var capacity: Dictionary = {}
for id in physical: for id in physical:
var entity = world.catalog.entities[id] var entity = world.catalog.entities[id]
@@ -62,7 +64,7 @@ static func stellar_budget(world, target: Dictionary, rules: Dictionary) -> Dict
var free: Dictionary = {} var free: Dictionary = {}
for home in capacity: for home in capacity:
free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0))) free[home] = E.maximum(0, E.subtract(capacity[home], requested.get(home, 0)))
return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules)} return {"capacity": capacity, "requested": requested, "free": free, "populations": populations, "habitats": habitat_budget(world, target, rules, physical)}
# Account for the source's existing reservation when changing stages. A failed # Account for the source's existing reservation when changing stages. A failed
# transition retains its population at the source, rather than losing objects. # transition retains its population at the source, rather than losing objects.
@@ -105,8 +107,9 @@ static func admit_transition(budget: Dictionary, rules: Dictionary, source: Stri
# advanced populations priority and lets a demotion cascade in the same cycle. # advanced populations priority and lets a demotion cascade in the same cycle.
static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void: static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year: Variant) -> void:
detach_space_civilizations(target, rules) detach_space_civilizations(target, rules)
reconcile(world, target, rules) var physical: Dictionary = world.ledger.physical_amounts(target)
var budget: Dictionary = stellar_budget(world, target, rules) reconcile(world, target, rules, physical)
var budget: Dictionary = stellar_budget(world, target, rules, physical)
var remaining: Dictionary = budget.capacity.duplicate() var remaining: Dictionary = budget.capacity.duplicate()
var remaining_homes: Dictionary = budget.habitats.capacity.duplicate() var remaining_homes: Dictionary = budget.habitats.capacity.duplicate()
var stages: Array = [] var stages: Array = []
@@ -141,10 +144,10 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic]) target.ethics[previous][ethic] = E.add(target.ethics[previous].get(ethic, 0), removed[ethic])
if E.eq(previous, "C1.9"): if E.eq(previous, "C1.9"):
var stars: Dictionary = {} var stars: Dictionary = {}
var physical: Dictionary = world.ledger.physical_amounts(target) var current_physical: Dictionary = world.ledger.physical_amounts(target)
for star in physical: for star in current_physical:
if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(physical[star], 0)): if (E.eq(world.catalog.entities[star].event_group, "DwStar") and E.gt(current_physical[star], 0)):
stars[star] = physical[star] stars[star] = current_physical[star]
var weights: Dictionary = QuantityMath.normalize_distribution(stars) var weights: Dictionary = QuantityMath.normalize_distribution(stars)
# Worlds are aggregates: a star may host several inhabited planets. # Worlds are aggregates: a star may host several inhabited planets.
var assigned: Variant = 0 var assigned: Variant = 0
@@ -158,13 +161,15 @@ static func enforce_capacity(world, target: Dictionary, rules: Dictionary, year:
target.binds[owner] = {} target.binds[owner] = {}
target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount) target.binds[owner][previous] = E.add(target.binds[owner].get(previous, 0), amount)
target.amount[previous] = E.subtract(target.amount[previous], amount) target.amount[previous] = E.subtract(target.amount[previous], amount)
reconcile(world, target, rules) physical = world.ledger.physical_amounts(target)
assign_homes(world, target, rules) reconcile(world, target, rules, physical)
assign_homes(world, target, rules, physical)
# Unity's structure chain: direct Claim resets the required structure count; # Unity's structure chain: direct Claim resets the required structure count;
# intervening stages require ten times more units of the preceding structure. # intervening stages require ten times more units of the preceding structure.
static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dictionary: static func habitat_budget(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> Dictionary:
var physical: Dictionary = world.ledger.physical_amounts(target) if physical.is_empty():
physical = world.ledger.physical_amounts(target)
var structures: Array = [] var structures: Array = []
var capacity: Dictionary = {} var capacity: Dictionary = {}
for id in world.catalog.entities: for id in world.catalog.entities:
@@ -198,8 +203,8 @@ static func habitat_budget(world, target: Dictionary, rules: Dictionary) -> Dict
requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units)) requested[req.id] = E.add(requested.get(req.id, 0), E.multiply(physical.get(id, 0), req.units))
return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors} return {"capacity": capacity, "requested": requested, "requirements": requirements, "structures": structures, "anchors": anchors}
static func assign_homes(world, target: Dictionary, rules: Dictionary) -> void: static func assign_homes(world, target: Dictionary, rules: Dictionary, physical: Dictionary = {}) -> void:
var budget: Dictionary = habitat_budget(world, target, rules) var budget: Dictionary = habitat_budget(world, target, rules, physical)
var used: Dictionary = {} var used: Dictionary = {}
target.civilization_homes = {} target.civilization_homes = {}
var stages: Array = budget.anchors.keys() var stages: Array = budget.anchors.keys()
+3 -2
View File
@@ -92,8 +92,9 @@ func evolve_batch_generation(target: Dictionary, to_year: Variant, completed: Di
return false return false
var fractions: Dictionary = {} var fractions: Dictionary = {}
var outputs: Dictionary = {} var outputs: Dictionary = {}
var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules) var current_quantities: Dictionary = world.ledger.physical_amounts(target)
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules) var star_budget: Dictionary = CivilizationState.stellar_budget(world, target, Global.CivilizationRules, current_quantities)
var civilization_transaction: Dictionary = CivilizationEvolution.prepare(world, target, expired, to_year, star_budget, Global.CivilizationRules, current_quantities)
for id in expired: for id in expired:
var total: Variant = 0 var total: Variant = 0
for part : Dictionary in expired[id]: for part : Dictionary in expired[id]:
+4 -2
View File
@@ -11,12 +11,14 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
trial.binds = target.binds.duplicate(true) trial.binds = target.binds.duplicate(true)
trial.auto_resources = target.auto_resources.duplicate() trial.auto_resources = target.auto_resources.duplicate()
trial.batches = target.batches.duplicate() trial.batches = target.batches.duplicate()
# Physical object totals do not change while resources are withdrawn from
# this transaction. Keep one snapshot instead of scanning every resource.
var physical: Dictionary = world.ledger.physical_amounts(trial)
var copied_batches: Dictionary = {} var copied_batches: Dictionary = {}
for resource in entity.cost: for resource in entity.cost:
var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount)) var cost: Variant = E.round_value(E.multiply(E.maximum(0, entity.cost[resource]), amount))
if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)): if (automatic and E.lt(trial.auto_resources.get(resource, 0), cost)):
return false return false
var before: Dictionary = world.ledger.physical_amounts(trial)
var taken: Dictionary = world.ledger.take_available(trial, resource, cost) var taken: Dictionary = world.ledger.take_available(trial, resource, cost)
var total: Variant = 0 var total: Variant = 0
for quantity in taken.values(): for quantity in taken.values():
@@ -30,7 +32,7 @@ func try_production(target: Dictionary, entity: OwniverseEntity, amount: Variant
if (trial.batches.has(id) and (not copied_batches.has(id))): if (trial.batches.has(id) and (not copied_batches.has(id))):
trial.batches[id] = trial.batches[id].duplicate(true) trial.batches[id] = trial.batches[id].duplicate(true)
copied_batches[id] = true copied_batches[id] = true
var factor: Variant = (E.subtract(1, E.divide(taken[id], before[id])) if E.gt(before.get(id, 0), 0) else 0) var factor: Variant = (E.subtract(1, E.divide(taken[id], physical[id])) if E.gt(physical.get(id, 0), 0) else 0)
world.batches.scale_batches(trial, id, factor) world.batches.scale_batches(trial, id, factor)
if (not world.ledger.bind_new_owners(trial, {entity.id: amount}, true)): if (not world.ledger.bind_new_owners(trial, {entity.id: amount}, true)):
return false return false
+5
View File
@@ -21,6 +21,11 @@ polarity × (high × 10^14 + low + fraction)
## Операции ## Операции
Сложение, вычитание и сравнение работают непосредственно с блоками. Сложение, вычитание и сравнение работают непосредственно с блоками.
Деление int64 и объединённое умножение/деление при безопасном произведении
используют нативную арифметику. Перевод больших целых между блоками и временными
разрядами, извлечение коэффициента и сборка результата обходятся без строк.
Дробный коэффициент нормализуется численно до 16 значащих цифр; последние биты
float могут отличаться от результата прежнего строкового преобразования.
Для умножения и деления `big_integer.gd` используется как временный вычислитель Для умножения и деления `big_integer.gd` используется как временный вычислитель
с безопасными разрядами по основанию 10^9. Результат сразу возвращается в с безопасными разрядами по основанию 10^9. Результат сразу возвращается в
фиксированную структуру. Размер временных дробей определяется входными float, фиксированную структуру. Размер временных дробей определяется входными float,
+65 -24
View File
@@ -8,31 +8,55 @@ const MIN_INT: int = -9223372036854775807 - 1
const BLOCK: int = 100000000000000 # 10^14 const BLOCK: int = 100000000000000 # 10^14
const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive const LIMIT: Array = [0, 0, 0, 10] # 10^28, exclusive
static func max_value():
return make([999999999, 999999999, 9999])
static func make(n: Array, d: Array = [1], polarity: int = 1): static func make(n: Array, d: Array = [1], polarity: int = 1):
assert(d != [0], "Zero denominator") assert(d != [0], "Zero denominator")
if n.size() <= 2 and d.size() <= 2:
var numerator: int = n[0] + (n[1] * SMALL if n.size() == 2 else 0)
var denominator: int = d[0] + (d[1] * SMALL if d.size() == 2 else 0)
var whole: int = numerator / denominator
var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53))
return _compose(whole / BLOCK, whole % BLOCK, fraction, polarity)
var qr: Array = Big.divmod(n, d) var qr: Array = Big.divmod(n, d)
var fraction: float = 0.0 var fraction: float = _fraction_ratio(qr[1], d)
if qr[1] != [0]:
var numerator_text: String = Big.decimal(qr[1])
var denominator_text: String = Big.decimal(d)
if denominator_text.length() < 300:
fraction = float(numerator_text) / float(denominator_text)
else:
var exponent: int = maxi(0, numerator_text.length() - 16) - maxi(0, denominator_text.length() - 16)
fraction = float(numerator_text.left(16)) / float(denominator_text.left(16))
fraction *= pow(10.0, maxi(-308, exponent))
fraction *= pow(10.0, mini(0, exponent + 308))
# A remainder below one must not round up and create a whole object.
fraction = minf(fraction, 1.0 - pow(2.0, -53))
return _from_whole(qr[0], fraction, polarity) return _from_whole(qr[0], fraction, polarity)
static func _fraction_ratio(n: Array, d: Array) -> float:
if n == [0]: return 0.0
var numerator: float = 0.0
var denominator: float = 0.0
var n_start: int = maxi(0, n.size() - 3)
var d_start: int = maxi(0, d.size() - 3)
for i in range(n.size() - 1, n_start - 1, -1): numerator = numerator * SMALL + n[i]
for i in range(d.size() - 1, d_start - 1, -1): denominator = denominator * SMALL + d[i]
var exponent: int = 9 * (n_start - d_start)
var fraction: float = numerator / denominator * pow(10.0, maxi(-308, exponent))
fraction *= pow(10.0, mini(0, exponent + 308))
return minf(fraction, 1.0 - pow(2.0, -53))
static func _from_whole(n: Array, fraction: float, polarity: int): static func _from_whole(n: Array, fraction: float, polarity: int):
var text: String = Big.decimal(n)
assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range") assert(Big.compare(n, LIMIT) < 0, "Number exceeds E27 range")
var high: int = int(text.left(text.length() - 14)) if text.length() > 14 else 0 var middle: int = n[1] if n.size() > 1 else 0
var low: int = int(text.right(14)) var low: int = n[0] + (middle % 100000) * SMALL
var high: int = middle / 100000
if n.size() > 2: high += n[2] * 10000
if n.size() > 3: high += n[3] * 10000000000000
return _compose(high, low, fraction, polarity) return _compose(high, low, fraction, polarity)
static func _whole_limbs(high: int, low: int) -> Array:
return Big.trim([low % SMALL, low / SMALL + (high % 10000) * 100000,
(high / 10000) % SMALL, high / 10000000000000])
static func _int_limbs(value: int) -> Array:
var result: Array = []
# Modulo/division before abs also supports MIN_INT.
while value != 0:
result.append(absi(value % SMALL))
value /= SMALL
return result if not result.is_empty() else [0]
static func _compose(high: int, low: int, fraction: float, polarity: int): static func _compose(high: int, low: int, fraction: float, polarity: int):
if fraction >= 1.0: if fraction >= 1.0:
low += int(floorf(fraction)) low += int(floorf(fraction))
@@ -91,17 +115,23 @@ static func of(value):
static func parts(value) -> Array: static func parts(value) -> Array:
value = of(value) value = of(value)
if value is int: return [Big.parse(str(value).trim_prefix("-")), [1], -1 if value < 0 else 1] if value is int: return [_int_limbs(value), [1], -1 if value < 0 else 1]
var whole: Array = Big.parse(str(value.high) + "%014d" % value.low) var whole: Array = _whole_limbs(value.high, value.low)
if value.fraction == 0.0: return [whole, [1], value.polarity] if value.fraction == 0.0: return [whole, [1], value.polarity]
# Bound temporary decimal work to 16 significant digits of the float. # Bound temporary decimal work to 16 significant digits of the float.
# Normalize first so tiny rates retain their significant digits as well. # Normalize first so tiny rates retain their significant digits as well.
var exponent: int = maxi(-323, int(floor(log(value.fraction) / log(10.0)))) var exponent: int = maxi(-323, int(floor(log(value.fraction) / log(10.0))))
var text: String = String.num(value.fraction / pow(10.0, exponent), 15) var mantissa: int = roundi((value.fraction / pow(10.0, exponent)) * 1000000000000000.0)
var point: int = text.find(".") exponent -= 15
if point >= 0: exponent -= text.length() - point - 1 while mantissa % 10 == 0 and mantissa > 0:
var n: Array = Big.parse(text.replace(".", "")) mantissa /= 10
var d: Array = Big.parse("1" + "0".repeat(-exponent)) exponent += 1
var n: Array = _int_limbs(mantissa)
var d: Array = []
d.resize((-exponent) / 9 + 1)
d.fill(0)
const POWERS: Array = [1, 10, 100, 1000, 10000, 100000, 1000000, 10000000, 100000000]
d[d.size() - 1] = POWERS[(-exponent) % 9]
return [Big.add(Big.multiply(whole, d), n), d, value.polarity] return [Big.add(Big.multiply(whole, d), n), d, value.polarity]
static func numeric(value) -> bool: static func numeric(value) -> bool:
@@ -208,6 +238,8 @@ static func multiply(a, b):
## Fused ratio: the temporary product is never stored as a simulation value. ## Fused ratio: the temporary product is never stored as a simulation value.
static func mul_div(a, b, divisor): static func mul_div(a, b, divisor):
if not finite(a) or not finite(b) or not finite(divisor): return of(approx(a) * approx(b) / approx(divisor)) if not finite(a) or not finite(b) or not finite(divisor): return of(approx(a) * approx(b) / approx(divisor))
if a is int and b is int and a != MIN_INT and b != MIN_INT:
if b == 0 or absi(a) <= MAX_INT / absi(b): return divide(a * b, divisor)
var x: Array = parts(a) var x: Array = parts(a)
var y: Array = parts(b) var y: Array = parts(b)
var z: Array = parts(divisor) var z: Array = parts(divisor)
@@ -220,7 +252,16 @@ static func divide(a, b):
if eq(b, 1): return of(a) if eq(b, 1): return of(a)
if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1): if a is int and b is int and b != 0 and not (a == MIN_INT and b == -1):
if a % b == 0: return a / b if a % b == 0: return a / b
return mul_div(a, 1, b) if a != MIN_INT and b != MIN_INT:
var numerator: int = absi(a)
var denominator: int = absi(b)
var whole: int = numerator / denominator
var fraction: float = minf(float(numerator % denominator) / float(denominator), 1.0 - pow(2.0, -53))
return _compose(whole / BLOCK, whole % BLOCK, fraction, -1 if (a < 0) != (b < 0) else 1)
var x: Array = parts(a)
var y: Array = parts(b)
assert(y[0] != [0], "Division by zero")
return make(Big.multiply(x[0], y[1]), Big.multiply(x[1], y[0]), x[2] * y[2])
static func floor_value(a): static func floor_value(a):
if not finite(a) or a is int: return a if not finite(a) or a is int: return a
+11
View File
@@ -6,6 +6,17 @@ const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
func scale_batches(target: Dictionary, id: String, factor: Variant) -> void: func scale_batches(target: Dictionary, id: String, factor: Variant) -> void:
var batches: Array = target.batches.get(id, []) 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 total: Variant = 0
var assigned: Variant = 0 var assigned: Variant = 0
var shares: Array = [] var shares: Array = []
+7
View File
@@ -15,6 +15,13 @@ func physical_amounts(source: Dictionary) -> Dictionary:
return amounts return amounts
func physical_amount(source: Dictionary, id: String) -> Variant:
var amount: Variant = E.maximum(0, source.amount.get(id, 0))
for bindings: Dictionary in source.binds.values():
amount = E.add(amount, E.maximum(0, bindings.get(id, 0)))
return amount
func available(source: Dictionary, id: String) -> Variant: func available(source: Dictionary, id: String) -> Variant:
var amount: Variant = 0 var amount: Variant = 0
for member in world.catalog.members(id): for member in world.catalog.members(id):
+3 -2
View File
@@ -69,7 +69,6 @@ func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level
if (E.le(ratio, 0) or not E.contains([1, 2], level)): if (E.le(ratio, 0) or not E.contains([1, 2], level)):
return return
var held: Dictionary = target.binds.get(owner_id, {}) var held: Dictionary = target.binds.get(owner_id, {})
var physical: Dictionary = world.ledger.physical_amounts(target)
var rocky: Variant = 0 var rocky: Variant = 0
for id in held.keys(): for id in held.keys():
if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")): if ((not world.catalog.entities.has(id)) or E.ne(world.catalog.entities[id].group_as, "Planet")):
@@ -82,8 +81,10 @@ func destroy_planets(target: Dictionary, owner_id: String, ratio: Variant, level
target.destruction_remainders[key] = 0 target.destruction_remainders[key] = 0
if E.le(destroyed, 0): if E.le(destroyed, 0):
continue continue
var physical: Variant = world.ledger.physical_amount(target, id)
held[id] = E.subtract(held[id], destroyed) held[id] = E.subtract(held[id], destroyed)
world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical[id]))) if E.gt(physical, 0):
world.batches.scale_batches(target, id, E.subtract(1, E.divide(destroyed, physical)))
if E.eq(level, 1): if E.eq(level, 1):
rocky = E.add(rocky, destroyed) rocky = E.add(rocky, destroyed)
if E.gt(rocky, 0): if E.gt(rocky, 0):