From dea744bc305cb767a4dc68e8190c1a595696df3f Mon Sep 17 00:00:00 2001 From: Kirill Date: Sun, 13 Sep 2026 22:57:25 +0300 Subject: [PATCH] Refactor save management; enhance save_data function with verification and plaintext copy options, and update load_data logic for improved error handling --- autoload/global.gd | 5 +- autoload/save_manager.gd | 182 +++++++++++++++++++++++++++++--- stage/stage.gd | 77 ++++++++++---- ui/actions/actions.gd | 2 + world/owniverse/owniverse.gd | 197 +++++++++++++++++++++++++++-------- world/owniverse/state.gd | 176 ------------------------------- world/owniverse/state.gd.uid | 1 - 7 files changed, 380 insertions(+), 260 deletions(-) delete mode 100644 world/owniverse/state.gd delete mode 100644 world/owniverse/state.gd.uid diff --git a/autoload/global.gd b/autoload/global.gd index ef1c12a..c3b2052 100644 --- a/autoload/global.gd +++ b/autoload/global.gd @@ -2,6 +2,9 @@ extends Node const SPEED_LIMIT: int = 5 +# Write a readable diagnostic copy next to the encrypted save. Never used for loading. +var save_plaintext_copy: bool = true + var Settings: Dictionary = { "filename": "save-0001", "sound_volume": 1.0, @@ -39,7 +42,7 @@ func get_description(item_id: String, items: Array = []) -> String: func format_number(value: float, digits: int = 0) -> String: if value == 0: return "" if value < 10_000: - return "%.*f" % [digits, value] + return "%.*f" % [digits, value] var exp_value = int(floor(log(abs(value)) / log(10.0))) var mantissa = value / pow(10.0, exp_value) diff --git a/autoload/save_manager.gd b/autoload/save_manager.gd index 5c8049e..0ce6f24 100644 --- a/autoload/save_manager.gd +++ b/autoload/save_manager.gd @@ -5,34 +5,184 @@ extends Node const SETTING_SECTION := "Settings" const SAVE_PATH = "user://settings.cfg" +const FORMAT_VERSION: int = 1 +const STATE_VERSION: String = "0.1.0" +var last_error: String = "" +var blocked_saves: Dictionary = {} + #region Save Load routines +func _fail(message: String) -> bool: + last_error = message + push_error(message) + return false -func save_data(filename: String, password: String, data: Dictionary) -> void: - filename = "user://" + filename + ".adventure" - var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.WRITE, password) + +func save_data(filename: String, password: String, data: Dictionary) -> bool: + last_error = "" + if blocked_saves.has(filename): + return _fail("Save blocked to preserve an unreadable save: " + filename) + var payload: Dictionary = data.duplicate(true) + payload.format_version = FORMAT_VERSION + payload.app_version = str(ProjectSettings.get_setting("application/config/version", "")) + var problem: String = validate_data(payload) + if problem != "": + return _fail("Invalid save: " + problem) + var path: String = "user://" + filename + ".adventure" + var temporary: String = path + ".tmp" + var file := FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, password) if file == null: - push_error("Can't write to the file: ", filename) - return - - file.store_var(data) # бинарно + быстрее + сериализация + return _fail("Cannot write encrypted save: " + temporary) + file.store_var(payload, false) file.close() + # Check the actual encrypted file, including finalization, before replacing the old save. + var verification := FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, password) + if verification == null: + return _fail("Cannot verify encrypted save: " + temporary) + var restored: Variant = verification.get_var(false) + verification.close() + if not restored is Dictionary or restored != payload: + return _fail("Encrypted save verification failed: " + temporary) + if DirAccess.rename_absolute(temporary, path) != OK: + return _fail("Cannot replace encrypted save: " + path) + if Global.save_plaintext_copy: + var plain_path: String = path + ".json" + file = FileAccess.open(plain_path + ".tmp", FileAccess.WRITE) + if file == null: + return _fail("Encrypted save written, but cannot write diagnostic copy: " + plain_path) + file.store_string(JSON.stringify(payload, " ", true, true) + "\n") + file.flush() + var write_error: Error = file.get_error() + file.close() + if write_error != OK or DirAccess.rename_absolute(plain_path + ".tmp", plain_path) != OK: + return _fail("Encrypted save written, but diagnostic copy failed: " + plain_path) + return true func load_data(filename: String, password: String) -> Dictionary: - filename = "user://" + filename + ".adventure" - if not FileAccess.file_exists(filename): + last_error = "" + # Plaintext is diagnostic only: never use it as a fallback. + var path: String = "user://" + filename + ".adventure" + if not FileAccess.file_exists(path): return {} - - var file := FileAccess.open_encrypted_with_pass(filename, FileAccess.READ, password) + var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password) if file == null: - push_error(filename, " couldn't be decrypted.") + blocked_saves[filename] = true + _fail("Cannot decrypt save: " + path) return {} - - var data: Variant = file.get_var() + var data: Variant = file.get_var(false) file.close() - - return data if data is Dictionary else {} + var problem: String = validate_data(data) + if problem != "": + blocked_saves[filename] = true + _fail("Invalid save " + path + ": " + problem) + return {} + blocked_saves.erase(filename) + return data + + +func _number(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool: + if not (value is int or value is float) or not is_finite(float(value)): + return false + return (not whole or float(value) == floorf(float(value))) and (not nonnegative or value >= 0) + + +func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool: + if not value is Dictionary: + return false + for key in value: + if not (key is String or key is StringName) or not _number(value[key], whole, nonnegative): + return false + return true + + +func _plain_tree(value: Variant) -> bool: + if value is Dictionary: + for key in value: + if not (key is String or key is StringName) or not _plain_tree(value[key]): + return false + return true + if value is Array: + for item in value: + if not _plain_tree(item): + return false + return true + return value == null or value is String or value is StringName or value is bool or _number(value, false, false) + + +## Return a diagnostic path on failure; an empty string means the structure is valid. +func validate_data(data: Variant) -> String: + if not data is Dictionary or not data.get("state") is Dictionary: + return "state must be a Dictionary" + if not _plain_tree(data): + return "unsupported value or non-finite number" + # Legacy files contained only {state}; accept their known state schema. + if data.has("format_version") and (not data.format_version is int or data.format_version != FORMAT_VERSION): + return "unsupported format_version" + if data.has("ui"): + if not data.ui is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0: + return "invalid ui.current_page" + var saved: Dictionary = data.state + if saved.get("version") != STATE_VERSION: + return "unsupported state.version" + if not saved.get("time") is Dictionary: + return "state.time must be a Dictionary" + if not _number(saved.time.get("datetime")) or not _number(saved.time.get("year"), true): + return "invalid state.time" + if saved.time.year != floorf(saved.time.datetime): + return "state.time.year must equal floor(datetime)" + if not _number(saved.get("speed"), true) or saved.speed < 1 or saved.speed > Global.SPEED_LIMIT: + return "invalid state.speed" + if not _number(saved.get("speed_in_year")) or saved.speed_in_year != pow(10.0, saved.speed - 1): + return "invalid state.speed_in_year" + if not _number(saved.get("energy"), true): + return "invalid state.energy" + if not _number_map(saved.get("amount"), true): + return "invalid state.amount" + if not saved.get("stats") is Dictionary or not _number_map(saved.stats.get("total_amount"), true): + return "invalid state.stats.total_amount" + for id in saved.amount: + if not saved.stats.total_amount.has(id): + return "missing total_amount for " + id + if not saved.get("binds") is Dictionary or not saved.get("batches") is Dictionary: + return "state.binds and state.batches must be Dictionaries" + for owner in saved.binds: + if not saved.amount.has(owner) or not _number_map(saved.binds[owner], true): + return "invalid state.binds." + owner + for id in saved.binds[owner]: + if not saved.amount.has(id): + return "unknown bound resource " + id + for id in saved.batches: + if not saved.amount.has(id) or not saved.batches[id] is Array: + return "invalid state.batches." + id + for batch in saved.batches[id]: + if not batch is Dictionary: + return "batch must be a Dictionary" + if not _number(batch.get("amount"), true) or not _number(batch.get("start_year")) or not _number(batch.get("end_year")): + return "invalid batch in " + id + if batch.end_year < batch.start_year: + return "reversed batch interval in " + id + if batch.has("created_from") or batch.has("created_to"): + if not _number(batch.get("created_from")) or not _number(batch.get("created_to")): + return "invalid batch creation interval in " + id + if batch.created_to <= batch.created_from or batch.start_year < batch.created_from: + return "reversed batch creation interval in " + id + if batch.has("processed_until"): + if not _number(batch.processed_until) or batch.processed_until < batch.start_year or batch.processed_until > batch.end_year: + return "invalid batch.processed_until in " + id + if batch.has("withdraw_remainder") and (not _number(batch.withdraw_remainder) or batch.withdraw_remainder > batch.amount): + return "invalid batch.withdraw_remainder in " + id + for field in ["batch_origins", "distribution", "auto_resources", "production_remainders", "auto_star_deficits", "evolution_remainders"]: + if saved.has(field) and not _number_map(saved[field], field == "auto_resources", field != "auto_star_deficits"): + return "invalid state." + field + if saved.has("resource_remainders"): + if not saved.resource_remainders is Dictionary: + return "invalid state.resource_remainders" + for channel in ["basic", "automatic"]: + # Signed allocation errors are valid, even though resource balances are whole. + if not _number_map(saved.resource_remainders.get(channel), false, false): + return "invalid state.resource_remainders." + channel + return "" #endregion diff --git a/stage/stage.gd b/stage/stage.gd index e05d5f3..493c94b 100644 --- a/stage/stage.gd +++ b/stage/stage.gd @@ -71,6 +71,8 @@ var g_counters: Dictionary[String, Counter] = { var g_active_items: Dictionary = {} +var pending_page: int = -1 + var owniverse:Owniverse = Owniverse.new() var owniverse_items: Dictionary[String, OwniverseItem] = {} var locked_items: Array = [] @@ -83,8 +85,10 @@ func set_datetime(value: float): func set_owniverse_item_value() -> void: - for _item_id in owniverse_items: - owniverse_items[_item_id].set_label(owniverse.state.amount[_item_id]) + for item_id in owniverse.items_to_update: + if owniverse_items.has(item_id): + owniverse_items[item_id].set_label(owniverse.items_to_update[item_id]) + owniverse.items_to_update.clear() #endregion @@ -93,16 +97,21 @@ func set_owniverse_item_value() -> void: func _unlock_items(): if locked_items.size() == 0: return var _new_locked_items = [] + var display_totals: Dictionary = owniverse.get_item_amounts(true) for _item_id in locked_items: #print(_item_id ' | ', _entity.unlock) var _flag_to_unlock = true for _elem in owniverse.entities[_item_id].unlock: - if owniverse.state.stats.total_amount[_elem] < owniverse.entities[_item_id].unlock[_elem]: + if maxf(owniverse.state.stats.total_amount.get(_elem, 0.0), display_totals.get(_elem, 0.0)) < owniverse.entities[_item_id].unlock[_elem]: _flag_to_unlock = false break if _flag_to_unlock: - owniverse_items[_item_id].set_visible(true) + var item_id: String = owniverse.entities[_item_id].count_as + if item_id == "": + item_id = _item_id + if owniverse_items.has(item_id): + owniverse_items[item_id].set_visible(true) else: _new_locked_items.append(_item_id) locked_items = _new_locked_items @@ -268,6 +277,11 @@ func _ready() -> void: locked_items = inited_result.locked_items load_data() _init_owniverse_items() + owniverse.refresh_item_amounts() + _change_speed(0) + set_datetime(owniverse.state.time.year) + set_owniverse_item_value() + _unlock_items() g_counters.swipe.sec = 0.2 @@ -279,6 +293,7 @@ func _ready() -> void: func _process(delta: float) -> void: owniverse.evolute(delta) + set_owniverse_item_value() set_datetime(owniverse.state.time.year) #owniverse.cycle(delta) @@ -342,31 +357,47 @@ func _on_unlock_and_save_timeout() -> void: func load_data() -> bool: - # Получаем версию приложения - var app_version = ProjectSettings.get_setting("application/config/version") - print("Версия приложения: ", app_version) - var game_data: Dictionary = SaveManager.load_data("owniverse", "qwerty") - if game_data == {}: return false - #if game_data.get('version', '0.0.1') != 'app_version': return false - if game_data.get('version', str(app_version)) != '0.1.0': - print(game_data.get('version', 7)) - print('wrong load version') + if game_data.is_empty(): return false - - owniverse.state = game_data["state"] - - for item in owniverse.state.amount: - owniverse.items_to_update[item] = owniverse.state.amount[item] + var restored: Dictionary = game_data.state + # Current definitions are initialized first; reject incompatible saves before publishing. + for id in owniverse.state.amount: + if not restored.amount.has(id) or not restored.stats.total_amount.has(id): + SaveManager.blocked_saves["owniverse"] = true + push_error("Save is missing the current entity: " + id) + return false + var complete: Dictionary = owniverse.state.duplicate(true) + complete.merge(restored, true) + owniverse.state = complete + owniverse.new_state = complete + owniverse.refresh_item_amounts() + pending_page = clampi(int(game_data.get("ui", {}).get("current_page", 0)), 0, PAGE_COUNT - 1) + _restore_page.call_deferred() return true +func _restore_page() -> void: + # Wait until containers have calculated the scrollable width. + await get_tree().process_frame + if pending_page >= 0: + stage_page_scroller.scroll_horizontal = pending_page * PAGE_WIDTH + pending_page = -1 + + func save_data() -> void: - var game_data: Dictionary = {} - game_data["state"] = owniverse.state - - SaveManager.save_data("owniverse", "qwerty", game_data) - #print("saved ", game_data.version, "\n\n\n") + var page: int = pending_page + if page < 0: + page = clampi(roundi(float(stage_page_scroller.scroll_horizontal) / PAGE_WIDTH), 0, PAGE_COUNT - 1) + SaveManager.save_data("owniverse", "qwerty", { + "state": owniverse.state, + "ui": {"current_page": page}, + }) + + +func _notification(what: int) -> void: + if is_node_ready() and (what == NOTIFICATION_WM_CLOSE_REQUEST or what == NOTIFICATION_APPLICATION_PAUSED): + save_data() func init_enitities() -> Dictionary: diff --git a/ui/actions/actions.gd b/ui/actions/actions.gd index 6a14d8f..ba79326 100644 --- a/ui/actions/actions.gd +++ b/ui/actions/actions.gd @@ -28,6 +28,8 @@ func set_actions(item_id: String, value: float) -> void: _set_action_values(_get_distribution(value)) func _get_distribution(value: float) -> Array: + if value <= 0.0 or not is_finite(value): + return [0.0, 0.0, 0.0, 0.0, 0.0] var distribution: Array = [value, 0, 0, 0, 0] var exp_value = int(floor(log(abs(value)) / log(10.0))) diff --git a/world/owniverse/owniverse.gd b/world/owniverse/owniverse.gd index 1c8fa7e..71c3d10 100644 --- a/world/owniverse/owniverse.gd +++ b/world/owniverse/owniverse.gd @@ -75,9 +75,32 @@ func evolute(delta: float) -> void: # Publish only after every phase has finished; previous stays untouched. new_state = next state = next - for id in next.amount: - if next.amount[id] != previous.amount.get(id, 0.0): - items_to_update[id] = next.amount[id] + if to_year > from_year: + refresh_item_amounts() + + +## Aggregate only for presentation; never write aliases back into simulation state. +func get_item_amounts(total_produced: bool = false) -> Dictionary[String, float]: + var source: Dictionary = state.stats.total_amount if total_produced else state.amount + var result: Dictionary[String, float] = {} + for id in source: + result[id] = 0.0 + for id in source: + # group_as counters are already sums and must not be counted a second time. + if _is_group(id): + continue + var item_id: String = id + if entities.has(id) and entities[id].count_as != "": + item_id = entities[id].count_as + result[item_id] = result.get(item_id, 0.0) + float(source[id]) + for id in source: + if _is_group(id): + result[id] = float(source[id]) + return result + + +func refresh_item_amounts() -> void: + items_to_update = get_item_amounts() func _add_time_delta(delta: float) -> Dictionary: @@ -335,20 +358,31 @@ func _withdraw_batches(target: Dictionary, id: String, to_year: float) -> Array: return expired -func _evolution_outputs(entity: OwniverseEntity) -> Dictionary: - if entity.output_or.is_empty(): - return entity.output_and.duplicate(true) - # Expected quantities keep evolution independent of frame rate and RNG order. - var chance: float = clampf(entity.output_or.get("chance", 0.0), 0.0, 1.0) +func _add_output_terms(outputs: Dictionary, terms: String, count: float) -> void: + if count <= 0.0: + return + for term in terms.split("&"): + var parts: PackedStringArray = term.strip_edges().split(":") + var id: String = parts[0].strip_edges() + var amount: float = float(parts[1]) if parts.size() == 2 else 1.0 + if id != "" and amount > 0.0: + outputs[id] = outputs.get(id, 0.0) + count * amount + + +func _evolution_products(target: Dictionary, entity: OwniverseEntity, count: float) -> Dictionary: var outputs: Dictionary = {} - for branch in ["negative", "positive"]: - var weight: float = chance if branch == "positive" else 1.0 - chance - for term in str(entity.output_or.get(branch, "")).split("&"): - var parts: PackedStringArray = term.split(":") - var id: String = parts[0] - var amount: float = float(parts[1]) if parts.size() == 2 else 1.0 - if id != "" and weight > 0.0: - outputs[id] = outputs.get(id, 0.0) + amount * weight + for id in entity.output_and: + outputs[id] = count * maxf(0.0, entity.output_and[id]) + if not entity.output_or.is_empty(): + # The parser stores the percentage after '%', e.g. 0.1 means 0.1 percent. + var chance: float = clampf(entity.output_or.get("chance", 0.0) / 100.0, 0.0, 1.0) + if not target.has("evolution_remainders"): + target.evolution_remainders = {} + var expected: float = count * chance + target.evolution_remainders.get(entity.id, 0.0) + var positive: float = minf(count, floorf(snappedf(expected, 1.0e-12))) + target.evolution_remainders[entity.id] = maxf(0.0, expected - positive) + _add_output_terms(outputs, str(entity.output_or.get("positive", "")), positive) + _add_output_terms(outputs, str(entity.output_or.get("negative", "")), count - positive) return outputs @@ -379,7 +413,15 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool: for part: Dictionary in expired[id]: total += part.amount fractions[id] = clampf(total / quantities[id], 0.0, 1.0) if quantities.get(id, 0.0) > 0.0 else 0.0 - outputs[id] = _evolution_outputs(entities[id]) + var combined: Dictionary = {} + for part: Dictionary in expired[id]: + part.products = _evolution_products(target, entities[id], part.amount) + for successor in part.products: + combined[successor] = combined.get(successor, 0.0) + part.products[successor] + outputs[id] = {} + if total > 0.0: + for successor in combined: + outputs[id][successor] = combined[successor] / total # Use a frozen binding table so simultaneous transitions cannot cascade by type order. var bindings: Dictionary = target.binds.duplicate(true) @@ -406,8 +448,8 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool: var new_owners: Dictionary = {} for id in expired: for part: Dictionary in expired[id]: - for successor in outputs[id]: - var amount: float = part.amount * maxf(0.0, outputs[id][successor]) + for successor in part.products: + var amount: float = part.products[successor] amount = _credit(target, successor, amount) _append_batch(target, successor, amount, part.from, part.to) new_owners[successor] = new_owners.get(successor, 0.0) + amount @@ -421,7 +463,74 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool: return true -func _bind_new_owners(target: Dictionary, amounts: Dictionary) -> void: +# Plan all bindings on a copy, reserving specific types before broader groups. +func _bind_full(target: Dictionary, amounts: Dictionary) -> bool: + var trial: Dictionary = target.duplicate(true) + var requests: Array = [] + for owner in amounts: + if not entities.has(owner): + return false + for id in entities[owner].bind: + var quantity: float = amounts[owner] * maxf(0.0, entities[owner].bind[id]) + if not is_equal_approx(quantity, roundf(quantity)): + return false + if quantity > 0.0: + requests.append({"owner": owner, "id": id, "amount": roundf(quantity), "members": _members(id).size()}) + requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: + if a.members != b.members: + return a.members < b.members + return a.id < b.id if a.id != b.id else a.owner < b.owner) + for request: Dictionary in requests: + var taken: Dictionary = _take_available(trial, request.id, request.amount) + var total: float = 0.0 + for quantity in taken.values(): + total += quantity + if total != request.amount: + return false + if not trial.binds.has(request.owner): + trial.binds[request.owner] = {} + for id in taken: + trial.binds[request.owner][id] = trial.binds[request.owner].get(id, 0.0) + taken[id] + target.amount = trial.amount + target.binds = trial.binds + return true + + +# Neither costs nor bindings are committed unless the whole batch is funded. +func _try_production(target: Dictionary, entity: OwniverseEntity, amount: float, + automatic: bool = false) -> bool: + if amount <= 0.0 or amount != floorf(amount): + return false + var trial: Dictionary = target.duplicate(true) + for resource in entity.cost: + var cost: float = roundf(maxf(0.0, entity.cost[resource]) * amount) + if automatic and trial.auto_resources.get(resource, 0.0) < cost: + return false + var before: Dictionary = _physical_amounts(trial) + var taken: Dictionary = _take_available(trial, resource, cost) + var total: float = 0.0 + for quantity in taken.values(): + total += quantity + if total != cost: + return false + if automatic: + trial.auto_resources[resource] = maxf(0.0, trial.auto_resources.get(resource, 0.0) - cost) + for id in taken: + var factor: float = 1.0 - taken[id] / before[id] if before.get(id, 0.0) > 0.0 else 0.0 + _scale_batches(trial, id, factor) + if not _bind_new_owners(trial, {entity.id: amount}, true): + return false + _refresh_groups(trial) + _clamp_auto_resources(trial) + for field in ["amount", "binds", "batches", "auto_resources"]: + if trial.has(field): + target[field] = trial[field] + return true + + +func _bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: bool = false) -> bool: + if require_full: + return _bind_full(target, amounts) var requests: Dictionary = {} for owner in amounts: if not entities.has(owner): @@ -449,6 +558,8 @@ func _bind_new_owners(target: Dictionary, amounts: Dictionary) -> void: target.binds[owner][id] = target.binds[owner].get(id, 0.0) + quantity target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity) + return true + # Integrate survival over the period, including partial batch expiry. func _object_years(source: Dictionary, id: String, quantity: float, @@ -619,16 +730,9 @@ func _produce_for_period(previous: Dictionary, target: Dictionary, quantities: D var actual: float = minf(desired, _auto_capacity(target, entity, budget)) if actual <= 0.0: continue - for resource in entity.cost: - var cost: float = roundf(maxf(0.0, entity.cost[resource]) * actual) - budget[resource] = maxf(0.0, budget.get(resource, 0.0) - cost) - var before: Dictionary = _physical_amounts(target) - var taken: Dictionary = _take_available(target, resource, cost) - for consumed_id in taken: - var factor: float = 1.0 - taken[consumed_id] / before[consumed_id] if before.get(consumed_id, 0.0) > 0.0 else 0.0 - _scale_batches(target, consumed_id, factor) - _bind_new_owners(target, {id: actual}) - _round_object_allocations(target) + if not _try_production(target, entity, actual, true): + continue + budget = target.auto_resources created[id] = actual total_created += actual for id in candidates: @@ -685,8 +789,8 @@ func produce(item_id: String, desired_amount: float) -> float: if max_producible <= 0: return 0.0 - apply_costs(entity, max_producible) - apply_binds(item_id, entity, max_producible) + if not _try_production(state, entity, max_producible): + return 0.0 # Add produced entities _process_enitity(item_id, max_producible) @@ -723,7 +827,7 @@ func produce_hs(entity_id: String, value: float) -> float: # Decimal costs are represented as integer fractions; combine their denominators. func _production_step(entity: OwniverseEntity) -> float: var step: float = 1.0 - for value in entity.cost.values(): + for value in entity.cost.values() + entity.bind.values(): var cost: float = maxf(0.0, value) if cost == 0.0: continue @@ -753,6 +857,14 @@ func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1 requirements[resource] = maxf(0.0, entity.cost[resource]) for resource in entity.bind: requirements[resource] = requirements.get(resource, 0.0) + maxf(0.0, entity.bind[resource]) + var has_requirements: bool = false + for quantity in requirements.values(): + if quantity > 0.0: + has_requirements = true + break + # No recipe means unavailable for production, not unlimited free objects. + if not has_requirements: + return 0.0 var available: float = maxf(0.0, desired_amount) for resource in requirements: if requirements[resource] > 0.0: @@ -776,19 +888,20 @@ func apply_costs(entity: OwniverseEntity, amount: float) -> void: for id in taken: var factor: float = 1.0 - taken[id] / quantities[id] if quantities.get(id, 0.0) > 0.0 else 0.0 _scale_batches(state, id, factor) - items_to_update[id] = state.amount[id] _refresh_groups(state) _clamp_auto_resources(state) + refresh_item_amounts() ## Keep concrete bound types, so their ages and transitions remain traceable. -func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> void: - _bind_new_owners(state, {item_id: amount}) - _round_object_allocations(state) +func apply_binds(item_id: String, _entity: OwniverseEntity, amount: float) -> bool: + if not _bind_new_owners(state, {item_id: amount}, true): + return false _refresh_groups(state) _clamp_auto_resources(state) - for id in state.amount: - items_to_update[id] = state.amount[id] + refresh_item_amounts() + + return true ## Computes a normalized distribution of dictionary values (sum equals 1.0) @@ -822,7 +935,7 @@ func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float state.binds[owner][entity.id] *= 1.0 - fraction _round_object_allocations(state) _refresh_groups(state) - items_to_update[entity.id] = state.amount[entity.id] + refresh_item_amounts() return total @@ -877,15 +990,13 @@ func _process_enitity(entity_id: String, amount: float) -> void: state.amount[entity_id] = state.amount.get(entity_id, 0.0) + amount state.stats.total_amount[entity_id] = state.stats.total_amount.get(entity_id, 0.0) + maxf(0.0, amount) - # Update items to display - items_to_update[entity_id] = state.amount[entity_id] # Handle group updates if entity belongs to a group var entity = entities[entity_id] if entity.group_as != "": state.amount[entity.group_as] = state.amount.get(entity.group_as, 0.0) + amount - items_to_update[entity.group_as] = state.amount[entity.group_as] state.stats.total_amount[entity.group_as] = state.stats.total_amount.get(entity.group_as, 0.0) + maxf(0.0, amount) + refresh_item_amounts() diff --git a/world/owniverse/state.gd b/world/owniverse/state.gd deleted file mode 100644 index 63e9e50..0000000 --- a/world/owniverse/state.gd +++ /dev/null @@ -1,176 +0,0 @@ -extends Node - -class_name State - -const FLOAT_THRESHOLD: float = 1.0E14 - -var data: Dictionary = { - 'time': { - 'timestamp': 0.0, - 'year': 0.0, - }, - 'timestamp': 0.0, - 'year': 0.0, - 'speed_in_year': 0, - 'energy': 1E14, -} - -var entities: Dictionary[String, OwniverseEntity] = {} - -var producing_entities: Array[OwniverseEntity] = [] - -# TODO to save !!! -var auto_production_rest: Dictionary = { - 'H': 0.0, - 'S': 0.0, - 'auto': 0.0, -} - -var evolving_entities: Array[OwniverseEntity] = [] - -# TODO to save !!! -var batches: Dictionary[String, Array] = {} -# TODO to save !!! -var gamount: Dictionary[String, float] = {} - - -var distribution: Dictionary[String, float] = {} - - -# new functions -func evolute(prev_state: State, delta: float, current_speed_in_year: float) -> float: - var _new_year: float = _add_year_delta(delta, current_speed_in_year) - if data.year == _new_year: - return data.year - - return _new_year - - -func _add_year_delta(delta: float, current_speed_in_year: float) -> float: - var _new_year:float = floorf(data.year + delta * current_speed_in_year) - #print(data.year, " | ", _new_year, " | ", data) - return _new_year - - - -# end of new functions - - - -func add(entity: OwniverseEntity, year: float, amount: float): - var _batch_array: Array = batches[entity.id] - var _from_year: float = year + entity.lifespan - entity.lifespan_delta - var _to_year: float = year + entity.lifespan + entity.lifespan_delta - if _batch_array.size() != 0 and _batch_array[-1].to_year > year + entity.lifespan: - _batch_array[-1].amount += amount - else: - var _batch := { - 'from_year': _from_year, - 'to_year': _to_year, - 'amount': amount, - } - _batch_array.append(_batch) - - #print('---------') - #for id in batches: - #print(id) - # - -func process(from_year: float, to_year: float) -> void: - # производим - # эволюционируем - # записываем новые - var _to_produce := {} - var _to_evolute := {} - for _entity_id in batches: - _to_produce[_entity_id] = 0 - _to_evolute[_entity_id] = 0 - var _new_batch_array := [] - for _batch: Dictionary in batches[_entity_id]: - # production - _to_produce[_entity_id] += _batch.amount - # evolution - if _batch.to_year < to_year: - _to_produce[_entity_id] += _batch.amount - else: - var _max_from_year: float = max(from_year, _batch.from_year) - var _min_to_year: float = min(to_year, _batch.to_year) - var _part_of_amount: float = _batch.amount \ - * (_min_to_year - _max_from_year) \ - / (_batch.to_year - _batch.from_year) - _to_evolute[_entity_id] += _part_of_amount - var _rest_of_amount: float = _batch.amount - _part_of_amount - - if _rest_of_amount > 0: - _new_batch_array.append({ - 'from_year': _batch._from_year, - 'to_year': _batch._to_year, - 'amount': _rest_of_amount, - }) - - # entity level - batches[_entity_id] = _new_batch_array - - -func produce(from_year: float, to_year: float): - var _time_delta = to_year - from_year - # get entity array for production - for _entity: OwniverseEntity in producing_entities: - var _to_produce: float = 0.0 - for _batch: Dictionary in batches[_entity.id]: - _to_produce += _batch.amount - _to_produce *= _time_delta * _entity.product_per_year_amount - if _entity.auto > 0: - var _auto_production_part: float = _entity.auto * _to_produce - _to_produce -= _auto_production_part - auto_production_rest.auto += _auto_production_part - - auto_production_rest[_entity.product_per_year_type] += _to_produce - print("auto prod rest: ", auto_production_rest) - auto_production_rest["H"] = produce_hs("H", auto_production_rest["H"]) - auto_production_rest["S"] = produce_hs("S", auto_production_rest["S"]) - - -func produce_hs(entity_id: String, value: float) -> float: - var _to_produce: float = floorf(value) - gamount[entity_id] += _to_produce - # TODO double double math - if _to_produce > data["energy"]: - _to_produce = data["energy"] - data["energy"] -= _to_produce - return value - _to_produce - -func produce_auto() -> float: - return 0.0 - -func init_entity(entity: OwniverseEntity) -> void: - entities[entity.id] = entity - gamount[entity.id] = 0.0 - if entity.product_per_year_type != "": - producing_entities.append(entity) - - if entity.lifespan > 0: - evolving_entities.append(entity) - - batches[entity.id] = [] - - -func update_distribution(_distribute_by: Dictionary[String, float]) -> void: - distribution = {} - var _total: float = 0 - for id in _distribute_by: - if _distribute_by[id] != 0: - var _value: float = _distribute_by[id] - _total += _value - distribution[id] = _value - - if _total == 0: return - - for id in distribution: - distribution[id] /= _total - -#func _add_float(a: float, b: Array[float]) -> Array[float]: - #if a < FLOAT_THRESHOLD: - #return [a+b[0], 0] - #else: - #return [a+b[0], 0] diff --git a/world/owniverse/state.gd.uid b/world/owniverse/state.gd.uid deleted file mode 100644 index 4224459..0000000 --- a/world/owniverse/state.gd.uid +++ /dev/null @@ -1 +0,0 @@ -uid://mgnxt6kbf80n