Implement Lua API for VFS

macos_ci_fix
Andrei Kortunov 9 months ago
parent 3e99103beb
commit c04a0ca3a5

@ -77,6 +77,7 @@
Feature #6491: Add support for Qt6
Feature #6556: Lua API for sounds
Feature #6726: Lua API for creating new objects
Feature #6864: Lua file access API
Feature #6922: Improve launcher appearance
Feature #6933: Support high-resolution cursor textures
Feature #6945: Support S3TC-compressed and BGR/BGRA NiPixelData

@ -71,7 +71,7 @@ message(STATUS "Configuring OpenMW...")
set(OPENMW_VERSION_MAJOR 0)
set(OPENMW_VERSION_MINOR 49)
set(OPENMW_VERSION_RELEASE 0)
set(OPENMW_LUA_API_REVISION 45)
set(OPENMW_LUA_API_REVISION 46)
set(OPENMW_VERSION_COMMITHASH "")
set(OPENMW_VERSION_TAGHASH "")

@ -61,7 +61,7 @@ add_openmw_dir (mwscript
add_openmw_dir (mwlua
luamanagerimp object objectlists userdataserializer luaevents engineevents objectvariant
context globalscripts localscripts playerscripts luabindings objectbindings cellbindings mwscriptbindings
camerabindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings
camerabindings vfsbindings uibindings soundbindings inputbindings nearbybindings postprocessingbindings stats debugbindings
types/types types/door types/item types/actor types/container types/lockable types/weapon types/npc types/creature types/player types/activator types/book types/lockpick types/probe types/apparatus types/potion types/ingredient types/misc types/repair types/armor types/light types/static types/clothing types/levelledlist types/terminal
worker magicbindings
)

@ -43,6 +43,7 @@
#include "soundbindings.hpp"
#include "types/types.hpp"
#include "uibindings.hpp"
#include "vfsbindings.hpp"
namespace MWLua
{
@ -331,6 +332,7 @@ namespace MWLua
{ "openmw.core", initCorePackage(context) },
{ "openmw.types", initTypesPackage(context) },
{ "openmw.util", LuaUtil::initUtilPackage(lua) },
{ "openmw.vfs", initVFSPackage(context) },
};
}

@ -0,0 +1,354 @@
#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<VFS::Manager::StatefulIterator> vfsIterator
= context.mLua->sol().new_usertype<VFS::Manager::StatefulIterator>("VFSIterator");
vfsIterator[sol::meta_function::to_string] = [](const VFS::Manager::StatefulIterator& vfsIterator) {
return "VFSIterator{'" + vfsIterator.getPath() + "'}";
};
vfsIterator["path"] = sol::readonly_property(
[](const VFS::Manager::StatefulIterator& vfsIterator) { return vfsIterator.getPath(); });
auto createIter = [](VFS::Manager::StatefulIterator& vfsIterator) {
return sol::as_function([vfsIterator, i = 1]() mutable {
if (auto v = vfsIterator.next())
return std::tuple<sol::optional<int>, sol::optional<std::string_view>>(i++, *v);
else
return std::tuple<sol::optional<int>, sol::optional<std::string_view>>(sol::nullopt, sol::nullopt);
});
};
vfsIterator["__pairs"] = createIter;
vfsIterator["__ipairs"] = createIter;
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["getIterator"]
= [vfs](std::string_view path) -> VFS::Manager::StatefulIterator { return vfs->getStatefulIterator(path); };
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

@ -89,4 +89,17 @@ namespace VFS
++normalized.back();
return { it, mIndex.lower_bound(normalized) };
}
Manager::StatefulIterator Manager::getStatefulIterator(std::string_view path) const
{
if (path.empty())
return { mIndex.begin(), mIndex.end(), std::string() };
std::string normalized = Path::normalizeFilename(path);
const auto it = mIndex.lower_bound(normalized);
if (it == mIndex.end() || !startsWith(it->first, normalized))
return { it, it, normalized };
std::string upperBound = normalized;
++upperBound.back();
return { it, mIndex.lower_bound(upperBound), normalized };
}
}

