Preserve production code, regression fixtures, audio assets and provenance, test infrastructure, and the original-completion plan and progress ledger. Final game and Windows acceptance remain pending; excluded local artifacts and native experiments are not part of this snapshot.
267 lines
7.2 KiB
GDScript
267 lines
7.2 KiB
GDScript
class_name StateTransaction
|
|
extends RefCounted
|
|
|
|
enum Status {
|
|
OPEN,
|
|
COMMITTED,
|
|
ROLLED_BACK,
|
|
FAILED,
|
|
}
|
|
|
|
class StagedChange:
|
|
extends RefCounted
|
|
|
|
var label := ""
|
|
var validate_action := Callable()
|
|
var apply_action := Callable()
|
|
var rollback_action := Callable()
|
|
|
|
func _init(
|
|
p_label: String,
|
|
p_validate_action: Callable,
|
|
p_apply_action: Callable,
|
|
p_rollback_action: Callable
|
|
) -> void:
|
|
label = p_label
|
|
validate_action = p_validate_action
|
|
apply_action = p_apply_action
|
|
rollback_action = p_rollback_action
|
|
|
|
|
|
var status: Status = Status.OPEN
|
|
var _changes: Array[StagedChange] = []
|
|
var _validators: Array[Callable] = []
|
|
var _configuration_errors: Array[String] = []
|
|
|
|
|
|
func stage_dictionary_set(
|
|
target: Dictionary,
|
|
key: Variant,
|
|
value: Variant,
|
|
label: String = ""
|
|
) -> bool:
|
|
if not _require_open("stage_dictionary_set"):
|
|
return false
|
|
|
|
var had_key := target.has(key)
|
|
var original_value: Variant = target.get(key)
|
|
var original_snapshot: Variant = _copy_value(original_value)
|
|
var staged_value: Variant = _copy_value(value)
|
|
var change_label := label if not label.is_empty() else "set dictionary key %s" % str(key)
|
|
|
|
return stage_custom(
|
|
change_label,
|
|
func() -> Variant:
|
|
if target.has(key) != had_key:
|
|
return "Dictionary key presence changed before commit."
|
|
if had_key and target[key] != original_snapshot:
|
|
return "Dictionary value changed before commit."
|
|
return true,
|
|
func() -> Variant:
|
|
target[key] = _copy_value(staged_value)
|
|
return true,
|
|
func() -> void:
|
|
if had_key:
|
|
target[key] = _copy_value(original_snapshot)
|
|
else:
|
|
target.erase(key)
|
|
)
|
|
|
|
|
|
func stage_dictionary_erase(
|
|
target: Dictionary,
|
|
key: Variant,
|
|
label: String = ""
|
|
) -> bool:
|
|
if not _require_open("stage_dictionary_erase"):
|
|
return false
|
|
|
|
var had_key := target.has(key)
|
|
var original_value: Variant = target.get(key)
|
|
var original_snapshot: Variant = _copy_value(original_value)
|
|
var change_label := label if not label.is_empty() else "erase dictionary key %s" % str(key)
|
|
|
|
return stage_custom(
|
|
change_label,
|
|
func() -> Variant:
|
|
if target.has(key) != had_key:
|
|
return "Dictionary key presence changed before commit."
|
|
if had_key and target[key] != original_snapshot:
|
|
return "Dictionary value changed before commit."
|
|
return true,
|
|
func() -> Variant:
|
|
target.erase(key)
|
|
return true,
|
|
func() -> void:
|
|
if had_key:
|
|
target[key] = _copy_value(original_snapshot)
|
|
)
|
|
|
|
|
|
func stage_dictionary_increment(
|
|
target: Dictionary,
|
|
key: Variant,
|
|
delta: Variant,
|
|
minimum: Variant = null,
|
|
maximum: Variant = null,
|
|
label: String = ""
|
|
) -> bool:
|
|
if not _require_open("stage_dictionary_increment"):
|
|
return false
|
|
if not target.has(key) or not _is_number(target[key]) or not _is_number(delta):
|
|
_configuration_errors.append("Increment requires an existing numeric value and numeric delta.")
|
|
return false
|
|
|
|
var next_value: Variant = target[key] + delta
|
|
if minimum != null and (not _is_number(minimum) or next_value < minimum):
|
|
_configuration_errors.append("Increment result is below its minimum.")
|
|
return false
|
|
if maximum != null and (not _is_number(maximum) or next_value > maximum):
|
|
_configuration_errors.append("Increment result is above its maximum.")
|
|
return false
|
|
return stage_dictionary_set(target, key, next_value, label)
|
|
|
|
|
|
func add_validator(validator: Callable) -> bool:
|
|
if not _require_open("add_validator"):
|
|
return false
|
|
if not validator.is_valid():
|
|
_configuration_errors.append("Validator is not callable.")
|
|
return false
|
|
_validators.append(validator)
|
|
return true
|
|
|
|
|
|
func stage_custom(
|
|
label: String,
|
|
validate_action: Callable,
|
|
apply_action: Callable,
|
|
rollback_action: Callable
|
|
) -> bool:
|
|
if not _require_open("stage_custom"):
|
|
return false
|
|
if not apply_action.is_valid() or not rollback_action.is_valid():
|
|
_configuration_errors.append("Custom change '%s' requires apply and rollback actions." % label)
|
|
return false
|
|
_changes.append(StagedChange.new(label, validate_action, apply_action, rollback_action))
|
|
return true
|
|
|
|
|
|
func validate() -> CommandResult:
|
|
if status != Status.OPEN:
|
|
return CommandResult.failed(
|
|
CommandResult.Code.TRANSACTION_CLOSED,
|
|
0,
|
|
"Transaction is not open."
|
|
)
|
|
if not _configuration_errors.is_empty():
|
|
return CommandResult.failed(
|
|
CommandResult.Code.VALIDATION_FAILED,
|
|
0,
|
|
_configuration_errors[0],
|
|
{"errors": _configuration_errors.duplicate()}
|
|
)
|
|
|
|
for index in _validators.size():
|
|
var validator_result := _validation_result(_validators[index].call(), "validator %d" % index)
|
|
if validator_result != null:
|
|
return validator_result
|
|
|
|
for change in _changes:
|
|
if not change.validate_action.is_valid():
|
|
continue
|
|
var change_result := _validation_result(change.validate_action.call(), change.label)
|
|
if change_result != null:
|
|
return change_result
|
|
return CommandResult.succeeded()
|
|
|
|
|
|
func commit() -> CommandResult:
|
|
var validation_result := validate()
|
|
if not validation_result.is_success():
|
|
if status == Status.OPEN:
|
|
status = Status.FAILED
|
|
return validation_result
|
|
|
|
var applied: Array[StagedChange] = []
|
|
for change in _changes:
|
|
var returned: Variant = change.apply_action.call()
|
|
var apply_failure := _apply_result(returned, change.label)
|
|
if apply_failure != null:
|
|
change.rollback_action.call()
|
|
for index in range(applied.size() - 1, -1, -1):
|
|
applied[index].rollback_action.call()
|
|
status = Status.ROLLED_BACK
|
|
apply_failure.details["rolled_back"] = true
|
|
return apply_failure
|
|
applied.append(change)
|
|
|
|
status = Status.COMMITTED
|
|
return CommandResult.succeeded(0, {"change_count": _changes.size()})
|
|
|
|
|
|
func rollback() -> CommandResult:
|
|
if status != Status.OPEN:
|
|
return CommandResult.failed(
|
|
CommandResult.Code.TRANSACTION_CLOSED,
|
|
0,
|
|
"Only an open transaction can be cancelled."
|
|
)
|
|
status = Status.ROLLED_BACK
|
|
return CommandResult.succeeded()
|
|
|
|
|
|
func change_count() -> int:
|
|
return _changes.size()
|
|
|
|
|
|
func _require_open(operation: String) -> bool:
|
|
if status == Status.OPEN:
|
|
return true
|
|
_configuration_errors.append("%s cannot run on a closed transaction." % operation)
|
|
return false
|
|
|
|
|
|
func _validation_result(returned: Variant, label: String) -> CommandResult:
|
|
if returned == null or (returned is bool and returned):
|
|
return null
|
|
if returned is CommandResult:
|
|
var command_result: CommandResult = returned
|
|
if command_result.is_success():
|
|
return null
|
|
return command_result.copy()
|
|
var message := str(returned) if returned is String and not returned.is_empty() else "Validation failed."
|
|
return CommandResult.failed(
|
|
CommandResult.Code.VALIDATION_FAILED,
|
|
0,
|
|
"%s: %s" % [label, message],
|
|
{"change": label}
|
|
)
|
|
|
|
|
|
func _apply_result(returned: Variant, label: String) -> CommandResult:
|
|
if returned == null or (returned is bool and returned):
|
|
return null
|
|
if returned is CommandResult:
|
|
var command_result: CommandResult = returned
|
|
if command_result.is_success():
|
|
return null
|
|
return command_result.copy()
|
|
var message := str(returned) if returned is String and not returned.is_empty() else "Apply action failed."
|
|
return CommandResult.failed(
|
|
CommandResult.Code.TRANSACTION_APPLY_FAILED,
|
|
0,
|
|
"%s: %s" % [label, message],
|
|
{"change": label}
|
|
)
|
|
|
|
|
|
static func _copy_value(value: Variant) -> Variant:
|
|
if value is Dictionary or value is Array:
|
|
return value.duplicate(true)
|
|
return value
|
|
|
|
|
|
static func _is_number(value: Variant) -> bool:
|
|
return value is int or value is float
|