Search the paths defined in a game's configuration for JSON files and merge them into the game's configuration. This allows a build to load config specific to that build by packaging the override JSON in the distribution. Remove rules for loading Android and WASM config files in favor of using the search path. Add all searched JSON to the auto config refresh list when auto refresh is enabled. Modify sb::glob so that it requires a directory argument and a pattern to search for within the directory. Add a test for config search path lookup.
859 lines
29 KiB
C++
859 lines
29 KiB
C++
/* +=======================================================+
|
|
____/ \____ /: Open source game framework licensed to freely use, :
|
|
\ / / : copy, and modify - created for dank.game :
|
|
+==\ ^__^ /==+ : :
|
|
: ~/ \~ : : Download at https://open.shampoo.ooo/shampoo/spacebox :
|
|
: ~~~~~~~~~~~~ : +=======================================================+
|
|
: SPACE ~~~~~ : /
|
|
: ~~~~~~~ BOX :/
|
|
+=============*/
|
|
|
|
#include "Pixels.hpp"
|
|
#include "extension.hpp"
|
|
|
|
/* Edit a vector in place, giving it the specified magnitude while maintaining the direction */
|
|
void sb::set_magnitude(glm::vec2& vector, float magnitude)
|
|
{
|
|
vector = glm::normalize(vector) * magnitude;
|
|
}
|
|
|
|
Box sb::get_texture_box(SDL_Texture* texture)
|
|
{
|
|
int width, height;
|
|
SDL_QueryTexture(texture, nullptr, nullptr, &width, &height);
|
|
return Box(glm::vec2(0, 0), glm::vec2(width, height));
|
|
}
|
|
|
|
glm::vec2 sb::fit_and_preserve_aspect(const glm::vec2& inner, const glm::vec2& outer)
|
|
{
|
|
glm::vec2 delta = inner - outer;
|
|
float aspect = inner.x / inner.y;
|
|
glm::vec2 fit;
|
|
if (delta.x > delta.y)
|
|
{
|
|
fit.x = outer.x;
|
|
fit.y = fit.x / aspect;
|
|
}
|
|
else
|
|
{
|
|
fit.y = outer.y;
|
|
fit.x = fit.y * aspect;
|
|
}
|
|
return fit;
|
|
}
|
|
|
|
std::vector<std::vector<Box>> sb::get_blinds_boxes(glm::vec2 size, float step, int count)
|
|
{
|
|
std::vector<Box> blinds;
|
|
float blind_height = size.y / count;
|
|
for (int ii = 1; ii <= count; ii++)
|
|
{
|
|
blinds.push_back(Box({0, blind_height * ii}, {size.x, 0}));
|
|
}
|
|
float inflate_per_frame = blind_height * step;
|
|
std::vector<std::vector<Box>> frames;
|
|
float bottom_save;
|
|
while (blinds[0].height() < blind_height)
|
|
{
|
|
frames.push_back({});
|
|
for (Box& blind : blinds)
|
|
{
|
|
bottom_save = blind.bottom();
|
|
blind.expand({0, inflate_per_frame});
|
|
blind.bottom(bottom_save);
|
|
frames.back().push_back(blind);
|
|
}
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
void sb::populate_pixel_2d_array(SDL_Renderer* renderer, SDL_Texture* texture, std::vector<std::vector<SDL_Color>>& pixels)
|
|
{
|
|
populate_pixel_2d_array(renderer, texture, pixels, get_texture_box(texture));
|
|
}
|
|
|
|
void sb::populate_pixel_2d_array(
|
|
SDL_Renderer* renderer, SDL_Texture* texture, std::vector<std::vector<SDL_Color>>& pixels, const Box& region)
|
|
{
|
|
int access;
|
|
if (SDL_QueryTexture(texture, nullptr, &access, nullptr, nullptr) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not query texture for access flag");
|
|
}
|
|
else
|
|
{
|
|
if (access != SDL_TEXTUREACCESS_TARGET)
|
|
{
|
|
texture = duplicate_texture(renderer, texture);
|
|
}
|
|
if (SDL_SetRenderTarget(renderer, texture) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not set render target");
|
|
}
|
|
else
|
|
{
|
|
Uint32 format = SDL_PIXELFORMAT_RGBA32;
|
|
int bytes_per_pixel = SDL_BYTESPERPIXEL(format);
|
|
int bytes_per_row = bytes_per_pixel * region.width();
|
|
int bytes_total = bytes_per_row * region.height();
|
|
Uint8* source = new Uint8[bytes_total];
|
|
SDL_Rect int_rect = region;
|
|
if (SDL_RenderReadPixels(renderer, &int_rect, format, source, bytes_per_row) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not read pixels after setting remapped texture as target");
|
|
}
|
|
else
|
|
{
|
|
pixels.reserve(region.width());
|
|
for (int x = 0; x < region.width(); x++)
|
|
{
|
|
std::vector<SDL_Color> column;
|
|
pixels.push_back(column);
|
|
pixels[x].reserve(region.height());
|
|
}
|
|
for (int y = 0, ii = 0; y < region.height(); y++)
|
|
{
|
|
for (int x = 0; x < region.width(); x++)
|
|
{
|
|
pixels[x][y] = {source[ii++], source[ii++], source[ii++], source[ii++]};
|
|
}
|
|
}
|
|
}
|
|
delete[] source;
|
|
}
|
|
}
|
|
}
|
|
|
|
std::vector<SDL_Texture*> sb::get_halo_frames(
|
|
SDL_Renderer* renderer, float radius, int segment_count, const std::vector<Color>& colors, float min_radius, bool fade)
|
|
{
|
|
std::vector<SDL_Texture*> frames;
|
|
frames.reserve(segment_count);
|
|
SDL_Texture* frame;
|
|
float alpha = 255, alpha_step = 255.0f / segment_count, segment_radius;
|
|
int color_count = colors.size();
|
|
Color color;
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
|
for (int color_offset = 0; color_offset < color_count; color_offset++)
|
|
{
|
|
if (fade)
|
|
{
|
|
alpha = alpha_step;
|
|
}
|
|
frame = get_filled_texture(renderer, {2 * radius, 2 * radius}, {0, 0, 0, 0});
|
|
SDL_SetTextureBlendMode(frame, SDL_BLENDMODE_BLEND);
|
|
SDL_SetRenderTarget(renderer, frame);
|
|
for (int segment_ii = 0; segment_ii < segment_count; segment_ii++)
|
|
{
|
|
color = colors[(color_offset + segment_ii) % color_count];
|
|
color.a = std::round(alpha);
|
|
segment_radius = min_radius + (segment_count - 1.0f - segment_ii) / (segment_count - 1.0f) * (radius - min_radius);
|
|
filledCircleRGBA(renderer, radius, radius, static_cast<int>(std::round(segment_radius)),
|
|
color.r, color.g, color.b, color.a);
|
|
if (fade)
|
|
{
|
|
alpha += alpha_step;
|
|
}
|
|
}
|
|
frames.push_back(frame);
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
std::vector<SDL_Texture*> sb::get_portal_frames(
|
|
SDL_Renderer* renderer, glm::vec2 size, float hue_start, float hue_end, int dy, int count)
|
|
{
|
|
std::vector<SDL_Texture*> frames;
|
|
frames.reserve(count);
|
|
float y_margin = 10;
|
|
float max_y = size.y - y_margin;
|
|
std::vector<float> hues = range_count(hue_start, hue_end, count);
|
|
SDL_Texture* frame;
|
|
Color color;
|
|
for (int frame_ii = 0; frame_ii < count; frame_ii++)
|
|
{
|
|
frame = get_filled_texture(renderer, size, {255, 255, 255, 0});
|
|
SDL_SetRenderTarget(renderer, frame);
|
|
SDL_SetTextureBlendMode(frame, SDL_BLENDMODE_BLEND);
|
|
for (int ellipse_ii = 0, y = max_y; y > y_margin - 3; ellipse_ii++, y -= dy)
|
|
{
|
|
color.a = y / max_y * 255.0f;
|
|
color.hsv(hues[glm::mod(ellipse_ii - frame_ii, count)]);
|
|
aaFilledEllipseRGBA(renderer, size.x / 2, y, size.x / 2, y_margin - 3, color.r, color.g, color.b, color.a);
|
|
}
|
|
frames.push_back(frame);
|
|
}
|
|
return frames;
|
|
}
|
|
|
|
void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, const SDL_Color& color, const Box& box)
|
|
{
|
|
SDL_SetRenderTarget(renderer, texture);
|
|
SDL_SetRenderDrawBlendMode(renderer, SDL_BLENDMODE_NONE);
|
|
SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
|
SDL_RenderFillRectF(renderer, &box);
|
|
}
|
|
|
|
void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, const SDL_Color& color)
|
|
{
|
|
fill_texture(renderer, texture, color, get_texture_box(texture));
|
|
}
|
|
|
|
void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_Texture* tile, const Box& box)
|
|
{
|
|
Box texture_box = get_texture_box(texture), tile_box = get_texture_box(tile);
|
|
SDL_FRect draw_rect;
|
|
if (SDL_SetRenderTarget(renderer, texture) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not set render target");
|
|
}
|
|
else
|
|
{
|
|
SDL_Rect int_rect = box;
|
|
if (SDL_RenderSetClipRect(renderer, &int_rect) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not set clip");
|
|
}
|
|
else
|
|
{
|
|
for (int x = 0; x < texture_box.width(); x += tile_box.width())
|
|
{
|
|
for (int y = 0; y < texture_box.height(); y += tile_box.height())
|
|
{
|
|
draw_rect = {(float) x, (float) y, tile_box.width(), tile_box.height()};
|
|
SDL_RenderCopyF(renderer, tile, nullptr, &draw_rect);
|
|
}
|
|
}
|
|
SDL_RenderSetClipRect(renderer, nullptr);
|
|
}
|
|
}
|
|
}
|
|
|
|
void sb::fill_texture(SDL_Renderer* renderer, SDL_Texture* texture, SDL_Texture* tile)
|
|
{
|
|
fill_texture(renderer, texture, tile, get_texture_box(texture));
|
|
}
|
|
|
|
SDL_Texture* sb::get_filled_texture(SDL_Renderer* renderer, glm::vec2 size, const SDL_Color& color, Uint32 format)
|
|
{
|
|
SDL_Texture* texture;
|
|
if ((texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y)) == nullptr)
|
|
{
|
|
sb::Log::sdl_error("could not create texture to fill");
|
|
}
|
|
else
|
|
{
|
|
fill_texture(renderer, texture, color);
|
|
}
|
|
return texture;
|
|
}
|
|
|
|
SDL_Texture* sb::get_filled_texture(SDL_Renderer* renderer, glm::vec2 size, SDL_Texture* tile, Uint32 format)
|
|
{
|
|
SDL_Texture* texture;
|
|
if ((texture = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y)) == nullptr)
|
|
{
|
|
sb::Log::sdl_error("could not create texture to fill");
|
|
}
|
|
else
|
|
{
|
|
fill_texture(renderer, texture, tile);
|
|
}
|
|
return texture;
|
|
}
|
|
|
|
SDL_Texture* sb::get_hue_shifted_texture(SDL_Renderer* renderer, SDL_Texture* base, float offset)
|
|
{
|
|
SDL_Texture* hue_shifted_texture = duplicate_texture(renderer, base);
|
|
Uint32 pixel_format;
|
|
int w, h;
|
|
if (SDL_QueryTexture(hue_shifted_texture, &pixel_format, nullptr, &w, &h) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not query texture");
|
|
}
|
|
else
|
|
{
|
|
SDL_PixelFormat* pixel_format_struct = SDL_AllocFormat(pixel_format);
|
|
SDL_SetRenderTarget(renderer, hue_shifted_texture);
|
|
int bytes_per_pixel = SDL_BYTESPERPIXEL(pixel_format);
|
|
int bytes_per_row = bytes_per_pixel * w;
|
|
int bytes_total = bytes_per_row * h;
|
|
int length = bytes_total / 4 + (bytes_total % 4 ? 1 : 0);
|
|
Uint32* pixels = new Uint32[length];
|
|
if (SDL_RenderReadPixels(renderer, NULL, pixel_format, pixels, bytes_per_row) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not read pixels");
|
|
}
|
|
else
|
|
{
|
|
Color rgba;
|
|
for (int ii = 0; ii < length; ii++)
|
|
{
|
|
SDL_GetRGBA(pixels[ii], const_cast<const SDL_PixelFormat*>(pixel_format_struct),
|
|
&rgba.r, &rgba.g, &rgba.b, &rgba.a);
|
|
rgba.shift_hue(offset);
|
|
pixels[ii] = SDL_MapRGBA(const_cast<const SDL_PixelFormat*>(pixel_format_struct), rgba.r, rgba.g, rgba.b, rgba.a);
|
|
}
|
|
if (SDL_UpdateTexture(hue_shifted_texture, NULL, pixels, bytes_per_row) < 0)
|
|
{
|
|
sb::Log::sdl_error("Could not apply hue shifted pixels update to texture");
|
|
}
|
|
}
|
|
delete[] pixels;
|
|
SDL_FreeFormat(pixel_format_struct);
|
|
}
|
|
return hue_shifted_texture;
|
|
}
|
|
|
|
SDL_Texture* sb::duplicate_texture(SDL_Renderer* renderer, SDL_Texture* base)
|
|
{
|
|
Box box = get_texture_box(base);
|
|
return duplicate_texture(renderer, base, box.size());
|
|
}
|
|
|
|
SDL_Texture* sb::duplicate_texture(SDL_Renderer* renderer, SDL_Texture* base, const glm::vec2& size)
|
|
{
|
|
SDL_BlendMode original_blend_mode;
|
|
SDL_GetTextureBlendMode(base, &original_blend_mode);
|
|
Uint32 format;
|
|
SDL_QueryTexture(base, &format, nullptr, nullptr, nullptr);
|
|
SDL_Texture* duplicate = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y);
|
|
if (duplicate == NULL)
|
|
{
|
|
sb::Log::sdl_error("could not create texture from base");
|
|
return NULL;
|
|
}
|
|
if ((SDL_SetRenderTarget(renderer, duplicate)) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not set render target to duplicate");
|
|
return NULL;
|
|
}
|
|
SDL_SetTextureBlendMode(base, SDL_BLENDMODE_NONE);
|
|
SDL_SetTextureBlendMode(duplicate, SDL_BLENDMODE_BLEND);
|
|
if ((SDL_RenderCopyF(renderer, base, nullptr, nullptr)) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not render base onto duplicate");
|
|
return nullptr;
|
|
}
|
|
SDL_SetTextureBlendMode(base, original_blend_mode);
|
|
SDL_SetTextureBlendMode(duplicate, original_blend_mode);
|
|
return duplicate;
|
|
}
|
|
|
|
SDL_Texture* sb::get_remapped_texture(
|
|
SDL_Renderer* renderer, SDL_Texture* base, const std::map<Color, Color>& map)
|
|
{
|
|
SDL_Texture* remapped = duplicate_texture(renderer, base);
|
|
if (remapped == nullptr)
|
|
{
|
|
sb::Log::sdl_error("could not duplicate base texture");
|
|
return nullptr;
|
|
}
|
|
if ((SDL_SetRenderTarget(renderer, remapped)) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not set render target to remapped texture");
|
|
return nullptr;
|
|
}
|
|
Pixels pixels = Pixels(renderer, remapped);
|
|
for (int x = 0; x < pixels.rect.w; x++)
|
|
{
|
|
for (int y = 0; y < pixels.rect.h; y++)
|
|
{
|
|
for (auto& [original, replacement] : map)
|
|
{
|
|
if (pixels.get(x, y) == original)
|
|
{
|
|
pixels.set(replacement, x, y);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
pixels.apply();
|
|
return remapped;
|
|
}
|
|
|
|
SDL_Texture* sb::get_remapped_texture(
|
|
SDL_Renderer* renderer, const std::string& path, const std::map<Color, Color>& map)
|
|
{
|
|
SDL_Texture* base = IMG_LoadTexture(renderer, path.c_str());
|
|
if (base == nullptr)
|
|
{
|
|
sb::Log::sdl_error("error loading file");
|
|
return nullptr;
|
|
}
|
|
SDL_Texture* remapped = get_remapped_texture(renderer, base, map);
|
|
if (remapped == nullptr)
|
|
{
|
|
sb::Log::log("could not remap texture", sb::Log::ERR);
|
|
return nullptr;
|
|
}
|
|
SDL_DestroyTexture(base);
|
|
return remapped;
|
|
}
|
|
|
|
#include "superxbr.cpp"
|
|
|
|
/*
|
|
- Base texture must be set to SDL_TEXTUREACCESS_TARGET
|
|
- Scale2x implementation based on http://www.scale2x.it/algorithm.html
|
|
*/
|
|
SDL_Texture* sb::get_pixel_scaled_texture(SDL_Renderer* renderer, SDL_Texture* base, int count, int version)
|
|
{
|
|
if ((SDL_SetRenderTarget(renderer, base)) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not set render target to remapped texture");
|
|
return nullptr;
|
|
}
|
|
glm::ivec2 size = get_texture_box(base).size();
|
|
Uint32 format = SDL_PIXELFORMAT_RGBA32;
|
|
int bytes_per_pixel, bytes_per_row, bytes_total [[maybe_unused]];
|
|
Uint32 *src, *dst, *src_begin, *dst_begin;
|
|
for (int ii = 0; ii < count; ii++, size *= 2)
|
|
{
|
|
bytes_per_pixel = SDL_BYTESPERPIXEL(format);
|
|
bytes_per_row = bytes_per_pixel * size.x;
|
|
bytes_total = bytes_per_row * size.y;
|
|
if (ii == 0)
|
|
{
|
|
src = new Uint32[size.x * size.y];
|
|
src_begin = src;
|
|
if ((SDL_RenderReadPixels(renderer, NULL, format, src, bytes_per_row)) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not read pixels after setting remapped texture as target");
|
|
return NULL;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
src = dst_begin;
|
|
src_begin = src;
|
|
}
|
|
dst = new Uint32[size.x * size.y * 4];
|
|
dst_begin = dst;
|
|
if (version == scaler::scale2x)
|
|
{
|
|
Uint32 A [[maybe_unused]], B, C [[maybe_unused]], D, E, F, G [[maybe_unused]], H, I [[maybe_unused]];
|
|
for (int y = 0; y < size.y; y++)
|
|
{
|
|
for (int x = 0; x < size.x; x++)
|
|
{
|
|
E = *src;
|
|
B = y == 0 ? E : *(src - size.x);
|
|
D = x == 0 ? E : *(src - 1);
|
|
F = x == size.x - 1 ? E : *(src + 1);
|
|
H = y == size.y - 1 ? E : *(src + size.x);
|
|
if (y != 0 && x != 0 && y != size.y - 1 && x != size.x - 1)
|
|
{
|
|
A = *(src - size.x - 1);
|
|
C = *(src - size.x + 1);
|
|
G = *(src + size.x - 1);
|
|
I = *(src + size.x + 1);
|
|
}
|
|
if (x == 0)
|
|
{
|
|
A = B;
|
|
G = H;
|
|
}
|
|
if (y == 0)
|
|
{
|
|
A = D;
|
|
C = F;
|
|
}
|
|
if (x == size.x - 1)
|
|
{
|
|
C = B;
|
|
I = H;
|
|
}
|
|
if (y == size.y - 1)
|
|
{
|
|
G = D;
|
|
I = F;
|
|
}
|
|
if (B != H && D != F)
|
|
{
|
|
*dst = D == B ? D : E;
|
|
*(dst + 1) = B == F ? F : E;
|
|
*(dst + 2 * size.x) = D == H ? D : E;
|
|
*(dst + 2 * size.x + 1) = H == F ? F : E;
|
|
}
|
|
else
|
|
{
|
|
*dst = E;
|
|
*(dst + 1) = E;
|
|
*(dst + 2 * size.x) = E;
|
|
*(dst + 2 * size.x + 1) = E;
|
|
}
|
|
src++;
|
|
dst += 2;
|
|
}
|
|
dst += 2 * size.x;
|
|
}
|
|
}
|
|
else if (version == scaler::xbr)
|
|
{
|
|
scaleSuperXBRT<2>(src, dst, size.x, size.y);
|
|
}
|
|
delete[] src_begin;
|
|
}
|
|
SDL_Texture* scaled = SDL_CreateTexture(renderer, format, SDL_TEXTUREACCESS_TARGET, size.x, size.y);
|
|
if (scaled == nullptr)
|
|
{
|
|
sb::Log::sdl_error("could not create scaled texture");
|
|
}
|
|
if (SDL_UpdateTexture(scaled, nullptr, dst_begin, bytes_per_row * 2) < 0)
|
|
{
|
|
sb::Log::sdl_error("could not copy pixels to scaled texture");
|
|
}
|
|
delete[] dst_begin;
|
|
return scaled;
|
|
}
|
|
|
|
std::vector<fs::path> sb::glob(const fs::path& query, const fs::path& search_path)
|
|
{
|
|
/* Try to open a directory and throw an exception if the directory cannot be opened. */
|
|
fs::directory_iterator directory;
|
|
try
|
|
{
|
|
directory = fs::directory_iterator {search_path};
|
|
}
|
|
catch (fs::filesystem_error error)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not search non-existent directory " << search_path << ". " << error.what();
|
|
throw fs::filesystem_error(message.str(), error.code());
|
|
}
|
|
|
|
/* Create a new list to record matches to. */
|
|
std::vector<fs::path> files;
|
|
|
|
/* Turn the given pattern into a regex. */
|
|
std::regex expression(query.string(), std::regex_constants::icase);
|
|
|
|
/* Check every file path in the directory to see if matches the given pattern. */
|
|
for (const fs::directory_entry& entry : directory)
|
|
{
|
|
if (std::regex_match(entry.path().string(), expression))
|
|
{
|
|
files.push_back(entry.path());
|
|
}
|
|
}
|
|
|
|
/* Sort lexicographically */
|
|
std::sort(files.begin(), files.end());
|
|
|
|
return files;
|
|
}
|
|
|
|
SDL_Surface* sb::get_surface_from_pixels(Pixels& pixels)
|
|
{
|
|
SDL_Surface* surface = SDL_CreateRGBSurfaceFrom(
|
|
pixels.source, pixels.rect.w, pixels.rect.h, pixels.format->BitsPerPixel,
|
|
pixels.get_bytes_per_row(), pixels.format->Rmask, pixels.format->Gmask, pixels.format->Bmask,
|
|
pixels.format->Amask);
|
|
if (surface == nullptr)
|
|
{
|
|
sb::Log::sdl_error("could not create RGB surface from texture pixel data");
|
|
}
|
|
else
|
|
{
|
|
return surface;
|
|
}
|
|
return nullptr;
|
|
}
|
|
|
|
fs::path sb::get_next_file_name(fs::path directory, int zfill, std::string prefix, std::string extension)
|
|
{
|
|
std::stringstream file_pattern;
|
|
file_pattern << prefix << "([0-9]+)" << extension;
|
|
std::vector<fs::path> files = glob(file_pattern.str(), directory);
|
|
int index = 1;
|
|
if (files.size())
|
|
{
|
|
const std::string last = files.back().string();
|
|
std::smatch matches;
|
|
std::regex_match(last, matches, std::regex((directory / file_pattern.str()).string()));
|
|
index = std::stoi(matches[1]) + 1;
|
|
}
|
|
std::stringstream filename;
|
|
fs::path path;
|
|
do
|
|
{
|
|
filename << prefix << pad(index++, zfill) << extension;
|
|
path = directory / filename.str();
|
|
filename.str("");
|
|
filename.clear();
|
|
}
|
|
while (fs::exists(path));
|
|
return path;
|
|
}
|
|
|
|
std::string sb::file_to_string(const fs::path& path)
|
|
{
|
|
std::string contents = "";
|
|
|
|
#if !defined(__ANDROID__)
|
|
|
|
if (!fs::exists(path))
|
|
{
|
|
std::ostringstream message;
|
|
message << "No file found at " << path;
|
|
throw std::runtime_error(message.str());
|
|
}
|
|
else
|
|
{
|
|
std::ifstream file;
|
|
file.open(path);
|
|
if (!file.is_open())
|
|
{
|
|
std::ostringstream message;
|
|
message << "Failed to open " << path;
|
|
throw std::runtime_error(message.str());
|
|
}
|
|
else
|
|
{
|
|
/* Read file using std::string's range constructor, from the beginning of the file stream to end of stream
|
|
* (represented by {} (?)) */
|
|
contents = std::string(std::istreambuf_iterator(file), {});
|
|
std::size_t size = file.tellg();
|
|
std::ostringstream message;
|
|
message << "Opened file " << path << " (" << (size / 1000.0f) << "KB)";
|
|
sb::Log::log(message);
|
|
sb::Log::log(contents, sb::Log::Level::DEBUG);
|
|
}
|
|
}
|
|
|
|
#else
|
|
|
|
std::unique_ptr<SDL_RWops, decltype(&SDL_RWclose)> sdl_rw(SDL_RWFromFile(path.c_str(), "r"), SDL_RWclose);
|
|
if (sdl_rw.get() == nullptr)
|
|
{
|
|
__android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "Unable to open file %s", SDL_GetError());
|
|
}
|
|
else
|
|
{
|
|
int byte_count = SDL_RWsize(sdl_rw.get());
|
|
__android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "File at %s is %fKB", path.c_str(), (byte_count / 1000.0f));
|
|
|
|
contents.resize(byte_count);
|
|
|
|
int nb_read_total = 0, nb_read = 1;
|
|
while (nb_read_total < byte_count && nb_read != 0)
|
|
{
|
|
nb_read = SDL_RWread(sdl_rw.get(), &contents[nb_read_total], 1, (byte_count - nb_read_total));
|
|
nb_read_total += nb_read;
|
|
}
|
|
if (nb_read_total != byte_count)
|
|
{
|
|
__android_log_print(
|
|
ANDROID_LOG_VERBOSE, "spacebox",
|
|
"File could not be read because of a mismatch in file size and number of bytes read");
|
|
}
|
|
else
|
|
{
|
|
__android_log_print(ANDROID_LOG_VERBOSE, "spacebox", "%s", contents.c_str());
|
|
}
|
|
}
|
|
|
|
#endif
|
|
|
|
return contents;
|
|
}
|
|
|
|
fs::path sb::copy_file(fs::path from, fs::path to, bool overwrite_ok)
|
|
{
|
|
/* Open source */
|
|
SDL_RWops* source_rw;
|
|
fs::path destination;
|
|
if ((source_rw = SDL_RWFromFile(from.string().c_str(), "r")) != nullptr)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Copying " << (SDL_RWsize(source_rw) / 1000) << " KB from " << from << " to " << to;
|
|
sb::Log::log(message);
|
|
|
|
/* Allocate storage for source contents */
|
|
std::string content;
|
|
content.resize(SDL_RWsize(source_rw));
|
|
|
|
/* Read entire contents in one call and ensure the entire size in bytes was read. */
|
|
if (SDL_RWread(source_rw, content.data(), 1, SDL_RWsize(source_rw)) == static_cast<std::size_t>(SDL_RWsize(source_rw)))
|
|
{
|
|
SDL_RWclose(source_rw);
|
|
|
|
/* Append the filename of the source file to the destination path if destination is a directory. */
|
|
if (fs::is_directory(to))
|
|
{
|
|
to /= from.filename();
|
|
}
|
|
|
|
if (!fs::exists(to) || overwrite_ok)
|
|
{
|
|
/* Open destination, write entire contents in one call and ensure the entire size in bytes was written. */
|
|
SDL_RWops* to_rw;
|
|
if ((to_rw = SDL_RWFromFile(to.string().c_str(), "w")) != nullptr)
|
|
{
|
|
if ((SDL_RWwrite(to_rw, content.data(), 1, content.size())) == content.size())
|
|
{
|
|
std::ostringstream message;
|
|
message << "Wrote CA bundle to internal storage at " << to;
|
|
sb::Log::log(message);
|
|
destination = to;
|
|
}
|
|
else
|
|
{
|
|
sb::Log::sdl_error("Error writing to internal storage");
|
|
}
|
|
SDL_RWclose(to_rw);
|
|
}
|
|
else
|
|
{
|
|
sb::Log::sdl_error("Error opening internal storage for writing");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb::Log::log("Could not copy file: destination already exists and overwrite was not set", sb::Log::WARN);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb::Log::sdl_error("Error reading file");
|
|
}
|
|
}
|
|
else
|
|
{
|
|
sb::Log::sdl_error("Error getting file handle");
|
|
}
|
|
return destination;
|
|
}
|
|
|
|
nlohmann::json sb::json_from_file(const fs::path& path)
|
|
{
|
|
nlohmann::json json;
|
|
|
|
/* Load and parse JSON in the given path. If the file is not able to be loaded, the underlying function will throw
|
|
* an exception. If the file cannot be parsed as JSON, an exception will be thrown with an error message. */
|
|
std::string contents = sb::file_to_string(path);
|
|
try
|
|
{
|
|
json = nlohmann::json::parse(contents);
|
|
}
|
|
catch (const nlohmann::json::parse_error& error)
|
|
{
|
|
std::ostringstream message;
|
|
message << "Invalid JSON in " << path << ": " << error.what();
|
|
throw nlohmann::json::parse_error::create(101, error.byte, message.str(), nullptr);
|
|
}
|
|
|
|
return json;
|
|
}
|
|
|
|
nlohmann::json sb::json_from_file(const std::string& path)
|
|
{
|
|
return json_from_file(fs::path(path));
|
|
}
|
|
|
|
nlohmann::json sb::json_from_file(const char* path)
|
|
{
|
|
return json_from_file(fs::path(path));
|
|
}
|
|
|
|
std::shared_ptr<SDL_Surface> sb::extract_area(const std::shared_ptr<SDL_Surface>& source, const sb::Box& area)
|
|
{
|
|
/* Create a destination surface with the same format as the source and is the size of the tile. */
|
|
std::shared_ptr<SDL_Surface> destination {
|
|
SDL_CreateRGBSurfaceWithFormat(source->flags, area.w, area.h, source->format->BitsPerPixel, source->format->format), SDL_FreeSurface};
|
|
|
|
if (destination.get() != nullptr)
|
|
{
|
|
/* Blit the source onto the destination */
|
|
SDL_Rect rect = area;
|
|
if (SDL_BlitSurface(source.get(), &rect, destination.get(), nullptr) == 0)
|
|
{
|
|
/* Rotate and mirror the tile for compatibility with OpenGL */
|
|
std::shared_ptr<SDL_Surface> flipped {rotozoomSurfaceXY(destination.get(), 0, 1, -1, 0), SDL_FreeSurface};
|
|
if (flipped.get() != nullptr)
|
|
{
|
|
return flipped;
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not rotate source surface for tile extraction. " << SDL_GetError();
|
|
throw std::runtime_error(message.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not blit source pixels to destination surface for tile extraction. " << SDL_GetError();
|
|
throw std::runtime_error(message.str());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
std::ostringstream message;
|
|
message << "Could not create destination surface for tile extraction. " << SDL_GetError();
|
|
throw std::runtime_error(message.str());
|
|
}
|
|
}
|
|
|
|
std::vector<std::shared_ptr<SDL_Surface>> sb::extract_tiles_by_count(const std::shared_ptr<SDL_Surface>& source, const glm::ivec2& count)
|
|
{
|
|
glm::ivec2 source_size {source->w, source->h};
|
|
|
|
/* Get the floor of the division of the source size by the tile count. Then add one to each dimension that has any remainder. This leads
|
|
* to a tile size that may be bigger than the available pixels in the last tile in the dimension. In that case, the last tile will contain
|
|
* the rest of the available pixels and will be smaller than the tile size. */
|
|
glm::ivec2 tile_size {source_size / count + glm::ivec2(glm::bvec2(glm::mod(glm::fvec2(source_size), glm::fvec2(count))))};
|
|
|
|
/* Use SDL coordinate system for the area box, meaning at the top of the image y=0, and at the bottom y=height. The Box class uses GL
|
|
* coordinates by default, but for image manipulation and SDL surfaces, the Box class supports flipping the Y-axis to use SDL coordinates. */
|
|
sb::Box area {{0.0f, 0.0f}, tile_size, false};
|
|
|
|
/* Iterate over rows and columns, moving the area by one tile size each iteration and copying that area into a new surface in the vector
|
|
* of tiles. */
|
|
std::vector<std::shared_ptr<SDL_Surface>> tiles;
|
|
for (int y = 0; y < count.y; y++)
|
|
{
|
|
area.left(0.0f);
|
|
for (int x = 0; x < count.x; x++)
|
|
{
|
|
tiles.push_back(sb::extract_area(source, area));
|
|
area.move({tile_size.x, 0.0f});
|
|
}
|
|
area.move({0.0f, tile_size.y});
|
|
}
|
|
|
|
return tiles;
|
|
}
|
|
|
|
int SDL_SetRenderDrawColor(SDL_Renderer* renderer, const Color& color)
|
|
{
|
|
return SDL_SetRenderDrawColor(renderer, color.r, color.g, color.b, color.a);
|
|
}
|
|
|
|
int SDL_RenderFillRect(SDL_Renderer* renderer, const Box& box)
|
|
{
|
|
SDL_Rect rect = box;
|
|
return SDL_RenderFillRect(renderer, &rect);
|
|
}
|
|
|
|
int lineColor(SDL_Renderer* renderer, const Segment& segment, const Color& color, std::uint8_t thickness)
|
|
{
|
|
if (thickness == 1)
|
|
{
|
|
return lineColor(renderer, segment.start().x, segment.start().y, segment.end().x, segment.end().y, color);
|
|
}
|
|
else
|
|
{
|
|
return thickLineColor(renderer, segment.start().x, segment.start().y, segment.end().x, segment.end().y, thickness, color);
|
|
}
|
|
}
|
|
|
|
std::ostream& std::operator<<(std::ostream& out, const SDL_Color& color)
|
|
{
|
|
out << "{" << static_cast<int>(color.r) << ", " << static_cast<int>(color.g) << ", " <<
|
|
static_cast<int>(color.b) << ", " << static_cast<int>(color.a) << "}";
|
|
return out;
|
|
}
|