From 63026f53bc261e53a15937159c651520b2ceaa0e Mon Sep 17 00:00:00 2001 From: James Huston Date: Sun, 21 Dec 2025 19:43:30 -0500 Subject: [PATCH] Lots of small changes to server stuff --- AGENTS.md | 4 +- src/araxiaonline/araxia.conf.dist | 26 +++ .../eventbus/AraxiaEventBusConfig.cpp | 40 ++-- src/araxiaonline/mcp/MCPPlayerManager.cpp | 6 +- src/araxiaonline/mcp/ServerTools.cpp | 193 +++++++++++++++++- .../tools/eventbus_integration_test.py | 0 src/araxiaonline/tools/zmq_subscriber.py | 69 ------- .../game/AuctionHouse/AuctionHouseMgr.cpp | 2 + src/server/worldserver/worldserver.conf.dist | 73 +++++++ 9 files changed, 319 insertions(+), 94 deletions(-) delete mode 100644 src/araxiaonline/tools/eventbus_integration_test.py delete mode 100644 src/araxiaonline/tools/zmq_subscriber.py diff --git a/AGENTS.md b/AGENTS.md index df5bd113bc..bc29313c51 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -69,7 +69,7 @@ The Event Bus is a ZeroMQ-based real-time event system that publishes game event ### Quick Test ```bash -cd /opt/trinitycore/TrinityCore/src/araxiaonline/tools +cd /opt/trinitycore/araxia-content-tools/scripts/zeromq source .venv/bin/activate python zmq_subscriber.py ``` @@ -103,7 +103,7 @@ Scarletseer can trigger events for the ZeroMQ event bus: - **Loot events**: item looted - **Encounter events**: start, wipe, end (requires dungeon/raid) -Monitor events with: `python /opt/trinitycore/TrinityCore/src/araxiaonline/tools/zmq_subscriber.py` +Monitor events with: `python /opt/trinitycore/araxia-content-tools/scripts/zeromq/zmq_subscriber.py` --- diff --git a/src/araxiaonline/araxia.conf.dist b/src/araxiaonline/araxia.conf.dist index efe2eef34d..8883412d1d 100644 --- a/src/araxiaonline/araxia.conf.dist +++ b/src/araxiaonline/araxia.conf.dist @@ -148,10 +148,35 @@ Araxia.EventBus.EnableGuildEvents = 1 # When disabled, uses HTTP transport. # Default: 1 (stdio mode) # +# Araxia.MCP.Enable +# Enable the embedded MCP server for AI assistant integration. +# Default: 0 (disabled) +# +# Araxia.MCP.Port +# Port for the MCP HTTP server. +# Default: 8765 +# +# Araxia.MCP.AuthToken +# Bearer token for authentication. Leave empty for no auth (development only!). +# Default: "" (no auth) +# +# Araxia.MCP.AllowRemote +# Allow connections from non-localhost addresses. +# Default: 0 (localhost only) +# ############################################################################### Araxia.MCP.Enable = 1 Araxia.MCP.StdioMode = 1 +Araxia.MCP.Enable = 1 +Araxia.MCP.Port = 8765 +Araxia.MCP.AuthToken = "" +Araxia.MCP.AllowRemote = 1 + +# MCP Player Settings +Araxia.MCP.Player.MaxSessions = 10 +Araxia.MCP.Player.DefaultAccountId = 0 +Araxia.MCP.Player.SessionTimeout = 3600 ############################################################################### # @@ -166,3 +191,4 @@ Araxia.MCP.StdioMode = 1 Araxia.Eluna.EnableSharedData = 1 +Logger.araxia.eventbus=1,Console Server \ No newline at end of file diff --git a/src/araxiaonline/eventbus/AraxiaEventBusConfig.cpp b/src/araxiaonline/eventbus/AraxiaEventBusConfig.cpp index 3f3e9fa491..3cb306c52a 100644 --- a/src/araxiaonline/eventbus/AraxiaEventBusConfig.cpp +++ b/src/araxiaonline/eventbus/AraxiaEventBusConfig.cpp @@ -29,25 +29,27 @@ AraxiaEventBusConfig* AraxiaEventBusConfig::Instance() void AraxiaEventBusConfig::LoadConfig() { - _publishEndpoint = sConfigMgr->GetStringDefault("Araxia.EventBus.PublishEndpoint"sv, "tcp://*:5555"sv); - _subscribeEndpoint = sConfigMgr->GetStringDefault("Araxia.EventBus.SubscribeEndpoint"sv, "tcp://127.0.0.1:5556"sv); - _enableSpawnEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableSpawnEvents"sv, true); - _enableEncounterEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableEncounterEvents"sv, true); - _enablePlayerEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnablePlayerEvents"sv, true); - _enableCombatEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableCombatEvents"sv, true); - _enableLootEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableLootEvents"sv, true); - _enableQuestEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableQuestEvents"sv, true); - _enableZoneEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableZoneEvents"sv, true); - _enablePartyEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnablePartyEvents"sv, true); - _enableItemEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableItemEvents"sv, true); - _enableSpellEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableSpellEvents"sv, false); // High volume - _enableLevelEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableLevelEvents"sv, true); - _enableChatEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableChatEvents"sv, true); - _enableAchievementEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableAchievementEvents"sv, true); - _enableAuctionEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableAuctionEvents"sv, true); - _enableMailEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableMailEvents"sv, true); - _enableTradeEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableTradeEvents"sv, true); - _enableGuildEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableGuildEvents"sv, true); + // Use quiet=true to suppress "Missing name" warnings since we have sensible defaults + // Config can be set in worldserver.conf or worldserver.conf.d/*.conf + _publishEndpoint = sConfigMgr->GetStringDefault("Araxia.EventBus.PublishEndpoint"sv, "tcp://*:5555"sv, true); + _subscribeEndpoint = sConfigMgr->GetStringDefault("Araxia.EventBus.SubscribeEndpoint"sv, "tcp://127.0.0.1:5556"sv, true); + _enableSpawnEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableSpawnEvents"sv, true, true); + _enableEncounterEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableEncounterEvents"sv, true, true); + _enablePlayerEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnablePlayerEvents"sv, true, true); + _enableCombatEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableCombatEvents"sv, true, true); + _enableLootEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableLootEvents"sv, true, true); + _enableQuestEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableQuestEvents"sv, true, true); + _enableZoneEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableZoneEvents"sv, true, true); + _enablePartyEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnablePartyEvents"sv, true, true); + _enableItemEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableItemEvents"sv, true, true); + _enableSpellEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableSpellEvents"sv, false, true); // High volume - disabled by default + _enableLevelEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableLevelEvents"sv, true, true); + _enableChatEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableChatEvents"sv, true, true); + _enableAchievementEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableAchievementEvents"sv, true, true); + _enableAuctionEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableAuctionEvents"sv, true, true); + _enableMailEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableMailEvents"sv, true, true); + _enableTradeEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableTradeEvents"sv, true, true); + _enableGuildEvents = sConfigMgr->GetBoolDefault("Araxia.EventBus.EnableGuildEvents"sv, true, true); TC_LOG_INFO("araxia.eventbus", "EventBus config loaded:"); TC_LOG_INFO("araxia.eventbus", " PublishEndpoint: {}", _publishEndpoint); diff --git a/src/araxiaonline/mcp/MCPPlayerManager.cpp b/src/araxiaonline/mcp/MCPPlayerManager.cpp index 1df8410020..bb506a8e1e 100644 --- a/src/araxiaonline/mcp/MCPPlayerManager.cpp +++ b/src/araxiaonline/mcp/MCPPlayerManager.cpp @@ -881,7 +881,7 @@ bool MCPPlayerManager::CastSpell(uint32 sessionId, uint32 spellId, ObjectGuid ta return player->CastSpell(target, spellId, false) == SPELL_CAST_OK; } -bool MCPPlayerManager::InteractWith(uint32 sessionId, ObjectGuid targetGuid) +bool MCPPlayerManager::InteractWith(uint32 sessionId, ObjectGuid /*targetGuid*/) { Player* player = GetPlayer(sessionId); if (!player) @@ -942,7 +942,7 @@ std::string MCPPlayerManager::ExecuteCommand(uint32 sessionId, const std::string // Perception // ============================================================================ -std::vector MCPPlayerManager::GetNearbyEntities(uint32 sessionId, float range, uint32 typeMask) const +std::vector MCPPlayerManager::GetNearbyEntities(uint32 sessionId, float /*range*/, uint32 /*typeMask*/) const { std::vector result; @@ -993,7 +993,7 @@ void MCPPlayerManager::CapturePacket(uint32 sessionId, WorldPacket const& packet session->outboundPackets.pop(); } -void MCPPlayerManager::QueueInboundPacket(uint32 sessionId, uint16 opcode, const std::vector& data) +void MCPPlayerManager::QueueInboundPacket(uint32 /*sessionId*/, uint16 /*opcode*/, const std::vector& /*data*/) { // TODO: Implement when we need to inject packets into the session TC_LOG_DEBUG("araxia.mcp", "[MCPPlayerManager] QueueInboundPacket not yet implemented"); diff --git a/src/araxiaonline/mcp/ServerTools.cpp b/src/araxiaonline/mcp/ServerTools.cpp index 837afdeb93..61564f9bd9 100644 --- a/src/araxiaonline/mcp/ServerTools.cpp +++ b/src/araxiaonline/mcp/ServerTools.cpp @@ -51,7 +51,12 @@ #include "ObjectMgr.h" #include "SmartScriptMgr.h" #include "Chat.h" +#include "AraxiaEventBus.h" +#include "AuctionHouseMgr.h" +#include "Item.h" #include +#include +#include namespace Araxia { @@ -719,7 +724,193 @@ void RegisterServerTools() } ); - TC_LOG_INFO("araxia.mcp", "[MCP] Server tools registered (server_info, player_list, gm_command, reload_scripts, log_search, shared_data_*)"); + // publish_test_event - Publish a test event to the event bus for UI testing + // This tool allows AI assistants to test event-driven UIs like the Auction House page + // without needing actual in-game activity. + sMCPServer->RegisterTool( + "publish_test_event", + "Publish a test event to the ZeroMQ event bus. Useful for testing event-driven UIs.", + { + {"type", "object"}, + {"properties", { + {"topic", {{"description", "Event topic (e.g., 'world.auction.create', 'world.player.login')"}, {"type", "string"}}}, + {"payload", {{"description", "JSON payload object for the event"}, {"type", "object"}}} + }}, + {"required", {"topic", "payload"}} + }, + [](const json& params) -> json { + std::string topic = params.value("topic", ""); + + if (topic.empty()) + return {{"success", false}, {"error", "Topic is required"}}; + + if (!params.contains("payload")) + return {{"success", false}, {"error", "Payload is required"}}; + + // Get payload as string + std::string payloadStr = params["payload"].dump(); + + // Publish to event bus + sAraxiaEventBus->Publish(topic, payloadStr); + + TC_LOG_DEBUG("araxia.mcp", "[MCP] Published test event: {} with payload: {}", topic, payloadStr); + + return { + {"success", true}, + {"topic", topic}, + {"payload", params["payload"]}, + {"message", "Event published to event bus"} + }; + } + ); + + // auction_search - Search the auction house using the same bucket system the client uses. + // The server stores auctions in "buckets" indexed by item, with pre-computed FullName + // arrays for each locale. We search the bucket FullName to match client behavior. + // AHBot items are stored in memory, not in item_instance table, so we must query + // the server directly rather than the database. + sMCPServer->RegisterTool( + "auction_search", + "Search the auction house using the server's native bucket search (same as game client).", + { + {"type", "object"}, + {"properties", { + {"name", {{"description", "Item name to search for (partial match, case-insensitive)"}, {"type", "string"}}}, + {"quality", {{"description", "Minimum quality (0=Poor, 1=Common, 2=Uncommon, 3=Rare, 4=Epic, 5=Legendary)"}, {"type", "integer"}}}, + {"faction", {{"description", "Faction: 'alliance', 'horde', 'neutral', or 'all' (default)"}, {"type", "string"}}}, + {"limit", {{"description", "Max results to return (default 50)"}, {"type", "integer"}}} + }} + }, + [](const json& params) -> json { + std::string nameFilter = params.value("name", ""); + int qualityFilter = params.value("quality", -1); + std::string factionFilter = params.value("faction", "all"); + int limit = params.value("limit", 50); + + // Convert name filter to wide string for searching bucket FullName + // The server uses wide strings for localized names + std::wstring wideNameFilter; + if (!nameFilter.empty()) + { + // Convert to lowercase wide string for case-insensitive matching + for (char c : nameFilter) + wideNameFilter += std::towlower(static_cast(c)); + } + + json results = json::array(); + int total = 0; + + // Helper lambda to search an auction house using bucket data + auto searchAuctionHouse = [&](AuctionHouseObject* ah, const std::string& faction) { + if (!ah) return; + + for (auto itr = ah->GetAuctionsBegin(); itr != ah->GetAuctionsEnd(); ++itr) + { + AuctionPosting& auction = itr->second; + + // Get bucket data for proper name searching (same as client) + if (!auction.Bucket) continue; + AuctionsBucketData* bucket = auction.Bucket; + + // Get item info from the first item + if (auction.Items.empty()) continue; + Item* item = auction.Items[0]; + if (!item) continue; + + ItemTemplate const* itemTemplate = item->GetTemplate(); + if (!itemTemplate) continue; + + // Quality filter - check bucket's quality mask + if (qualityFilter >= 0 && itemTemplate->GetQuality() < qualityFilter) + continue; + + // Name filter using bucket's FullName (same as server's BuildListBuckets) + if (!wideNameFilter.empty()) + { + // Get the localized full name from the bucket + const std::wstring& fullName = bucket->FullName[LOCALE_enUS]; + + // Convert to lowercase for case-insensitive search + std::wstring lowerFullName; + for (wchar_t wc : fullName) + lowerFullName += std::towlower(wc); + + // Search for substring match + if (lowerFullName.find(wideNameFilter) == std::wstring::npos) + continue; + } + + total++; + + if ((int)results.size() < limit) + { + // Calculate time remaining + auto now = GameTime::GetSystemTime(); + auto remaining = std::chrono::duration_cast(auction.EndTime - now).count(); + std::string timeLeft = "Expired"; + if (remaining > 0) + { + if (remaining < 1800) timeLeft = "Short"; + else if (remaining < 7200) timeLeft = "Medium"; + else if (remaining < 43200) timeLeft = "Long"; + else timeLeft = "Very Long"; + } + + // Convert wide string name back to UTF-8 for JSON + std::string itemName; + for (wchar_t wc : bucket->FullName[LOCALE_enUS]) + { + if (wc < 128) + itemName += static_cast(wc); + else + itemName += '?'; // Non-ASCII placeholder + } + + // Fallback to template name if bucket name is empty + if (itemName.empty()) + itemName = itemTemplate->GetName(LOCALE_enUS); + + // Get item stats from bucket data + // RequiredLevel and ItemLevel are stored in the bucket for efficiency + uint8 requiredLevel = bucket->RequiredLevel; + uint16 itemLevel = bucket->Key.ItemLevel; + + results.push_back({ + {"auctionId", auction.Id}, + {"itemId", itemTemplate->GetId()}, + {"itemName", itemName}, + {"itemQuality", itemTemplate->GetQuality()}, + {"itemLevel", itemLevel}, + {"requiredLevel", requiredLevel}, + {"count", auction.GetTotalItemCount()}, + {"buyout", auction.BuyoutOrUnitPrice}, + {"bid", auction.MinBid}, + {"currentBid", auction.BidAmount}, + {"timeLeft", timeLeft}, + {"faction", faction} + }); + } + } + }; + + // Search appropriate auction houses based on faction filter + if (factionFilter == "all" || factionFilter == "alliance") + searchAuctionHouse(sAuctionMgr->GetAuctionsById(2), "alliance"); + if (factionFilter == "all" || factionFilter == "horde") + searchAuctionHouse(sAuctionMgr->GetAuctionsById(6), "horde"); + if (factionFilter == "all" || factionFilter == "neutral") + searchAuctionHouse(sAuctionMgr->GetAuctionsById(7), "neutral"); + + return { + {"success", true}, + {"total", total}, + {"count", results.size()}, + {"auctions", results} + }; + } + ); + + TC_LOG_INFO("araxia.mcp", "[MCP] Server tools registered (server_info, player_list, gm_command, reload_scripts, log_search, shared_data_*, publish_test_event, auction_search)"); } } // namespace Araxia diff --git a/src/araxiaonline/tools/eventbus_integration_test.py b/src/araxiaonline/tools/eventbus_integration_test.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/src/araxiaonline/tools/zmq_subscriber.py b/src/araxiaonline/tools/zmq_subscriber.py deleted file mode 100644 index 1fb889035d..0000000000 --- a/src/araxiaonline/tools/zmq_subscriber.py +++ /dev/null @@ -1,69 +0,0 @@ -#!/usr/bin/env python3 -""" -ZeroMQ Event Bus Subscriber - Test Script - -Connects to the worldserver's ZMQ publisher and prints all received events. -Use this to verify spawn/encounter/player events are being published. - -Usage: - python3 zmq_subscriber.py [endpoint] [topic_filter] - -Examples: - python3 zmq_subscriber.py # All events on default endpoint - python3 zmq_subscriber.py tcp://localhost:5555 world.spawn - python3 zmq_subscriber.py tcp://localhost:5555 dungeon. -""" - -import sys -import json -import zmq -from datetime import datetime - -def main(): - endpoint = sys.argv[1] if len(sys.argv) > 1 else "tcp://localhost:5555" - topic_filter = sys.argv[2] if len(sys.argv) > 2 else "" - - context = zmq.Context() - socket = context.socket(zmq.SUB) - - print(f"Connecting to {endpoint}...") - socket.connect(endpoint) - - # Subscribe to topic filter (empty string = all topics) - socket.setsockopt_string(zmq.SUBSCRIBE, topic_filter) - print(f"Subscribed to topics: '{topic_filter}*'") - print("Waiting for events... (Ctrl+C to quit)\n") - - try: - while True: - message = socket.recv_string() - - # Parse topic and payload (format: "topic {json}") - space_idx = message.find(' ') - if space_idx > 0: - topic = message[:space_idx] - payload = message[space_idx + 1:] - - try: - data = json.loads(payload) - ts = datetime.fromtimestamp(data.get('ts', 0) / 1000) - - print(f"[{ts.strftime('%H:%M:%S.%f')[:-3]}] {topic}") - print(f" Context: map={data.get('context', {}).get('map_id')}, " - f"instance={data.get('context', {}).get('instance_id')}, " - f"type={data.get('context', {}).get('type')}") - print(f" Payload: {json.dumps(data.get('payload', {}), indent=None)}") - print() - except json.JSONDecodeError: - print(f"[RAW] {message}\n") - else: - print(f"[RAW] {message}\n") - - except KeyboardInterrupt: - print("\nShutting down...") - finally: - socket.close() - context.term() - -if __name__ == "__main__": - main() diff --git a/src/server/game/AuctionHouse/AuctionHouseMgr.cpp b/src/server/game/AuctionHouse/AuctionHouseMgr.cpp index 59242e5999..851e334b5c 100644 --- a/src/server/game/AuctionHouse/AuctionHouseMgr.cpp +++ b/src/server/game/AuctionHouse/AuctionHouseMgr.cpp @@ -1000,6 +1000,8 @@ void AuctionHouseObject::AddAuction(CharacterDatabaseTransaction trans, AuctionP ownerName = owner->GetName(); else if (CharacterCacheEntry const* entry = sCharacterCache->GetCharacterCacheByGuid(addedAuction->Owner)) ownerName = entry->Name; + else if (addedAuction->Owner.GetCounter() == 0) + ownerName = "AHBot"; // Auction House Bot auctions have no real owner uint32 itemEntry = 0; uint32 itemCount = 0; diff --git a/src/server/worldserver/worldserver.conf.dist b/src/server/worldserver/worldserver.conf.dist index 0d98592e9d..f7f96cd2d7 100644 --- a/src/server/worldserver/worldserver.conf.dist +++ b/src/server/worldserver/worldserver.conf.dist @@ -3882,6 +3882,41 @@ AuctionHouseBot.Class.Key = 1 AuctionHouseBot.Class.Misc = 5 AuctionHouseBot.Class.Glyph = 3 +# +# AuctionHouseBot.Items.Scaling.* +# Description: Item level scaling for AHBot items. When enabled, equipment items +# posted by AHBot can receive random item level bonuses, allowing +# the AH to stock gear suitable for all player progression levels. +# Uses the same ItemBonusMgr system as Timewalking/Remix scaling. +# +# AuctionHouseBot.Items.Scaling.Enabled +# Description: Enable item level scaling for AHBot items +# Default: 0 - (Disabled, items use base item level) +# 1 - (Enabled, items may receive random item level bonuses) +# +# AuctionHouseBot.Items.Scaling.MinItemLevel +# Description: Minimum target item level for scaled items +# Default: 0 - (Use item's base level as minimum) +# +# AuctionHouseBot.Items.Scaling.MaxItemLevel +# Description: Maximum target item level for scaled items +# Default: 550 - (MoP raid gear level) +# +# AuctionHouseBot.Items.Scaling.Chance +# Description: Percentage chance (0-100) for each item to be scaled +# Default: 50 - (50% of eligible items get scaled) +# +# AuctionHouseBot.Items.Scaling.EquipmentOnly +# Description: Only scale equipment (weapons/armor), not consumables/materials +# Default: 1 - (Only equipment is scaled) +# 0 - (All item types can be scaled) + +AuctionHouseBot.Items.Scaling.Enabled = 1 +AuctionHouseBot.Items.Scaling.MinItemLevel = 200 +AuctionHouseBot.Items.Scaling.MaxItemLevel = 550 +AuctionHouseBot.Items.Scaling.Chance = 75 +AuctionHouseBot.Items.Scaling.EquipmentOnly = 1 + # ################################################################################################### @@ -4459,3 +4494,41 @@ Load.Locales = 1 # ################################################################################################### + +################################################################################################### +# ELUNA CONFIGURATION +################################################################################################### + +# Enable Eluna Lua scripting engine +Eluna.Enabled = 1 + +# Path to Lua scripts directory +Eluna.ScriptPath = "/opt/trinitycore/lua_scripts" + +# Enable unsafe methods +Eluna.UseUnsafeMethods = 1 + +# Enable deprecated methods for backward compatibility +Eluna.UseDeprecatedMethods = 1 + +# Enable .reload lua command +Eluna.ReloadCommand = 1 + +# Minimum account level for .reload lua command (0-3) +# 0 = Player, 1 = Moderator, 2 = GameMaster, 3 = Administrator +Eluna.ReloadSecurityLevel = 3 + +# Enable automatic script reloading on file changes +Eluna.ScriptReloader = 0 + +# Enable detailed error tracebacks +Eluna.TraceBack = 1 + +# Only load Eluna on specific maps (comma-separated, empty = all maps) +Eluna.OnlyOnMaps = + +# Extra paths for Lua require() function (semicolon-separated) +Eluna.RequirePaths = + +# Extra C library paths for Lua require() (semicolon-separated) +Eluna.RequireCPaths =