Lots of small changes to server stuff

This commit is contained in:
2025-12-21 19:43:30 -05:00
parent d3bc42b9e9
commit 63026f53bc
9 changed files with 319 additions and 94 deletions

View File

@@ -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`
---

View File

@@ -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

View File

@@ -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);

View File

@@ -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<ObjectGuid> MCPPlayerManager::GetNearbyEntities(uint32 sessionId, float range, uint32 typeMask) const
std::vector<ObjectGuid> MCPPlayerManager::GetNearbyEntities(uint32 sessionId, float /*range*/, uint32 /*typeMask*/) const
{
std::vector<ObjectGuid> 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<uint8>& data)
void MCPPlayerManager::QueueInboundPacket(uint32 /*sessionId*/, uint16 /*opcode*/, const std::vector<uint8>& /*data*/)
{
// TODO: Implement when we need to inject packets into the session
TC_LOG_DEBUG("araxia.mcp", "[MCPPlayerManager] QueueInboundPacket not yet implemented");

View File

@@ -51,7 +51,12 @@
#include "ObjectMgr.h"
#include "SmartScriptMgr.h"
#include "Chat.h"
#include "AraxiaEventBus.h"
#include "AuctionHouseMgr.h"
#include "Item.h"
#include <sstream>
#include <algorithm>
#include <cwctype>
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<wchar_t>(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<std::chrono::seconds>(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<char>(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

View File

@@ -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()

View File

@@ -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;

View File

@@ -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 =