@ -6,6 +6,7 @@
#include <filesystem>
#include <map>
#include <memory>
#include <optional>
#include <string>
#include <vector>
@ -48,11 +49,18 @@ namespace VFS
const std::string& operator*() const { return mIt->first; }
const std::string* operator->() const { return &mIt->first; }
bool operator!=(const RecursiveDirectoryIterator& other) { return mIt != other.mIt; }
bool operator==(const RecursiveDirectoryIterator& other) const { return mIt == other.mIt; }
RecursiveDirectoryIterator& operator++()
{
++mIt;
return *this;
}
RecursiveDirectoryIterator operator++(int)
{
RecursiveDirectoryIterator old = *this;
mIt++;
return old;
}
private:
std::map<std::string, File*>::const_iterator mIt;
@ -61,6 +69,31 @@ namespace VFS
using RecursiveDirectoryRange = IteratorPair<RecursiveDirectoryIterator>;
public:
class StatefulIterator : RecursiveDirectoryRange
{
public:
StatefulIterator(RecursiveDirectoryIterator first, RecursiveDirectoryIterator last, const std::string& path)
: RecursiveDirectoryRange(first, last)
, mCurrent(first)
, mPath(path)
{
}
const std::string& getPath() const { return mPath; }
std::optional<std::string_view> next()
{
if (mCurrent == end())
return std::nullopt;
return *mCurrent++;
}
private:
RecursiveDirectoryIterator mCurrent;
std::string mPath;
};
// Empty the file index and unregister archives.
void reset();
@ -93,6 +126,13 @@ namespace VFS
/// @note May be called from any thread once the index has been built.
RecursiveDirectoryRange getRecursiveDirectoryIterator(std::string_view path) const;
/// Recursively iterate over the elements of the given path
/// In practice it return all files of the VFS starting with the given path
/// Stores iterator to current element.
/// @note the path is normalized
/// @note May be called from any thread once the index has been built.
StatefulIterator getStatefulIterator(std::string_view path) const;
/// Retrieve the absolute path to the file
/// @note Throws an exception if the file can not be found.
/// @note May be called from any thread once the index has been built.

@ -17,6 +17,7 @@ Lua API reference
openmw_core
openmw_types
openmw_async
openmw_vfs
openmw_world
openmw_self
openmw_nearby

@ -0,0 +1,7 @@
Package openmw.vfs
==================
.. include:: version.rst
.. raw:: html
:file: generated_html/openmw_vfs.html

@ -15,6 +15,8 @@
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.async <Package openmw.async>` | everywhere | | Timers and callbacks. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.vfs <Package openmw.vfs>` | everywhere | | Read-only access to data directories via VFS. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.world <Package openmw.world>` | by global scripts | | Read-write access to the game world. |
+------------------------------------------------------------+--------------------+---------------------------------------------------------------+
|:ref:`openmw.self <Package openmw.self>` | by local scripts | | Full access to the object the script is attached to. |

@ -12,14 +12,15 @@ set(LUA_API_FILES
openmw/ambient.lua
openmw/async.lua
openmw/core.lua
openmw/debug.lua
openmw/nearby.lua
openmw/postprocessing.lua
openmw/self.lua
openmw/types.lua
openmw/ui.lua
openmw/util.lua
openmw/vfs.lua
openmw/world.lua
openmw/types.lua
openmw/postprocessing.lua
openmw/debug.lua
)
foreach (f ${LUA_API_FILES})

@ -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 VFSIterator
-- @field #string path VFS prefix path
---
-- @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 to fetch file names with given path prefix from VFS
-- @function [parent=#vfs] getIterator
-- @param #string path Path prefix
-- @return #VFSIterator Opened iterator
-- @usage local dir = vfs.getIterator("Music\\Explore");
-- for _, fileName in pairs(dir) do
-- print(fileName);
-- end
---
-- 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…
Cancel
Save