mirror of https://github.com/OpenMW/openmw.git
Merge branch 'lua_files' into 'master'
Implement Lua API for VFS Closes #6864 See merge request OpenMW/openmw!3373macos_ci_fix
commit
5faf56950b
@ -0,0 +1,346 @@
|
||||
#include "vfsbindings.hpp"
|
||||
|
||||
#include <components/files/istreamptr.hpp>
|
||||
#include <components/resource/resourcesystem.hpp>
|
||||
#include <components/settings/values.hpp>
|
||||
#include <components/vfs/manager.hpp>
|
||||
#include <components/vfs/pathutil.hpp>
|
||||
|
||||
#include "../mwbase/environment.hpp"
|
||||
|
||||
#include "context.hpp"
|
||||
#include "luamanagerimp.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
namespace
|
||||
{
|
||||
// Too many arguments may cause stack corruption and crash.
|
||||
constexpr std::size_t sMaximumReadArguments = 20;
|
||||
|
||||
// Print a message if we read a large chunk of file to string.
|
||||
constexpr std::size_t sFileSizeWarningThreshold = 1024 * 1024;
|
||||
|
||||
struct FileHandle
|
||||
{
|
||||
public:
|
||||
FileHandle(Files::IStreamPtr stream, std::string_view fileName)
|
||||
{
|
||||
mFilePtr = std::move(stream);
|
||||
mFileName = fileName;
|
||||
}
|
||||
|
||||
Files::IStreamPtr mFilePtr;
|
||||
std::string mFileName;
|
||||
};
|
||||
|
||||
std::ios_base::seekdir getSeekDir(FileHandle& self, std::string_view whence)
|
||||
{
|
||||
if (whence == "cur")
|
||||
return std::ios_base::cur;
|
||||
if (whence == "set")
|
||||
return std::ios_base::beg;
|
||||
if (whence == "end")
|
||||
return std::ios_base::end;
|
||||
|
||||
throw std::runtime_error(
|
||||
"Error when handling '" + self.mFileName + "': invalid seek direction: '" + std::string(whence) + "'.");
|
||||
}
|
||||
|
||||
size_t getBytesLeftInStream(Files::IStreamPtr& file)
|
||||
{
|
||||
auto oldPos = file->tellg();
|
||||
file->seekg(0, std::ios_base::end);
|
||||
auto newPos = file->tellg();
|
||||
file->seekg(oldPos, std::ios_base::beg);
|
||||
|
||||
return newPos - oldPos;
|
||||
}
|
||||
|
||||
void printLargeDataMessage(FileHandle& file, size_t size)
|
||||
{
|
||||
if (!file.mFilePtr || !Settings::lua().mLuaDebug || size < sFileSizeWarningThreshold)
|
||||
return;
|
||||
|
||||
Log(Debug::Verbose) << "Read a large data chunk (" << size << " bytes) from '" << file.mFileName << "'.";
|
||||
}
|
||||
|
||||
sol::object readFile(LuaUtil::LuaState* lua, FileHandle& file)
|
||||
{
|
||||
std::ostringstream os;
|
||||
if (file.mFilePtr && file.mFilePtr->peek() != EOF)
|
||||
os << file.mFilePtr->rdbuf();
|
||||
|
||||
auto result = os.str();
|
||||
printLargeDataMessage(file, result.size());
|
||||
return sol::make_object<std::string>(lua->sol(), std::move(result));
|
||||
}
|
||||
|
||||
sol::object readLineFromFile(LuaUtil::LuaState* lua, FileHandle& file)
|
||||
{
|
||||
std::string result;
|
||||
if (file.mFilePtr && std::getline(*file.mFilePtr, result))
|
||||
{
|
||||
printLargeDataMessage(file, result.size());
|
||||
return sol::make_object<std::string>(lua->sol(), result);
|
||||
}
|
||||
|
||||
return sol::nil;
|
||||
}
|
||||
|
||||
sol::object readNumberFromFile(LuaUtil::LuaState* lua, Files::IStreamPtr& file)
|
||||
{
|
||||
double number = 0;
|
||||
if (file && *file >> number)
|
||||
return sol::make_object<double>(lua->sol(), number);
|
||||
|
||||
return sol::nil;
|
||||
}
|
||||
|
||||
sol::object readCharactersFromFile(LuaUtil::LuaState* lua, FileHandle& file, size_t count)
|
||||
{
|
||||
if (count <= 0 && file.mFilePtr->peek() != EOF)
|
||||
return sol::make_object<std::string>(lua->sol(), std::string());
|
||||
|
||||
auto bytesLeft = getBytesLeftInStream(file.mFilePtr);
|
||||
if (bytesLeft <= 0)
|
||||
return sol::nil;
|
||||
|
||||
if (count > bytesLeft)
|
||||
count = bytesLeft;
|
||||
|
||||
std::string result(count, '\0');
|
||||
if (file.mFilePtr->read(&result[0], count))
|
||||
{
|
||||
printLargeDataMessage(file, result.size());
|
||||
return sol::make_object<std::string>(lua->sol(), result);
|
||||
}
|
||||
|
||||
return sol::nil;
|
||||
}
|
||||
|
||||
void validateFile(const FileHandle& self)
|
||||
{
|
||||
if (self.mFilePtr)
|
||||
return;
|
||||
|
||||
throw std::runtime_error("Error when handling '" + self.mFileName + "': attempt to use a closed file.");
|
||||
}
|
||||
|
||||
sol::variadic_results seek(
|
||||
LuaUtil::LuaState* lua, FileHandle& self, std::ios_base::seekdir dir, std::streamoff off)
|
||||
{
|
||||
sol::variadic_results values;
|
||||
try
|
||||
{
|
||||
self.mFilePtr->seekg(off, dir);
|
||||
if (self.mFilePtr->fail() || self.mFilePtr->bad())
|
||||
{
|
||||
auto msg = "Failed to seek in file '" + self.mFileName + "'";
|
||||
values.push_back(sol::nil);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
else
|
||||
values.push_back(sol::make_object<std::streampos>(lua->sol(), self.mFilePtr->tellg()));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
auto msg = "Failed to seek in file '" + self.mFileName + "': " + std::string(e.what());
|
||||
values.push_back(sol::nil);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
||||
sol::table initVFSPackage(const Context& context)
|
||||
{
|
||||
sol::table api(context.mLua->sol(), sol::create);
|
||||
|
||||
auto vfs = MWBase::Environment::get().getResourceSystem()->getVFS();
|
||||
|
||||
sol::usertype<FileHandle> handle = context.mLua->sol().new_usertype<FileHandle>("FileHandle");
|
||||
handle["fileName"] = sol::readonly_property([](const FileHandle& self) { return self.mFileName; });
|
||||
handle[sol::meta_function::to_string] = [](const FileHandle& self) {
|
||||
return "FileHandle{'" + self.mFileName + "'" + (!self.mFilePtr ? ", closed" : "") + "}";
|
||||
};
|
||||
handle["seek"] = sol::overload(
|
||||
[lua = context.mLua](FileHandle& self, std::string_view whence, sol::optional<long> offset) {
|
||||
validateFile(self);
|
||||
|
||||
auto off = static_cast<std::streamoff>(offset.value_or(0));
|
||||
auto dir = getSeekDir(self, whence);
|
||||
|
||||
return seek(lua, self, dir, off);
|
||||
},
|
||||
[lua = context.mLua](FileHandle& self, sol::optional<long> offset) {
|
||||
validateFile(self);
|
||||
|
||||
auto off = static_cast<std::streamoff>(offset.value_or(0));
|
||||
|
||||
return seek(lua, self, std::ios_base::cur, off);
|
||||
});
|
||||
handle["lines"] = [lua = context.mLua](FileHandle& self) {
|
||||
return sol::as_function([&lua, &self]() mutable {
|
||||
validateFile(self);
|
||||
return readLineFromFile(lua, self);
|
||||
});
|
||||
};
|
||||
|
||||
api["lines"] = [lua = context.mLua, vfs](std::string_view fileName) {
|
||||
auto normalizedName = VFS::Path::normalizeFilename(fileName);
|
||||
return sol::as_function(
|
||||
[lua, file = FileHandle(vfs->getNormalized(normalizedName), normalizedName)]() mutable {
|
||||
validateFile(file);
|
||||
auto result = readLineFromFile(lua, file);
|
||||
if (result == sol::nil)
|
||||
file.mFilePtr.reset();
|
||||
|
||||
return result;
|
||||
});
|
||||
};
|
||||
|
||||
handle["close"] = [lua = context.mLua](FileHandle& self) {
|
||||
sol::variadic_results values;
|
||||
try
|
||||
{
|
||||
self.mFilePtr.reset();
|
||||
if (self.mFilePtr)
|
||||
{
|
||||
auto msg = "Can not close file '" + self.mFileName + "': file handle is still opened.";
|
||||
values.push_back(sol::nil);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
else
|
||||
values.push_back(sol::make_object<bool>(lua->sol(), true));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
auto msg = "Can not close file '" + self.mFileName + "': " + std::string(e.what());
|
||||
values.push_back(sol::nil);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
handle["read"] = [lua = context.mLua](FileHandle& self, const sol::variadic_args args) {
|
||||
validateFile(self);
|
||||
|
||||
if (args.size() > sMaximumReadArguments)
|
||||
throw std::runtime_error(
|
||||
"Error when handling '" + self.mFileName + "': too many arguments for 'read'.");
|
||||
|
||||
sol::variadic_results values;
|
||||
// If there are no arguments, read a string
|
||||
if (args.size() == 0)
|
||||
{
|
||||
values.push_back(readLineFromFile(lua, self));
|
||||
return values;
|
||||
}
|
||||
|
||||
bool success = true;
|
||||
size_t i = 0;
|
||||
for (i = 0; i < args.size() && success; i++)
|
||||
{
|
||||
if (args[i].is<std::string_view>())
|
||||
{
|
||||
auto format = args[i].as<std::string_view>();
|
||||
|
||||
if (format == "*a" || format == "*all")
|
||||
{
|
||||
values.push_back(readFile(lua, self));
|
||||
continue;
|
||||
}
|
||||
|
||||
if (format == "*n" || format == "*number")
|
||||
{
|
||||
auto result = readNumberFromFile(lua, self.mFilePtr);
|
||||
values.push_back(result);
|
||||
if (result == sol::nil)
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (format == "*l" || format == "*line")
|
||||
{
|
||||
auto result = readLineFromFile(lua, self);
|
||||
values.push_back(result);
|
||||
if (result == sol::nil)
|
||||
success = false;
|
||||
continue;
|
||||
}
|
||||
|
||||
throw std::runtime_error("Error when handling '" + self.mFileName + "': bad argument #"
|
||||
+ std::to_string(i + 1) + " to 'read' (invalid format)");
|
||||
}
|
||||
else if (args[i].is<int>())
|
||||
{
|
||||
int number = args[i].as<int>();
|
||||
auto result = readCharactersFromFile(lua, self, number);
|
||||
values.push_back(result);
|
||||
if (result == sol::nil)
|
||||
success = false;
|
||||
}
|
||||
}
|
||||
|
||||
// We should return nil if we just reached the end of stream
|
||||
if (!success && self.mFilePtr->eof())
|
||||
return values;
|
||||
|
||||
if (!success && (self.mFilePtr->fail() || self.mFilePtr->bad()))
|
||||
{
|
||||
auto msg = "Error when handling '" + self.mFileName + "': can not read data for argument #"
|
||||
+ std::to_string(i);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
api["open"] = [lua = context.mLua, vfs](std::string_view fileName) {
|
||||
sol::variadic_results values;
|
||||
try
|
||||
{
|
||||
auto normalizedName = VFS::Path::normalizeFilename(fileName);
|
||||
auto handle = FileHandle(vfs->getNormalized(normalizedName), normalizedName);
|
||||
values.push_back(sol::make_object<FileHandle>(lua->sol(), std::move(handle)));
|
||||
}
|
||||
catch (std::exception& e)
|
||||
{
|
||||
auto msg = "Can not open file: " + std::string(e.what());
|
||||
values.push_back(sol::nil);
|
||||
values.push_back(sol::make_object<std::string>(lua->sol(), msg));
|
||||
}
|
||||
|
||||
return values;
|
||||
};
|
||||
|
||||
api["type"] = sol::overload(
|
||||
[](const FileHandle& handle) -> std::string {
|
||||
if (handle.mFilePtr)
|
||||
return "file";
|
||||
|
||||
return "closed file";
|
||||
},
|
||||
[](const sol::object&) -> sol::object { return sol::nil; });
|
||||
|
||||
api["fileExists"] = [vfs](std::string_view fileName) -> bool { return vfs->exists(fileName); };
|
||||
api["pathsWithPrefix"] = [vfs](std::string_view prefix) {
|
||||
auto iterator = vfs->getRecursiveDirectoryIterator(prefix);
|
||||
return sol::as_function([iterator, current = iterator.begin()]() mutable -> sol::optional<std::string> {
|
||||
if (current != iterator.end())
|
||||
{
|
||||
const std::string& result = *current;
|
||||
++current;
|
||||
return result;
|
||||
}
|
||||
|
||||
return sol::nullopt;
|
||||
});
|
||||
};
|
||||
|
||||
return LuaUtil::makeReadOnly(api);
|
||||
}
|
||||
}
|
@ -0,0 +1,13 @@
|
||||
#ifndef MWLUA_VFSBINDINGS_H
|
||||
#define MWLUA_VFSBINDINGS_H
|
||||
|
||||
#include <sol/forward.hpp>
|
||||
|
||||
#include "context.hpp"
|
||||
|
||||
namespace MWLua
|
||||
{
|
||||
sol::table initVFSPackage(const Context&);
|
||||
}
|
||||
|
||||
#endif // MWLUA_VFSBINDINGS_H
|
@ -0,0 +1,7 @@
|
||||
Package openmw.vfs
|
||||
==================
|
||||
|
||||
.. include:: version.rst
|
||||
|
||||
.. raw:: html
|
||||
:file: generated_html/openmw_vfs.html
|
@ -0,0 +1,159 @@
|
||||
---
|
||||
-- `openmw.vfs` provides read-only access to data directories via VFS.
|
||||
-- Interface is very similar to "io" library.
|
||||
-- @module vfs
|
||||
-- @usage local vfs = require('openmw.vfs')
|
||||
|
||||
|
||||
|
||||
---
|
||||
-- @type FileHandle
|
||||
-- @field #string fileName VFS path to related file
|
||||
|
||||
---
|
||||
-- Close a file handle
|
||||
-- @function [parent=#FileHandle] close
|
||||
-- @param self
|
||||
-- @return #boolean true if a call succeeds without errors.
|
||||
-- @return #nil, #string nil plus the error message in case of any error.
|
||||
|
||||
---
|
||||
-- Get an iterator function to fetch the next line from given file.
|
||||
-- Throws an exception if file is closed.
|
||||
--
|
||||
-- Hint: since garbage collection works once per frame,
|
||||
-- you will get the whole file in RAM if you read it in one frame.
|
||||
-- So if you need to read a really large file, it is better to split reading
|
||||
-- between different frames (e.g. by keeping a current position in file
|
||||
-- and using a "seek" to read from saved position).
|
||||
-- @function [parent=#FileHandle] lines
|
||||
-- @param self
|
||||
-- @return #function Iterator function to get next line
|
||||
-- @usage f = vfs.open("Test\\test.txt");
|
||||
-- for line in f:lines() do
|
||||
-- print(line);
|
||||
-- end
|
||||
|
||||
---
|
||||
-- Set new position in file.
|
||||
-- Throws an exception if file is closed or seek base is incorrect.
|
||||
-- @function [parent=#FileHandle] seek
|
||||
-- @param self
|
||||
-- @param #string whence Seek base (optional, "cur" by default). Can be:
|
||||
--
|
||||
-- * "set" - seek from beginning of file;
|
||||
-- * "cur" - seek from current position;
|
||||
-- * "end" - seek from end of file (offset needs to be <= 0);
|
||||
-- @param #number offset Offset from given base (optional, 0 by default)
|
||||
-- @return #number new position in file if a call succeeds without errors.
|
||||
-- @return #nil, #string nil plus the error message in case of any error.
|
||||
-- @usage -- set pointer to beginning of file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- f:seek("set");
|
||||
-- @usage -- print current position in file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- print(f:seek());
|
||||
-- @usage -- print file size
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- print(f:seek("end"));
|
||||
|
||||
---
|
||||
-- Read data from file to strings.
|
||||
-- Throws an exception if file is closed, if there is too many arguments or if an invalid format encountered.
|
||||
--
|
||||
-- Hint: since garbage collection works once per frame,
|
||||
-- you will get the whole file in RAM if you read it in one frame.
|
||||
-- So if you need to read a really large file, it is better to split reading
|
||||
-- between different frames (e.g. by keeping a current position in file
|
||||
-- and using a "seek" to read from saved position).
|
||||
-- @function [parent=#FileHandle] read
|
||||
-- @param self
|
||||
-- @param ... Read formats (up to 20 arguments, default value is one "*l"). Can be:
|
||||
--
|
||||
-- * "\*a" (or "*all") - reads the whole file, starting at the current position as #string. On end of file, it returns the empty string.
|
||||
-- * "\*l" (or "*line") - reads the next line (skipping the end of line), returning nil on end of file (nil and error message if error occured);
|
||||
-- * "\*n" (or "*number") - read a floating point value as #number (nil and error message if error occured);
|
||||
-- * number - reads a #string with up to this number of characters, returning nil on end of file (nil and error message if error occured). If number is 0 and end of file is not reached, it reads nothing and returns an empty string;
|
||||
-- @return #string One #string for every format if a call succeeds without errors. One #string for every successfully handled format, nil for first failed format.
|
||||
-- @usage -- read three numbers from file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- local n1, n2, n3 = f:read("*number", "*number", "*number");
|
||||
-- @usage -- read 10 bytes from file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- local n4 = f:read(10);
|
||||
-- @usage -- read until end of file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- local n5 = f:read("*all");
|
||||
-- @usage -- read a line from file
|
||||
-- f = vfs.open("Test\\test.txt");
|
||||
-- local n6 = f:read();
|
||||
-- @usage -- try to read three numbers from file with "1" content
|
||||
-- f = vfs.open("one.txt");
|
||||
-- print(f:read("*number", "*number", "*number"));
|
||||
-- -- prints(1, nil)
|
||||
|
||||
---
|
||||
-- Check if file exists in VFS
|
||||
-- @function [parent=#vfs] fileExists
|
||||
-- @param #string fileName Path to file in VFS
|
||||
-- @return #boolean (true - exists, false - does not exist)
|
||||
-- @usage local exists = vfs.fileExists("Test\\test.txt");
|
||||
|
||||
---
|
||||
-- Open a file
|
||||
-- @function [parent=#vfs] open
|
||||
-- @param #string fileName Path to file in VFS
|
||||
-- @return #FileHandle Opened file handle if a call succeeds without errors.
|
||||
-- @return #nil, #string nil plus the error message in case of any error.
|
||||
-- @usage f, msg = vfs.open("Test\\test.txt");
|
||||
-- -- print file name or error message
|
||||
-- if (f == nil)
|
||||
-- print(msg);
|
||||
-- else
|
||||
-- print(f.fileName);
|
||||
-- end
|
||||
|
||||
---
|
||||
-- Get an iterator function to fetch the next line from file with given path.
|
||||
-- Throws an exception if file is closed or file with given path does not exist.
|
||||
-- Closes file automatically when it fails to read any more bytes.
|
||||
--
|
||||
-- Hint: since garbage collection works once per frame,
|
||||
-- you will get the whole file in RAM if you read it in one frame.
|
||||
-- So if you need to read a really large file, it is better to split reading
|
||||
-- between different frames (e.g. by keeping a current position in file
|
||||
-- and using a "seek" to read from saved position).
|
||||
-- @function [parent=#vfs] lines
|
||||
-- @param #string fileName Path to file in VFS
|
||||
-- @return #function Iterator function to get next line
|
||||
-- @usage for line in vfs.lines("Test\\test.txt") do
|
||||
-- print(line);
|
||||
-- end
|
||||
|
||||
---
|
||||
-- Get iterator function to fetch file names with given path prefix from VFS
|
||||
-- @function [parent=#vfs] pathsWithPrefix
|
||||
-- @param #string path Path prefix
|
||||
-- @return #function Function to get next file name
|
||||
-- @usage -- get all files with given prefix from VFS index
|
||||
-- for fileName in vfs.pathsWithPrefix("Music\\Explore") do
|
||||
-- print(fileName);
|
||||
-- end
|
||||
-- @usage -- get some first files
|
||||
-- local getNextFile = vfs.pathsWithPrefix("Music\\Explore");
|
||||
-- local firstFile = getNextFile();
|
||||
-- local secondFile = getNextFile();
|
||||
|
||||
---
|
||||
-- Detect a file handle type
|
||||
-- @function [parent=#vfs] type
|
||||
-- @param #any handle Object to check
|
||||
-- @return #string File handle type. Can be:
|
||||
--
|
||||
-- * "file" - an argument is a valid opened @{openmw.vfs#FileHandle};
|
||||
-- * "closed file" - an argument is a valid closed @{openmw.vfs#FileHandle};
|
||||
-- * nil - an argument is not a @{openmw.vfs#FileHandle};
|
||||
-- @usage f = vfs.open("Test\\test.txt");
|
||||
-- print(vfs.type(f));
|
||||
|
||||
return nil
|
Loading…
Reference in New Issue