class_name CharacterCache extends RefCounted ## Character pictures kept on disk in the [code]characters[/code] folder next to the ## settings, so each one is downloaded only when it changed on the server. ## ## [code]index.json[/code] in the folder records, for each picture, when the server last ## changed it and when it was downloaded: ## [codeblock] ## {"Crono.png": {"modified": 1730000000, "downloaded": 1730100000}} ## [/codeblock] ## A cached picture is used while the server's change time matches the index. Servers that ## can't tell when pictures changed answer 404 for [code]/character/details[/code]; then a ## cached picture is used until it is [constant MAX_AGE_SECONDS] old. When the server ## can't be reached at all, the cached pictures are used as they are. const FOLDER_NAME: String = "characters" const INDEX_FILE_NAME: String = FileCache.INDEX_FILE_NAME ## How long a picture is used before downloading it again, when the server can't tell ## whether it changed. const MAX_AGE_SECONDS: int = 7 * 24 * 60 * 60 ## The change time of a picture when the server can't tell it. const UNKNOWN_TIME: int = FileCache.UNKNOWN_TIME # The pictures on disk and their index var _files: FileCache = FileCache.new(FOLDER_NAME) # When the server last changed each picture on the list, or UNKNOWN_TIME var _modified: Dictionary[String, int] = {} # True when the server could not be reached and only cached pictures are shown var _cache_only: bool = false ## The folder the pictures are kept in. static func folder() -> String: return Settings.config_dir().path_join(FOLDER_NAME) ## Deletes every cached picture. They are downloaded again when next asked for. func clear() -> void: _files.clear() ## Fetches the names of the characters and calls [param on_done] with them as an ## [code]Array[String][/code]. Cached pictures of characters no longer on the server are ## deleted. func load_list(on_done: Callable) -> void: _files.load_index() _cache_only = false var details_fetched: Callable = func details_fetched(details: Array) -> void: _modified.clear() for character: CharacterDetails in details: _modified[character.name] = character.modified _list_loaded(on_done) MusicServerClient.fetch_character_details(details_fetched, _load_names_only.bind(on_done)) ## Calls [param on_done] with the picture of [param character_name] as an [Image], from ## disk when the cached one is still fresh, otherwise from the server. func fetch_image(character_name: String, on_done: Callable) -> void: if (_cache_only || _is_fresh(character_name)) && _files.has_file(character_name): var cached: Image = CharacterCard.image_from_bytes(_files.read(character_name)) if cached != null: on_done.call(cached) return GameLog.warning("The cached picture of ", character_name, " is broken") var image_fetched: Callable = func image_fetched(bytes: PackedByteArray) -> void: var picture: Image = CharacterCard.image_from_bytes(bytes) if picture == null: GameLog.warning("Could not load the picture of ", character_name) return _files.store(character_name, bytes, _modified.get(character_name, UNKNOWN_TIME)) on_done.call(picture) MusicServerClient.fetch_character_image(character_name, image_fetched) # Servers older than /character/details only list the names func _load_names_only(on_done: Callable) -> void: var list_fetched: Callable = func list_fetched(list: Variant) -> void: _modified.clear() if list is Array: for character_name: Variant in list: _modified[str(character_name)] = UNKNOWN_TIME _list_loaded(on_done) MusicServerClient.fetch_character_list(list_fetched, _load_cached_only.bind(on_done)) # The server can't be reached, so the pictures already cached are shown and kept func _load_cached_only(on_done: Callable) -> void: GameLog.warning("Could not get the characters from the server, showing the cached ones") _cache_only = true var names: Array[String] = [] for character_name: String in _files.keys(): if _files.has_file(character_name): names.append(character_name) names.sort_custom(func(a: String, b: String) -> bool: return a.naturalnocasecmp_to(b) < 0) on_done.call(names) func _list_loaded(on_done: Callable) -> void: _files.keep_only(_modified) var names: Array[String] = [] names.assign(_modified.keys()) on_done.call(names) func _is_fresh(character_name: String) -> bool: if !_files.keys().has(character_name): return false var modified: int = _modified.get(character_name, UNKNOWN_TIME) if modified != UNKNOWN_TIME: return _files.modified(character_name) == modified return _files.age(character_name) < MAX_AGE_SECONDS