This commit is contained in:
Adrien Ufferte
2025-10-07 16:23:05 +02:00
parent aa9de91996
commit 2c511b9100
3 changed files with 72 additions and 77 deletions
+64 -65
View File
@@ -1,7 +1,7 @@
class_name StudentProgression class_name StudentProgression
extends Resource extends Resource
signal unlocks_changed() signal progression_changed()
enum Status{ enum Status{
Locked, Locked,
@@ -12,9 +12,7 @@ enum Status{
@export var version: String = ProjectSettings.get_setting("application/config/version") @export var version: String = ProjectSettings.get_setting("application/config/version")
@export var unlocks: Dictionary = {}: @export var unlocks: Dictionary = {}:
set(value): set(value):
unlocks = check_data_integrity(value) unlocks = ensure_data_integrity(value)
@export var level_times: Dictionary[int, PackedInt32Array] = {} # Level ID, Last number of seconds for minigames 0, 1 and 2
@export var level_total_times: Dictionary[int, PackedInt32Array] = {} # Level ID, Total number of seconds for minigames 0, 1 and 2
@export var last_modified: String @export var last_modified: String
@@ -25,7 +23,7 @@ func _init() -> void:
# Make sure the unlocks are correct # Make sure the unlocks are correct
func init_unlocks() -> void: func init_unlocks() -> void:
if not unlocks: if not unlocks:
unlocks = {} unlocks = {} # Triggers ensure_data_integrity(), that will fill the default values
# Verify the lessons # Verify the lessons
var number_of_lessons: int = Database.get_lessons_count() var number_of_lessons: int = Database.get_lessons_count()
@@ -41,64 +39,60 @@ func init_unlocks() -> void:
] ]
} }
while unlocks.size() > number_of_lessons:
unlocks.erase(unlocks.size())
# Make sure that the first garden is always accessible # Make sure that the first garden is always accessible
if unlocks[1]["look_and_learn"] == Status.Locked: if unlocks[1]["look_and_learn"] == Status.Locked:
unlocks[1]["look_and_learn"] = Status.Unlocked unlocks[1]["look_and_learn"] = Status.Unlocked
func check_data_integrity(data: Dictionary) -> Dictionary: func ensure_data_integrity(data: Dictionary) -> Dictionary:
var is_init: bool = data.is_empty()
var result: Dictionary = data.duplicate(true) var result: Dictionary = data.duplicate(true)
var number_of_lessons: int = Database.get_lessons_count()
# Check missin keys # Check too much keys
while result.size() > number_of_lessons:
result.erase(result.size())
# Check missing keys
var min_key: int = 1 var min_key: int = 1
var max_key: int = 1 var max_key: int = number_of_lessons
if result.size() > 0:
if result.keys()[0] is int:
min_key = result.keys().min() as int
max_key = result.keys().max() as int
else:
Log.error("StudentProgression: Received keys with wrong datatype")
for index: int in range(min_key, max_key + 1): for index: int in range(min_key, max_key + 1):
if not result.has(index): if not result.has(index):
Log.warn("StudentProgression: Garden %d missing → added with default values." % index) if not is_init:
Log.warn("StudentProgression: Garden %d missing → added with default values." % index)
result[index] = { result[index] = {
"games": [0, 0, 0], "games": [Status.Locked, Status.Locked, Status.Locked],
"look_and_learn": 0, "look_and_learn": Status.Locked,
"last_duration": 0.0, "last_duration": PackedInt32Array([0, 0, 0]),
"total_duration": 0.0 "total_duration": PackedInt32Array([0, 0, 0])
} }
# Check internal structure # Check internal structure
for index: int in result.keys(): for index: int in result.keys():
var garden: Dictionary = result[index] var garden: Dictionary = result[index]
# Check missing keys # Check missing keys
if not garden.has("games"): if not garden.has("games"):
Log.warn("Garden %d : Add missing key 'games'." % index) if not is_init:
garden["games"] = [0, 0, 0] Log.warn("Garden %d : Add missing key 'games'." % index)
garden["games"] = [Status.Locked, Status.Locked, Status.Locked]
if not garden.has("look_and_learn"): if not garden.has("look_and_learn"):
Log.warn("Garden %d : Add missing key 'look_and_learn'." % index) if not is_init:
garden["look_and_learn"] = 0 Log.warn("Garden %d : Add missing key 'look_and_learn'." % index)
garden["look_and_learn"] = Status.Locked
if not garden.has("last_duration"): if not garden.has("last_duration"):
garden["last_duration"] = 0.0 garden["last_duration"] =PackedInt32Array([0, 0, 0])
if not garden.has("total_duration"): if not garden.has("total_duration"):
garden["total_duration"] = 0.0 garden["total_duration"] = PackedInt32Array([0, 0, 0])
# Check array "games" # Check array "games"
if typeof(garden["games"]) != TYPE_ARRAY or (garden["games"] as Array).size() != 3: if typeof(garden["games"]) != TYPE_ARRAY or (garden["games"] as Array).size() != 3:
Log.warn("Garden %d : invalid format for 'games' → reset." % index) if not is_init:
garden["games"] = [0, 0, 0] Log.warn("Garden %d : invalid format for 'games' → reset." % index)
garden["games"] = [Status.Locked, Status.Locked, Status.Locked]
# Check value outside of 0/1/2 # Check value outside of possible enum values
for game_index: int in range(3): for game_index: int in range(3):
if garden["games"][game_index] not in [0, 1, 2]: if garden["games"][game_index] not in [Status.Locked, Status.Unlocked, Status.Completed]:
garden["games"][game_index] = 0 garden["games"][game_index] = Status.Locked
if garden["look_and_learn"] not in [0, 1, 2]: if garden["look_and_learn"] not in [Status.Locked, Status.Unlocked, Status.Completed]:
garden["look_and_learn"] = 0 garden["look_and_learn"] = Status.Locked
# Check progression rules # Check progression rules
for index: int in range(min_key, max_key + 1): for index: int in range(min_key, max_key + 1):
@@ -109,8 +103,8 @@ func check_data_integrity(data: Dictionary) -> Dictionary:
if result.has(index - 1): if result.has(index - 1):
var prev: Dictionary = result[index - 1] var prev: Dictionary = result[index - 1]
prev_completed = ( prev_completed = (
prev["look_and_learn"] == 2 and prev["look_and_learn"] == Status.Completed and
(prev["games"] as Array).all(func(x: int) -> bool: return x == 2) (prev["games"] as Array).all(func(x: int) -> bool: return x == Status.Completed)
) )
else: else:
# First garden (key 1) is always unlocked # First garden (key 1) is always unlocked
@@ -119,24 +113,27 @@ func check_data_integrity(data: Dictionary) -> Dictionary:
# Case : previous garden not completed # Case : previous garden not completed
if not prev_completed: if not prev_completed:
for game_index: int in range(3): for game_index: int in range(3):
if garden["games"][game_index] != 0 or garden["look_and_learn"] != 0: if garden["games"][game_index] != Status.Locked or garden["look_and_learn"] != Status.Locked:
Log.warn("Garden %d : invalid progression (previous not finished) → reset." % index) if not is_init:
garden["games"] = [0, 0, 0] Log.warn("Garden %d : invalid progression (previous not finished) → reset." % index)
garden["look_and_learn"] = 0 garden["games"] = [Status.Locked, Status.Locked, Status.Locked]
garden["look_and_learn"] = Status.Locked
break break
continue continue
# Case : lesson completed → games unlocked if needed # Case : lesson completed → games unlocked if needed
if garden["look_and_learn"] == 2: if garden["look_and_learn"] == Status.Completed:
for game_index: int in range(3): for game_index: int in range(3):
if garden["games"][game_index] == 0: if garden["games"][game_index] == Status.Locked:
garden["games"][game_index] = 1 garden["games"][game_index] = Status.Unlocked
Log.warn("Garden %d : game %d unlocked because lesson is completed" % [index, game_index + 1]) if not is_init:
Log.warn("Garden %d : game %d unlocked because lesson is completed" % [index, game_index + 1])
# Case : previous garden completed → lesson unlocked if needed # Case : previous garden completed → lesson unlocked if needed
elif garden["look_and_learn"] == 0: elif garden["look_and_learn"] == Status.Locked:
garden["look_and_learn"] = 1 garden["look_and_learn"] = Status.Unlocked
Log.warn("Garden %d : lesson unlocked because previous garden is completed" % index) if not is_init:
Log.warn("Garden %d : lesson unlocked because previous garden is completed" % index)
return result return result
@@ -167,7 +164,7 @@ func look_and_learn_completed(lesson_number: int) -> bool:
unlocks[lesson_number]["games"][index] = Status.Unlocked unlocks[lesson_number]["games"][index] = Status.Unlocked
last_modified = Time.get_datetime_string_from_system(true) last_modified = Time.get_datetime_string_from_system(true)
unlocks_changed.emit() progression_changed.emit()
return true return true
@@ -187,7 +184,7 @@ func game_completed(lesson_number: int, game_number: int) -> bool:
unlocks[lesson_number + 1]["look_and_learn"] = Status.Unlocked unlocks[lesson_number + 1]["look_and_learn"] = Status.Unlocked
last_modified = Time.get_datetime_string_from_system(true) last_modified = Time.get_datetime_string_from_system(true)
unlocks_changed.emit() progression_changed.emit()
return true return true
@@ -197,17 +194,19 @@ func add_level_time(lesson_number: int, game_number: int, time_spent: int) -> vo
Log.error("StudentProgression: Cannot log a level time for a minigame number superior to 2") Log.error("StudentProgression: Cannot log a level time for a minigame number superior to 2")
return return
if level_times.has(lesson_number): if not unlocks.has(lesson_number):
if level_times[lesson_number].size() != 3: Log.error("StudentProgression: Cannot log a level time for lesson %d because it does not exists in progression data" % lesson_number)
level_times[lesson_number] = [0, 0, 0] return
else:
level_times.set(lesson_number, PackedInt32Array([0, 0, 0]))
level_times[lesson_number][game_number] = time_spent
if level_total_times.has(lesson_number): if not (unlocks[lesson_number] as Dictionary).has("last_duration") or not (unlocks[lesson_number]["last_duration"] as Array).size() > game_number:
if level_total_times[lesson_number].size() != 3: Log.error("StudentProgression: Cannot log a last_duration for lesson %d, game %d, because it does not exists" % [lesson_number, game_number])
level_total_times[lesson_number] = PackedInt32Array([0, 0, 0])
else: else:
level_total_times.set(lesson_number, PackedInt32Array([0, 0, 0])) unlocks[lesson_number]["last_duration"][game_number] = time_spent
level_total_times[lesson_number][game_number] += time_spent
if not (unlocks[lesson_number] as Dictionary).has("total_duration") or not (unlocks[lesson_number]["total_duration"] as Array).size() > game_number:
Log.error("StudentProgression: Cannot log a total_duration for lesson %d, game %d, because it does not exists" % [lesson_number, game_number])
else:
unlocks[lesson_number]["total_duration"][game_number] += time_spent
last_modified = Time.get_datetime_string_from_system(true) last_modified = Time.get_datetime_string_from_system(true)
progression_changed.emit()
+5 -5
View File
@@ -165,6 +165,8 @@ func login(infos: Dictionary) -> bool:
return true return true
## Safely loads a resource from the given path.
## If the file contains outdated text patterns (old_texts), they are replaced with new ones before loading.
func safe_load_and_fix_resource(path: String, old_texts: Array[String], new_texts: Array[String]) -> Resource: func safe_load_and_fix_resource(path: String, old_texts: Array[String], new_texts: Array[String]) -> Resource:
if not FileAccess.file_exists(path): if not FileAccess.file_exists(path):
Log.error("UserDataManager: File not found: " + path) Log.error("UserDataManager: File not found: " + path)
@@ -521,7 +523,7 @@ func _load_student_progression() -> void:
_save_student_progression() _save_student_progression()
student_progression.init_unlocks() student_progression.init_unlocks()
student_progression.unlocks_changed.connect(_on_user_progression_unlocks_changed) student_progression.progression_changed.connect(_on_user_progression_changed)
func _save_student_progression() -> void: func _save_student_progression() -> void:
@@ -529,7 +531,7 @@ func _save_student_progression() -> void:
ResourceSaver.save(student_progression, get_student_progression_path()) ResourceSaver.save(student_progression, get_student_progression_path())
func _on_user_progression_unlocks_changed() -> void: func _on_user_progression_changed() -> void:
_save_student_progression() _save_student_progression()
@@ -570,14 +572,12 @@ func save_student_progression_for_code(device: int, code: int, progression: Stud
Log.error("UserDataManager: save_student_progression_for_code(device = %s, code = %s): error %s" % [str(device), str(code), error_string(error)]) Log.error("UserDataManager: save_student_progression_for_code(device = %s, code = %s): error %s" % [str(device), str(code), error_string(error)])
func set_student_progression_data(student_code: int, version: String, new_data: Dictionary, updated_at: String, last_duration: Dictionary[int, PackedInt32Array], total_duration: Dictionary[int, PackedInt32Array]) -> void: func set_student_progression_data(student_code: int, version: String, new_data: Dictionary, updated_at: String) -> void:
var current_data: StudentProgression = get_student_progression_for_code(0, student_code) var current_data: StudentProgression = get_student_progression_for_code(0, student_code)
if current_data == null: if current_data == null:
current_data = StudentProgression.new() current_data = StudentProgression.new()
current_data.version = version current_data.version = version
current_data.unlocks = new_data current_data.unlocks = new_data
current_data.level_times = last_duration
current_data.level_total_times = total_duration
current_data.last_modified = updated_at current_data.last_modified = updated_at
var err: Error = ResourceSaver.save(current_data, get_student_progression_path(0, student_code)) var err: Error = ResourceSaver.save(current_data, get_student_progression_path(0, student_code))
if err != OK: if err != OK:
@@ -275,8 +275,6 @@ func _build_message_to_server(need_update_user: UpdateNeeded, need_update_studen
progression_block = { progression_block = {
"version": student_progression.version, "version": student_progression.version,
"unlocked": student_progression.unlocks, "unlocked": student_progression.unlocks,
"level_times": student_progression.level_times,
"level_total_times": student_progression.level_total_times,
"updated_at": student_progression.last_modified "updated_at": student_progression.last_modified
} }
elif student_entry.progression == UpdateNeeded.FromServer: elif student_entry.progression == UpdateNeeded.FromServer:
@@ -402,15 +400,13 @@ func _apply_server_response(response_body: Dictionary) -> void:
# Cleaning data because of JSON parsing changing types int / float / string # Cleaning data because of JSON parsing changing types int / float / string
var received_unlock_data: Dictionary = response_student_data.progression.unlocked as Dictionary var received_unlock_data: Dictionary = response_student_data.progression.unlocked as Dictionary
var new_unlock_data: Dictionary = {} var new_unlock_data: Dictionary = {}
var last_duration: Dictionary[int, PackedInt32Array] = {}
var total_duration: Dictionary[int, PackedInt32Array] = {}
for key_lesson: Variant in received_unlock_data.keys(): for key_lesson: Variant in received_unlock_data.keys():
new_unlock_data[int(key_lesson as int)] = {"games": [], "look_and_learn": received_unlock_data[key_lesson]["look_and_learn"] as int} new_unlock_data[int(key_lesson as int)] = {"games": [], "look_and_learn": received_unlock_data[key_lesson]["look_and_learn"] as int}
for game_result: Variant in received_unlock_data[key_lesson]["games"]: for game_result: Variant in received_unlock_data[key_lesson]["games"]:
(new_unlock_data[key_lesson as int]["games"] as Array).push_back(game_result as int) (new_unlock_data[key_lesson as int]["games"] as Array).push_back(game_result as int)
last_duration.set(key_lesson as int, PackedInt32Array(received_unlock_data[key_lesson]["last_duration"] as Array)) (new_unlock_data[int(key_lesson as int)] as Dictionary).merge({"last_duration": PackedInt32Array(received_unlock_data[key_lesson]["last_duration"] as Array)})
total_duration.set(key_lesson as int, PackedInt32Array(received_unlock_data[key_lesson]["total_duration"] as Array)) (new_unlock_data[int(key_lesson as int)] as Dictionary).merge({"total_duration": PackedInt32Array(received_unlock_data[key_lesson]["total_duration"] as Array)})
UserDataManager.set_student_progression_data(int(response_student_code), response_student_data.progression.version as String, new_unlock_data, response_student_data.progression.updated_at as String, last_duration, total_duration) UserDataManager.set_student_progression_data(int(response_student_code), response_student_data.progression.version as String, new_unlock_data, response_student_data.progression.updated_at as String)
if response_student_data.has("remediation_gp") and (response_student_data.remediation_gp as Dictionary).has("score_remediation") and (response_student_data.remediation_gp as Dictionary).has("updated_at"): if response_student_data.has("remediation_gp") and (response_student_data.remediation_gp as Dictionary).has("score_remediation") and (response_student_data.remediation_gp as Dictionary).has("updated_at"):
# TODO ADD SECURITY # TODO ADD SECURITY
var new_array: Array = JSON.parse_string(response_student_data.remediation_gp.score_remediation as String) as Array var new_array: Array = JSON.parse_string(response_student_data.remediation_gp.score_remediation as String) as Array