A new row, "Downloaded pictures and songs", shows how much space the cached character pictures, covers and downloaded songs take, with a Clear cache button. Clearing deletes them, but keeps the songs the current quiz uses, the settings, config.ini and the saved quiz. The characters are loaded again at once; covers come back when they are shown. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
112 lines
4.5 KiB
GDScript
112 lines
4.5 KiB
GDScript
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
|