Skip to content
Merged
66 changes: 66 additions & 0 deletions client/src/FileGroup.lua
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
local class = require("common.lib.class")
local FileUtils = require("client.src.FileUtils")

---@param filename1 string
---@param filename2 string
---@return string # the filename that won the collision
local function resolveCollision(filename1, filename2)
-- TODO: establish a consistent priority by something more coherent than string sort?
-- e.g. going by extension, preferring lossless over lossy, better compression over worse
-- could also prefer leading 0 over non-leading etc.
return filename1
end

---@return string[]
local function indexMatchingFiles(matchingFiles, pattern, separator)
table.sort(matchingFiles)
local indexed = {}
local patternLength = pattern:len()
local separatorLength = separator:len()
local index
for i, filename in ipairs(matchingFiles) do
local cut = FileUtils.getFileNameWithoutExtension(filename)
cut = cut:sub(patternLength + separatorLength + 1)
if cut:len() == 0 then
index = 1
else
index = tonumber(cut)
end
---@cast index integer
if not indexed[index] then
indexed[index] = filename
else
-- oh no, we have a collision
indexed[index] = resolveCollision(indexed[index], filename)
end
end

return indexed
end

-- A FileGroup is a group of files matching a certain pattern
-- beyond the pattern they are in particular numbered with integers
-- creating a FileGroup makes all file names belonging to the group available in matchingFiles
-- a concrete assignment of indices has happened in indexedFiles; \n
-- in the process, files that have the same index will get eliminated until only one is left for each index
---@class FileGroup
---@field path string
---@field pattern string The pattern to search for as plain text; luaregex disabled
---@field validExtensions string[]
---@field separator string the separator by which alternative copies separate their index, default ""
---@field matchingFiles string[] the files that got matched during the load process of the FileGroup
---@field indexedFiles string[] the files indexed by their suffix; only one file per index guaranteed
---@overload fun(path: string, pattern: string, validExtensions: string[], separator: string?): FileGroup
local FileGroup = class(
function(self, path, pattern, validExtensions, separator)
self.path = path
self.pattern = pattern
self.validExtensions = validExtensions
self.separator = separator or ""

local files = FileUtils.getFilteredDirectoryItems(self.path, "file")
self.matchingFiles = FileUtils.getMatchingFiles(files, self.pattern, self.validExtensions, self.separator)
self.indexedFiles = indexMatchingFiles(self.matchingFiles, self.pattern, self.separator)
end)

return FileGroup
99 changes: 90 additions & 9 deletions client/src/FileUtils.lua
Original file line number Diff line number Diff line change
@@ -1,14 +1,20 @@
local logger = require("common.lib.logger")
local Replay = require("common.data.Replay")
local tableUtils = require("common.lib.tableUtils")

local PREFIX_OF_IGNORED_DIRECTORIES = "__"

-- Collection of functions for file operations
local fileUtils = {}

fileUtils.SUPPORTED_IMAGE_FORMATS = {".png", ".jpg", ".jpeg"}
fileUtils.SUPPORTED_SOUND_FORMATS = {".mp3", ".ogg", ".wav", ".it", ".flac"}

-- returns the directory items with a default filter and an optional filetype filter
-- by default, filters out everything starting with __ and Mac's .DS_Store file
-- optionally the result can be filtered to return only "file" or "directory" items
---@param path string
---@param fileType ("file" | "directory")?
function fileUtils.getFilteredDirectoryItems(path, fileType)
local results = {}

Expand Down Expand Up @@ -88,6 +94,9 @@ function fileUtils.recursiveRemoveFiles(folder, targetName)
end
end

-- returns the table for the deserialized json at the specified file path
---@param file string
---@return table? # nil if the file could not be read or deserialization failed
function fileUtils.readJsonFile(file)
if not love.filesystem.getInfo(file, "file") then
logger.debug("No file at specified path " .. file)
Expand All @@ -105,16 +114,16 @@ function fileUtils.readJsonFile(file)
logger.error("Error reading " .. file .. ":\n" .. errorMsg .. ":\n" .. fileContent)
return nil
else
---@cast value table
return value
end
end
end
end

local SUPPORTED_SOUND_FORMATS = {".mp3", ".ogg", ".wav", ".it", ".flac"}
--returns a source, or nil if it could not find a file
function fileUtils.loadSoundFromSupportExtensions(path_and_filename, streamed)
for k, extension in ipairs(SUPPORTED_SOUND_FORMATS) do
for k, extension in ipairs(fileUtils.SUPPORTED_SOUND_FORMATS) do
if love.filesystem.getInfo(path_and_filename .. extension) then
return love.audio.newSource(path_and_filename .. extension, streamed and "stream" or "static")
end
Expand All @@ -136,7 +145,7 @@ function fileUtils.findSound(sound_name, dirs_to_check, streamed)
end

