feat: add Lua 5.1 addon system with .toc loader and /run command

Foundation for WoW-compatible addon support:

- Vendor Lua 5.1.5 source as a static library (extern/lua-5.1.5)
- TocParser: parses .toc files (## directives + file lists)
- LuaEngine: Lua 5.1 VM with sandboxed stdlib (no io/os/debug),
  WoW-compatible print() that outputs to chat, GetTime() stub
- AddonManager: scans Data/interface/AddOns/ for .toc files,
  loads .lua files on world entry, skips LoadOnDemand addons
- /run <code> slash command for inline Lua execution
- HelloWorld test addon that prints to chat on load

Integration: AddonManager initialized after asset manager, addons
loaded once on first world entry, reset on logout. XML frame
parsing is deferred to a future step.
This commit is contained in:
Kelsi 2026-03-20 11:12:07 -07:00
parent 52064eb438
commit 290e9bfbd8
115 changed files with 29035 additions and 2 deletions

84
src/addons/toc_parser.cpp Normal file
View file

@ -0,0 +1,84 @@
#include "addons/toc_parser.hpp"
#include <fstream>
#include <algorithm>
namespace wowee::addons {
std::string TocFile::getTitle() const {
auto it = directives.find("Title");
return (it != directives.end()) ? it->second : addonName;
}
std::string TocFile::getInterface() const {
auto it = directives.find("Interface");
return (it != directives.end()) ? it->second : "";
}
bool TocFile::isLoadOnDemand() const {
auto it = directives.find("LoadOnDemand");
return (it != directives.end()) && it->second == "1";
}
std::optional<TocFile> parseTocFile(const std::string& tocPath) {
std::ifstream f(tocPath);
if (!f.is_open()) return std::nullopt;
TocFile toc;
toc.basePath = tocPath;
// Strip filename to get directory
size_t lastSlash = tocPath.find_last_of("/\\");
if (lastSlash != std::string::npos) {
toc.basePath = tocPath.substr(0, lastSlash);
toc.addonName = tocPath.substr(lastSlash + 1);
}
// Strip .toc extension from addon name
size_t dotPos = toc.addonName.rfind(".toc");
if (dotPos != std::string::npos) toc.addonName.resize(dotPos);
std::string line;
while (std::getline(f, line)) {
// Strip trailing CR (Windows line endings)
if (!line.empty() && line.back() == '\r') line.pop_back();
// Skip empty lines
if (line.empty()) continue;
// ## directives
if (line.size() >= 3 && line[0] == '#' && line[1] == '#') {
std::string directive = line.substr(2);
size_t colon = directive.find(':');
if (colon != std::string::npos) {
std::string key = directive.substr(0, colon);
std::string val = directive.substr(colon + 1);
// Trim whitespace
auto trim = [](std::string& s) {
size_t start = s.find_first_not_of(" \t");
size_t end = s.find_last_not_of(" \t");
s = (start == std::string::npos) ? "" : s.substr(start, end - start + 1);
};
trim(key);
trim(val);
if (!key.empty()) toc.directives[key] = val;
}
continue;
}
// Single # comment
if (line[0] == '#') continue;
// Whitespace-only line
size_t firstNonSpace = line.find_first_not_of(" \t");
if (firstNonSpace == std::string::npos) continue;
// File entry — normalize backslashes to forward slashes
std::string filename = line.substr(firstNonSpace);
size_t lastNonSpace = filename.find_last_not_of(" \t");
if (lastNonSpace != std::string::npos) filename.resize(lastNonSpace + 1);
std::replace(filename.begin(), filename.end(), '\\', '/');
toc.files.push_back(std::move(filename));
}
return toc;
}
} // namespace wowee::addons