Refactor save management; update validation logic in save_manager.gd and enhance action handling in actions.gd for improved data integrity and user experience

This commit is contained in:
Kirill
2026-09-14 07:47:21 +03:00
parent dea744bc30
commit 85a13eb241
6 changed files with 160 additions and 61 deletions
+4 -4
View File
@@ -146,10 +146,10 @@ func validate_data(data: Variant) -> String:
return "missing total_amount for " + id return "missing total_amount for " + id
if not saved.get("binds") is Dictionary or not saved.get("batches") is Dictionary: if not saved.get("binds") is Dictionary or not saved.get("batches") is Dictionary:
return "state.binds and state.batches must be Dictionaries" return "state.binds and state.batches must be Dictionaries"
for owner in saved.binds: for owner_id in saved.binds:
if not saved.amount.has(owner) or not _number_map(saved.binds[owner], true): if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
return "invalid state.binds." + owner return "invalid state.binds." + owner_id
for id in saved.binds[owner]: for id in saved.binds[owner_id]:
if not saved.amount.has(id): if not saved.amount.has(id):
return "unknown bound resource " + id return "unknown bound resource " + id
for id in saved.batches: for id in saved.batches:
+13 -7
View File
@@ -1,14 +1,17 @@
[runnable_presets]
"Windows Desktop"="Windows Desktop"
[preset.0] [preset.0]
name="Windows Desktop" name="Windows Desktop"
platform="Windows Desktop" platform="Windows Desktop"
runnable=true
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
include_filter="" include_filter=""
exclude_filter="" exclude_filter=""
export_path="../../Documents/Owad/Owniverse.I/Builds/owniverse.0.6.13-129.exe" export_path="../../Documents/Owad/Owniverse.I/Builds/owniverse.0.6.28-144.exe"
patches=PackedStringArray() patches=PackedStringArray()
patch_delta_encoding=false patch_delta_encoding=false
patch_delta_compression_level_zstd=19 patch_delta_compression_level_zstd=19
@@ -74,7 +77,6 @@ Remove-Item -Recurse -Force '{temp_dir}'"
name="Android" name="Android"
platform="Android" platform="Android"
runnable=false
dedicated_server=false dedicated_server=false
custom_features="" custom_features=""
export_filter="all_resources" export_filter="all_resources"
@@ -110,7 +112,7 @@ architectures/armeabi-v7a=false
architectures/arm64-v8a=true architectures/arm64-v8a=true
architectures/x86=false architectures/x86=false
architectures/x86_64=false architectures/x86_64=false
version/code=143 version/code=144
version/name="" version/name=""
package/unique_name="com.KIDGameProduction.Owniverse" package/unique_name="com.KIDGameProduction.Owniverse"
package/name="Owniverse" package/name="Owniverse"
@@ -136,11 +138,12 @@ screen/support_normal=true
screen/support_large=true screen/support_large=true
screen/support_xlarge=true screen/support_xlarge=true
screen/background_color=Color(0, 0, 0, 1) screen/background_color=Color(0, 0, 0, 1)
splash_screen/disable_godot_boot_splash=false
splash_screen/icon=""
splash_screen/branding_image=""
splash_screen/background_color=Color(0, 0, 0, 1)
user_data_backup/allow=false user_data_backup/allow=false
command_line/extra_args="" command_line/extra_args=""
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
permissions/custom_permissions=PackedStringArray() permissions/custom_permissions=PackedStringArray()
permissions/access_checkin_properties=false permissions/access_checkin_properties=false
permissions/access_coarse_location=false permissions/access_coarse_location=false
@@ -295,3 +298,6 @@ permissions/write_sms=false
permissions/write_social_stream=false permissions/write_social_stream=false
permissions/write_sync_settings=false permissions/write_sync_settings=false
permissions/write_user_dictionary=false permissions/write_user_dictionary=false
apk_expansion/enable=false
apk_expansion/SALT=""
apk_expansion/public_key=""
+1 -1
View File
@@ -11,7 +11,7 @@ config_version=5
[application] [application]
config/name="Owniverse" config/name="Owniverse"
config/version="0.6.27" config/version="0.6.28"
run/main_scene="uid://bv384dpmvjv8o" run/main_scene="uid://bv384dpmvjv8o"
config/features=PackedStringArray("4.7", "Mobile") config/features=PackedStringArray("4.7", "Mobile")
boot_splash/show_image=false boot_splash/show_image=false
+10 -1
View File
@@ -5,6 +5,7 @@ const OWNIVERSE_ITEM = preload("uid://d3xjfxwe3w16v")
const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png" const SPEED_TEXTURE_NAME_TEMPLATE = "res://ui/topmenu/time&speed/time%s.png"
const PAGE_COUNT = 5 const PAGE_COUNT = 5
const RESOURCE_PAGE = 1
const PAGE_WIDTH = 1080 const PAGE_WIDTH = 1080
const DRAG_TOLERANCE = 360 const DRAG_TOLERANCE = 360
const SWIPE_DURATION: float = 0.4 const SWIPE_DURATION: float = 0.4
@@ -256,6 +257,11 @@ func _set_active_page_actions() -> void:
#var amount = owniverse.get_available_to_produce(entity) #var amount = owniverse.get_available_to_produce(entity)
var amount = owniverse.calculate_max_producible(entity) var amount = owniverse.calculate_max_producible(entity)
#print(item_id, " : ", amount) #print(item_id, " : ", amount)
if item_id == "SMBH":
var preview: Callable = func(requested: float) -> float:
return owniverse.calculate_max_producible(entity, requested)
page_actions[pa].set_actions(item_id, amount, preview)
else:
page_actions[pa].set_actions(item_id, amount) page_actions[pa].set_actions(item_id, amount)
@@ -329,7 +335,7 @@ func _change_speed(delta: int) -> void:
g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1)) g_time_plus.texture_normal = load(SPEED_TEXTURE_NAME_TEMPLATE % str(speed + 1))
func _on_topmenu_button_pressed(ButtonName) -> void: func _on_topmenu_button_pressed(_ButtonName) -> void:
print("Debug → ") print("Debug → ")
#, owniverse.state) #, owniverse.state)
for id in owniverse.state.batches: for id in owniverse.state.batches:
@@ -359,6 +365,9 @@ func _on_unlock_and_save_timeout() -> void:
func load_data() -> bool: func load_data() -> bool:
var game_data: Dictionary = SaveManager.load_data("owniverse", "qwerty") var game_data: Dictionary = SaveManager.load_data("owniverse", "qwerty")
if game_data.is_empty(): if game_data.is_empty():
if SaveManager.last_error == "":
pending_page = RESOURCE_PAGE
_restore_page.call_deferred()
return false return false
var restored: Dictionary = game_data.state var restored: Dictionary = game_data.state
# Current definitions are initialized first; reject incompatible saves before publishing. # Current definitions are initialized first; reject incompatible saves before publishing.
+10 -4
View File
@@ -23,9 +23,14 @@ var action_values: Dictionary = {
} }
func set_actions(item_id: String, value: float) -> void: func set_actions(item_id: String, value: float, preview: Callable = Callable()) -> void:
current_item_id = item_id current_item_id = item_id
_set_action_values(_get_distribution(value)) var requests: Array = _get_distribution(value)
var displayed: Array = requests.duplicate()
if preview.is_valid():
for i in range(displayed.size()):
displayed[i] = preview.call(requests[i]) if requests[i] > 0.0 else 0.0
_set_action_values(requests, displayed)
func _get_distribution(value: float) -> Array: func _get_distribution(value: float) -> Array:
if value <= 0.0 or not is_finite(value): if value <= 0.0 or not is_finite(value):
@@ -38,10 +43,11 @@ func _get_distribution(value: float) -> Array:
distribution.push_front(pow(10.0, ev)) distribution.push_front(pow(10.0, ev))
return distribution return distribution
func _set_action_values(values: Array) -> void: func _set_action_values(values: Array, displayed: Array) -> void:
for ind in range(0, 5): for ind in range(0, 5):
action_values[ind] = values[ind] action_values[ind] = values[ind]
action_labels[ind].text = Global.format_number(values[ind]) if values[ind] > 0 else "" # Keep the original request for execution; show the exact planned output.
action_labels[ind].text = Global.format_number(displayed[ind]) if displayed[ind] > 0 else ""
func _on_action_button_pressed(button_id: int) -> void: func _on_action_button_pressed(button_id: int) -> void:
+120 -42
View File
@@ -12,13 +12,8 @@ var items_to_update: Dictionary[String, float] = {}
var auto_production: Array = [] var auto_production: Array = []
var locked_entities: Array = [] var locked_entities: Array = []
#var cumulative_amount: Dictionary[String, float] = {}
#var current_amount: Dictionary[String, float] = {}
# ------------------------------------ # ------------------------------------
var state: Dictionary = { var state: Dictionary = {
'version': '0.1.0', 'version': '0.1.0',
'time': { 'time': {
@@ -226,7 +221,7 @@ func _credit(target: Dictionary, id: String, amount: float) -> float:
target.production_remainders[id] = expected - amount target.production_remainders[id] = expected - amount
target.amount[id] = target.amount.get(id, 0.0) + amount target.amount[id] = target.amount.get(id, 0.0) + amount
target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount target.stats.total_amount[id] = target.stats.total_amount.get(id, 0.0) + amount
if entities.has(id) and entities[id].group_as != "": if entities.has(id) and entities[id].group_as != "" and entities[id].group_as != id:
var group: String = entities[id].group_as var group: String = entities[id].group_as
target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount target.stats.total_amount[group] = target.stats.total_amount.get(group, 0.0) + amount
return amount return amount
@@ -239,9 +234,9 @@ func _round_object_allocations(target: Dictionary) -> void:
if not _is_discrete(id): if not _is_discrete(id):
continue continue
var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}] var slots: Array = [ {"owner": "", "value": target.amount.get(id, 0.0)}]
for owner in target.binds: for owner_id in target.binds:
if target.binds[owner].has(id): if target.binds[owner_id].has(id):
slots.append({"owner": owner, "value": target.binds[owner][id]}) slots.append({"owner": owner_id, "value": target.binds[owner_id][id]})
var assigned: float = 0.0 var assigned: float = 0.0
for slot: Dictionary in slots: for slot: Dictionary in slots:
slot.whole = floorf(slot.value) slot.whole = floorf(slot.value)
@@ -426,21 +421,21 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
# Use a frozen binding table so simultaneous transitions cannot cascade by type order. # Use a frozen binding table so simultaneous transitions cannot cascade by type order.
var bindings: Dictionary = target.binds.duplicate(true) var bindings: Dictionary = target.binds.duplicate(true)
var incoming: Dictionary = {} var incoming: Dictionary = {}
for owner in bindings: for owner_id in bindings:
var owner_survival: float = 1.0 - fractions.get(owner, 0.0) var owner_survival: float = 1.0 - fractions.get(owner_id, 0.0)
target.binds[owner] = {} target.binds[owner_id] = {}
for id in bindings[owner]: for id in bindings[owner_id]:
var held: float = bindings[owner][id] var held: float = bindings[owner_id][id]
var released: float = held * (1.0 - owner_survival) var released: float = held * (1.0 - owner_survival)
target.amount[id] = target.amount.get(id, 0.0) + released target.amount[id] = target.amount.get(id, 0.0) + released
var fraction: float = fractions.get(id, 0.0) var fraction: float = fractions.get(id, 0.0)
var remaining: float = held * owner_survival * (1.0 - fraction) var remaining: float = held * owner_survival * (1.0 - fraction)
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + remaining target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + remaining
for successor in outputs.get(id, {}): for successor in outputs.get(id, {}):
var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor]) var transferred: float = held * owner_survival * fraction * maxf(0.0, outputs[id][successor])
if not incoming.has(owner): if not incoming.has(owner_id):
incoming[owner] = {} incoming[owner_id] = {}
incoming[owner][successor] = incoming[owner].get(successor, 0.0) + transferred incoming[owner_id][successor] = incoming[owner_id].get(successor, 0.0) + transferred
for id in fractions: for id in fractions:
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id])) target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) * (1.0 - fractions[id]))
@@ -453,10 +448,10 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
amount = _credit(target, successor, amount) amount = _credit(target, successor, amount)
_append_batch(target, successor, amount, part.from, part.to) _append_batch(target, successor, amount, part.from, part.to)
new_owners[successor] = new_owners.get(successor, 0.0) + amount new_owners[successor] = new_owners.get(successor, 0.0) + amount
for owner in incoming: for owner_id in incoming:
for id in incoming[owner]: for id in incoming[owner_id]:
var transferred: float = minf(incoming[owner][id], target.amount.get(id, 0.0)) var transferred: float = minf(incoming[owner_id][id], target.amount.get(id, 0.0))
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + transferred target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + transferred
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred) target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - transferred)
_bind_new_owners(target, new_owners) _bind_new_owners(target, new_owners)
_round_object_allocations(target) _round_object_allocations(target)
@@ -467,15 +462,15 @@ func _evolve_batch_generation(target: Dictionary, to_year: float) -> bool:
func _bind_full(target: Dictionary, amounts: Dictionary) -> bool: func _bind_full(target: Dictionary, amounts: Dictionary) -> bool:
var trial: Dictionary = target.duplicate(true) var trial: Dictionary = target.duplicate(true)
var requests: Array = [] var requests: Array = []
for owner in amounts: for owner_id in amounts:
if not entities.has(owner): if not entities.has(owner_id):
return false return false
for id in entities[owner].bind: for id in entities[owner_id].bind:
var quantity: float = amounts[owner] * maxf(0.0, entities[owner].bind[id]) var quantity: float = amounts[owner_id] * maxf(0.0, entities[owner_id].bind[id])
if not is_equal_approx(quantity, roundf(quantity)): if not is_equal_approx(quantity, roundf(quantity)):
return false return false
if quantity > 0.0: if quantity > 0.0:
requests.append({"owner": owner, "id": id, "amount": roundf(quantity), "members": _members(id).size()}) requests.append({"owner": owner_id, "id": id, "amount": roundf(quantity), "members": _members(id).size()})
requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool: requests.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
if a.members != b.members: if a.members != b.members:
return a.members < b.members return a.members < b.members
@@ -532,11 +527,11 @@ func _bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: boo
if require_full: if require_full:
return _bind_full(target, amounts) return _bind_full(target, amounts)
var requests: Dictionary = {} var requests: Dictionary = {}
for owner in amounts: for owner_id in amounts:
if not entities.has(owner): if not entities.has(owner_id):
continue continue
for id in entities[owner].bind: for id in entities[owner_id].bind:
var quantity: float = amounts[owner] * maxf(0.0, entities[owner].bind[id]) var quantity: float = amounts[owner_id] * maxf(0.0, entities[owner_id].bind[id])
var weights: Dictionary = {} var weights: Dictionary = {}
for member in _members(id): for member in _members(id):
weights[member] = target.amount.get(member, 0.0) weights[member] = target.amount.get(member, 0.0)
@@ -544,18 +539,18 @@ func _bind_new_owners(target: Dictionary, amounts: Dictionary, require_full: boo
for member in weights: for member in weights:
if not requests.has(member): if not requests.has(member):
requests[member] = {} requests[member] = {}
requests[member][owner] = requests[member].get(owner, 0.0) + quantity * weights[member] requests[member][owner_id] = requests[member].get(owner_id, 0.0) + quantity * weights[member]
for id in requests: for id in requests:
var requested: float = 0.0 var requested: float = 0.0
for quantity in requests[id].values(): for quantity in requests[id].values():
requested += quantity requested += quantity
var available: float = _available(target, id) var available: float = _available(target, id)
var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0 var factor: float = minf(1.0, available / requested) if requested > 0.0 else 0.0
for owner in requests[id]: for owner_id in requests[id]:
var quantity: float = requests[id][owner] * factor var quantity: float = requests[id][owner_id] * factor
if not target.binds.has(owner): if not target.binds.has(owner_id):
target.binds[owner] = {} target.binds[owner_id] = {}
target.binds[owner][id] = target.binds[owner].get(id, 0.0) + quantity target.binds[owner_id][id] = target.binds[owner_id].get(id, 0.0) + quantity
target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity) target.amount[id] = maxf(0.0, target.amount.get(id, 0.0) - quantity)
return true return true
@@ -774,7 +769,90 @@ func _auto_capacity(source: Dictionary, entity: OwniverseEntity, budget: Diction
## @param item_id - entity id to produce ## @param item_id - entity id to produce
## @param desired_amount - desired quantity to produce ## @param desired_amount - desired quantity to produce
## @return actual amount produced based on available resources ## @return actual amount produced based on available resources
func _smbh_sources() -> Dictionary:
var masses: Dictionary = {}
for id in entities:
var entity: OwniverseEntity = entities[id]
if entity.count_as == "BH" and entity.mass > 0.0:
masses[id] = floorf(maxf(0.0, state.amount.get(id, 0.0))) * entity.mass
return masses
func _smbh_available() -> float:
var mass: float = 0.0
for value in _smbh_sources().values():
mass += value
return floorf(mass)
func _plan_smbh(desired_amount: float) -> Dictionary:
var empty: Dictionary = {"amount": 0.0, "taken": {}}
if desired_amount <= 0.0 or not is_finite(desired_amount):
return empty
var total_mass: float = _smbh_available()
if total_mass <= 0.0:
return empty
var requested: float = minf(floorf(desired_amount), total_mass)
if requested <= 0.0:
return empty
var masses: Dictionary = _smbh_sources()
var distribution: Dictionary = _normalize_distribution(masses)
var taken: Dictionary = {}
var order: Array = []
var consumed_mass: float = 0.0
for id in distribution:
var expected: float = requested * distribution[id] / entities[id].mass
taken[id] = minf(floorf(expected), floorf(state.amount[id]))
consumed_mass += taken[id] * entities[id].mass
order.append({"id": id, "remainder": expected - taken[id]})
order.sort_custom(func(a: Dictionary, b: Dictionary) -> bool:
return a.remainder > b.remainder if a.remainder != b.remainder else a.id < b.id)
# Consume whole BH only; all excess mass becomes SMBH, never disappears.
for candidate: Dictionary in order:
if consumed_mass >= requested:
break
var id: String = candidate.id
if taken[id] < floorf(state.amount[id]):
taken[id] += 1.0
consumed_mass += entities[id].mass
if consumed_mass <= 0.0 or consumed_mass != floorf(consumed_mass):
return empty
return {"amount": consumed_mass, "taken": taken}
func _produce_smbh(desired_amount: float) -> float:
var plan: Dictionary = _plan_smbh(desired_amount)
var consumed_mass: float = plan.amount
var taken: Dictionary = plan.taken
if consumed_mass <= 0.0:
return 0.0
var next: Dictionary = state.duplicate(true)
var physical: Dictionary = _physical_amounts(state)
for id in taken:
if taken[id] <= 0.0:
continue
next.amount[id] -= taken[id]
var fraction: float = taken[id] / physical[id]
_scale_batches(next, id, 1.0 - fraction)
# Release the consumed owners' share of any resources they held.
for resource in next.binds.get(id, {}):
var released: float = next.binds[id][resource] * fraction
next.binds[id][resource] -= released
next.amount[resource] = next.amount.get(resource, 0.0) + released
_credit(next, "SMBH", consumed_mass)
_append_batch(next, "SMBH", consumed_mass, state.time.datetime, state.time.datetime)
_round_object_allocations(next)
_refresh_groups(next)
_clamp_auto_resources(next)
state = next
new_state = next
refresh_item_amounts()
return consumed_mass
func produce(item_id: String, desired_amount: float) -> float: func produce(item_id: String, desired_amount: float) -> float:
if item_id == "SMBH":
return _produce_smbh(desired_amount)
# Use special production for S and H # Use special production for S and H
if item_id == "S" or item_id == "H": if item_id == "S" or item_id == "H":
return produce_hs(item_id, desired_amount) return produce_hs(item_id, desired_amount)
@@ -852,6 +930,8 @@ func _whole_gcd(a: float, b: float) -> float:
func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float: func calculate_max_producible(entity: OwniverseEntity, desired_amount: float = 1.0E150) -> float:
if entity.id == "SMBH":
return _plan_smbh(desired_amount).amount
var requirements: Dictionary = {} var requirements: Dictionary = {}
for resource in entity.cost: for resource in entity.cost:
requirements[resource] = maxf(0.0, entity.cost[resource]) requirements[resource] = maxf(0.0, entity.cost[resource])
@@ -930,9 +1010,9 @@ func consume_from_batches(entity: OwniverseEntity, current_year: float) -> float
total += part.amount total += part.amount
var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0 var fraction: float = clampf(total / physical[entity.id], 0.0, 1.0) if physical.get(entity.id, 0.0) > 0.0 else 0.0
state.amount[entity.id] = state.amount.get(entity.id, 0.0) * (1.0 - fraction) state.amount[entity.id] = state.amount.get(entity.id, 0.0) * (1.0 - fraction)
for owner in state.binds: for owner_id in state.binds:
if state.binds[owner].has(entity.id): if state.binds[owner_id].has(entity.id):
state.binds[owner][entity.id] *= 1.0 - fraction state.binds[owner_id][entity.id] *= 1.0 - fraction
_round_object_allocations(state) _round_object_allocations(state)
_refresh_groups(state) _refresh_groups(state)
refresh_item_amounts() refresh_item_amounts()
@@ -999,8 +1079,6 @@ func _process_enitity(entity_id: String, amount: float) -> void:
refresh_item_amounts() refresh_item_amounts()
#endregion #endregion
#region Time #region Time