function fileUtils.soundFileExists(soundName, path)
for _, extension in pairs(SUPPORTED_SOUND_FORMATS) do
for _, extension in pairs(fileUtils.SUPPORTED_SOUND_FORMATS) do
if love.filesystem.getInfo(path .. "/" .. soundName .. extension, "file") then
return true
end
Expand Down Expand Up @@ -169,17 +178,89 @@ function fileUtils.saveTextureToFile(texture, filePath, format)
love.filesystem.write(filePath .. "." .. format, data)
end

---@param replay Replay
function fileUtils.saveReplay(replay)
local path = replay:generatePath("/")
local filename = replay:generateFileName()
local replayJson = json.encode(replay)
-- TODO: This is for legacy support of the replay browser only;
-- as Replay is a common.data object, client should not use it to write client specific fields
Replay.lastPath = path
pcall(
function()
love.filesystem.createDirectory(path)
love.filesystem.write(path .. "/" .. filename .. ".json", replayJson)
fileUtils.writeJson(path, filename .. ".json", replay)
end

---@param files string[]
---@param pattern string
---@param validExtensions string[]
---@param separator string?
---@return string[] # files filtered down to only strings matching the specified pattern, separator and valid extension list
function fileUtils.getMatchingFiles(files, pattern, validExtensions, separator)
separator = separator or ""
local stringLen = string.len(pattern)
local matchedFiles = tableUtils.filter(files,
function(file)
local startIndex, endIndex = string.find(file, pattern, nil, true)
if not startIndex then
return false
elseif startIndex > 1 then
-- this means the name is prefixed with something else
return false
else
local goodExtension
-- this check is doubly good because it enforces lower case extensions even on windows
for i, extension in ipairs(validExtensions) do
local length = extension:len()
if file:sub(-length) == extension then
goodExtension = extension
break
end
end
if not goodExtension then
return false
else
-- now check for actual exact matching:
-- first cut off the matching part
local middlePart = file:sub(stringLen + 1)
-- and then the extension
middlePart = middlePart:sub(1, - goodExtension:len() - 1)
if middlePart:len() == 0 then
-- this is just the exact pattern + file extension
return true
else
local sepLen = separator:len()
if middlePart:sub(1, sepLen) ~= separator then
return false
else
local numberPart = middlePart:sub(sepLen + 1)
-- we need to string.match on top of casting tonumber because of Lua accepting scientific notation strings as numbers
if string.match(numberPart, "%d+") == numberPart and tonumber(numberPart) then
-- there are really only digits that form a number in the number part
return true
else
return false
end
end
end
end
end
)
end)

return matchedFiles
end

---@param path string
---@param data string
function fileUtils.write(path, filename, data)
love.filesystem.createDirectory(path)
local success, message = love.filesystem.write(path .. "/" .. filename, data)
if not success then
error("Failed to write to " .. path .. " : " .. message)
end
end

function fileUtils.writeJson(path, filename, tab)
local encoded = json.encode(tab)
---@cast encoded string # json.encode always returns a string if not called with a second argument
fileUtils.write(path, filename, encoded)
end

return fileUtils
8 changes: 4 additions & 4 deletions client/src/graphics/graphics_util.lua
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
local consts = require("common.engine.consts")
local logger = require("common.lib.logger")
local FileUtils = require("client.src.FileUtils")

-- Utility methods for drawing
local GraphicsUtil = {
Expand Down Expand Up @@ -62,7 +63,7 @@ function GraphicsUtil.privateLoadImageWithExtensionAndScale(pathAndName, extensi
end
return result
end

logger.error("Error loading image: " .. fileName .. " Check it is valid and try resaving it in an image editor. If you are not the owner please get them to update it or download the latest version.")
result = GraphicsUtil.privateLoadImageWithExtensionAndScale("themes/Panel Attack/transparent", ".png", 1)
assert(result ~= next)
Expand All @@ -72,10 +73,9 @@ function GraphicsUtil.privateLoadImageWithExtensionAndScale(pathAndName, extensi
return nil
end

local supportedScales = {3, 2, 1}
function GraphicsUtil.loadImageFromSupportedExtensions(pathAndName)
local supportedImageFormats = {".png", ".jpg", ".jpeg"}
local supportedScales = {3, 2, 1}
for _, extension in ipairs(supportedImageFormats) do
for _, extension in ipairs(FileUtils.SUPPORTED_IMAGE_FORMATS) do
for _, scale in ipairs(supportedScales) do
local image = GraphicsUtil.privateLoadImageWithExtensionAndScale(pathAndName, extension, scale)
if image then
Expand Down
Loading