ASW Lib
A.D.S. Games SDL Wrapper Library. A library targeted at Allegro4 users who want to switch to SDL3 and use modern c++.
Loading...
Searching...
No Matches
assets.cpp
Go to the documentation of this file.
2
3#include <filesystem>
4#include <fstream>
5#include <sstream>
6#include <system_error>
7#ifdef __EMSCRIPTEN__
8#include <emscripten.h>
9#endif
10
11#include <SDL3/SDL.h>
12#include <SDL3_image/SDL_image.h>
13#include <SDL3_mixer/SDL_mixer.h>
14#include <SDL3_ttf/SDL_ttf.h>
15#include <algorithm>
16#include <cmath>
17#include <string>
18#include <unordered_map>
19
21#include "./asw/modules/draw.h"
23#include "./asw/modules/sound.h"
24#include "./asw/modules/types.h"
25#include "./asw/modules/util.h"
26
27namespace {
28std::unordered_map<std::string, asw::Texture> textures;
29std::unordered_map<std::string, asw::Font> fonts;
30std::unordered_map<std::string, asw::Sample> samples;
31std::unordered_map<std::string, asw::Music> music;
32
33// Destroys audio only while the mixer it was loaded with is running. Shutting
34// the mixer down frees all its audio, so audio loaded before a shutdown is not
35// destroyed again, even after the mixer is started again.
37{
38 return [session = asw::sound::_get_session()](MIX_Audio* a) {
39 if (asw::sound::get_mixer() != nullptr && asw::sound::_get_session() == session) {
40 MIX_DestroyAudio(a);
41 }
42 };
43}
44} // namespace
45
46// --- Paths ---
47
48std::string asw::assets::get_path(const std::string& filename)
49{
50 // base_path is usually ".../YourGame.app/Contents/Resources/" on mac
51 // simply the directory of the executable on other platforms
52 const char* base_path = SDL_GetBasePath();
53 if (base_path == nullptr) {
54 return filename;
55 }
56 return std::string(base_path) + filename;
57}
58
59std::string asw::assets::get_save_path(const std::string& org, const std::string& app)
60{
61 char* pref_path = SDL_GetPrefPath(org.c_str(), app.c_str());
62 if (pref_path == nullptr) {
63 return "";
64 }
65
66 std::string path(pref_path);
67 SDL_free(pref_path);
68 return path;
69}
70
71// --- Texture ---
72
73asw::Texture asw::assets::load_texture(const std::string& filename)
74{
75 const auto full_path = get_path(filename);
76 SDL_Texture* temp = IMG_LoadTexture(asw::display::get_renderer(), full_path.c_str());
77
78 if (temp == nullptr) {
79 asw::util::abort_on_error("Failed to load texture: " + full_path);
80 }
81
82 SDL_SetTextureScaleMode(temp, SDL_SCALEMODE_NEAREST);
83 SDL_SetTextureBlendMode(temp, SDL_BLENDMODE_BLEND);
84
85 // Guard: if the renderer is already gone when the deleter fires (e.g. a
86 // shared_ptr outliving core::shutdown()), skip the SDL call - SDL has
87 // already freed the texture via SDL_DestroyRenderer.
88 return { temp, [](SDL_Texture* t) {
89 if (asw::display::get_renderer() != nullptr) {
90 SDL_DestroyTexture(t);
91 }
92 } };
93}
94
95asw::Texture asw::assets::load_texture(const std::string& filename, const std::string& key)
96{
97 if (auto it = textures.find(key); it != textures.end()) {
98 return it->second;
99 }
100
101 Texture tex = load_texture(filename);
102 textures.try_emplace(key, tex);
103 return tex;
104}
105
107{
108 auto it = textures.find(key);
109 if (it == textures.end()) {
110 asw::util::abort_on_error("Texture not found: " + key);
111 }
112 return it->second;
113}
114
115void asw::assets::unload_texture(const std::string& key)
116{
117 textures.erase(key);
118}
119
121{
122 auto* r = asw::display::get_renderer();
123 if (r == nullptr) {
124 asw::util::abort_on_error("Renderer not initialized");
125 }
126
127 SDL_Texture* txr
128 = SDL_CreateTexture(r, SDL_PIXELFORMAT_RGBA8888, SDL_TEXTUREACCESS_TARGET, w, h);
129
130 if (txr == nullptr) {
131 asw::util::abort_on_error("Failed to create texture: " + std::string(SDL_GetError()));
132 }
133
134 SDL_SetTextureScaleMode(txr, SDL_SCALEMODE_NEAREST);
135 SDL_SetTextureBlendMode(txr, SDL_BLENDMODE_BLEND);
136
137 return { txr, [](SDL_Texture* t) {
138 if (asw::display::get_renderer() != nullptr) {
139 SDL_DestroyTexture(t);
140 }
141 } };
142}
143
145 int size, asw::Color inner, asw::Color outer, asw::Falloff falloff)
146{
147 auto* r = asw::display::get_renderer();
148 if (r == nullptr) {
149 asw::util::abort_on_error("Renderer not initialized");
150 }
151
152 SDL_Surface* surface = SDL_CreateSurface(size, size, SDL_PIXELFORMAT_RGBA32);
153 if (surface == nullptr) {
154 asw::util::abort_on_error("Failed to create gradient surface");
155 }
156
157 const auto* details = SDL_GetPixelFormatDetails(surface->format);
158 const float half = static_cast<float>(size) / 2.0F;
159
160 auto mix = [](uint8_t a, uint8_t b, float t) {
161 return static_cast<uint8_t>(
162 std::lround(static_cast<float>(a) + (static_cast<float>(b) - a) * t));
163 };
164
165 for (int y = 0; y < size; y++) {
166 auto* row = reinterpret_cast<uint32_t*>(
167 static_cast<uint8_t*>(surface->pixels) + y * surface->pitch);
168
169 for (int x = 0; x < size; x++) {
170 const float distance = std::hypot(static_cast<float>(x) + 0.5F - half,
171 static_cast<float>(y) + 0.5F - half)
172 / half;
173
174 const float amount = asw::easing::falloff(std::clamp(distance, 0.0F, 1.0F), falloff);
175
176 row[x] = SDL_MapRGBA(details, nullptr, mix(inner.r, outer.r, amount),
177 mix(inner.g, outer.g, amount), mix(inner.b, outer.b, amount),
178 mix(inner.a, outer.a, amount));
179 }
180 }
181
182 SDL_Texture* txr = SDL_CreateTextureFromSurface(r, surface);
183 SDL_DestroySurface(surface);
184
185 if (txr == nullptr) {
186 asw::util::abort_on_error("Failed to create gradient texture");
187 }
188
189 SDL_SetTextureScaleMode(txr, SDL_SCALEMODE_LINEAR);
190 SDL_SetTextureBlendMode(txr, SDL_BLENDMODE_BLEND);
191
192 return { txr, [](SDL_Texture* t) {
193 if (asw::display::get_renderer() != nullptr) {
194 SDL_DestroyTexture(t);
195 }
196 } };
197}
198
199// --- Font ---
200
201asw::Font asw::assets::load_font(const std::string& filename, float size, asw::FontStyle style)
202{
203 const auto full_path = get_path(filename);
204 TTF_Font* temp = TTF_OpenFont(full_path.c_str(), size);
205
206 if (temp == nullptr) {
207 asw::util::abort_on_error("Failed to load font: " + full_path);
208 }
209
210 // Mono hinting renders glyphs without anti-aliasing. draw::text reads it
211 // back to pick nearest filtering.
212 if (style == asw::FontStyle::Pixel) {
213 TTF_SetFontHinting(temp, TTF_HINTING_MONO);
214 }
215
216 // Only close while SDL_ttf is running. Checking the renderer instead
217 // leaked every font in OpenGL mode, which has no renderer.
218 return { temp, [](TTF_Font* f) {
219 if (TTF_WasInit() > 0) {
220 TTF_CloseFont(f);
221 }
222 } };
223}
224
226 const std::string& filename, float size, const std::string& key, asw::FontStyle style)
227{
228 if (auto it = fonts.find(key); it != fonts.end()) {
229 return it->second;
230 }
231
232 Font font = load_font(filename, size, style);
233 fonts.try_emplace(key, font);
234 return font;
235}
236
237asw::Font asw::assets::get_font(const std::string& key)
238{
239 auto it = fonts.find(key);
240 if (it == fonts.end()) {
241 asw::util::abort_on_error("Font not found: " + key);
242 }
243 return it->second;
244}
245
246void asw::assets::unload_font(const std::string& key)
247{
248 fonts.erase(key);
251}
252
253// --- Sample ---
254
255asw::Sample asw::assets::load_sample(const std::string& filename)
256{
257 const auto full_path = get_path(filename);
258 MIX_Audio* temp = MIX_LoadAudio(asw::sound::get_mixer(), full_path.c_str(), true);
259
260 if (temp == nullptr) {
261 asw::util::abort_on_error("Failed to load sample: " + full_path);
262 }
263
264 return { temp, audio_deleter() };
265}
266
267asw::Sample asw::assets::load_sample(const std::string& filename, const std::string& key)
268{
269 if (auto it = samples.find(key); it != samples.end()) {
270 return it->second;
271 }
272
273 Sample sample = load_sample(filename);
274 samples.try_emplace(key, sample);
275 return sample;
276}
277
279{
280 auto it = samples.find(key);
281 if (it == samples.end()) {
282 asw::util::abort_on_error("Sample not found: " + key);
283 }
284 return it->second;
285}
286
287void asw::assets::unload_sample(const std::string& key)
288{
289 samples.erase(key);
290}
291
292// --- Music ---
293
294asw::Music asw::assets::load_music(const std::string& filename)
295{
296 const auto full_path = get_path(filename);
297 MIX_Audio* temp = MIX_LoadAudio(asw::sound::get_mixer(), full_path.c_str(), false);
298
299 if (temp == nullptr) {
300 asw::util::abort_on_error("Failed to load music: " + full_path);
301 }
302
303 return { temp, audio_deleter() };
304}
305
306asw::Music asw::assets::load_music(const std::string& filename, const std::string& key)
307{
308 if (auto it = music.find(key); it != music.end()) {
309 return it->second;
310 }
311
312 Music mus = load_music(filename);
313 music.try_emplace(key, mus);
314 return mus;
315}
316
317asw::Music asw::assets::get_music(const std::string& key)
318{
319 auto it = music.find(key);
320 if (it == music.end()) {
321 asw::util::abort_on_error("Music not found: " + key);
322 }
323 return it->second;
324}
325
326void asw::assets::unload_music(const std::string& key)
327{
328 music.erase(key);
329}
330
331// --- Global ---
332
334{
337 textures.clear();
338 fonts.clear();
339 samples.clear();
340 music.clear();
341}
342
343#ifdef __EMSCRIPTEN__
344namespace {
345// Storage can be blocked, which reads as empty and fails to write. The read
346// is a length then a copy into a buffer C++ owns, so JS does not allocate.
347// clang-format off
348EM_JS_DEPS(asw_storage, "$UTF8ToString,$lengthBytesUTF8,$stringToUTF8");
349
350EM_JS(int, asw_storage_length, (const char* key), {
351 try {
352 const value = localStorage.getItem(UTF8ToString(key));
353 return value === null ? -1 : lengthBytesUTF8(value);
354 } catch (e) {
355 return -1;
356 }
357});
358
359EM_JS(void, asw_storage_copy, (const char* key, char* out, int size), {
360 try {
361 const value = localStorage.getItem(UTF8ToString(key));
362 if (value !== null) {
363 stringToUTF8(value, out, size);
364 }
365 } catch (e) {
366 }
367});
368
369EM_JS(int, asw_storage_write, (const char* key, const char* value), {
370 try {
371 localStorage.setItem(UTF8ToString(key), UTF8ToString(value));
372 return 1;
373 } catch (e) {
374 return 0;
375 }
376});
377// clang-format on
378
379std::string storage_key(const std::string& org, const std::string& app, const std::string& name)
380{
381 return org + "/" + app + "/" + name;
382}
383} // namespace
384
385std::string asw::assets::read_save(
386 const std::string& org, const std::string& app, const std::string& name)
387{
388 const auto key = storage_key(org, app, name);
389 const int length = asw_storage_length(key.c_str());
390 if (length <= 0) {
391 return "";
392 }
393 // One more byte for the terminator stringToUTF8 writes
394 std::string out(static_cast<std::size_t>(length), '\0');
395 asw_storage_copy(key.c_str(), out.data(), length + 1);
396 return out;
397}
398
399bool asw::assets::write_save(const std::string& org, const std::string& app,
400 const std::string& name, const std::string& data)
401{
402 return asw_storage_write(storage_key(org, app, name).c_str(), data.c_str()) != 0;
403}
404#else
406 const std::string& org, const std::string& app, const std::string& name)
407{
408 const auto folder = get_save_path(org, app);
409 if (folder.empty()) {
410 return "";
411 }
412 std::ifstream file(folder + name, std::ios::binary);
413 if (!file) {
414 return "";
415 }
416 std::stringstream buffer;
417 buffer << file.rdbuf();
418 return buffer.str();
419}
420
421bool asw::assets::write_save(const std::string& org, const std::string& app,
422 const std::string& name, const std::string& data)
423{
424 const auto folder = get_save_path(org, app);
425 if (folder.empty()) {
426 return false;
427 }
428
429 // Write a temporary file and swap it in, so a crash mid write keeps the
430 // old save
431 const std::string path = folder + name;
432 const std::string temp = path + ".tmp";
433 {
434 std::ofstream file(temp, std::ios::trunc | std::ios::binary);
435 if (!file || !(file << data)) {
436 return false;
437 }
438 }
439 std::error_code ec;
440 std::filesystem::rename(temp, path, ec);
441 if (ec) {
442 // Some platforms do not replace an existing file on rename
443 std::filesystem::remove(path, ec);
444 std::filesystem::rename(temp, path, ec);
445 }
446 return !ec;
447}
448#endif
Asset routines for the ASW library.
Display and window routines for the ASW library.
Routines for drawing sprites and primitives to the screen.
Easing functions for animations.
std::unordered_map< std::string, asw::Music > music
Definition assets.cpp:31
std::unordered_map< std::string, asw::Sample > samples
Definition assets.cpp:30
std::unordered_map< std::string, asw::Texture > textures
Definition assets.cpp:28
std::unordered_map< std::string, asw::Font > fonts
Definition assets.cpp:29
asw::Sample get_sample(const std::string &key)
Get a cached sample.
Definition assets.cpp:278
std::string read_save(const std::string &org, const std::string &app, const std::string &name)
Read save data written by write_save, e.g. progress or settings.
Definition assets.cpp:405
bool write_save(const std::string &org, const std::string &app, const std::string &name, const std::string &data)
Write save data, replacing what was saved under the name before.
Definition assets.cpp:421
asw::Texture create_texture(int w, int h)
Create a Texture given the specified dimensions.
Definition assets.cpp:120
void unload_music(const std::string &key)
Remove a cached music.
Definition assets.cpp:326
std::string get_path(const std::string &filename)
Get the full path to an asset given its filename. The filename is relative to the base path of the ap...
Definition assets.cpp:48
asw::Texture get_texture(const std::string &key)
Get a cached texture.
Definition assets.cpp:106
void unload_sample(const std::string &key)
Remove a cached sample.
Definition assets.cpp:287
asw::Texture create_radial_gradient(int size, asw::Color inner, asw::Color outer, asw::Falloff falloff=asw::Falloff::Smooth)
Create a square texture with a radial gradient. The colour goes from inner at the centre to outer at ...
Definition assets.cpp:144
std::string get_save_path(const std::string &org, const std::string &app)
Get a writable folder for save files and settings, unique to the organisation and application....
Definition assets.cpp:59
void unload_font(const std::string &key)
Remove a cached font.
Definition assets.cpp:246
asw::Music load_music(const std::string &filename)
Loads a music file from a file. Formats supported are WAV, AIFF, RIFF, OGG and VOC....
Definition assets.cpp:294
asw::Font load_font(const std::string &filename, float size, asw::FontStyle style=asw::FontStyle::Smooth)
Loads a TTF font from a file. This will abort if the file is not found.
Definition assets.cpp:201
void clear_all()
Clear all cached assets. This will remove all cached textures, fonts, samples, and music.
Definition assets.cpp:333
void unload_texture(const std::string &key)
Remove a cached texture.
Definition assets.cpp:115
asw::Music get_music(const std::string &key)
Get a cached music.
Definition assets.cpp:317
asw::Sample load_sample(const std::string &filename)
Loads a sample from a file. Formats supported are WAV, AIFF, RIFF, OGG and VOC. This will abort if th...
Definition assets.cpp:255
asw::Font get_font(const std::string &key)
Get a cached font.
Definition assets.cpp:237
asw::Texture load_texture(const std::string &filename)
Loads a texture from a file. Formats supported are PNG, ICO, CUR, BMP, GIF, JPG, LBM,...
Definition assets.cpp:73
asw::Renderer * get_renderer()
Get the SDL renderer.
Definition display.cpp:139
void clear_text_cache()
Clear cached rendered text textures.
Definition draw.cpp:475
float falloff(float t, asw::Falloff mode)
Definition easing.cpp:160
MIX_Mixer * get_mixer()
Get the SDL mixer device.
Definition sound.cpp:439
uint32_t _get_session()
Get the number of times the sound module has been shut down.
Definition sound.cpp:460
void clear_text_size_cache()
Clear cached text metrics.
Definition util.cpp:115
void abort_on_error(const std::string &message)
Abort program and display error message.
Definition util.cpp:67
std::shared_ptr< TTF_Font > Font
Alias for a shared pointer to an TTF_Font.
Definition types.h:79
std::shared_ptr< MIX_Audio > Sample
Alias for a shared pointer to an MIX_Audio.
Definition types.h:82
std::shared_ptr< SDL_Texture > Texture
Alias for a shared pointer to an SDL_Texture.
Definition types.h:76
std::shared_ptr< MIX_Audio > Music
Alias for a shared pointer to an MIX_Audio.
Definition types.h:85
Falloff
How a gradient or light fades from its centre to its edge.
Definition types.h:46
FontStyle
How a font's glyphs are rendered.
Definition types.h:65
Sound module for the ASW library.
RGBA color struct with 8-bit channels.
Definition color.h:18
uint8_t b
Definition color.h:32
uint8_t r
Definition color.h:30
uint8_t g
Definition color.h:31
uint8_t a
Definition color.h:33
Types used throughout the ASW library.
General utility functions.