diff --git a/data/lib/compat/compat.lua b/data/lib/compat/compat.lua index b1dc5f7b61..659bcc19af 100644 --- a/data/lib/compat/compat.lua +++ b/data/lib/compat/compat.lua @@ -237,6 +237,7 @@ do self:onThink(value) return elseif key == "onTime" then + self:type("timer") self:onTime(value) return elseif key == "onStartup" then @@ -1350,8 +1351,10 @@ function doSetGameState(state) end function doExecuteRaid(raidName) - return Game.startRaid(raidName) + debugPrint("Deprecated function, use Game.startEvent('" .. raidName .. "') instead.") + return Game.startEvent(raidName) end +Game.startRaid = doExecuteRaid function Game.convertIpToString(ip) print("[Warning - " .. debug.getinfo(2).source:match("@?(.*)") .. "] Function Game.convertIpToString is deprecated and will be removed in the future. Use the return value of player:getIp() instead.") diff --git a/data/lib/core/raids.lua b/data/lib/core/raids.lua new file mode 100644 index 0000000000..8891a4c3d4 --- /dev/null +++ b/data/lib/core/raids.lua @@ -0,0 +1,43 @@ +local raids = {} + +Raid = setmetatable({ + getRaids = function() return raids end +}, { + __call = function(self, name) + local events = {} + + local obj = { + margin = 0, + name = name, + repeats = false, + } + + function obj:__newindex(key, value) + if key == "interval" or key == "margin" or key == "repeats" then + rawset(self, key, value) + else + io.write("[Warning] Invalid attribute for raid: " .. key .. ". Ignoring...\n") + end + end + + function obj:addEvent(delay, fn) + events[#events] = { delay = delay, fn = fn } + end + + function obj:execute() + for _, event in ipairs(events) do + addEvent(event.delay, event.fn) + end + end + + function obj:register() + raids[self.name] = self + end + + function obj:unregister() + raids[self.name] = nil + end + + return obj + end +}) diff --git a/data/raids/raids.xml b/data/raids/raids.xml deleted file mode 100644 index e61a5a6e49..0000000000 --- a/data/raids/raids.xml +++ /dev/null @@ -1,7 +0,0 @@ - - - - diff --git a/data/raids/testraid.xml b/data/raids/testraid.xml deleted file mode 100644 index ebfe9d0c9c..0000000000 --- a/data/raids/testraid.xml +++ /dev/null @@ -1,21 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - diff --git a/data/scripts/globalevents/#testraid.lua b/data/scripts/globalevents/#testraid.lua new file mode 100644 index 0000000000..6dcac09aaf --- /dev/null +++ b/data/scripts/globalevents/#testraid.lua @@ -0,0 +1,51 @@ +local raid = GlobalEvent("Testraid") +raid:interval(1800) + +local function event0() + Game.broadcastMessage("Rats are attacking near Trekolt Temple!", MESSAGE_STATUS_WARNING) +end + +local function event1() + local center, radius, z = {x=94, y=126}, 5, 7 + for _ = 1, 3 do + local x, y = math.random(center.x - radius, center.x + radius), math.random(center.y - radius, center.y + radius) + Game.createMonster("Rat", Position(x, y, z)) + end +end + +local function event2() + Game.createMonster("Cave Rat", Position(93, 123, 7)) +end + +local function event3() + Game.broadcastMessage("Rats attack continues!", MESSAGE_STATUS_WARNING) +end + +local function event4() + local from, to, z = {x=89, y=122}, {x=99, y=130}, 7 + for _ = 1, math.random(4, 10) do + local x, y = math.random(from.x, to.x), math.random(from.y, to.y) + Game.createMonster("Rat", Position(x, y, z)) + end +end + +local function event5() + Game.createMonster("Cave Rat", Position(98, 125, 7)) +end + +local function event6() + Game.createMonster("Cave Rat", Position(94, 128, 7)) +end + +function raid.onTime(interval) + addEvent(event0, 1000) + addEvent(event1, 2000) + addEvent(event2, 15000) + addEvent(event3, 25000) + addEvent(event4, 30000) + addEvent(event5, 30000) + addEvent(event6, 30000) + return true +end + +raid:register() diff --git a/data/scripts/globalevents/raids.lua b/data/scripts/globalevents/raids.lua new file mode 100644 index 0000000000..0c1a26e0cb --- /dev/null +++ b/data/scripts/globalevents/raids.lua @@ -0,0 +1,36 @@ +local event = GlobalEvent("raids") + +local CHECK_RAIDS_INTERVAL = 60 +local MAX_RAND_RANGE = 10000000 + +event:interval(CHECK_RAIDS_INTERVAL) + +local running = nil +local lastRaidEnd = 0 + +function event.onTime(interval) + io.write("Executing raids event...\n") + if running then + return + end + + local now = os.mtime() + + local raids = Raid.getRaids() + for key, raid in pairs(raids) do + local chance = (MAX_RAND_RANGE * CHECK_RAIDS_INTERVAL) / raid.interval + if now >= lastRaidEnd + raid.margin and chance >= math.random(0, MAX_RAND_RANGE) then + running = key + + io.write("Executing raid: " .. raid.name .. "\n") + raid:execute() + + if not raid.repeats then + raids[key] = nil + end + break + end + end +end + +event:register() diff --git a/data/scripts/globalevents/raids_xml.lua b/data/scripts/globalevents/raids_xml.lua new file mode 100644 index 0000000000..63c721e820 --- /dev/null +++ b/data/scripts/globalevents/raids_xml.lua @@ -0,0 +1,240 @@ +-- loads legacy raids from data/raids/raids.xml +local messageTypes = { + ["warning"] = MESSAGE_STATUS_WARNING, + ["event"] = MESSAGE_EVENT_ADVANCE, + ["default"] = MESSAGE_EVENT_DEFAULT, + ["description"] = MESSAGE_INFO_DESCR, + ["smallstatus"] = MESSAGE_STATUS_SMALL, + ["blueconsole"] = MESSAGE_STATUS_CONSOLE_BLUE, + ["redconsole"] = MESSAGE_STATUS_CONSOLE_RED, +} +local defaultMessageType = "event" + +local function parseAnnounce(node, filename) + local message = node:attribute("message") + if not message then + io.write("[Error] Missing message attribute, check data/raids/" .. filename .. "\n") + end + + local type = node:attribute("type") + if not type then + io.write("[Notice] Missing type for announce event in " .. filename .. ". Using default: " .. defaultMessageType .. ".\n") + type = defaultMessageType + end + + local messageType = messageTypes[type:lower()] + if not messageType then + io.write("[Notice] Unknown type " .. type .. " for announce event in " .. filename .. ". Using default: " .. messageTypes[defaultMessageType] .. ".\n") + messageType = messageTypes[defaultMessageType] + end + + return function() + Game.broadcastMessage(message, messageType) + end +end + +local function parseAreaSpawn(node, filename) + local fromx, fromy, fromz, tox, toy, toz + + local radius = tonumber(node:attribute("radius")) + if radius then + local centerx, centery, centerz = tonumber(node:attribute("centerx")), tonumber(node:attribute("centery")), tonumber(node:attribute("centerz")) + if not centerx or not centery or not centerz then + io.write("[Error] Missing one of: centerx, centery, centerz, check data/raids/" .. filename .. "\n") + end + + fromx, fromy, fromz = centerx - radius, centery - radius, z + tox, toy, toz = centerx + radius, centery + radius, z + else + fromx, fromy, fromz = tonumber(node:attribute("fromx")), tonumber(node:attribute("fromy")), tonumber(node:attribute("fromz")) + if not fromx or not fromy or not fromz then + io.write("[Error] Missing one of: fromx, fromy, fromz, check data/raids/" .. filename .. "\n") + end + + tox, toy, toz = tonumber(node:attribute("tox")), tonumber(node:attribute("toy")), tonumber(node:attribute("toz")) + if not tox or not toy or not toz then + io.write("[Error] Missing one of: tox, toy, toz, check data/raids/" .. filename .. "\n") + end + end + + local spawns = {} + for spawnNode in node:children() do + local name = spawnNode:attribute("name") + if not name then + io.write("[Error] Missing attribute name, check data/raids/" .. filename .. "\n") + return nil + end + + local minAmount, maxAmount = tonumber(spawnNode:attribute("minamount")), tonumber(spawnNode:attribute("maxamount")) + if not minAmount and not maxAmount then + local amount = tonumber(spawnNode:attribute("amount")) + if not amount then + io.write("[Error] Missing attributes minamount/maxamount or amount, check data/raids/" .. filename .. "\n") + end + + minAmount, maxAmount = amount, amount + elseif not minAmount then + io.write("[Warning] Missing attribute minamount in " .. filename .. ", using maxamount as default.\n") + minAmount = maxAmount + elseif not maxAmount then + io.write("[Warning] Missing attribute maxamount in " .. filename .. ", using minamount as default.\n") + maxAmount = minAmount + end + + spawns[#spawns] = { monsterName = name, minAmount = minAmount, maxAmount = maxAmount } + end + + return function() + for _, spawn in ipairs(spawns) do + for _ = 1, math.random(spawn.minAmount, spawn.maxAmount) do + local x, y = math.random(fromx, tox), math.random(fromy, toy) + Game.createMonster(spawns.name, Position(x, y, z)) + end + end + end +end + +local function parseScript(node) + local script = node:attribute("script") + if not script then + io.write("[Error] Missing attribute script, check data/raids/" .. filename .. "\n") + return nil + end + + local scriptFile = "data/raids/scripts/" .. script + dofile(script) + if not onRaid then + io.write("[Error] Can not load raid script, check " .. scriptFile .. " for a missing onRaid callback\n") + return nil + end + + local callback = onRaid + + -- let it be garbage collected + onRaid = nil + + return callback +end + +local function parseSingleSpawn(node, filename) + local name = node:attribute("name") + if not name then + io.write("[Error] Missing attribute name, check data/raids/" .. filename .. "\n") + return nil + end + + local x, y, z = tonumber(node:attribute("x")), tonumber(node:attribute("y")), tonumber(node:attribute("z")) + if not x or not y or not z then + io.write("[Error] Missing one of: x, y, z, check data/raids/" .. filename .. "\n") + end + + return function() + Game.createMonster(spawns.name, Position(x, y, z)) + end +end + +local eventParsers = { + ["announce"] = parseAnnounce, + ["areaspawn"] = parseAreaSpawn, + ["script"] = parseScript, + ["singlespawn"] = parseSingleSpawn, +} + +local function parseRaid(filename) + local doc = XMLDocument("data/raids/" .. filename) + local eventNodes = doc:child("raid") + + local events = {} + for eventNode in eventNodes:children() do + local parse = eventParsers[eventNode:name()] + if not parse then + io.write("[Error] Unknown event type: " .. eventNode:name() .. ".\n") + return nil + end + + local delay = tonumber(eventNode:attribute("delay")) + if not delay then + io.write("[Error] Missing attribute delay, check data/raids/" .. filename .. "\n") + return nil + end + + local callback = parse(eventNode, filename) + if not callback then + return nil + end + + events[#events] = { delay = delay, callback = callback } + end + + return events +end + +local function configureRaidEvent(node) + local name = node:attribute("name") + if not name then + io.write("[Error] Missing attribute name for raid.\n") + return nil + end + + local filename = node:attribute("file") + if not filename then + io.write('[Warning] file attribute missing for raid "' .. name .. '". Using default: "' .. name .. '.xml"\n') + filename = name .. ".xml" + end + + -- using filename instead of name because name can be duplicate, but filename cannot + local raid = Raid("data/raids/" .. filename) + + local interval = tonumber(node:attribute("interval2")) + if not interval or interval == 0 then + io.write("[Error] interval2 attribute missing or zero (would divide by 0), check raid " .. name .. " in data/raids/raids.xml\n") + return nil + end + raid.interval = interval + + local margin = tonumber(node:attribute("margin")) + if margin and margin > 0 then + raid.margin = margin * 60 * 1000 + else + io.write("[Warning] margin attribute missing for raid " .. name .. ". Using default: 0\n") + end + + local repeats = tobool(node:attribute("repeat")) + if repeats then + raid.repeats = repeats + end + + local events = parseRaid(filename) + if not events then + return nil + end + + for _, event in ipairs(events) do + raid:addEvent(event.delay, event.callback) + end + + return raid +end + +local event = GlobalEvent("load raids.xml") + +function event.onStartup() + local doc = XMLDocument("data/raids/raids.xml") + local raids = doc:child("raids") + + io.write(">> Loading legacy XML raids from data/raids/raids.xml...\n") + local loaded, start = 0, os.mtime() + for node in raids:children() do + local enabled = node:attribute("enabled") + if enabled == nil or tobool(enabled) then + local raid = configureRaidEvent(node) + if raid then + raid:register() + loaded = loaded + 1 + end + end + end + io.write(">> Loaded " .. loaded .. " raids in " .. os.mtime() - start .. "ms.\n") +end + +event:register() diff --git a/data/scripts/talkactions/force_event.lua b/data/scripts/talkactions/force_event.lua new file mode 100644 index 0000000000..e9f9e68000 --- /dev/null +++ b/data/scripts/talkactions/force_event.lua @@ -0,0 +1,25 @@ +local talk = TalkAction("/event", "!event") + +function talk.onSay(player, words, param) + if not player:getGroup():getAccess() then + return true + end + + if player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then + return false + end + + logCommand(player, words, param) + + local returnValue = Game.startEvent(param) + if returnValue == nil then + player:sendTextMessage(MESSAGE_INFO_DESCR, "No such event exists.") + return false + end + + player:sendTextMessage(MESSAGE_INFO_DESCR, "Event started.") + return returnValue +end + +talk:separator(" ") +talk:register() diff --git a/data/talkactions/scripts/force_raid.lua b/data/talkactions/scripts/force_raid.lua deleted file mode 100644 index 99dd2e9a4f..0000000000 --- a/data/talkactions/scripts/force_raid.lua +++ /dev/null @@ -1,20 +0,0 @@ -function onSay(player, words, param) - if not player:getGroup():getAccess() then - return true - end - - if player:getAccountType() < ACCOUNT_TYPE_GAMEMASTER then - return false - end - - logCommand(player, words, param) - - local returnValue = Game.startRaid(param) - if returnValue ~= RETURNVALUE_NOERROR then - player:sendTextMessage(MESSAGE_INFO_DESCR, Game.getReturnMessage(returnValue)) - else - player:sendTextMessage(MESSAGE_INFO_DESCR, "Raid started.") - end - - return false -end diff --git a/data/talkactions/scripts/reload.lua b/data/talkactions/scripts/reload.lua index b98c08902c..2ba4e88cd3 100644 --- a/data/talkactions/scripts/reload.lua +++ b/data/talkactions/scripts/reload.lua @@ -39,9 +39,6 @@ local reloadTypes = { ["quest"] = RELOAD_TYPE_QUESTS, ["quests"] = RELOAD_TYPE_QUESTS, - ["raid"] = RELOAD_TYPE_RAIDS, - ["raids"] = RELOAD_TYPE_RAIDS, - ["spell"] = RELOAD_TYPE_SPELLS, ["spells"] = RELOAD_TYPE_SPELLS, diff --git a/data/talkactions/talkactions.xml b/data/talkactions/talkactions.xml index 11469b48d4..b3990d6636 100644 --- a/data/talkactions/talkactions.xml +++ b/data/talkactions/talkactions.xml @@ -33,7 +33,6 @@ - diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index e650b94b60..c24ab40b05 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -55,7 +55,6 @@ set(tfs_SRC ${CMAKE_CURRENT_LIST_DIR}/protocollogin.cpp ${CMAKE_CURRENT_LIST_DIR}/protocolold.cpp ${CMAKE_CURRENT_LIST_DIR}/protocolstatus.cpp - ${CMAKE_CURRENT_LIST_DIR}/raids.cpp ${CMAKE_CURRENT_LIST_DIR}/rsa.cpp ${CMAKE_CURRENT_LIST_DIR}/scheduler.cpp ${CMAKE_CURRENT_LIST_DIR}/script.cpp @@ -143,7 +142,6 @@ set(tfs_HDR ${CMAKE_CURRENT_LIST_DIR}/protocolold.h ${CMAKE_CURRENT_LIST_DIR}/protocolstatus.h ${CMAKE_CURRENT_LIST_DIR}/pugicast.h - ${CMAKE_CURRENT_LIST_DIR}/raids.h ${CMAKE_CURRENT_LIST_DIR}/rsa.h ${CMAKE_CURRENT_LIST_DIR}/scheduler.h ${CMAKE_CURRENT_LIST_DIR}/script.h diff --git a/src/const.h b/src/const.h index 56484b1949..058da2a083 100644 --- a/src/const.h +++ b/src/const.h @@ -677,7 +677,6 @@ enum ReloadTypes_t : uint8_t RELOAD_TYPE_MOVEMENTS, RELOAD_TYPE_NPCS, RELOAD_TYPE_QUESTS, - RELOAD_TYPE_RAIDS, RELOAD_TYPE_SCRIPTS, RELOAD_TYPE_SPELLS, RELOAD_TYPE_TALKACTIONS, diff --git a/src/enums.h b/src/enums.h index 3a05d77553..64606fb708 100644 --- a/src/enums.h +++ b/src/enums.h @@ -479,8 +479,6 @@ enum ReturnValue RETURNVALUE_CANONLYUSEONESHIELD, RETURNVALUE_NOPARTYMEMBERSINRANGE, RETURNVALUE_YOUARENOTTHEOWNER, - RETURNVALUE_NOSUCHRAIDEXISTS, - RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING, RETURNVALUE_TRADEPLAYERFARAWAY, RETURNVALUE_YOUDONTOWNTHISHOUSE, RETURNVALUE_TRADEPLAYERALREADYOWNSAHOUSE, diff --git a/src/game.cpp b/src/game.cpp index 0700aa8850..ca2635a9f5 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -99,9 +99,6 @@ void Game::setGameState(GameState_t newState) map.spawns.startup(); - raids.loadFromXml(); - raids.startup(); - mounts.loadFromXml(); loadPlayersRecord(); @@ -4882,7 +4879,6 @@ void Game::shutdown() g_databaseTasks.shutdown(); g_dispatcher.shutdown(); map.spawns.clear(); - raids.clear(); cleanup(); @@ -5931,9 +5927,6 @@ bool Game::reload(ReloadTypes_t reloadType) return true; } - case RELOAD_TYPE_RAIDS: - return raids.reload() && raids.startup(); - case RELOAD_TYPE_SPELLS: { if (!g_spells->reload()) { std::cout << "[Error - Game::reload] Failed to reload spells." << std::endl; @@ -5968,7 +5961,6 @@ bool Game::reload(ReloadTypes_t reloadType) g_creatureEvents->removeInvalidEvents(); /* Npcs::reload(); - raids.reload() && raids.startup(); Item::items.reload(); mounts.reload(); g_config.reload(); @@ -5993,7 +5985,6 @@ bool Game::reload(ReloadTypes_t reloadType) g_monsters.reload(); g_moveEvents->reload(); Npcs::reload(); - raids.reload() && raids.startup(); g_talkActions->reload(); Item::items.reload(); g_weapons->reload(); diff --git a/src/game.h b/src/game.h index 3eaab7e2f9..2a3d011ff9 100644 --- a/src/game.h +++ b/src/game.h @@ -9,7 +9,6 @@ #include "mounts.h" #include "player.h" #include "position.h" -#include "raids.h" #include "wildcardtree.h" class Monster; @@ -494,7 +493,6 @@ class Game Groups groups; Map map; Mounts mounts; - Raids raids; std::forward_list toDecayItems; diff --git a/src/globalevent.cpp b/src/globalevent.cpp index da28c3ac43..a164cb3804 100644 --- a/src/globalevent.cpp +++ b/src/globalevent.cpp @@ -117,7 +117,7 @@ void GlobalEvents::save() const { execute(GLOBALEVENT_SAVE); } void GlobalEvents::timer() { - time_t now = time(nullptr); + auto now = OTSYS_TIME(); int64_t nextScheduledTime = std::numeric_limits::max(); @@ -140,7 +140,7 @@ void GlobalEvents::timer() continue; } - nextExecutionTime = 86400; + nextExecutionTime = 86400000; if (nextExecutionTime < nextScheduledTime) { nextScheduledTime = nextExecutionTime; } @@ -152,7 +152,7 @@ void GlobalEvents::timer() if (nextScheduledTime != std::numeric_limits::max()) { timerEventId = g_scheduler.addEvent( - createSchedulerTask(std::max(1000, nextScheduledTime * 1000), [this]() { timer(); })); + createSchedulerTask(std::max(1000, nextScheduledTime), [this]() { timer(); })); } } @@ -282,7 +282,7 @@ bool GlobalEvent::configureEvent(const pugi::xml_node& node) difference += 86400; } - nextExecution = current_time + difference; + nextExecution = (current_time + difference) * 1000; eventType = GLOBALEVENT_TIMER; } else if ((attr = node.attribute("type"))) { const char* value = attr.value(); diff --git a/src/luascript.cpp b/src/luascript.cpp index bdeaf88854..ed0bb6c8d5 100644 --- a/src/luascript.cpp +++ b/src/luascript.cpp @@ -41,6 +41,7 @@ extern Chat* g_chat; extern Game g_game; +extern GlobalEvents* g_globalEvents; extern Monsters g_monsters; extern ConfigManager g_config; extern Vocations g_vocations; @@ -2072,7 +2073,6 @@ void LuaScriptInterface::registerFunctions() registerEnum(RELOAD_TYPE_MOVEMENTS); registerEnum(RELOAD_TYPE_NPCS); registerEnum(RELOAD_TYPE_QUESTS); - registerEnum(RELOAD_TYPE_RAIDS); registerEnum(RELOAD_TYPE_SCRIPTS); registerEnum(RELOAD_TYPE_SPELLS); registerEnum(RELOAD_TYPE_TALKACTIONS); @@ -2262,7 +2262,7 @@ void LuaScriptInterface::registerFunctions() registerMethod("Game", "createTile", LuaScriptInterface::luaGameCreateTile); registerMethod("Game", "createMonsterType", LuaScriptInterface::luaGameCreateMonsterType); - registerMethod("Game", "startRaid", LuaScriptInterface::luaGameStartRaid); + registerMethod("Game", "startEvent", LuaScriptInterface::luaGameStartEvent); registerMethod("Game", "getClientVersion", LuaScriptInterface::luaGameGetClientVersion); @@ -5050,25 +5050,17 @@ int LuaScriptInterface::luaGameCreateMonsterType(lua_State* L) return 1; } -int LuaScriptInterface::luaGameStartRaid(lua_State* L) +int LuaScriptInterface::luaGameStartEvent(lua_State* L) { - // Game.startRaid(raidName) - const std::string& raidName = getString(L, 1); + // Game.startEvent(event) + const std::string& eventName = getString(L, 1); - Raid* raid = g_game.raids.getRaidByName(raidName); - if (!raid || !raid->isLoaded()) { - lua_pushnumber(L, RETURNVALUE_NOSUCHRAIDEXISTS); - return 1; - } - - if (g_game.raids.getRunning()) { - lua_pushnumber(L, RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING); - return 1; + const auto& eventMap = g_globalEvents->getEventMap(GLOBALEVENT_TIMER); + if (auto it = eventMap.find(eventName); it != eventMap.end()) { + pushBoolean(L, it->second.executeEvent()); + } else { + lua_pushnil(L); } - - g_game.raids.setRunning(raid); - raid->startRaid(); - lua_pushnumber(L, RETURNVALUE_NOERROR); return 1; } @@ -17504,6 +17496,8 @@ int LuaScriptInterface::luaGlobalEventType(lua_State* L) global->setEventType(GLOBALEVENT_SHUTDOWN); } else if (tmpStr == "record") { global->setEventType(GLOBALEVENT_RECORD); + } else if (tmpStr == "timer") { + global->setEventType(GLOBALEVENT_TIMER); } else if (tmpStr == "save") { global->setEventType(GLOBALEVENT_SAVE); } else { diff --git a/src/luascript.h b/src/luascript.h index 205c8d69dd..f2637a590d 100644 --- a/src/luascript.h +++ b/src/luascript.h @@ -551,7 +551,7 @@ class LuaScriptInterface static int luaGameCreateTile(lua_State* L); static int luaGameCreateMonsterType(lua_State* L); - static int luaGameStartRaid(lua_State* L); + static int luaGameStartEvent(lua_State* L); static int luaGameGetClientVersion(lua_State* L); diff --git a/src/raids.cpp b/src/raids.cpp deleted file mode 100644 index 053f40af30..0000000000 --- a/src/raids.cpp +++ /dev/null @@ -1,572 +0,0 @@ -// Copyright 2023 The Forgotten Server Authors. All rights reserved. -// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file. - -#include "otpch.h" - -#include "raids.h" - -#include "configmanager.h" -#include "game.h" -#include "monster.h" -#include "pugicast.h" -#include "scheduler.h" - -extern Game g_game; -extern ConfigManager g_config; - -Raids::Raids() { scriptInterface.initState(); } - -Raids::~Raids() -{ - for (Raid* raid : raidList) { - delete raid; - } -} - -bool Raids::loadFromXml() -{ - if (isLoaded()) { - return true; - } - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file("data/raids/raids.xml"); - if (!result) { - printXMLError("Error - Raids::loadFromXml", "data/raids/raids.xml", result); - return false; - } - - for (auto raidNode : doc.child("raids").children()) { - std::string name, file; - uint32_t interval, margin; - - pugi::xml_attribute attr; - if ((attr = raidNode.attribute("name"))) { - name = attr.as_string(); - } else { - std::cout << "[Error - Raids::loadFromXml] Name tag missing for raid" << std::endl; - continue; - } - - if ((attr = raidNode.attribute("file"))) { - file = attr.as_string(); - } else { - file = fmt::format("raids/{:s}.xml", name); - std::cout << "[Warning - Raids::loadFromXml] File tag missing for raid " << name - << ". Using default: " << file << std::endl; - } - - interval = pugi::cast(raidNode.attribute("interval2").value()) * 60; - if (interval == 0) { - std::cout << "[Error - Raids::loadFromXml] interval2 tag missing or zero (would divide by 0) for raid: " - << name << std::endl; - continue; - } - - if ((attr = raidNode.attribute("margin"))) { - margin = pugi::cast(attr.value()) * 60 * 1000; - } else { - std::cout << "[Warning - Raids::loadFromXml] margin tag missing for raid: " << name << std::endl; - margin = 0; - } - - bool repeat; - if ((attr = raidNode.attribute("repeat"))) { - repeat = booleanString(attr.as_string()); - } else { - repeat = false; - } - - Raid* newRaid = new Raid(name, interval, margin, repeat); - if (newRaid->loadFromXml("data/raids/" + file)) { - raidList.push_back(newRaid); - } else { - std::cout << "[Error - Raids::loadFromXml] Failed to load raid: " << name << std::endl; - delete newRaid; - } - } - - loaded = true; - return true; -} - -static constexpr int32_t MAX_RAND_RANGE = 10000000; - -bool Raids::startup() -{ - if (!isLoaded() || isStarted()) { - return false; - } - - setLastRaidEnd(OTSYS_TIME()); - - checkRaidsEvent = - g_scheduler.addEvent(createSchedulerTask(CHECK_RAIDS_INTERVAL * 1000, [this]() { checkRaids(); })); - - started = true; - return started; -} - -void Raids::checkRaids() -{ - if (!getRunning()) { - uint64_t now = OTSYS_TIME(); - - for (auto it = raidList.begin(), end = raidList.end(); it != end; ++it) { - Raid* raid = *it; - if (now >= (getLastRaidEnd() + raid->getMargin())) { - if (((MAX_RAND_RANGE * CHECK_RAIDS_INTERVAL) / raid->getInterval()) >= - static_cast(uniform_random(0, MAX_RAND_RANGE))) { - setRunning(raid); - raid->startRaid(); - - if (!raid->canBeRepeated()) { - raidList.erase(it); - } - break; - } - } - } - } - - checkRaidsEvent = - g_scheduler.addEvent(createSchedulerTask(CHECK_RAIDS_INTERVAL * 1000, [this]() { checkRaids(); })); -} - -void Raids::clear() -{ - g_scheduler.stopEvent(checkRaidsEvent); - checkRaidsEvent = 0; - - for (Raid* raid : raidList) { - raid->stopEvents(); - delete raid; - } - raidList.clear(); - - loaded = false; - started = false; - running = nullptr; - lastRaidEnd = 0; - - scriptInterface.reInitState(); -} - -bool Raids::reload() -{ - clear(); - return loadFromXml(); -} - -Raid* Raids::getRaidByName(const std::string& name) -{ - for (Raid* raid : raidList) { - if (caseInsensitiveEqual(raid->getName(), name)) { - return raid; - } - } - return nullptr; -} - -Raid::~Raid() -{ - for (RaidEvent* raidEvent : raidEvents) { - delete raidEvent; - } -} - -bool Raid::loadFromXml(const std::string& filename) -{ - if (isLoaded()) { - return true; - } - - pugi::xml_document doc; - pugi::xml_parse_result result = doc.load_file(filename.c_str()); - if (!result) { - printXMLError("Error - Raid::loadFromXml", filename, result); - return false; - } - - for (auto eventNode : doc.child("raid").children()) { - RaidEvent* event; - if (caseInsensitiveEqual(eventNode.name(), "announce")) { - event = new AnnounceEvent(); - } else if (caseInsensitiveEqual(eventNode.name(), "singlespawn")) { - event = new SingleSpawnEvent(); - } else if (caseInsensitiveEqual(eventNode.name(), "areaspawn")) { - event = new AreaSpawnEvent(); - } else if (caseInsensitiveEqual(eventNode.name(), "script")) { - event = new ScriptEvent(&g_game.raids.getScriptInterface()); - } else { - continue; - } - - if (event->configureRaidEvent(eventNode)) { - raidEvents.push_back(event); - } else { - std::cout << "[Error - Raid::loadFromXml] In file (" << filename << "), eventNode: " << eventNode.name() - << std::endl; - delete event; - } - } - - // sort by delay time - std::sort(raidEvents.begin(), raidEvents.end(), - [](const RaidEvent* lhs, const RaidEvent* rhs) { return lhs->getDelay() < rhs->getDelay(); }); - - loaded = true; - return true; -} - -void Raid::startRaid() -{ - RaidEvent* raidEvent = getNextRaidEvent(); - if (raidEvent) { - state = RAIDSTATE_EXECUTING; - nextEventEvent = g_scheduler.addEvent( - createSchedulerTask(raidEvent->getDelay(), [=, this]() { executeRaidEvent(raidEvent); })); - } -} - -void Raid::executeRaidEvent(RaidEvent* raidEvent) -{ - if (raidEvent->executeEvent()) { - nextEvent++; - RaidEvent* newRaidEvent = getNextRaidEvent(); - - if (newRaidEvent) { - uint32_t ticks = static_cast( - std::max(RAID_MINTICKS, newRaidEvent->getDelay() - raidEvent->getDelay())); - nextEventEvent = - g_scheduler.addEvent(createSchedulerTask(ticks, [=, this]() { executeRaidEvent(newRaidEvent); })); - } else { - resetRaid(); - } - } else { - resetRaid(); - } -} - -void Raid::resetRaid() -{ - nextEvent = 0; - state = RAIDSTATE_IDLE; - g_game.raids.setRunning(nullptr); - g_game.raids.setLastRaidEnd(OTSYS_TIME()); -} - -void Raid::stopEvents() -{ - if (nextEventEvent != 0) { - g_scheduler.stopEvent(nextEventEvent); - nextEventEvent = 0; - } -} - -RaidEvent* Raid::getNextRaidEvent() -{ - if (nextEvent < raidEvents.size()) { - return raidEvents[nextEvent]; - } - return nullptr; -} - -bool RaidEvent::configureRaidEvent(const pugi::xml_node& eventNode) -{ - pugi::xml_attribute delayAttribute = eventNode.attribute("delay"); - if (!delayAttribute) { - std::cout << "[Error] Raid: delay tag missing." << std::endl; - return false; - } - - delay = std::max(RAID_MINTICKS, pugi::cast(delayAttribute.value())); - return true; -} - -bool AnnounceEvent::configureRaidEvent(const pugi::xml_node& eventNode) -{ - if (!RaidEvent::configureRaidEvent(eventNode)) { - return false; - } - - pugi::xml_attribute messageAttribute = eventNode.attribute("message"); - if (!messageAttribute) { - std::cout << "[Error] Raid: message tag missing for announce event." << std::endl; - return false; - } - message = messageAttribute.as_string(); - - pugi::xml_attribute typeAttribute = eventNode.attribute("type"); - if (typeAttribute) { - std::string tmpStrValue = boost::algorithm::to_lower_copy(typeAttribute.as_string()); - if (tmpStrValue == "warning") { - messageType = MESSAGE_STATUS_WARNING; - } else if (tmpStrValue == "event") { - messageType = MESSAGE_EVENT_ADVANCE; - } else if (tmpStrValue == "default") { - messageType = MESSAGE_EVENT_DEFAULT; - } else if (tmpStrValue == "description") { - messageType = MESSAGE_INFO_DESCR; - } else if (tmpStrValue == "smallstatus") { - messageType = MESSAGE_STATUS_SMALL; - } else if (tmpStrValue == "blueconsole" || tmpStrValue == "redconsole") { - std::cout << "[Notice] Raid: Deprecated type tag for announce event. Using default: " - << static_cast(messageType) << std::endl; - } else { - std::cout << "[Notice] Raid: Unknown type tag missing for announce event. Using default: " - << static_cast(messageType) << std::endl; - } - } else { - messageType = MESSAGE_EVENT_ADVANCE; - std::cout << "[Notice] Raid: type tag missing for announce event. Using default: " - << static_cast(messageType) << std::endl; - } - return true; -} - -bool AnnounceEvent::executeEvent() -{ - g_game.broadcastMessage(message, messageType); - return true; -} - -bool SingleSpawnEvent::configureRaidEvent(const pugi::xml_node& eventNode) -{ - if (!RaidEvent::configureRaidEvent(eventNode)) { - return false; - } - - pugi::xml_attribute attr; - if ((attr = eventNode.attribute("name"))) { - monsterName = attr.as_string(); - } else { - std::cout << "[Error] Raid: name tag missing for singlespawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("x"))) { - position.x = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: x tag missing for singlespawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("y"))) { - position.y = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: y tag missing for singlespawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("z"))) { - position.z = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: z tag missing for singlespawn event." << std::endl; - return false; - } - return true; -} - -bool SingleSpawnEvent::executeEvent() -{ - Monster* monster = Monster::createMonster(monsterName); - if (!monster) { - std::cout << "[Error] Raids: Cant create monster " << monsterName << std::endl; - return false; - } - - if (!g_game.placeCreature(monster, position, false, true)) { - delete monster; - std::cout << "[Error] Raids: Cant place monster " << monsterName << std::endl; - return false; - } - return true; -} - -bool AreaSpawnEvent::configureRaidEvent(const pugi::xml_node& eventNode) -{ - if (!RaidEvent::configureRaidEvent(eventNode)) { - return false; - } - - pugi::xml_attribute attr; - if ((attr = eventNode.attribute("radius"))) { - int32_t radius = pugi::cast(attr.value()); - Position centerPos; - - if ((attr = eventNode.attribute("centerx"))) { - centerPos.x = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: centerx tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("centery"))) { - centerPos.y = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: centery tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("centerz"))) { - centerPos.z = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: centerz tag missing for areaspawn event." << std::endl; - return false; - } - - fromPos.x = std::max(0, centerPos.getX() - radius); - fromPos.y = std::max(0, centerPos.getY() - radius); - fromPos.z = centerPos.z; - - toPos.x = std::min(0xFFFF, centerPos.getX() + radius); - toPos.y = std::min(0xFFFF, centerPos.getY() + radius); - toPos.z = centerPos.z; - } else { - if ((attr = eventNode.attribute("fromx"))) { - fromPos.x = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: fromx tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("fromy"))) { - fromPos.y = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: fromy tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("fromz"))) { - fromPos.z = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: fromz tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("tox"))) { - toPos.x = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: tox tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("toy"))) { - toPos.y = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: toy tag missing for areaspawn event." << std::endl; - return false; - } - - if ((attr = eventNode.attribute("toz"))) { - toPos.z = pugi::cast(attr.value()); - } else { - std::cout << "[Error] Raid: toz tag missing for areaspawn event." << std::endl; - return false; - } - } - - for (auto monsterNode : eventNode.children()) { - const char* name; - - if ((attr = monsterNode.attribute("name"))) { - name = attr.value(); - } else { - std::cout << "[Error] Raid: name tag missing for monster node." << std::endl; - return false; - } - - uint32_t minAmount; - if ((attr = monsterNode.attribute("minamount"))) { - minAmount = pugi::cast(attr.value()); - } else { - minAmount = 0; - } - - uint32_t maxAmount; - if ((attr = monsterNode.attribute("maxamount"))) { - maxAmount = pugi::cast(attr.value()); - } else { - maxAmount = 0; - } - - if (maxAmount == 0 && minAmount == 0) { - if ((attr = monsterNode.attribute("amount"))) { - minAmount = pugi::cast(attr.value()); - maxAmount = minAmount; - } else { - std::cout << "[Error] Raid: amount tag missing for monster node." << std::endl; - return false; - } - } - - spawnList.emplace_back(name, minAmount, maxAmount); - } - return true; -} - -bool AreaSpawnEvent::executeEvent() -{ - for (const MonsterSpawn& spawn : spawnList) { - uint32_t amount = uniform_random(spawn.minAmount, spawn.maxAmount); - for (uint32_t i = 0; i < amount; ++i) { - Monster* monster = Monster::createMonster(spawn.name); - if (!monster) { - std::cout << "[Error - AreaSpawnEvent::executeEvent] Can't create monster " << spawn.name << std::endl; - return false; - } - - bool success = false; - for (int32_t tries = 0; tries < MAXIMUM_TRIES_PER_MONSTER; tries++) { - Tile* tile = g_game.map.getTile(uniform_random(fromPos.x, toPos.x), uniform_random(fromPos.y, toPos.y), - uniform_random(fromPos.z, toPos.z)); - if (tile && !tile->isMoveableBlocking() && !tile->hasFlag(TILESTATE_PROTECTIONZONE) && - !tile->getTopCreature() && g_game.placeCreature(monster, tile->getPosition(), false, true)) { - success = true; - break; - } - } - - if (!success) { - delete monster; - } - } - } - return true; -} - -bool ScriptEvent::configureRaidEvent(const pugi::xml_node& eventNode) -{ - if (!RaidEvent::configureRaidEvent(eventNode)) { - return false; - } - - pugi::xml_attribute scriptAttribute = eventNode.attribute("script"); - if (!scriptAttribute) { - std::cout << "Error: [ScriptEvent::configureRaidEvent] No script file found for raid" << std::endl; - return false; - } - - if (!loadScript("data/raids/scripts/" + std::string(scriptAttribute.as_string()))) { - std::cout << "Error: [ScriptEvent::configureRaidEvent] Can not load raid script." << std::endl; - return false; - } - return true; -} - -bool ScriptEvent::executeEvent() -{ - // onRaid() - if (!scriptInterface->reserveScriptEnv()) { - std::cout << "[Error - ScriptEvent::onRaid] Call stack overflow" << std::endl; - return false; - } - - ScriptEnvironment* env = scriptInterface->getScriptEnv(); - env->setScriptId(scriptId, scriptInterface); - - scriptInterface->pushFunction(scriptId); - - return scriptInterface->callFunction(0); -} diff --git a/src/raids.h b/src/raids.h deleted file mode 100644 index d09a3b597d..0000000000 --- a/src/raids.h +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright 2023 The Forgotten Server Authors. All rights reserved. -// Use of this source code is governed by the GPL-2.0 License that can be found in the LICENSE file. - -#ifndef FS_RAIDS_H -#define FS_RAIDS_H - -#include "baseevents.h" -#include "const.h" -#include "luascript.h" -#include "position.h" - -enum RaidState_t -{ - RAIDSTATE_IDLE, - RAIDSTATE_EXECUTING, -}; - -struct MonsterSpawn -{ - MonsterSpawn(std::string name, uint32_t minAmount, uint32_t maxAmount) : - name(std::move(name)), minAmount(minAmount), maxAmount(maxAmount) - {} - - std::string name; - uint32_t minAmount; - uint32_t maxAmount; -}; - -// How many times it will try to find a tile to add the monster to before giving -// up -static constexpr int32_t MAXIMUM_TRIES_PER_MONSTER = 10; -static constexpr int32_t CHECK_RAIDS_INTERVAL = 60; -static constexpr int32_t RAID_MINTICKS = 1000; - -class Raid; -class RaidEvent; - -class Raids -{ -public: - Raids(); - ~Raids(); - - // non-copyable - Raids(const Raids&) = delete; - Raids& operator=(const Raids&) = delete; - - bool loadFromXml(); - bool startup(); - - void clear(); - bool reload(); - - bool isLoaded() const { return loaded; } - bool isStarted() const { return started; } - - Raid* getRunning() { return running; } - void setRunning(Raid* newRunning) { running = newRunning; } - - Raid* getRaidByName(const std::string& name); - - uint64_t getLastRaidEnd() const { return lastRaidEnd; } - void setLastRaidEnd(uint64_t newLastRaidEnd) { lastRaidEnd = newLastRaidEnd; } - - void checkRaids(); - - LuaScriptInterface& getScriptInterface() { return scriptInterface; } - -private: - LuaScriptInterface scriptInterface{"Raid Interface"}; - - std::list raidList; - Raid* running = nullptr; - uint64_t lastRaidEnd = 0; - uint32_t checkRaidsEvent = 0; - bool loaded = false; - bool started = false; -}; - -class Raid -{ -public: - Raid(std::string name, uint32_t interval, uint32_t marginTime, bool repeat) : - name(std::move(name)), interval(interval), margin(marginTime), repeat(repeat) - {} - ~Raid(); - - // non-copyable - Raid(const Raid&) = delete; - Raid& operator=(const Raid&) = delete; - - bool loadFromXml(const std::string& filename); - - void startRaid(); - - void executeRaidEvent(RaidEvent* raidEvent); - void resetRaid(); - - RaidEvent* getNextRaidEvent(); - void setState(RaidState_t newState) { state = newState; } - const std::string& getName() const { return name; } - - bool isLoaded() const { return loaded; } - uint64_t getMargin() const { return margin; } - uint32_t getInterval() const { return interval; } - bool canBeRepeated() const { return repeat; } - - void stopEvents(); - -private: - std::vector raidEvents; - std::string name; - uint32_t interval; - uint32_t nextEvent = 0; - uint64_t margin; - RaidState_t state = RAIDSTATE_IDLE; - uint32_t nextEventEvent = 0; - bool loaded = false; - bool repeat; -}; - -class RaidEvent -{ -public: - virtual ~RaidEvent() = default; - - virtual bool configureRaidEvent(const pugi::xml_node& eventNode); - - virtual bool executeEvent() = 0; - uint32_t getDelay() const { return delay; } - -private: - uint32_t delay; -}; - -class AnnounceEvent final : public RaidEvent -{ -public: - AnnounceEvent() = default; - - bool configureRaidEvent(const pugi::xml_node& eventNode) override; - - bool executeEvent() override; - -private: - std::string message; - MessageClasses messageType = MESSAGE_EVENT_ADVANCE; -}; - -class SingleSpawnEvent final : public RaidEvent -{ -public: - bool configureRaidEvent(const pugi::xml_node& eventNode) override; - - bool executeEvent() override; - -private: - std::string monsterName; - Position position; -}; - -class AreaSpawnEvent final : public RaidEvent -{ -public: - bool configureRaidEvent(const pugi::xml_node& eventNode) override; - - bool executeEvent() override; - -private: - std::list spawnList; - Position fromPos, toPos; -}; - -class ScriptEvent final : public RaidEvent, public Event -{ -public: - explicit ScriptEvent(LuaScriptInterface* interface) : Event(interface) {} - - bool configureRaidEvent(const pugi::xml_node& eventNode) override; - bool configureEvent(const pugi::xml_node&) override { return false; } - - bool executeEvent() override; - -private: - std::string_view getScriptEventName() const override { return "onRaid"; } -}; - -#endif // FS_RAIDS_H diff --git a/src/signals.cpp b/src/signals.cpp index 3b8fd6d846..0f1bdabc65 100644 --- a/src/signals.cpp +++ b/src/signals.cpp @@ -15,7 +15,6 @@ #include "mounts.h" #include "movement.h" #include "npc.h" -#include "raids.h" #include "scheduler.h" #include "spells.h" #include "talkaction.h" @@ -73,10 +72,6 @@ void sighupHandler() Npcs::reload(); std::cout << "Reloaded npcs." << std::endl; - g_game.raids.reload(); - g_game.raids.startup(); - std::cout << "Reloaded raids." << std::endl; - g_monsters.reload(); std::cout << "Reloaded monsters." << std::endl; diff --git a/src/tools.cpp b/src/tools.cpp index a047b690ba..3b5d506dce 100644 --- a/src/tools.cpp +++ b/src/tools.cpp @@ -1156,12 +1156,6 @@ const char* getReturnMessage(ReturnValue value) case RETURNVALUE_YOUARENOTTHEOWNER: return "You are not the owner."; - case RETURNVALUE_NOSUCHRAIDEXISTS: - return "No such raid exists."; - - case RETURNVALUE_ANOTHERRAIDISALREADYEXECUTING: - return "Another raid is already executing."; - case RETURNVALUE_TRADEPLAYERFARAWAY: return "Trade player is too far away."; diff --git a/vc17/theforgottenserver.vcxproj b/vc17/theforgottenserver.vcxproj index e358a52a6b..d2765ce3d7 100644 --- a/vc17/theforgottenserver.vcxproj +++ b/vc17/theforgottenserver.vcxproj @@ -222,7 +222,6 @@ - @@ -307,7 +306,6 @@ -