Add exact arithmetic implementation and tests
- Introduced exact arithmetic with support for large integers and fractions in `exact.gd`, `big_integer.gd`, and `exact_value.gd`. - Implemented a comprehensive test suite in `test_exact_math.gd` to validate arithmetic operations against expected results. - Added a README in `math` directory to document the exact arithmetic system and its usage. - Ensured that all arithmetic operations maintain precision without floating-point errors, using a custom representation for large numbers. - Included methods for encoding and decoding exact values for serialization.
This commit is contained in:
+6
-25
@@ -1,4 +1,5 @@
|
||||
extends Node
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
const SPEED_LIMIT: int = 6
|
||||
|
||||
@@ -57,37 +58,15 @@ func get_description(item_id: String, items: Array = []) -> String:
|
||||
return render_template(row, variables)
|
||||
|
||||
|
||||
func format_number(value: float, digits: int = 0) -> String:
|
||||
if value == 0: return ""
|
||||
# Keep ordinary quantities readable, grouping thousands with spaces.
|
||||
# Scientific notation is reserved for values whose fixed form would be
|
||||
# unwieldy in the UI.
|
||||
if absf(value) < 1_000_000_000.0:
|
||||
var fixed: String = "%.*f" % [digits, value]
|
||||
var parts: PackedStringArray = fixed.split(".", true)
|
||||
var integer: String = parts[0]
|
||||
var sign_prefix: String = ""
|
||||
if integer.begins_with("-") or integer.begins_with("+"):
|
||||
sign_prefix = integer.left(1)
|
||||
integer = integer.substr(1)
|
||||
var grouped: String = ""
|
||||
while integer.length() > 3:
|
||||
grouped = " " + integer.right(3) + grouped
|
||||
integer = integer.left(integer.length() - 3)
|
||||
grouped = sign_prefix + integer + grouped
|
||||
return grouped + ("." + parts[1] if parts.size() > 1 else "")
|
||||
|
||||
var exp_value = int(floor(log(abs(value)) / log(10.0)))
|
||||
var mantissa = value / pow(10.0, exp_value)
|
||||
|
||||
return "%.*f" % [digits, mantissa] + "E%+d" % exp_value
|
||||
func format_number(value, digits: int = 0) -> String:
|
||||
return E.grouped(value, digits)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
CivilizationRules.clear()
|
||||
for row: Dictionary in _read_from_csv("res://data/civilization_rules.txt").values():
|
||||
if not CivilizationRules.has(row.stage):
|
||||
CivilizationRules[row.stage] = {"home": row.home, "span": float(row.span), "stars": float(row.stars), "ethics": {},
|
||||
CivilizationRules[row.stage] = {"home": row.home, "span": E.of(row.span), "stars": E.of(row.stars), "ethics": {},
|
||||
"colonizes": row.colonizes == "1", "fights": row.fights == "1", "diplomacy": row.diplomacy == "1", "worships": row.worships == "1"}
|
||||
CivilizationRules[row.stage].ethics[row.ethic] = row
|
||||
var stages: Array = CivilizationRules.keys()
|
||||
@@ -107,6 +86,8 @@ func _ready() -> void:
|
||||
Templates["en"] = _read_from_csv("res://data/templates.en.txt")
|
||||
Templates["ru"] = _read_from_csv("res://data/templates.ru.txt")
|
||||
|
||||
#Templates = OwniverseTemplates.get_data()
|
||||
|
||||
|
||||
func _read_from_csv(file_path: String) -> Dictionary:
|
||||
if not FileAccess.file_exists(file_path):
|
||||
|
||||
+131
-132
@@ -1,18 +1,19 @@
|
||||
# SaveManager.gd (добавить в AutoLoad)
|
||||
extends Node
|
||||
const E = preload("res://world/owniverse/math/exact.gd")
|
||||
|
||||
# ~/Library/Application Support/Godot/app_userdata/Owniverse Adventure/savegame.cfg
|
||||
const SETTING_SECTION := "Settings"
|
||||
const SETTING_SECTION = "Settings"
|
||||
const SAVE_PATH = "user://settings.cfg"
|
||||
|
||||
const FORMAT_VERSION: int = 2
|
||||
const FORMAT_VERSION : int = 3
|
||||
const SimulationTime = preload("res://world/owniverse/simulation_time.gd")
|
||||
const WorldParameters = preload("res://world/owniverse/world_parameters.gd")
|
||||
const STATE_VERSION: String = SimulationTime.STATE_VERSION
|
||||
const STATE_VERSION : String = SimulationTime.STATE_VERSION
|
||||
var last_error: String = ""
|
||||
var blocked_saves: Dictionary = {}
|
||||
var save_directory: String = "user://"
|
||||
signal save_completed(filename: String, success: bool, error: String)
|
||||
signal save_completed ( filename : String , success : bool , error : String )
|
||||
signal saves_drained
|
||||
|
||||
# Queue ownership stays on the main thread. The worker only sees its snapshot.
|
||||
@@ -21,30 +22,28 @@ var _active_filename: String = ""
|
||||
var _pending_saves: Dictionary = {}
|
||||
|
||||
|
||||
func _process(_delta: float) -> void:
|
||||
if _save_thread != null and not _save_thread.is_alive():
|
||||
func _process(_delta: Variant) -> void:
|
||||
if (E.ne(_save_thread, null) and (not _save_thread.is_alive())):
|
||||
_finish_save()
|
||||
|
||||
|
||||
func is_saving() -> bool:
|
||||
return _save_thread != null or not _pending_saves.is_empty()
|
||||
return (E.ne(_save_thread, null) or (not _pending_saves.is_empty()))
|
||||
|
||||
|
||||
func _snapshot(filename: String, password: String, data: Dictionary) -> Dictionary:
|
||||
var payload: Dictionary = data.duplicate(true)
|
||||
var payload: Dictionary = E.encode(data)
|
||||
payload.format_version = FORMAT_VERSION
|
||||
payload.app_version = str(ProjectSettings.get_setting("application/config/version", ""))
|
||||
return {"filename": filename, "path": save_directory.path_join(filename + ".adventure"), "password": password, "payload": payload,
|
||||
"plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT,
|
||||
"stages": Global.CivilizationRules.keys()}
|
||||
return {"filename": filename, "path": save_directory.path_join(E.add(filename, ".adventure")), "password": password, "payload": payload, "plaintext": Global.save_plaintext_copy, "speed_limit": Global.SPEED_LIMIT, "stages": Global.CivilizationRules.keys()}
|
||||
|
||||
|
||||
## True means queued, not written. Completion is reported by save_completed.
|
||||
func request_save(filename: String, password: String, data: Dictionary) -> bool:
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||
_pending_saves[filename] = _snapshot(filename, password, data)
|
||||
if _save_thread == null:
|
||||
if E.eq(_save_thread, null):
|
||||
_start_save()
|
||||
return true
|
||||
|
||||
@@ -57,12 +56,12 @@ func _start_save() -> void:
|
||||
_pending_saves.erase(_active_filename)
|
||||
_save_thread = Thread.new()
|
||||
var error: Error = _save_thread.start(_write_snapshot.bind(job))
|
||||
if error != OK:
|
||||
if E.ne(error, OK):
|
||||
_save_thread = null
|
||||
_fail("Cannot start save worker: " + error_string(error))
|
||||
_fail(E.add("Cannot start save worker: ", error_string(error)))
|
||||
save_completed.emit(_active_filename, false, last_error)
|
||||
_start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
if (not is_saving()): saves_drained.emit()
|
||||
|
||||
|
||||
func _finish_save() -> void:
|
||||
@@ -70,17 +69,17 @@ func _finish_save() -> void:
|
||||
var filename: String = _active_filename
|
||||
_save_thread = null
|
||||
last_error = error
|
||||
if error != "": push_error(error)
|
||||
save_completed.emit(filename, error == "", error)
|
||||
if _save_thread == null: _start_save()
|
||||
if not is_saving(): saves_drained.emit()
|
||||
if E.ne(error, ""): push_error(error)
|
||||
save_completed.emit(filename, E.eq(error, ""), error)
|
||||
if E.eq(_save_thread, null): _start_save()
|
||||
if (not is_saving()): saves_drained.emit()
|
||||
|
||||
|
||||
## Used for application suspension and teardown, when frames may stop arriving.
|
||||
func flush_saves() -> void:
|
||||
while is_saving():
|
||||
if _save_thread == null: _start_save()
|
||||
if _save_thread != null: _finish_save()
|
||||
if E.eq(_save_thread, null): _start_save()
|
||||
if E.ne(_save_thread, null): _finish_save()
|
||||
|
||||
|
||||
func _exit_tree() -> void:
|
||||
@@ -99,44 +98,44 @@ func save_data(filename: String, password: String, data: Dictionary) -> bool:
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
if blocked_saves.has(filename):
|
||||
return _fail("Save blocked to preserve an unreadable save: " + filename)
|
||||
return _fail(E.add("Save blocked to preserve an unreadable save: ", filename))
|
||||
var error: String = _write_snapshot(_snapshot(filename, password, data))
|
||||
return _fail(error) if error != "" else true
|
||||
return (_fail(error) if E.ne(error, "") else true)
|
||||
|
||||
|
||||
static func _write_snapshot(job: Dictionary) -> String:
|
||||
var payload: Dictionary = job.payload
|
||||
var problem: String = _validate_payload(payload, job.speed_limit, job.stages)
|
||||
if problem != "":
|
||||
return "Invalid save: " + problem
|
||||
if E.ne(problem, ""):
|
||||
return E.add("Invalid save: ", problem)
|
||||
var path: String = job.path
|
||||
var temporary: String = path + ".tmp"
|
||||
var file := FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||
if file == null:
|
||||
return "Cannot write encrypted save: " + temporary
|
||||
var temporary: String = E.add(path, ".tmp")
|
||||
var file = FileAccess.open_encrypted_with_pass(temporary, FileAccess.WRITE, job.password)
|
||||
if E.eq(file, null):
|
||||
return E.add("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, job.password)
|
||||
if verification == null:
|
||||
return "Cannot verify encrypted save: " + temporary
|
||||
var verification = FileAccess.open_encrypted_with_pass(temporary, FileAccess.READ, job.password)
|
||||
if E.eq(verification, null):
|
||||
return E.add("Cannot verify encrypted save: ", temporary)
|
||||
var restored: Variant = verification.get_var(false)
|
||||
verification.close()
|
||||
if not restored is Dictionary or restored != payload:
|
||||
return "Encrypted save verification failed: " + temporary
|
||||
if DirAccess.rename_absolute(temporary, path) != OK:
|
||||
return "Cannot replace encrypted save: " + path
|
||||
if ((not (restored is Dictionary)) or E.ne(restored, payload)):
|
||||
return E.add("Encrypted save verification failed: ", temporary)
|
||||
if E.ne(DirAccess.rename_absolute(temporary, path), OK):
|
||||
return E.add("Cannot replace encrypted save: ", path)
|
||||
if job.plaintext:
|
||||
var plain_path: String = path + ".json"
|
||||
file = FileAccess.open(plain_path + ".tmp", FileAccess.WRITE)
|
||||
if file == null:
|
||||
return "Encrypted save written, but cannot write diagnostic copy: " + plain_path
|
||||
file.store_string(JSON.stringify(payload, " ", true, true) + "\n")
|
||||
var plain_path: String = E.add(path, ".json")
|
||||
file = FileAccess.open(E.add(plain_path, ".tmp"), FileAccess.WRITE)
|
||||
if E.eq(file, null):
|
||||
return E.add("Encrypted save written, but cannot write diagnostic copy: ", plain_path)
|
||||
file.store_string(E.add(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 "Encrypted save written, but diagnostic copy failed: " + plain_path
|
||||
if (E.ne(write_error, OK) or E.ne(DirAccess.rename_absolute(E.add(plain_path, ".tmp"), plain_path), OK)):
|
||||
return E.add("Encrypted save written, but diagnostic copy failed: ", plain_path)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -144,68 +143,66 @@ func load_data(filename: String, password: String) -> Dictionary:
|
||||
flush_saves()
|
||||
last_error = ""
|
||||
# Plaintext is diagnostic only: never use it as a fallback.
|
||||
var path: String = save_directory.path_join(filename + ".adventure")
|
||||
if not FileAccess.file_exists(path):
|
||||
var path: String = save_directory.path_join(E.add(filename, ".adventure"))
|
||||
if (not FileAccess.file_exists(path)):
|
||||
return {}
|
||||
var file := FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||
if file == null:
|
||||
var file = FileAccess.open_encrypted_with_pass(path, FileAccess.READ, password)
|
||||
if E.eq(file, null):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot decrypt save: " + path)
|
||||
_fail(E.add("Cannot decrypt save: ", path))
|
||||
return {}
|
||||
var data: Variant = file.get_var(false)
|
||||
file.close()
|
||||
# Unsupported formats are archived, never converted or overwritten.
|
||||
# Returning no data lets the stage start a new, saveable session.
|
||||
if data is Dictionary and data.get("format_version") != FORMAT_VERSION:
|
||||
var archive: String = path + ".incompatible.bak"
|
||||
if ((data is Dictionary) and E.ne(data.get("format_version"), FORMAT_VERSION)):
|
||||
var archive: String = E.add(path, ".incompatible.bak")
|
||||
var index: int = 0
|
||||
while FileAccess.file_exists(archive):
|
||||
index += 1
|
||||
archive = path + ".incompatible.bak." + str(index)
|
||||
index = E.add(index, 1)
|
||||
archive = E.add(E.add(path, ".incompatible.bak."), str(index))
|
||||
var error: Error = DirAccess.rename_absolute(path, archive)
|
||||
if error != OK:
|
||||
if E.ne(error, OK):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Cannot archive incompatible save: " + error_string(error))
|
||||
_fail(E.add("Cannot archive incompatible save: ", error_string(error)))
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
push_warning("Incompatible save preserved at " + archive + "; starting a new game.")
|
||||
push_warning(E.add(E.add("Incompatible save preserved at ", archive), "; starting a new game."))
|
||||
return {}
|
||||
var problem: String = validate_data(data)
|
||||
if problem != "":
|
||||
if E.ne(problem, ""):
|
||||
blocked_saves[filename] = true
|
||||
_fail("Invalid save " + path + ": " + problem)
|
||||
_fail(E.add(E.add(E.add("Invalid save ", path), ": "), problem))
|
||||
return {}
|
||||
blocked_saves.erase(filename)
|
||||
return data
|
||||
return E.decode(data)
|
||||
|
||||
|
||||
static 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)
|
||||
return E.numeric(value) and E.finite(value) and (not whole or E.eq(value, E.floor_value(value))) and (not nonnegative or E.ge(value, 0))
|
||||
|
||||
|
||||
static func _number_map(value: Variant, whole: bool = false, nonnegative: bool = true) -> bool:
|
||||
if not value is Dictionary:
|
||||
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):
|
||||
if ((not (((key is String) or (key is StringName)))) or (not _number(value[key], whole, nonnegative))):
|
||||
return false
|
||||
return true
|
||||
|
||||
|
||||
static func _plain_tree(value: Variant) -> bool:
|
||||
if value is Dictionary:
|
||||
if (value is Dictionary):
|
||||
for key in value:
|
||||
if not (key is String or key is StringName) or not _plain_tree(value[key]):
|
||||
if ((not (((key is String) or (key is StringName)))) or (not _plain_tree(value[key]))):
|
||||
return false
|
||||
return true
|
||||
if value is Array:
|
||||
if (value is Array):
|
||||
for item in value:
|
||||
if not _plain_tree(item):
|
||||
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 ((((E.eq(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.
|
||||
@@ -214,104 +211,106 @@ func validate_data(data: Variant) -> String:
|
||||
|
||||
|
||||
static func _validate_payload(data: Variant, speed_limit: int, stages: Array) -> String:
|
||||
if not data is Dictionary or not _plain_tree(data):
|
||||
if not E.valid_encoded(data): return "invalid exact number encoding"
|
||||
data = E.decode(data)
|
||||
if ((not (data is Dictionary)) or (not _plain_tree(data))):
|
||||
return "save must contain plain finite values"
|
||||
if data.get("format_version") != FORMAT_VERSION or not data.get("app_version") is String:
|
||||
if (E.ne(data.get("format_version"), FORMAT_VERSION) or (not (data.get("app_version") is String))):
|
||||
return "unsupported save format"
|
||||
if not data.get("ui") is Dictionary or not data.ui.get("current_page") is int or data.ui.current_page < 0:
|
||||
if (((not (data.get("ui") is Dictionary)) or (not (data.ui.get("current_page") is int))) or E.lt(data.ui.current_page, 0)):
|
||||
return "invalid ui.current_page"
|
||||
if not data.get("state") is Dictionary:
|
||||
if (not (data.get("state") is Dictionary)):
|
||||
return "state must be a Dictionary"
|
||||
var saved: Dictionary = data.state
|
||||
var required := [
|
||||
"version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins",
|
||||
"binds", "amount", "distribution", "auto_resources", "auto_star_deficits",
|
||||
"production_remainders", "resource_remainders", "evolution_remainders", "life_remainders",
|
||||
"planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders",
|
||||
"civilization_stats", "civilization_homes", "ethics", "stats",
|
||||
]
|
||||
var required = ["version", "time", "speed", "speed_in_year", "energy", "batches", "batch_origins", "binds", "amount", "distribution", "auto_resources", "auto_star_deficits", "production_remainders", "resource_remainders", "evolution_remainders", "life_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "civilization_remainders", "civilization_stats", "civilization_homes", "ethics", "stats"]
|
||||
# Optional additions to the current format. Saves predating indicators keep
|
||||
# their quantities and receive neutral defaults from Owniverse on load.
|
||||
for field in saved:
|
||||
if field not in required and field not in ["world_parameters", "trends"]:
|
||||
if (not E.contains(required, field) and not E.contains(["world_parameters", "trends"], field)):
|
||||
return "unsupported state structure"
|
||||
if saved.has("world_parameters"):
|
||||
if not _number_map(saved.world_parameters) or saved.world_parameters.size() != WorldParameters.defaults().size():
|
||||
if ((not _number_map(saved.world_parameters)) or E.ne(saved.world_parameters.size(), WorldParameters.defaults().size())):
|
||||
return "invalid state.world_parameters"
|
||||
for key in WorldParameters.defaults():
|
||||
if not saved.world_parameters.has(key) or saved.world_parameters[key] < 1.0:
|
||||
return "invalid world parameter " + key
|
||||
if key in ["Bur", "Exp", "Agg"] and saved.world_parameters[key] > 19.0:
|
||||
return "invalid world scale " + key
|
||||
if ((not saved.world_parameters.has(key)) or E.lt(saved.world_parameters[key], 1)):
|
||||
return E.add("invalid world parameter ", key)
|
||||
if (E.contains(["Bur", "Exp", "Agg"], key) and E.gt(saved.world_parameters[key], 19)):
|
||||
return E.add("invalid world scale ", key)
|
||||
if saved.has("trends"):
|
||||
if not _number_map(saved.trends) or saved.trends.size() != WorldParameters.TRENDS.size():
|
||||
if ((not _number_map(saved.trends)) or E.ne(saved.trends.size(), WorldParameters.TRENDS.size())):
|
||||
return "invalid state.trends"
|
||||
for key in WorldParameters.TRENDS:
|
||||
if not saved.trends.has(key) or saved.trends[key] >= 1.0:
|
||||
return "invalid trend " + key
|
||||
if ((not saved.trends.has(key)) or E.ge(saved.trends[key], 1)):
|
||||
return E.add("invalid trend ", key)
|
||||
for field in required:
|
||||
if not saved.has(field):
|
||||
return "missing state." + field
|
||||
if saved.version != STATE_VERSION:
|
||||
if (not saved.has(field)):
|
||||
return E.add("missing state.", field)
|
||||
if E.ne(saved.version, STATE_VERSION):
|
||||
return "unsupported state.version"
|
||||
if not saved.time is Dictionary or not _number(saved.time.get("datetime")) or not _number(saved.time.get("year")):
|
||||
if (((not (saved.time is Dictionary)) or (not _number(saved.time.get("datetime")))) or (not _number(saved.time.get("year")))):
|
||||
return "invalid state.time"
|
||||
if not is_finite(float(saved.time.datetime) * 10.0) or saved.time.year != SimulationTime.processed_year(saved.time.datetime):
|
||||
if ((not E.finite(E.multiply(E.of(saved.time.datetime), 10))) or E.ne(saved.time.year, SimulationTime.processed_year(saved.time.datetime))):
|
||||
return "invalid processed time"
|
||||
if not _number(saved.speed, true) or saved.speed < 1 or saved.speed > speed_limit:
|
||||
if saved.time.has("ticks"):
|
||||
if not _number(saved.time.ticks, true) or not E.eq(saved.time.ticks, E.multiply(saved.time.datetime, SimulationTime.TICKS_PER_YEAR)):
|
||||
return "invalid time ticks"
|
||||
if saved.time.has("tick_remainder"):
|
||||
if not E.finite(saved.time.tick_remainder) or E.ge(E.absolute(saved.time.tick_remainder), 1):
|
||||
return "invalid fractional tick"
|
||||
if (((not _number(saved.speed, true)) or E.lt(saved.speed, 1)) or E.gt(saved.speed, speed_limit)):
|
||||
return "invalid state.speed"
|
||||
if not _number(saved.speed_in_year) or saved.speed_in_year != pow(10.0, saved.speed - 1):
|
||||
if ((not _number(saved.speed_in_year)) or E.ne(saved.speed_in_year, E.power(10, E.subtract(saved.speed, 1)))):
|
||||
return "invalid state.speed_in_year"
|
||||
if not _number(saved.energy, true) or not _number_map(saved.amount, true):
|
||||
if ((not _number(saved.energy, true)) or (not _number_map(saved.amount, true))):
|
||||
return "invalid state resources"
|
||||
if not saved.stats is Dictionary or not _number_map(saved.stats.get("total_amount"), true):
|
||||
if ((not (saved.stats is Dictionary)) or (not _number_map(saved.stats.get("total_amount"), true))):
|
||||
return "invalid state.stats.total_amount"
|
||||
if saved.amount.keys() != saved.stats.total_amount.keys():
|
||||
if E.ne(saved.amount.keys(), saved.stats.total_amount.keys()):
|
||||
return "amount and total_amount keys differ"
|
||||
if not saved.binds is Dictionary or not saved.batches is Dictionary:
|
||||
if ((not (saved.binds is Dictionary)) or (not (saved.batches is Dictionary))):
|
||||
return "state.binds and state.batches must be Dictionaries"
|
||||
for owner_id in saved.binds:
|
||||
if not saved.amount.has(owner_id) or not _number_map(saved.binds[owner_id], true):
|
||||
return "invalid state.binds." + owner_id
|
||||
if ((not saved.amount.has(owner_id)) or (not _number_map(saved.binds[owner_id], true))):
|
||||
return E.add("invalid state.binds.", owner_id)
|
||||
for id in saved.binds[owner_id]:
|
||||
if not saved.amount.has(id):
|
||||
return "unknown bound resource " + id
|
||||
if (not saved.amount.has(id)):
|
||||
return E.add("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
|
||||
if ((not saved.amount.has(id)) or (not (saved.batches[id] is Array))):
|
||||
return E.add("invalid state.batches.", id)
|
||||
for batch in saved.batches[id]:
|
||||
if not batch is Dictionary or 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") != batch.has("created_to"):
|
||||
return "incomplete batch creation interval in " + id
|
||||
if batch.has("created_from") and (not _number(batch.created_from) or not _number(batch.created_to) or batch.created_to <= batch.created_from or batch.start_year < batch.created_from):
|
||||
return "invalid batch creation interval in " + id
|
||||
if batch.has("processed_until") and (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
|
||||
if ((((not (batch is Dictionary)) or (not _number(batch.get("amount"), true))) or (not _number(batch.get("start_year")))) or (not _number(batch.get("end_year")))):
|
||||
return E.add("invalid batch in ", id)
|
||||
if E.lt(batch.end_year, batch.start_year):
|
||||
return E.add("reversed batch interval in ", id)
|
||||
if E.ne(batch.has("created_from"), batch.has("created_to")):
|
||||
return E.add("incomplete batch creation interval in ", id)
|
||||
if (batch.has("created_from") and (((((not _number(batch.created_from)) or (not _number(batch.created_to))) or E.le(batch.created_to, batch.created_from)) or E.lt(batch.start_year, batch.created_from)))):
|
||||
return E.add("invalid batch creation interval in ", id)
|
||||
if (batch.has("processed_until") and ((((not _number(batch.processed_until)) or E.lt(batch.processed_until, batch.start_year)) or E.gt(batch.processed_until, batch.end_year)))):
|
||||
return E.add("invalid batch.processed_until in ", id)
|
||||
if (batch.has("withdraw_remainder") and (((not _number(batch.withdraw_remainder)) or E.gt(batch.withdraw_remainder, batch.amount)))):
|
||||
return E.add("invalid batch.withdraw_remainder in ", id)
|
||||
for field in ["batch_origins", "distribution", "production_remainders", "evolution_remainders", "planet_remainders", "blast_remainders", "destruction_remainders", "life_remainders", "civilization_remainders", "civilization_stats"]:
|
||||
if not _number_map(saved[field], false, false):
|
||||
return "invalid state." + field
|
||||
if not _number_map(saved.auto_resources, true) or not _number_map(saved.auto_star_deficits, false, false):
|
||||
if (not _number_map(saved[field], false, false)):
|
||||
return E.add("invalid state.", field)
|
||||
if ((not _number_map(saved.auto_resources, true)) or (not _number_map(saved.auto_star_deficits, false, false))):
|
||||
return "invalid automatic production state"
|
||||
if not saved.resource_remainders is Dictionary or saved.resource_remainders.keys().size() != 2 or not saved.resource_remainders.has("basic") or not saved.resource_remainders.has("automatic"):
|
||||
if ((((not (saved.resource_remainders is Dictionary)) or E.ne(saved.resource_remainders.keys().size(), 2)) or (not saved.resource_remainders.has("basic"))) or (not saved.resource_remainders.has("automatic"))):
|
||||
return "invalid state.resource_remainders"
|
||||
for channel in ["basic", "automatic"]:
|
||||
if not _number_map(saved.resource_remainders[channel], false, false):
|
||||
return "invalid state.resource_remainders." + channel
|
||||
if not saved.ethics is Dictionary or not saved.civilization_homes is Dictionary:
|
||||
if (not _number_map(saved.resource_remainders[channel], false, false)):
|
||||
return E.add("invalid state.resource_remainders.", channel)
|
||||
if ((not (saved.ethics is Dictionary)) or (not (saved.civilization_homes is Dictionary))):
|
||||
return "invalid civilization state"
|
||||
for id in saved.ethics:
|
||||
if id not in stages or not _number_map(saved.ethics[id], true):
|
||||
return "invalid state.ethics." + str(id)
|
||||
if (not E.contains(stages, id) or (not _number_map(saved.ethics[id], true))):
|
||||
return E.add("invalid state.ethics.", str(id))
|
||||
for ethic in saved.ethics[id]:
|
||||
if ethic not in ["m", "l", "w", "d", "c", "b"]:
|
||||
return "invalid ethic " + str(ethic)
|
||||
if not E.contains(["m", "l", "w", "d", "c", "b"], ethic):
|
||||
return E.add("invalid ethic ", str(ethic))
|
||||
for id in saved.civilization_homes:
|
||||
if not _number_map(saved.civilization_homes[id], true):
|
||||
if (not _number_map(saved.civilization_homes[id], true)):
|
||||
return "invalid civilization homes"
|
||||
return ""
|
||||
|
||||
@@ -322,7 +321,7 @@ static func _validate_payload(data: Variant, speed_limit: int, stages: Array) ->
|
||||
#region Settings
|
||||
func save_settings():
|
||||
var config = ConfigFile.new()
|
||||
|
||||
|
||||
for item in Global.Settings:
|
||||
config.set_value(SETTING_SECTION, item, Global.Settings[item])
|
||||
config.save(SAVE_PATH)
|
||||
@@ -330,7 +329,7 @@ func save_settings():
|
||||
func load_settings():
|
||||
var config = ConfigFile.new()
|
||||
var err = config.load(SAVE_PATH)
|
||||
if err != OK:
|
||||
if E.ne(err, OK):
|
||||
return false
|
||||
for item in Global.Settings:
|
||||
Global.Settings[item] = config.get_value(SETTING_SECTION, item, Global.Settings[item])
|
||||
|
||||
Reference in New Issue
Block a user