Skip to content

Commit

Permalink
Merge branch 'lua_files' into 'master'
Browse files Browse the repository at this point in the history
Implement Lua API for VFS

Closes #6864

See merge request OpenMW/openmw!3373
  • Loading branch information
psi29a committed Sep 3, 2023
2 parents 0b74146 + 66bf7be commit 5faf569
Show file tree
Hide file tree
Showing 14 changed files with 541 additions and 5 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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 "")
Expand Down
2 changes: 1 addition & 1 deletion apps/openmw/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
)
Expand Down
2 changes: 2 additions & 0 deletions apps/openmw/mwlua/luabindings.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@
#include "soundbindings.hpp"
#include "types/types.hpp"
#include "uibindings.hpp"
#include "vfsbindings.hpp"

namespace MWLua
{
Expand Down Expand Up @@ -347,6 +348,7 @@ namespace MWLua
{ "openmw.core", initCorePackage(context) },
{ "openmw.types", initTypesPackage(context) },
{ "openmw.util", LuaUtil::initUtilPackage(lua) },
{ "openmw.vfs", initVFSPackage(context) },
};
}

Expand Down
346 changes: 346 additions & 0 deletions apps/openmw/mwlua/vfsbindings.cpp
Original file line number Diff line number Diff line change
@@ -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);
}
}
Loading

0 comments on commit 5faf569

Please sign in to comment.