Cache character pictures and songs on disk
Character pictures are kept in characters/ next to the settings, with an index.json of when the server last changed each one and when it was downloaded. Only new or changed pictures are downloaded, using the server's new /character/details. Older servers fall back to the name list, and pictures are downloaded again after 7 days. Downloaded songs are written to songs/ and only their paths are kept in memory. The folder is emptied when the game starts and when the playlist resets.
This commit is contained in:
1 parent
62fa63fd8b
commit
240c331ab2
17 files changed
+912
-48
No files matched your search
@@ -0,0 +1,191 @@
|
||||
extends GdUnitTestSuite
|
||||
## Character pictures cached on disk: downloaded once, then only again when they changed
|
||||
## on the server, or when they are old and the server can't tell.
|
||||
|
||||
var _server: FakeHttpTransport
|
||||
var _config_dir_before: String
|
||||
var _png: PackedByteArray
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_server = FakeHttpTransport.new().install()
|
||||
_config_dir_before = OS.get_environment(Settings.CONFIG_DIR_ENV)
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("character_cache"))
|
||||
if DirAccess.dir_exists_absolute(CharacterCache.folder()):
|
||||
for file_name: String in DirAccess.get_files_at(CharacterCache.folder()):
|
||||
DirAccess.remove_absolute(CharacterCache.folder().path_join(file_name))
|
||||
var image: Image = Image.create(4, 4, false, Image.FORMAT_RGB8)
|
||||
image.fill(Color.ORANGE)
|
||||
_png = image.save_png_to_buffer()
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, _config_dir_before)
|
||||
_server.uninstall()
|
||||
|
||||
|
||||
func _serve(characters: Dictionary) -> void:
|
||||
var details: Array = []
|
||||
for character_name: String in characters:
|
||||
details.append({"name": character_name, "modified": characters[character_name]})
|
||||
_server.respond_bytes(_image_path(character_name), _png)
|
||||
_server.respond_json("/character/details", details)
|
||||
|
||||
|
||||
# Serves the names only, like a server older than /character/details
|
||||
func _serve_names_only(names: Array[String]) -> void:
|
||||
_server.respond_json("/character/list", names)
|
||||
for character_name: String in names:
|
||||
_server.respond_bytes(_image_path(character_name), _png)
|
||||
|
||||
|
||||
func _image_path(character_name: String) -> String:
|
||||
return "/character?name=" + character_name.uri_encode()
|
||||
|
||||
|
||||
# Loads the list and every picture with a new cache, like starting the game
|
||||
func _load_all() -> Array[Image]:
|
||||
var cache: CharacterCache = CharacterCache.new()
|
||||
var images: Array[Image] = []
|
||||
cache.load_list(func(names: Array[String]) -> void:
|
||||
for character_name: String in names:
|
||||
cache.fetch_image(character_name, func(image: Image) -> void: images.append(image)))
|
||||
return images
|
||||
|
||||
|
||||
func _index() -> Dictionary:
|
||||
return JSON.parse_string(FileAccess.get_file_as_string(
|
||||
CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME)))
|
||||
|
||||
|
||||
func _set_index(index: Dictionary) -> void:
|
||||
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
||||
FileAccess.WRITE).store_string(JSON.stringify(index))
|
||||
|
||||
|
||||
func test_first_load_downloads_and_keeps_every_picture() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_true()
|
||||
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(200)
|
||||
|
||||
|
||||
func test_unchanged_pictures_come_from_disk() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
_load_all()
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(1)
|
||||
|
||||
|
||||
func test_only_changed_pictures_are_downloaded_again() -> void:
|
||||
_serve({"Crono.png": 100, "Marle.png": 200})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100, "Marle.png": 300})
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
||||
assert_int(int(_index()["Marle.png"]["modified"])).is_equal(300)
|
||||
|
||||
|
||||
func test_new_character_is_downloaded() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100, "Lucca.png": 100})
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Lucca.png"))).is_equal(1)
|
||||
|
||||
|
||||
func test_characters_removed_on_the_server_are_deleted() -> void:
|
||||
_serve({"Crono.png": 100, "Magus.png": 100})
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Magus.png"))) \
|
||||
.is_false()
|
||||
assert_bool(_index().has("Magus.png")).is_false()
|
||||
|
||||
|
||||
func test_broken_cached_picture_is_downloaded_again() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
FileAccess.open(CharacterCache.folder().path_join("Crono.png"), FileAccess.WRITE) \
|
||||
.store_string("not a picture")
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_older_server_lists_names_and_young_pictures_come_from_disk() -> void:
|
||||
_serve_names_only(["Crono.png"])
|
||||
_load_all()
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count("/character/details")).is_equal(2)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(int(_index()["Crono.png"]["modified"])).is_equal(CharacterCache.UNKNOWN_TIME)
|
||||
|
||||
|
||||
func test_older_server_pictures_are_downloaded_again_when_old() -> void:
|
||||
_serve_names_only(["Crono.png", "Marle.png"])
|
||||
_load_all()
|
||||
var index: Dictionary = _index()
|
||||
var too_old: int = int(Time.get_unix_time_from_system()) - CharacterCache.MAX_AGE_SECONDS - 1
|
||||
index["Marle.png"]["downloaded"] = too_old
|
||||
_set_index(index)
|
||||
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Marle.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_picture_cached_without_a_time_is_downloaded_once_the_server_has_times() -> void:
|
||||
_serve_names_only(["Crono.png"])
|
||||
_load_all()
|
||||
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
_load_all()
|
||||
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
|
||||
|
||||
func test_picture_that_cannot_be_read_is_not_cached() -> void:
|
||||
_server.respond_json("/character/details", [{"name": "Crono.png", "modified": 100}])
|
||||
_server.respond_bytes(_image_path("Crono.png"), "not a picture".to_utf8_buffer())
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_array(images).is_empty()
|
||||
assert_bool(FileAccess.file_exists(CharacterCache.folder().path_join("Crono.png"))).is_false()
|
||||
|
||||
|
||||
func test_broken_index_starts_over() -> void:
|
||||
_serve({"Crono.png": 100})
|
||||
_load_all()
|
||||
FileAccess.open(CharacterCache.folder().path_join(CharacterCache.INDEX_FILE_NAME),
|
||||
FileAccess.WRITE).store_string("{broken")
|
||||
|
||||
var images: Array[Image] = _load_all()
|
||||
|
||||
assert_int(images.size()).is_equal(1)
|
||||
assert_int(_server.count(_image_path("Crono.png"))).is_equal(2)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bb74d4gbslmcf
|
||||
@@ -0,0 +1,85 @@
|
||||
extends GdUnitTestSuite
|
||||
## Downloaded songs are kept in files in the songs folder, not in memory.
|
||||
|
||||
var _snapshot: AutoloadSnapshot
|
||||
var _config_dir_before: String
|
||||
var _mp3: PackedByteArray
|
||||
|
||||
|
||||
func before_test() -> void:
|
||||
_snapshot = AutoloadSnapshot.take([Playlist])
|
||||
_config_dir_before = OS.get_environment(Settings.CONFIG_DIR_ENV)
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, create_temp_dir("song_files"))
|
||||
Playlist.clear_song_files()
|
||||
Playlist.song_object_array.clear()
|
||||
_mp3 = FileAccess.get_file_as_bytes("res://tests/data/short.mp3")
|
||||
|
||||
|
||||
func after_test() -> void:
|
||||
Playlist.clear_song_files()
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, _config_dir_before)
|
||||
_snapshot.restore()
|
||||
|
||||
|
||||
func _add_song() -> SongObject:
|
||||
var song_object: SongObject = SongObject.new()
|
||||
Playlist.store_song(song_object, _mp3)
|
||||
Playlist.add_to_playlist(song_object)
|
||||
return song_object
|
||||
|
||||
|
||||
func test_stored_song_is_a_file_and_plays_back() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
assert_object(song_object.song).is_null()
|
||||
assert_bool(FileAccess.file_exists(song_object.song_path)).is_true()
|
||||
var stream: AudioStream = Playlist.get_song(0)
|
||||
assert_object(stream).is_instanceof(AudioStreamMP3)
|
||||
assert_float(stream.get_length()).is_greater(0.0)
|
||||
|
||||
|
||||
func test_every_song_gets_a_file_of_its_own() -> void:
|
||||
var first: SongObject = _add_song()
|
||||
var second: SongObject = _add_song()
|
||||
|
||||
assert_str(first.song_path).is_not_equal(second.song_path)
|
||||
assert_array(DirAccess.get_files_at(Playlist.songs_folder())).has_size(2)
|
||||
|
||||
|
||||
func test_downloading_a_song_again_replaces_its_file() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
var old_path: String = song_object.song_path
|
||||
|
||||
Playlist.add_song_to_song_object(0, _mp3)
|
||||
|
||||
assert_str(song_object.song_path).is_not_equal(old_path)
|
||||
assert_object(Playlist.get_song(0)).is_not_null()
|
||||
|
||||
|
||||
func test_resetting_the_playlist_deletes_the_files() -> void:
|
||||
_add_song()
|
||||
_add_song()
|
||||
|
||||
Playlist.reset_playlist()
|
||||
|
||||
assert_array(DirAccess.get_files_at(Playlist.songs_folder())).is_empty()
|
||||
|
||||
|
||||
func test_missing_file_gives_no_song_so_it_is_downloaded_again() -> void:
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
DirAccess.remove_absolute(song_object.song_path)
|
||||
|
||||
assert_object(Playlist.get_song(0)).is_null()
|
||||
|
||||
|
||||
func test_song_stays_in_memory_when_it_cannot_be_saved() -> void:
|
||||
# A file where the folder should be, so the folder can't be made
|
||||
var blocker: String = create_temp_dir("song_files_blocked").path_join("not_a_folder")
|
||||
FileAccess.open(blocker, FileAccess.WRITE).store_string("x")
|
||||
OS.set_environment(Settings.CONFIG_DIR_ENV, blocker)
|
||||
|
||||
var song_object: SongObject = _add_song()
|
||||
|
||||
assert_str(song_object.song_path).is_empty()
|
||||
assert_object(Playlist.get_song(0)).is_instanceof(AudioStreamMP3)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b0x7lfm0d5kmc
|
||||
Reference in new issue
Block a user