diff --git a/.agents/README.md b/.agents/README.md new file mode 100644 index 000000000..395f65b17 --- /dev/null +++ b/.agents/README.md @@ -0,0 +1,17 @@ +# .agents + +Source of truth for this repo's agent skills and rules: + +- `skills//SKILL.md` +- `rules/.md` + +Agent-specific directories expose each via a relative symlink — for Claude Code: + + .claude/skills/ -> ../../.agents/skills/ + .claude/rules/.md -> ../../.agents/rules/.md + +To add a skill or rule: create it here, then symlink it. + +The skills in this repository often build on those from +[agent-toolkit](https://github.com/eai-org/agent-toolkit/), invoking them when available. +Installing the toolkit at user level is therefore recommended. diff --git a/.agents/skills/generate-pr-description/SKILL.md b/.agents/skills/generate-pr-description/SKILL.md new file mode 100644 index 000000000..ea7b505cc --- /dev/null +++ b/.agents/skills/generate-pr-description/SKILL.md @@ -0,0 +1,69 @@ +--- +name: generate-pr-description +description: Generate the PR title and description from the current branch changes, following the repo's PR template. +metadata: + version: "1.0" +--- + +# Generate PR description + +Produce a copy-pasteable PR title and body for the current changes, based on the repo's +`pull_request_template.md`. Output text only — never create the PR, push, or run any +state-changing git command. + +If a `use-conversational-language` skill is available, invoke it and follow it when writing the +prose. If not available, fetch it from its [remote source](https://github.com/eai-org/agent-toolkit/blob/main/skills/use-conversational-language/SKILL.md). + +## Gather the changes + +"The changes" is the union of: + +- committed branch work: `git diff ...HEAD`, where `` is `origin/master` — + or `upstream/master` when the checkout is a fork with an `upstream` remote; +- staged and unstaged changes; +- the branch's commit messages (context only). + +If all three are empty, say so and stop. + +## Facts, never assumptions + +Only two sources may fill the description: the diff and facts stated in the conversation. Never +present anything else as fact — no assumed testing, sources, issue links, or TODOs. A section +without backing facts stays empty/unchecked, silently: an empty section is correct, a guessed one +is wrong. + +Ask the user only when unsure about substance — what the change does, which scope the title should +carry, contradictory information. Missing fill-in facts are never a reason to ask. + +## Title + +`type(Scope/Subscope): short description` — imperative, max 50 chars. Types: `feat`, `fix`, +`refactor`, `style`, `docs`, `test`, `chore`; scopes: `Core`, `Scripts`, `DB`. Copy subscope naming +from recent commits touching the same areas (`git log --oneline -- `). + +## Body + +Fill in `pull_request_template.md` (repo root): + +- Strip the instructional `` comments; keep the "How to Test AzerothCore PRs" footer + intact. +- Describe what the changes do and why. +- "Changes Proposed" checkboxes, from diff paths: `src/server/scripts/` → Scripts; `data/sql/` → + Database; other `src/` → Core. +- AI disclosure checkbox: always tick it; name the agent/model running this skill and state + whether AI produced the changes themselves or only this description, per the conversation. +- Issues Addressed, SOURCE, Tests Performed, How to Test, Known Issues/TODO: stated facts only + (see above). + +## Output + +Print exactly: + +~~~ +TITLE: + +PR DESCRIPTION: +```markdown +<body> +``` +~~~ diff --git a/.claude/skills/generate-pr-description b/.claude/skills/generate-pr-description new file mode 120000 index 000000000..3aff0944b --- /dev/null +++ b/.claude/skills/generate-pr-description @@ -0,0 +1 @@ +../../.agents/skills/generate-pr-description \ No newline at end of file diff --git a/.coderabbit.yml b/.coderabbit.yml index 506590fb5..c3c15eafc 100644 --- a/.coderabbit.yml +++ b/.coderabbit.yml @@ -1,5 +1,5 @@ # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json -# CodeRabbit review config. Mirrors the hard rules in CLAUDE.md so the AI +# CodeRabbit review config. Mirrors the hard rules in AGENTS.md so the AI # reviewer enforces project conventions on every PR. language: en-US @@ -74,10 +74,10 @@ reviews: knowledge_base: code_guidelines: enabled: true - # CLAUDE.md is in CodeRabbit's defaults, but list it explicitly so the project's + # AGENTS.md is in CodeRabbit's defaults, but list it explicitly so the project's # full guideline doc is always pulled into review context. filePatterns: - - "CLAUDE.md" + - "AGENTS.md" chat: auto_reply: true diff --git a/.github/SECURITY.md b/.github/SECURITY.md index 00d09000e..f2de9c7cc 100644 --- a/.github/SECURITY.md +++ b/.github/SECURITY.md @@ -23,10 +23,10 @@ We support the following versions of dependencies. ### Supported Operating Systems | Linux (Ubuntu) | Status | Recommended | -| :------------- | :----------------: | :------------------: | -| 24.04 | :white_check_mark: | :large_blue_diamond: | -| 22.04 | :white_check_mark: | | -| 20.04 ≤ | :red_circle: | | +|:---------------| :----------------: | :------------------: | +| 26.04 | :white_check_mark: | :large_blue_diamond: | +| 24.04 | :white_check_mark: | | +| 22.04 ≤ | :red_circle: | | | macOS | Status | Recommended | | :---- | :----------------: | :------------------: | diff --git a/.github/actions/linux-build/action.yml b/.github/actions/linux-build/action.yml index 43d6fd29f..20bc9d1f5 100644 --- a/.github/actions/linux-build/action.yml +++ b/.github/actions/linux-build/action.yml @@ -64,23 +64,14 @@ runs: run: | sudo apt update sudo apt remove needrestart #refer: https://github.com/actions/runner-images/issues/9937 - sudo apt-get -y install ccache clang cmake curl google-perftools \ - libmysqlclient-dev make unzip build-essential cmake-data \ - libboost-all-dev libbz2-dev libncurses5-dev libmysql++-dev \ + sudo apt-get -y install ccache clang cmake curl \ + make unzip build-essential cmake-data \ + libboost-all-dev libbz2-dev libmysql++-dev \ libreadline6-dev libssl-dev libtool openssl zlib1g-dev - - # Account for https://github.com/actions/runner-images/issues/8659 - # based off of https://github.com/actions/runner-images/issues/8659#issuecomment-1852353116 - UBUNTU_VERSION="$(grep VERSION_ID /etc/os-release | cut -f2 -d\")" - source /etc/os-release - if [[ "$VERSION_CODENAME" == "jammy" ]]; then - if [[ "${{ inputs.CC }}" =~ "clang-" ]]; then - CLANG_VERSION="$(echo '${{ inputs.CC }}' | cut -f2 -d\-)" - wget -qO- https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - - sudo add-apt-repository "deb http://apt.llvm.org/$VERSION_CODENAME/ llvm-toolchain-$VERSION_CODENAME-$CLANG_VERSION main" - sudo apt-get -qq update - sudo apt-get -qq install '${{ inputs.CC }}' - fi + if [[ "$(lsb_release -sr)" == "26.04" ]]; then + sudo apt-get -y install libgoogle-perftools-dev default-libmysqlclient-dev + else + sudo apt-get -y install google-perftools libmysqlclient-dev libncurses5-dev fi - name: setup ccache diff --git a/.github/agents/README.md b/.github/agents/README.md index 488bd1026..2e7e7d58c 100644 --- a/.github/agents/README.md +++ b/.github/agents/README.md @@ -19,14 +19,14 @@ The PR Reviewer agent is configured to review pull requests with deep understand - Testing and validation requirements - Security and quality standards -**Key Feature**: This agent always references `/CLAUDE.md`, the C++ Code Standards wiki, and the SQL Standards wiki before reviewing any PR. +**Key Feature**: This agent always references `/AGENTS.md`, the C++ Code Standards wiki, and the SQL Standards wiki before reviewing any PR. ## How It Works When an AI tool reviews a PR in this repository: 1. The tool reads the appropriate agent configuration from this directory -2. The agent configuration instructs it to read `/CLAUDE.md` for full project context +2. The agent configuration instructs it to read `/AGENTS.md` for full project context 3. For C++ changes, the agent also references the [C++ Code Standards wiki](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) 4. For SQL changes, the agent also references the [SQL Standards wiki](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) 5. The agent applies project-specific rules during review @@ -36,7 +36,7 @@ When an AI tool reviews a PR in this repository: If you're using AI tools to assist with contributions: -1. Familiarize yourself with `/CLAUDE.md` - it contains essential project knowledge +1. Familiarize yourself with `/AGENTS.md` - it contains essential project knowledge 2. Review the [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) for C++ contributions 3. Review the [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) for database contributions 4. The AI agents will help ensure your changes follow project standards @@ -47,13 +47,13 @@ If you're using AI tools to assist with contributions: To update agent configurations: 1. Edit the relevant `.md` file in this directory -2. Ensure any changes align with documentation in `/CLAUDE.md` and the wiki standards +2. Ensure any changes align with documentation in `/AGENTS.md` and the wiki standards 3. Test the updated configuration with a sample PR review 4. Commit changes following the project's commit message format ## References -- Project guidelines: `/CLAUDE.md` +- Project guidelines: `/AGENTS.md` - C++ standards: [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) - SQL standards: [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) - PR template: `/pull_request_template.md` diff --git a/.github/agents/pr-reviewer.md b/.github/agents/pr-reviewer.md index c685744c7..878f6aa97 100644 --- a/.github/agents/pr-reviewer.md +++ b/.github/agents/pr-reviewer.md @@ -5,13 +5,13 @@ You are an expert code reviewer for the AzerothCore project. When reviewing pull ## Required Reading Before reviewing any PR, you MUST read and follow the instructions in: -- `/CLAUDE.md` - Contains comprehensive project architecture, coding standards, build instructions, and PR requirements +- `/AGENTS.md` - Contains comprehensive project architecture, coding standards, build instructions, and PR requirements - [C++ Code Standards](https://github.com/azerothcore/wiki/blob/master/docs/cpp-code-standards.md) - Detailed C++ coding conventions and style guide - [SQL Standards](https://github.com/azerothcore/wiki/blob/master/docs/sql-standards.md) - SQL query formatting and database standards ## Key Review Focus Areas -Based on CLAUDE.md, always verify: +Based on AGENTS.md, always verify: ### 1. Commit Message Format - Uses Conventional Commits: `Type(Scope/Subscope): Short description` @@ -95,7 +95,7 @@ Based on CLAUDE.md, always verify: ## Review Process -1. Read CLAUDE.md to understand project context +1. Read AGENTS.md to understand project context 2. Review C++ Code Standards wiki for C++ changes 3. Review SQL Standards wiki for database changes 4. Check commit message format @@ -108,7 +108,7 @@ Based on CLAUDE.md, always verify: ## Feedback Style - Be constructive and educational -- Reference specific sections of CLAUDE.md, C++ Code Standards, or SQL Standards when applicable +- Reference specific sections of AGENTS.md, C++ Code Standards, or SQL Standards when applicable - Suggest specific fixes with code examples if necessary - Highlight both issues and good practices - For C++ issues, cite specific rule from C++ Code Standards wiki diff --git a/.github/workflows/core-build-nopch.yml b/.github/workflows/core-build-nopch.yml index d1faab4b9..ec4ce9352 100644 --- a/.github/workflows/core-build-nopch.yml +++ b/.github/workflows/core-build-nopch.yml @@ -25,10 +25,6 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - compiler: - CC: clang-15 - CXX: clang++-15 - os: ubuntu-24.04 compiler: CC: clang-18 @@ -37,6 +33,10 @@ jobs: compiler: CC: gcc-14 CXX: g++-14 + - os: ubuntu-26.04 + compiler: + CC: clang-20 + CXX: clang++-20 runs-on: ${{ matrix.os }} name: ${{ matrix.os }}-${{ matrix.compiler.CC }}-nopch if: github.repository == 'mod-playerbots/azerothcore-wotlk' && !github.event.pull_request.draft diff --git a/.github/workflows/core-build-pch.yml b/.github/workflows/core-build-pch.yml index 423dd9ec9..431d45aad 100644 --- a/.github/workflows/core-build-pch.yml +++ b/.github/workflows/core-build-pch.yml @@ -25,14 +25,14 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - compiler: - CC: clang-15 - CXX: clang++-15 - os: ubuntu-24.04 compiler: CC: clang-18 CXX: clang++-18 + - os: ubuntu-26.04 + compiler: + CC: clang-20 + CXX: clang++-20 runs-on: ${{ matrix.os }} name: ${{ matrix.os }}-${{ matrix.compiler.CC }}-pch if: github.repository == 'mod-playerbots/azerothcore-wotlk' && !github.event.pull_request.draft diff --git a/.github/workflows/dashboard-ci.yml b/.github/workflows/dashboard-ci.yml index 3bfda278c..9853b8bcd 100644 --- a/.github/workflows/dashboard-ci.yml +++ b/.github/workflows/dashboard-ci.yml @@ -42,8 +42,8 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - os: ubuntu-24.04 + - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: @@ -74,8 +74,8 @@ jobs: fail-fast: true matrix: include: - - os: ubuntu-22.04 - os: ubuntu-24.04 + - os: ubuntu-26.04 runs-on: ${{ matrix.os }} if: github.repository == 'azerothcore/azerothcore-wotlk' && !github.event.pull_request.draft steps: diff --git a/.github/workflows/windows_build.yml b/.github/workflows/windows_build.yml index 6955ab887..38e6693a9 100644 --- a/.github/workflows/windows_build.yml +++ b/.github/workflows/windows_build.yml @@ -39,15 +39,73 @@ jobs: - name: ccache uses: hendrikmuhs/ccache-action@v1.2.13 - + # Cache the fetched Boost + MySQL client so we don't re-download ~350 MB + # and re-run the Boost installer on every run. The Test-Path guards in + # "Configure OS" short-circuit the install work when these are restored. + - name: Cache Boost & MySQL + uses: actions/cache@v4 + with: + path: | + C:\local\boost_1_87_0 + C:\tools\mysql\current + key: winbuild-deps:boost-1.87.0:mysql-8.4.9 - name: Configure OS - shell: bash - env: - CONTINUOUS_INTEGRATION: true + shell: pwsh run: | - cd C:/ac - ./acore.sh install-deps + $ErrorActionPreference = 'Stop' + # The windows-latest runner image already ships CMake, Visual Studio + # (with the native desktop workload + MSVC) and OpenSSL, so we only need to + # provide Boost and the MySQL client libraries. We fetch these directly + # instead of via chocolatey, whose pinned packages keep breaking on CI + # (VS detection failures and slproweb removing old OpenSSL installers). + + # --- Boost 1.87.0 (msvc-14.3, 64-bit) prebuilt binaries -> C:\local\boost_1_87_0 (BOOST_ROOT) --- + # Use the direct-download host; the plain /download URL serves an HTML interstitial. + if (-not (Test-Path 'C:\local\boost_1_87_0\boost')) + { + $boostExe = Join-Path $env:RUNNER_TEMP 'boost.exe' + Invoke-WebRequest -Uri 'https://master.dl.sourceforge.net/project/boost/boost-binaries/1.87.0/boost_1_87_0-msvc-14.3-64.exe?viasf=1' -OutFile $boostExe + $boostMB = (Get-Item $boostExe).Length / 1MB + Write-Host ("Downloaded Boost installer: {0:N1} MB" -f $boostMB) + if ($boostMB -lt 50) { throw "Boost installer download looks wrong ($([int]$boostMB) MB) - likely an HTML page." } + $boostProc = Start-Process -FilePath $boostExe -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART','/DIR=C:\local\boost_1_87_0' -Wait -PassThru + if ($boostProc.ExitCode -ne 0) { throw "Boost installer failed with exit code $($boostProc.ExitCode)." } + # Fail fast if the installer "succeeded" but didn't lay down the expected tree, + # instead of surfacing a cryptic error later during CMake configuration. + if (-not (Test-Path 'C:\local\boost_1_87_0\boost\version.hpp')) { throw 'Boost install missing headers at C:\local\boost_1_87_0\boost.' } + if (-not (Test-Path 'C:\local\boost_1_87_0\lib64-msvc-14.3')) { throw 'Boost install missing prebuilt libs at C:\local\boost_1_87_0\lib64-msvc-14.3.' } + } + + # --- MySQL 8.4 client headers + libs -> C:\tools\mysql\current (path the build expects) --- + if (-not (Test-Path 'C:\tools\mysql\current\lib\mysqlclient.lib')) + { + # Use the archives CDN directly (keeps every version, unlike dev.mysql.com/get which only + # serves the current GA). A browser user-agent is required or Oracle returns an HTML error page. + $mysqlZip = Join-Path $env:RUNNER_TEMP 'mysql.zip' + Invoke-WebRequest -Uri 'https://cdn.mysql.com/archives/mysql-8.4/mysql-8.4.9-winx64.zip' -OutFile $mysqlZip -UserAgent 'Mozilla/5.0' + $mysqlMB = (Get-Item $mysqlZip).Length / 1MB + Write-Host ("Downloaded MySQL zip: {0:N1} MB" -f $mysqlMB) + if ($mysqlMB -lt 50) { throw "MySQL zip download looks wrong ($([int]$mysqlMB) MB)." } + New-Item -ItemType Directory -Force -Path 'C:\tools\mysql' | Out-Null + Expand-Archive -Path $mysqlZip -DestinationPath 'C:\tools\mysql' -Force + if (Test-Path 'C:\tools\mysql\current') { Remove-Item -Recurse -Force 'C:\tools\mysql\current' } + Rename-Item 'C:\tools\mysql\mysql-8.4.9-winx64' 'C:\tools\mysql\current' + # Fail fast if the archive layout changed and the client import library isn't + # where the build expects it, rather than failing later during CMake/link. + if (-not (Test-Path 'C:\tools\mysql\current\lib\mysqlclient.lib')) { throw 'MySQL client import library missing at C:\tools\mysql\current\lib\mysqlclient.lib.' } + } + + # --- OpenSSL: use the copy already installed on the runner image --- + $opensslRoot = @($env:OPENSSL_ROOT_DIR, 'C:\Program Files\OpenSSL', 'C:\Program Files\OpenSSL-Win64') | + Where-Object { $_ -and (Test-Path (Join-Path $_ 'include\openssl\opensslv.h')) } | + Select-Object -First 1 + if (-not $opensslRoot) + { + throw 'No OpenSSL development install found on the runner image.' + } + "OPENSSL_ROOT_DIR=$opensslRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8 + Write-Host "Using OpenSSL at $opensslRoot" - name: Build shell: bash run: | diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 000000000..02c62b97a --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,100 @@ +# AGENTS.md + +AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK), built with CMake, backed by MySQL. + +## Agent rules + +- **Do not configure or build unless explicitly asked.** Builds are slow and rarely needed for code changes. +- **Never edit SQL files outside `data/sql/updates/pending_db_*/` unless explicitly requested. ** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable. + +## Build + +Out-of-source build is required (in-source is blocked). + +```bash +mkdir -p build && cd build +cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \ + -DSCRIPTS=static -DMODULES=static +make -j$(nproc) && make install +``` + +C++20 required (`CMAKE_CXX_STANDARD 20`). Useful flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. + +Tests (Google Test, in `src/test/`): configure `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. + +## Repository layout + +- `src/common/` — networking (Asio), crypto, config, logging, shared utilities. +- `src/server/game/` — core gameplay; compiled into worldserver. +- `src/server/scripts/` — content scripts grouped by region (`EasternKingdoms/`, `Northrend/`, …), class (`Spells/spell_mage.cpp`, …), and domain (`Commands/`, `Pet/`, `OutdoorPvP/`, `World/`). +- `src/server/database/` — DB abstraction and schema updater. +- `src/server/shared/` — code shared by auth and world servers. +- `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085). +- `src/test/` — Google Test unit tests + mocks. +- `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored). +- `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`. +- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts. +- `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored. +- `deps/` — vendored third-party dependencies. + +## Adding SQL updates + +1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`). +2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` to write into. +3. Conventions enforced by `apps/codestyle/codestyle-sql.py`: every `INSERT` preceded by a matching `DELETE` (idempotency); no double semicolons; no multiple blank lines; InnoDB engine. + +The three databases: + +- `acore_auth` — accounts, realm list, IP/account bans, session keys. Shared across all realms. +- `acore_characters` — per-character state: characters, inventory, in-progress quests, mail, guilds, arena teams, achievements. One per realm. +- `acore_world` — static game content: creature/gameobject/item/quest templates, spawn lists, loot tables, SmartAI scripts, gossip, conditions. Read-mostly; rebuilt from SQL. + +## Code style + +Formatting (charset, indent width, line length, final newline, trailing whitespace) follows `.editorconfig`. + +Run the linters before claiming a change is done: + +```bash +python apps/codestyle/codestyle-cpp.py # C++ +python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master) +``` + +Hard rules (also enforced by CI with `-Werror`, plus `cppcheck`): + +- 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 120 cols, trailing newline. +- Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`. +- `auto const&` (not `const auto&`); `Type const*` (not `const Type*`). +- Use `{}` format specifiers (`fmt`-style), not `%u`/`%s`. +- Use the typed helpers, not raw flag access: + - `IsPlayer()`, `IsCreature()`, `IsItem()`, … instead of `GetTypeId() == TYPEID_*`. + - `GetNpcFlags()`, `HasNpcFlag()`, `SetNpcFlag()`, `RemoveNpcFlag()`, `ReplaceAllNpcFlags()` instead of `*Flag(UNIT_NPC_FLAGS, …)`. + - `IsRefundable()`, `IsBOPTradable()`, `IsWrapped()` instead of `HasFlag(ITEM_FIELD_FLAGS, …)`. + - `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`. + - `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`. + +## Project conventions + +- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`/`ERROR`/`DEBUG`/`TRACE`). Categories are hierarchical, dot-separated (`server.loading`, `entities.player`, `sql.dev`). No `printf`-style, no `sLog->`, no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. +- **Random**: use helpers in `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Not `std::rand` or `<random>`. +- **Strings**: `Acore::StringFormat(fmt, args...)` (`{}` placeholders) — `src/common/Utilities/StringFormat.h`. +- **Config**: `sConfigMgr->GetOption<T>("Name", default)`. +- **Namespace**: project-wide `Acore::` (no `Trinity::` remnants — rename when porting from upstream forks). +- **Long-lived references**: don't store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `Map::GetCreature(guid)`, etc. +- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums), not raw query strings. Non-blocking reads go async: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback`). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. +- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation), both members of `CreatureAI` — don't roll your own tick counters. See any boss script under `src/server/scripts/`. + +## Scripting registration + +Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist: + +- **Spell / aura scripts**: `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) inside `AddSC_<name>()`. +- **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern. + +Then declare and call `AddSC_<name>()` from the regional loader (`Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, …). + +**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table, not C++ (engine: `src/server/game/AI/SmartScripts/`). For new creature behaviour prefer SmartAI (via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. + +**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro) inside `AddSC_<name>()`. Full list: https://www.azerothcore.org/wiki/hooks-script. + +Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored). diff --git a/CLAUDE.md b/CLAUDE.md index 7e6e9031d..43c994c2d 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,104 +1 @@ -# CLAUDE.md - -AzerothCore is a C++ MMORPG server emulator for World of Warcraft 3.3.5a (WotLK), built with CMake, backed by MySQL. - -## Agent rules - -- **Do not configure or build unless explicitly asked.** Builds are slow (CMake + compile of a large C++ codebase) and rarely needed to make code changes. -- **Never edit SQL files outside `data/sql/updates/pending_db_*/`.** `data/sql/base/`, `data/sql/archive/`, and `data/sql/updates/db_*/` are immutable (do not modify). -- **Do not run git commands that modify repo state** (commit, branch, merge, rebase, reset, push, …) unless explicitly requested, and do not include them in plans. Read-only git (status, diff, log) is fine. - -## Build - -Out-of-source build is required (in-source is blocked by CMake). - -```bash -mkdir -p build && cd build -cmake .. -DCMAKE_INSTALL_PREFIX=$HOME/azeroth-server -DCMAKE_BUILD_TYPE=RelWithDebInfo \ - -DSCRIPTS=static -DMODULES=static -make -j$(nproc) && make install -``` - -Compiler: **C++20** required (`CMAKE_CXX_STANDARD 20`). Useful CMake flags: `BUILD_TESTING=ON` (Google Test), `NOPCH=1` (disable precompiled headers). Full flag set in `conf/dist/config.cmake`. `compile_commands.json` is exported automatically. - -Tests (Google Test, in `src/test/`): configure with `-DBUILD_TESTING=ON`, then `ctest` or `./src/test/unit_tests` from the build dir. - -## Repository layout - -- `src/common/` — networking (Asio), crypto, config, logging, shared utilities. -- `src/server/game/` — core gameplay; compiled into worldserver. -- `src/server/scripts/` — content scripts grouped by region (`EasternKingdoms/`, `Northrend/`, …), class (`Spells/spell_mage.cpp`, …), and domain (`Commands/`, `Pet/`, `OutdoorPvP/`, `World/`). -- `src/server/database/` — DB abstraction and schema updater. -- `src/server/shared/` — code shared by auth and world servers. -- `src/server/apps/{authserver,worldserver}/` — entry points (ports 3724 and 8085). -- `src/test/` — Google Test unit tests + mocks. -- `data/sql/` — `base/` (historical schema), `updates/db_*/` (merged), `updates/pending_db_*/` (in-flight, **edit here**), `custom/` (gitignored). -- `modules/` — external modules (each a subdir with its own `CMakeLists.txt`). Disable with `-DDISABLED_AC_MODULES="mod1;mod2"`. See `modules/how_to_make_a_module.md`. -- `apps/` — helper scripts; `apps/codestyle/` holds the lint scripts (see below). -- `conf/dist/` — distributed config templates; `conf/*.conf` is gitignored. -- `deps/` — vendored third-party dependencies. - -## Adding SQL updates - -1. `cd data/sql/updates/pending_db_world/` (or `pending_db_auth` / `pending_db_characters`). -2. `./create_sql.sh` generates an empty `rev_<timestamp>.sql` you write into. -3. Required SQL conventions (enforced by `apps/codestyle/codestyle-sql.py`): - - Every `INSERT` must be preceded by a matching `DELETE` (idempotency). - - 4-space indent (no tabs), trailing newline, no double semicolons, no multiple blank lines. - - Tables must use the InnoDB engine. - -The three databases: - -- `acore_auth` — accounts, realm list, IP/account bans, session keys. Shared across all realms. -- `acore_characters` — per-character state: characters, inventory, in-progress quests, mail, guilds, arena teams, achievements. One per realm. -- `acore_world` — static game content: creature/gameobject/item/quest templates, spawn lists, loot tables, SmartAI scripts, gossip, conditions. Read-mostly; rebuilt from SQL. - -## Code style - -Run the linters before claiming a change is done: - -```bash -python apps/codestyle/codestyle-cpp.py # C++ -python apps/codestyle/codestyle-sql.py # SQL (compares to origin/master) -``` - -Hard rules (also enforced by CI with `-Werror`): - -- 4-space indent for C++ (tabs forbidden); 2-space for JSON/YAML/sh/ts/js. UTF-8, LF, max 120 cols, trailing newline. -- Allman braces. No braces around single-line statements. `if (x)` — never `if(x)` or `if ( x )`. -- `auto const&` (not `const auto&`); `Type const*` (not `const Type*`). -- Use `{}` format specifiers (`fmt`-style), not `%u`/`%s`. -- Use the typed helpers, not raw flag access: - - `IsPlayer()`, `IsCreature()`, `IsItem()`, … instead of `GetTypeId() == TYPEID_*`. - - `GetNpcFlags()`, `HasNpcFlag()`, `SetNpcFlag()`, `RemoveNpcFlag()`, `ReplaceAllNpcFlags()` instead of `*Flag(UNIT_NPC_FLAGS, …)`. - - `IsRefundable()`, `IsBOPTradable()`, `IsWrapped()` instead of `HasFlag(ITEM_FIELD_FLAGS, …)`. - - `HasFlag(ItemFlag)` / `HasFlag2(ItemFlag2)` / `HasFlagCu(ItemFlagsCustom)` instead of bitwise `Flags & ITEM_FLAG…`. - - `ObjectGuid::ToString().c_str()` instead of `ObjectGuid::GetCounter()`. - -CI also runs `cppcheck`. - -## Project conventions - -- **Logging**: `LOG_INFO("category.sub", "msg with {}", arg)` (also `LOG_WARN`, `LOG_ERROR`, `LOG_DEBUG`, `LOG_TRACE`). Categories are hierarchical, dot-separated (e.g. `server.loading`, `entities.player`, `sql.dev`). No `printf`-style; no `sLog->`; no `TC_LOG_*`. Macro in `src/common/Logging/Log.h`. -- **Random**: use project helpers from `src/common/Utilities/Random.h` — `urand`, `irand`, `frand`, `rand32`, `rand_chance`, `roll_chance_f`, `roll_chance_i`. Do not use `std::rand` or `<random>` directly. -- **Strings**: `Acore::StringFormat(fmt, args...)` (wraps `fmt::format`, `{}` placeholders) — `src/common/Utilities/StringFormat.h`. -- **Config**: read options with `sConfigMgr->GetOption<T>("Name", default)`. -- **Namespace**: project-wide is `Acore::` (no `Trinity::` remnants — agents porting from upstream forks must rename). -- **Long-lived references**: do not store a raw `Player*` / `Creature*` / `Unit*` past the current call/tick — the object can be removed (logout, despawn, instance unload) and the pointer dangles. Store the `ObjectGuid` and resolve at use time via `ObjectAccessor::FindPlayer(guid)`, `ObjectAccessor::GetCreature(*from, guid)`, `Map::GetCreature(guid)`, etc. -- **DB queries**: use `PreparedStatement` (via `WorldDatabase` / `CharacterDatabase` / `LoginDatabase` and the prepared-statement enums) rather than raw query strings. Reads that don't need to block the world tick go through the async path: `_queryProcessor.AddCallback(db.AsyncQuery(stmt).WithPreparedCallback(...))` (or `WithCallback` for non-prepared). Multi-statement writes wrap in `SQLTransaction` + `Execute` / `AppendPreparedStatement`. -- **Timed actions in AI**: use `EventMap` (event id → delay; simple) or `TaskScheduler` (lambdas, repeats, cancellation). Both are members of `CreatureAI`; see any boss script under `src/server/scripts/` for examples — don't roll your own tick counters. - -## Scripting registration - -Scripts inherit from a `ScriptObject` subclass (`SpellScript`, `AuraScript`, `CreatureScript`, `InstanceMapScript`, `GameObjectScript`, `CommandScript`, …). Two registration styles coexist: - -- **Spell / aura scripts**: use the `RegisterSpellScript(ClassName)` (or `RegisterSpellAndAuraScriptPair(...)`) macro inside `AddSC_<name>()`. -- **Creature scripts**: prefer `RegisterCreatureAI(ClassName)` for new code; legacy zones still use `new ClassName();`. Match the surrounding pattern. - -Then declare and call `AddSC_<name>()` from the regional loader: `Spells/spells_script_loader.cpp`, `EasternKingdoms/eastern_kingdoms_script_loader.cpp`, etc. - -**SmartAI** (data-driven creature behaviour) lives in the world DB's `smart_scripts` table — not in C++. Engine: `src/server/game/AI/SmartScripts/`. For new creature behaviour prefer SmartAI (added via the SQL update workflow); reach for `CreatureScript` only when SmartAI's event/action vocabulary isn't enough. - -**Module hooks** (e.g. `OnPlayerLogin`, `OnWorldUpdate`, `OnSpellCast`) are declared in `src/server/game/Scripting/ScriptDefines/*.h`. Implement by inheriting the matching base (`PlayerScript`, `WorldScript`, …) and registering with `new MyClass();` (or its `RegisterXxxScript` macro where one exists) inside `AddSC_<name>()`. Full hook list: https://www.azerothcore.org/wiki/hooks-script. - -Custom (non-upstream) scripts go in `src/server/scripts/Custom/` (gitignored). +@AGENTS.md diff --git a/apps/docker/Dockerfile b/apps/docker/Dockerfile index 0ad7080bf..8b4ff60b2 100644 --- a/apps/docker/Dockerfile +++ b/apps/docker/Dockerfile @@ -225,9 +225,21 @@ LABEL description="AzerothCore client-data" ENV DATAPATH=/azerothcore/env/dist/data RUN apt-get update && \ - apt-get install -y curl unzip && \ + apt-get install -y curl unzip adduser && \ rm -rf /var/lib/apt/lists/* +ARG USER_ID=1000 +ARG GROUP_ID=1000 +ARG DOCKER_USER=acore + +# To use GID/UID 1000 in ubuntu > 23.04 the existing user must be deleted +# See https://bugs.launchpad.net/cloud-images/+bug/2005129 +RUN userdel --remove ubuntu \ + && addgroup --gid "$GROUP_ID" "$DOCKER_USER" \ + && adduser --disabled-password --gecos '' --uid "$USER_ID" --gid "$GROUP_ID" "$DOCKER_USER" \ + && passwd -d "$DOCKER_USER" \ + && chown -R "$DOCKER_USER:$DOCKER_USER" /azerothcore + COPY --chown=$DOCKER_USER:$DOCKER_USER apps apps VOLUME /azerothcore/env/dist/data diff --git a/apps/installer/includes/os_configs/ubuntu.sh b/apps/installer/includes/os_configs/ubuntu.sh index c2c84fff3..61288d571 100644 --- a/apps/installer/includes/os_configs/ubuntu.sh +++ b/apps/installer/includes/os_configs/ubuntu.sh @@ -12,15 +12,15 @@ fi UBUNTU_VERSION=$(lsb_release -sr); case $UBUNTU_VERSION in - "22.04") - ;; "24.04") ;; + "26.04") + ;; *) echo "########## ########## ##########" echo "" echo " using unsupported Ubuntu version " $UBUNTU_VERSION - echo " please update to Ubuntu 22.04 or later" + echo " please update to Ubuntu 24.04 or later" echo "" echo "########## ########## ##########" ;; @@ -30,21 +30,32 @@ $SUDO apt update # shared deps DEBIAN_FRONTEND="noninteractive" $SUDO \ -apt-get -y install ccache clang cmake curl google-perftools libmysqlclient-dev make unzip jq screen tmux \ - libreadline-dev libncurses5-dev libncursesw5-dev libbz2-dev git gcc g++ libssl-dev \ +apt-get -y install ccache clang cmake curl make unzip jq screen tmux \ + libreadline-dev libbz2-dev git gcc g++ libssl-dev \ libncurses-dev libboost-all-dev gdb gdbserver expect +# version-specific deps +if [[ "$UBUNTU_VERSION" == "26.04" ]]; then + DEBIAN_FRONTEND="noninteractive" $SUDO \ + apt-get -y install libgoogle-perftools-dev default-libmysqlclient-dev +else + DEBIAN_FRONTEND="noninteractive" $SUDO \ + apt-get -y install google-perftools libmysqlclient-dev libncurses5-dev libncursesw5-dev +fi + VAR_PATH="$CURRENT_PATH/../../../../var" # Do not install MySQL if we are in docker (It will be used a docker container instead) or we are explicitly skipping it. if [[ $DOCKER != 1 && $SKIP_MYSQL_INSTALL != 1 ]]; then - # run noninteractive install for MYSQL 8.4 LTS - wget https://dev.mysql.com/get/mysql-apt-config_0.8.35-1_all.deb -P "$VAR_PATH" - # resolve expired key issue - sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A8D3785C - DEBIAN_FRONTEND="noninteractive" $SUDO dpkg -i "$VAR_PATH/mysql-apt-config_0.8.35-1_all.deb" - $SUDO apt-get update + if [[ "$UBUNTU_VERSION" == "24.04" ]]; then + # run noninteractive install for MYSQL 8.4 LTS + wget https://dev.mysql.com/get/mysql-apt-config_0.8.35-1_all.deb -P "$VAR_PATH" + # resolve expired key issue + sudo apt-key adv --keyserver keyserver.ubuntu.com --recv-keys A8D3785C + DEBIAN_FRONTEND="noninteractive" $SUDO dpkg -i "$VAR_PATH/mysql-apt-config_0.8.35-1_all.deb" + $SUDO apt-get update + fi DEBIAN_FRONTEND="noninteractive" $SUDO apt-get install -y mysql-server fi diff --git a/apps/startup-scripts/src/starter b/apps/startup-scripts/src/starter index 4e58e8e7b..5585e7eba 100755 --- a/apps/startup-scripts/src/starter +++ b/apps/startup-scripts/src/starter @@ -45,6 +45,174 @@ if [ ! -f "$BINARY" ]; then exit 1 fi +function config_value() { + local key="$1" + + if [ -z "${CONFIG_ABS:-}" ] || [ ! -f "$CONFIG_ABS" ]; then + return 1 + fi + + awk -F= -v key="$key" ' + { + configKey = $1 + gsub(/^[[:space:]]+|[[:space:]]+$/, "", configKey) + } + configKey == key { + value = $0 + sub(/^[^=]*=/, "", value) + sub(/[[:space:]]*[#;].*$/, "", value) + gsub(/"/, "", value) + gsub(/^[[:space:]]+|[[:space:]]+$/, "", value) + print value + exit + } + ' "$CONFIG_ABS" +} + +function command_contains_config() { + local command_line="$1" + + if [ -z "${CONFIG_ABS:-}" ]; then + return 0 + fi + + case "$command_line" in + *" -c $CONFIG_ABS"*|*" -c \"$CONFIG_ABS\""*) + return 0 + ;; + esac + + return 1 +} + +function find_existing_instances() { + local line pid command_line + + while IFS= read -r line; do + pid="${line%% *}" + command_line="${line#* }" + + [ -z "$pid" ] && continue + [ "$pid" = "$$" ] && continue + [ "$pid" = "$PPID" ] && continue + + case "$command_line" in + *"/starter "*|*"pgrep "*) + continue + ;; + esac + + if command_contains_config "$command_line"; then + printf '%s\n' "$line" + fi + done < <(pgrep -fa "$EXECPATH" 2>/dev/null || true) +} + +function listening_on_port() { + local port="$1" + + command -v ss >/dev/null 2>&1 || return 1 + ss -H -ltn "sport = :$port" 2>/dev/null | grep -q . +} + +function guarded_ports() { + local port soap_enabled soap_port + + if [ "$BINFILE" != "worldserver" ]; then + return 0 + fi + + port="$(config_value "WorldServerPort" || true)" + if [ -n "$port" ]; then + printf '%s\n' "$port" + fi + + soap_enabled="$(config_value "SOAP.Enabled" || true)" + soap_port="$(config_value "SOAP.Port" || true)" + if [ "$soap_enabled" = "1" ] && [ -n "$soap_port" ]; then + printf '%s\n' "$soap_port" + fi +} + +function find_listening_guard_ports() { + local port + + while IFS= read -r port; do + [ -z "$port" ] && continue + if listening_on_port "$port"; then + printf '%s\n' "$port" + fi + done < <(guarded_ports) +} + +function wait_for_startup_guard() { + local timeout="${AC_STARTUP_GUARD_TIMEOUT:-90}" + local interval="${AC_STARTUP_GUARD_INTERVAL:-2}" + local elapsed=0 + local instances ports + local sleep_for + local warned=0 + + if [ "${AC_STARTUP_SKIP_GUARD:-0}" = "1" ]; then + return 0 + fi + + case "$timeout" in + ''|*[!0-9]*) + echo "Startup guard: invalid AC_STARTUP_GUARD_TIMEOUT '$timeout', using 90." + timeout=90 + ;; + esac + + case "$interval" in + ''|*[!0-9]*) + echo "Startup guard: invalid AC_STARTUP_GUARD_INTERVAL '$interval', using 2." + interval=2 + ;; + esac + + if [ "$interval" -lt 1 ]; then + echo "Startup guard: AC_STARTUP_GUARD_INTERVAL must be at least 1, using 1." + interval=1 + fi + + while true; do + instances="$(find_existing_instances)" + ports="$(find_listening_guard_ports)" + + if [ -z "$instances" ] && [ -z "$ports" ]; then + return 0 + fi + + if [ "$elapsed" -ge "$timeout" ]; then + echo "Startup guard: refusing to start duplicate $BINFILE after waiting ${timeout}s." + if [ -n "$instances" ]; then + echo "Startup guard: existing matching process(es):" + echo "$instances" + fi + if [ -n "$ports" ]; then + echo "Startup guard: configured port(s) still listening: $(echo "$ports" | paste -sd ',' -)" + fi + exit 0 + fi + + if [ "$warned" -eq 0 ]; then + echo "Startup guard: waiting for existing $BINFILE instance or configured port(s) to clear..." + warned=1 + fi + + sleep_for="$interval" + if [ $((elapsed + sleep_for)) -gt "$timeout" ]; then + sleep_for=$((timeout - elapsed)) + fi + + [ "$sleep_for" -le 0 ] && continue + + sleep "$sleep_for" + elapsed=$((elapsed + sleep_for)) + done +} + # Create crashes directory if it doesn't exist mkdir -p "$CRASHES_PATH" @@ -55,6 +223,8 @@ cd "$BINPATH" || { EXECPATH=$(realpath "$BINFILE") +wait_for_startup_guard + if [ "$GDB_ENABLED" -eq 1 ]; then echo "Starting $EXECPATH with GDB enabled" @@ -148,4 +318,4 @@ else exec "$EXECPATH" ${CONFIG_ABS:+-c "$CONFIG_ABS"} fi fi -fi \ No newline at end of file +fi diff --git a/apps/startup-scripts/test/test_startup_scripts.bats b/apps/startup-scripts/test/test_startup_scripts.bats index c7a90c7f6..82d49eba0 100755 --- a/apps/startup-scripts/test/test_startup_scripts.bats +++ b/apps/startup-scripts/test/test_startup_scripts.bats @@ -50,6 +50,86 @@ teardown() { [[ "$output" =~ "Test server starting" ]] } +@test "starter: should refuse duplicate binary/config start" { + cat > "$TEST_DIR/bin/duplicate-server" << 'EOF' +#!/usr/bin/env bash +sleep 30 +EOF + chmod +x "$TEST_DIR/bin/duplicate-server" + + "$TEST_DIR/bin/duplicate-server" -c "$TEST_DIR/test-server.conf" & + local duplicate_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "duplicate-server" "" "$TEST_DIR/test-server.conf" "" "" 0 + kill "$duplicate_pid" 2>/dev/null || true + wait "$duplicate_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "Startup guard: refusing to start duplicate duplicate-server" ]] + [[ "$output" =~ "existing matching process" ]] +} + +@test "starter: should refuse occupied configured worldserver ports" { + if ! command -v nc >/dev/null 2>&1; then + skip "nc is required for this test" + fi + + cat > "$TEST_DIR/bin/worldserver" << 'EOF' +#!/usr/bin/env bash +echo "worldserver should not start" +EOF + chmod +x "$TEST_DIR/bin/worldserver" + + local listen_port=40123 + cat > "$TEST_DIR/worldserver.conf" << EOF +WorldServerPort = $listen_port +SOAP.Enabled = 0 +EOF + nc -l 127.0.0.1 "$listen_port" >/dev/null & + local listener_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0 + kill "$listener_pid" 2>/dev/null || true + wait "$listener_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "Startup guard: refusing to start duplicate worldserver" ]] + [[ "$output" =~ "configured port(s) still listening: $listen_port" ]] +} + +@test "starter: should parse config keys literally for startup guard" { + if ! command -v nc >/dev/null 2>&1; then + skip "nc is required for this test" + fi + + cat > "$TEST_DIR/bin/worldserver" << 'EOF' +#!/usr/bin/env bash +echo "worldserver started" +EOF + chmod +x "$TEST_DIR/bin/worldserver" + + local listen_port=40124 + cat > "$TEST_DIR/worldserver.conf" << EOF +WorldServerPort = 40125 +SOAPXEnabled = 1 +SOAPXPort = $listen_port +SOAP.Enabled = 0 +EOF + nc -l 127.0.0.1 "$listen_port" >/dev/null & + local listener_pid=$! + + AC_STARTUP_GUARD_TIMEOUT=1 AC_STARTUP_GUARD_INTERVAL=1 run timeout 5s "$SCRIPT_DIR/starter" "$TEST_DIR/bin" "worldserver" "" "$TEST_DIR/worldserver.conf" "" "" 0 + kill "$listener_pid" 2>/dev/null || true + wait "$listener_pid" 2>/dev/null || true + + debug_on_failure + [ "$status" -eq 0 ] + [[ "$output" =~ "worldserver started" ]] + [[ ! "$output" =~ "Startup guard: refusing" ]] +} + # ===== SIMPLE RESTARTER TESTS ===== @test "simple-restarter: should fail with missing parameters" { diff --git a/data/sql/updates/db_auth/2026_07_15_00.sql b/data/sql/updates/db_auth/2026_07_15_00.sql new file mode 100644 index 000000000..1a73100b2 --- /dev/null +++ b/data/sql/updates/db_auth/2026_07_15_00.sql @@ -0,0 +1,34 @@ +-- DB update 2026_06_01_00 -> 2026_07_15_00 +-- RBAC permissions for core commands migrated off deprecated GM security levels. +DELETE FROM `rbac_permissions` WHERE `id` BETWEEN 926 AND 938; +INSERT INTO `rbac_permissions` (`id`, `name`) VALUES +(926, 'Command: autobroadcast list'), +(927, 'Command: autobroadcast add'), +(928, 'Command: autobroadcast locale'), +(929, 'Command: autobroadcast remove'), +(930, 'Command: mail list'), +(931, 'Command: mail return'), +(932, 'Command: npc load'), +(933, 'Command: pool info'), +(934, 'Command: pool lookup'), +(935, 'Command: spellinfo attributes'), +(936, 'Command: spellinfo effects'), +(937, 'Command: spellinfo targets'), +(938, 'Command: spellinfo all'); + +-- Link each command permission to its role: 196 = Administrator role, 197 = Gamemaster role. +DELETE FROM `rbac_linked_permissions` WHERE `linkedId` BETWEEN 926 AND 938; +INSERT INTO `rbac_linked_permissions` (`id`, `linkedId`) VALUES +(197, 926), +(196, 927), +(196, 928), +(196, 929), +(197, 930), +(197, 931), +(196, 932), +(197, 933), +(197, 934), +(197, 935), +(197, 936), +(197, 937), +(197, 938); diff --git a/data/sql/updates/db_world/2026_07_12_00.sql b/data/sql/updates/db_world/2026_07_12_00.sql new file mode 100644 index 000000000..841c39c26 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_12_00.sql @@ -0,0 +1,4 @@ +-- DB update 2026_07_11_00 -> 2026_07_12_00 +DELETE FROM `gameobject_template` WHERE `entry` = 301337; +DELETE FROM `gameobject_template_addon` WHERE `entry` = 301337; +DELETE FROM `gameobject_template_locale` WHERE `entry` = 301337; diff --git a/data/sql/updates/db_world/2026_07_12_01.sql b/data/sql/updates/db_world/2026_07_12_01.sql new file mode 100644 index 000000000..6e7b4705a --- /dev/null +++ b/data/sql/updates/db_world/2026_07_12_01.sql @@ -0,0 +1,14 @@ +-- DB update 2026_07_12_00 -> 2026_07_12_01 +DELETE FROM `areatrigger_teleport` WHERE `ID` = 5206; +INSERT INTO `areatrigger_teleport` (`ID`, `Name`, `target_map`, `target_position_x`, `target_position_y`, `target_position_z`, `target_orientation`) VALUES +(5206, 'Gundrak (North Entrance)', 604, 1882.32, 631.027, 176.696, 3.1415927); + +-- For below, Wrath Classic (51943) has the player arrive at 1880.74, 853.76, 176.696, 3.14159. +-- However, retail Wrath seems to put the player on the opposing side. +-- The above areatrigger_teleport position was also sniffed from Wrath Classic, so that is being used for the LFG teleport, +-- as it is on the correct side of the instance. + +DELETE FROM `lfg_dungeon_template` WHERE `dungeonId` IN (216, 217); +INSERT INTO `lfg_dungeon_template` (`dungeonId`, `name`, `position_x`, `position_y`, `position_z`, `orientation`, `VerifiedBuild`) VALUES +(216, 'Gundrak', 1882.32, 631.027, 176.696, 3.1415927, 0), -- Normal +(217, 'Gundrak', 1882.32, 631.027, 176.696, 3.1415927, 0); -- Heroic diff --git a/data/sql/updates/db_world/2026_07_12_02.sql b/data/sql/updates/db_world/2026_07_12_02.sql new file mode 100644 index 000000000..bd1ec74b0 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_12_02.sql @@ -0,0 +1,4 @@ +-- DB update 2026_07_12_01 -> 2026_07_12_02 +DELETE FROM `spell_proc` WHERE `SpellId` = -13960; +INSERT INTO `spell_proc` (`SpellId`, `SchoolMask`, `SpellFamilyName`, `SpellFamilyMask0`, `SpellFamilyMask1`, `SpellFamilyMask2`, `ProcFlags`, `SpellTypeMask`, `SpellPhaseMask`, `HitMask`, `AttributesMask`, `DisableEffectsMask`, `ProcsPerMinute`, `Chance`, `Cooldown`, `Charges`) VALUES +(-13960, 0, 0, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 0, 0); diff --git a/data/sql/updates/db_world/2026_07_13_00.sql b/data/sql/updates/db_world/2026_07_13_00.sql new file mode 100644 index 000000000..94f5fcb58 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_13_00.sql @@ -0,0 +1,5 @@ +-- DB update 2026_07_12_02 -> 2026_07_13_00 +-- Hodir helpers (#26330): swap PACIFIED (131072) for IMMUNE_TO_NPC (512) per sniffs, so Freya's +-- instance-wide Ground Tremor can't tag them and block her reset. SWIMMING kept where set; +-- immunity is cleared on Hodir engage (boss_hodir.cpp). +UPDATE `creature_template` SET `unit_flags` = (`unit_flags` & ~131072) | 512 WHERE `entry` IN (32893, 32897, 32900, 32901, 32941, 32946, 32948, 32950, 33325, 33326, 33327, 33328, 33330, 33331, 33332, 33333); diff --git a/data/sql/updates/db_world/2026_07_14_00.sql b/data/sql/updates/db_world/2026_07_14_00.sql new file mode 100644 index 000000000..c82cbc2ae --- /dev/null +++ b/data/sql/updates/db_world/2026_07_14_00.sql @@ -0,0 +1,247 @@ +-- DB update 2026_07_13_00 -> 2026_07_14_00 +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 1: Training Room (Upper Floor) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Corpulous (Entry 29205, GUID 125759) +-- Spawn, home anchor, waypoints +UPDATE `creature` SET `position_x`= 2438.5864, `position_y`= -5554.151, `position_z`= 420.79996, `orientation`= 3.9704, `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125759; + +DELETE FROM `creature_addon` WHERE `guid`= 125759; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125759, 1257590, 0, 0, 0, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257590; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257590, 1, 2438.5864, -5554.151, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 2, 2417.4072, -5577.2603, 420.76572, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 3, 2418.5613, -5602.91, 420.8, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 4, 2430.862, -5626.988, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 5, 2462.9768, -5636.701, 420.6738, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 6, 2484.44, -5626.886, 420.42795, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 7, 2500.5364, -5595.214, 420.70547, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 8, 2492.1143, -5571.9556, 420.5543, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257590, 9, 2468.1182, -5553.14, 420.66876, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Knight of the Ebon Blade (Entry 29202, GUIDs 125750-751) +-- Patrol pair +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125750; + +DELETE FROM `creature_addon` WHERE `guid`= 125750; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125750, 1257500, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257500; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257500, 1, 2433.57, -5553.8066, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 2, 2420.6594, -5566.9326, 420.63525, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 3, 2413.44, -5584.155, 420.79105, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 4, 2413.6936, -5602.8584, 420.8, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 5, 2420.8923, -5620.4087, 420.9104, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 6, 2433.7537, -5633.345, 420.73993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 7, 2450.98, -5640.4243, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 8, 2469.9941, -5640.2007, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 9, 2487.1394, -5633.4414, 420.70834, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 10, 2500.1155, -5620.1206, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 11, 2507.2742, -5602.695, 420.84003, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 12, 2507.081, -5584.0166, 420.79993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 13, 2500.0461, -5566.8726, 420.79993, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 14, 2486.89, -5553.5205, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 15, 2469.6587, -5546.532, 420.6999, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257500, 16, 2450.987, -5546.5557, 420.70724, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Follower 125751 (formation: follows 125750) +DELETE FROM `creature_formations` WHERE `leaderGUID`= 125750 OR `memberGUID` IN (125750, 125751); +INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES +(125750, 125750, 0, 0, 7, 0, 0), +(125750, 125751, 3, 90, 519, 0, 0); +-- Knight of the Ebon Blade (Entry 29202, GUID 125754) +-- Idle, no wander +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 0 WHERE `guid`= 125754; +-- Risen Drudge (Entry 29212, dead decorative corpses) +-- Make the dead-posed drudges (StandState 7) untargettable: NOT_SELECTABLE (0x2000000) | template flags (32784) = 33587216 +UPDATE `creature` SET `unit_flags`= 33587216 WHERE `guid` IN (125766, 125768, 125770, 125771, 125773, 125774, 125775, 125776, 125779, 125780, 125781, 125784, 125786, 125788, 125789, 125790, 125792, 125793, 125794, 125795, 125796, 125797, 125799, 125800, 125801, 125803, 125804, 125805, 125807, 125808, 125809, 125810); +-- Risen Drudge (Entry 29212) - remove 2 AC-only spawns absent from the retail sniff +DELETE FROM `creature_addon` WHERE `guid` IN (125767, 125806); +DELETE FROM `creature` WHERE `guid` IN (125767, 125806); +-- Knight of the Ebon Blade (Entry 29202) +-- Sheath weapons +DELETE FROM `creature_addon` WHERE `guid` IN (125651, 125718, 125737, 125742, 125744, 125745, 125748, 125752, 125754, 125755); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125651, 0, 0, 0, 0, 0, 0, NULL), +(125718, 0, 0, 0, 0, 0, 0, NULL), +(125737, 0, 0, 0, 0, 0, 0, NULL), +(125742, 0, 0, 0, 0, 0, 0, NULL), +(125744, 0, 0, 0, 0, 0, 0, NULL), +(125745, 0, 0, 0, 0, 0, 0, NULL), +(125748, 0, 0, 0, 0, 0, 0, NULL), +(125752, 0, 0, 0, 0, 0, 0, NULL), +(125754, 0, 0, 0, 0, 0, 0, NULL), +(125755, 0, 0, 0, 0, 0, 0, NULL); +-- Knight of the Ebon Blade (Entry 29202) - hidden in the rafters (Z~444) +DELETE FROM `creature_addon` WHERE `guid` IN (125714, 125716, 125721, 125732, 125734, 125735, 125739, 125740); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125714, 0, 0, 0, 0, 0, 0, NULL), +(125716, 0, 0, 0, 0, 0, 0, NULL), +(125721, 0, 0, 0, 0, 0, 0, NULL), +(125732, 0, 0, 0, 0, 0, 0, NULL), +(125734, 0, 0, 0, 0, 0, 0, NULL), +(125735, 0, 0, 0, 0, 0, 0, NULL), +(125739, 0, 0, 0, 0, 0, 0, NULL), +(125740, 0, 0, 0, 0, 0, 0, NULL); +-- Vigilant Gargoyle (Entry 29239) - all 8 (Training Room airspace) +UPDATE `creature_template` SET `AIName`= 'SmartAI' WHERE `entry`= 29239; + +UPDATE `creature` SET `position_x`= 2434.2053, `position_y`= -5662.7285, `position_z`= 445.16962, `orientation`= 1.17073, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125826; +UPDATE `creature` SET `position_x`= 2386.5322, `position_y`= -5591.9404, `position_z`= 445.1513, `orientation`= 6.21624, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125827; +UPDATE `creature` SET `position_x`= 2391.4082, `position_y`= -5567.293, `position_z`= 444.41574, `orientation`= 5.88485, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125828; +UPDATE `creature` SET `position_x`= 2483.3416, `position_y`= -5565.2993, `position_z`= 465.3641, `orientation`= 5.16390, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125829; +UPDATE `creature` SET `position_x`= 2451.695, `position_y`= -5646.18, `position_z`= 434.86523, `orientation`= 2.34530, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125830; +UPDATE `creature` SET `position_x`= 2430.0688, `position_y`= -5548.5366, `position_z`= 444.9371, `orientation`= 5.23599, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125831; +UPDATE `creature` SET `position_x`= 2458.673, `position_y`= -5667.2334, `position_z`= 445.40192, `orientation`= 1.52346, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125832; +UPDATE `creature` SET `position_x`= 2488.3079, `position_y`= -5584.386, `position_z`= 481.03842, `orientation`= 2.61030, `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125833; + +DELETE FROM `creature_addon` WHERE `guid` IN (125826, 125827, 125828, 125829, 125830, 125831, 125832, 125833); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125826, 1258260, 0, 9, 1, 444, 0, NULL), +(125827, 1258270, 0, 9, 1, 444, 0, NULL), +(125828, 1258280, 0, 9, 1, 444, 0, NULL), +(125829, 1258290, 0, 0, 1, 444, 0, NULL), +(125830, 1258300, 0, 0, 1, 444, 0, NULL), +(125831, 1258310, 0, 9, 1, 444, 0, NULL), +(125832, 1258320, 0, 9, 1, 444, 0, NULL), +(125833, 1258330, 0, 0, 1, 444, 0, NULL); +-- Flight paths: patrollers +DELETE FROM `waypoint_data` WHERE `id` IN (1258260, 1258270, 1258280, 1258290, 1258300, 1258310, 1258320, 1258330); +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1258260, 1, 2434.21, -5662.73, 445.17, 1.17073, 5, 65000, 0, 2, 0, 100, 0), +(1258260, 2, 2437.7, -5653.26, 447.963, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 3, 2449.12, -5599.83, 455.546, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 4, 2463.19, -5545.83, 455.546, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 5, 2462.16, -5519.81, 445.019, 4.71029, 5, 90000, 0, 2, 0, 100, 0), +(1258260, 6, 2461.56, -5528.22, 448.031, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 7, 2456.53, -5572.52, 454.475, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258260, 8, 2441, -5638.43, 454.475, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 1, 2386.53, -5591.94, 445.151, 6.21624, 5, 100000, 0, 2, 0, 100, 0), +(1258270, 2, 2396.07, -5594.14, 447.163, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 3, 2436.67, -5610.5, 451.023, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 4, 2473.23, -5633.96, 451.023, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 5, 2490.76, -5638.42, 445.051, 2.17773, 5, 70000, 0, 2, 0, 100, 0), +(1258270, 6, 2482.04, -5634.05, 447.43, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 7, 2437.04, -5610.4, 455.235, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258270, 8, 2406.09, -5597.51, 451.596, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 1, 2391.41, -5567.29, 444.416, 5.88485, 5, 82000, 0, 2, 0, 100, 0), +(1258280, 2, 2398.94, -5570.2, 446.666, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 3, 2440.29, -5586.84, 449.832, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 4, 2505.82, -5612.69, 449.832, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 5, 2529.11, -5619.67, 445.193, 2.81575, 5, 108000, 0, 2, 0, 100, 0), +(1258280, 6, 2523.4, -5615.98, 445.721, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 7, 2457.17, -5592.02, 452.471, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258280, 8, 2417.3, -5576.37, 452.471, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258290, 1, 2483.34, -5565.3, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 2, 2496.26, -5592, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 3, 2487.79, -5622.76, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 4, 2454.34, -5633.35, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 5, 2425.15, -5610.25, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 6, 2428.72, -5575.9, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258290, 7, 2455.77, -5556.59, 465.364, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 1, 2451.7, -5646.18, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 2, 2439.38, -5633.58, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 3, 2435.8, -5612.65, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 4, 2415.5, -5592.08, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 5, 2420.02, -5564.28, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 6, 2442.14, -5561.77, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 7, 2455.83, -5546.95, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 8, 2473.27, -5540.89, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 9, 2485.81, -5558.36, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 10, 2486.56, -5585.89, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 11, 2512.01, -5598.65, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 12, 2498.43, -5621.79, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 13, 2473.17, -5621.09, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258300, 14, 2464.31, -5642.48, 434.865, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258310, 1, 2430.07, -5548.54, 444.937, 5.23599, 5, 95000, 0, 3, 0, 100, 0), +(1258310, 2, 2440.2, -5553.77, 447.152, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 3, 2479.28, -5579.33, 458.901, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 4, 2508.33, -5596.56, 458.901, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 5, 2534.25, -5595.29, 445.207, 3.49066, 5, 60000, 0, 3, 0, 100, 0), +(1258310, 6, 2531.36, -5596.21, 447.298, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 7, 2486.9, -5597.17, 448.131, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258310, 8, 2441.01, -5584.44, 448.52, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 1, 2458.67, -5667.23, 445.402, 1.52346, 5, 75000, 0, 2, 0, 100, 0), +(1258320, 2, 2461.16, -5656.92, 448.499, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 3, 2478.7, -5619.14, 450.054, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 4, 2495.67, -5585.25, 450.054, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 5, 2504.88, -5563.37, 444.887, 3.80665, 5, 105000, 0, 2, 0, 100, 0), +(1258320, 6, 2499.79, -5572.24, 447.68, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258320, 7, 2471.41, -5619.1, 454.346, NULL, 5, 0, 0, 3, 0, 100, 0), +(1258330, 1, 2488.31, -5584.39, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 2, 2454.49, -5564.5, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 3, 2435.28, -5578, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 4, 2431.28, -5602.2, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 5, 2446.33, -5619.11, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 6, 2468.88, -5620.94, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0), +(1258330, 7, 2486.97, -5604.07, 481.038, NULL, 5, 0, 1, 0, 0, 100, 0); +-- SmartAI (5 patrollers) +DELETE FROM `smart_scripts` WHERE `entryorguid` IN (-125826, -125827, -125828, -125831, -125832) AND `source_type`= 0; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(-125826, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125826, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125826, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125826, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125827, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125827, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125827, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125827, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125828, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125828, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125828, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125828, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125831, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125831, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125831, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125831, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'), +(-125832, 0, 0, 0, 108, 0, 100, 0, 1, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch A: SUBMERGED (idle)'), +(-125832, 0, 1, 0, 108, 0, 100, 0, 2, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff A: STAND (flap)'), +(-125832, 0, 2, 0, 108, 0, 100, 0, 5, 0, 0, 0, 0, 0, 90, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - perch B: SUBMERGED (idle)'), +(-125832, 0, 3, 0, 108, 0, 100, 0, 6, 0, 0, 0, 0, 0, 91, 9, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Gargoyle - takeoff B: STAND (flap)'); +-- Faction correction for vendors +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry` IN (28500, 29203, 29205, 29207, 29208); +-- Master Siegesmith Corvus (Entry 28500, GUID 125869) +UPDATE `creature_template` SET `AIName`= 'SmartAI' WHERE `entry`= 28500; +UPDATE `creature` SET `MovementType`= 2, `wander_distance`= 0 WHERE `guid`= 125869; + +DELETE FROM `creature_addon` WHERE `guid`= 125869; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125869, 1258690, 0, 0, 1, 233, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1258690; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +-- 16-point weave loop (close-range sniff); ~22-32s craft pause at the 4 anvils (pt 5,7,10,16) +(1258690, 1, 2438.5435, -5634.903, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 2, 2449.8833, -5641.199, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 3, 2460.378, -5650.1074, 420.8016, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 4, 2467.7244, -5654.838, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 5, 2466.9749, -5656.5464, 420.79996, NULL, 0, 22000, 0, 0, 0, 100, 0), +(1258690, 6, 2440.863, -5647.832, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 7, 2433.588, -5649.348, 420.79996, NULL, 0, 32000, 0, 0, 0, 100, 0), +(1258690, 8, 2449.2322, -5647.929, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 9, 2456.4922, -5647.385, 420.77432, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 10, 2466.7793, -5648.771, 420.79996, NULL, 0, 28000, 0, 0, 0, 100, 0), +(1258690, 11, 2455.2024, -5647.0728, 420.78763, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 12, 2447.792, -5646.166, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 13, 2439.7908, -5641.177, 420.79996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 14, 2435.9255, -5634.6216, 420.72903, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 15, 2432.0063, -5633.902, 420.7254, NULL, 0, 0, 0, 0, 0, 100, 0), +(1258690, 16, 2431.6282, -5635.6772, 420.65244, NULL, 0, 25000, 0, 0, 0, 100, 0); + +DELETE FROM `creature_text` WHERE `CreatureID`= 28500; +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(28500, 0, 0, 'First, he gives away all of my precious Saronite, then he demands I begin crafting Runeblades of even greater power! Pah!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 1, 'Fix my armor, Corvus! My blade is rusty, Corvus! ... when will these damned mortals ever learn to care for themselves?', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 2, 'I may not have any mindless servants, but at least I can see a quality blade made from time to time.', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 3, 'There is work to be done here, but do you see anyone jumping to help me? No! Wretches, all of them!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 4, 'These Death Knights truly know no honor. First, they take my greatest arms and armor, crafted with care, forged with the Lich King''s blessing. Then they wander off to who knows where and come back dressed like Goblins, mumbling something about pawning off their armor for a few coppers! Pathetic!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'), +(28500, 0, 5, 'What is this nonsense about the Light? I have weapons and armor to make!', 12, 0, 100, 11, 0, 0, 0, 0, 'Master Siegesmith Corvus'); + +DELETE FROM `smart_scripts` WHERE `entryorguid`= 28500 AND `source_type`= 0; +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(28500, 0, 0, 0, 1, 0, 100, 0, 2100000, 2700000, 2100000, 2700000, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Corvus - ambient line at random ~40min'); diff --git a/data/sql/updates/db_world/2026_07_14_01.sql b/data/sql/updates/db_world/2026_07_14_01.sql new file mode 100644 index 000000000..74608d8c1 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_14_01.sql @@ -0,0 +1,95 @@ +-- DB update 2026_07_14_00 -> 2026_07_14_01 +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 2: Command Center (Bottom Floor) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Faction corrections +-- Siouxsie the Banshee (Entry 27928) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 27928; +-- Squire Edwards (Entry 28486) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 28486; +-- Coldwraith (Entry 28488) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 28488; +-- Disciple of Frost (Entry 28490) +UPDATE `creature_template` SET `faction`= 2082 WHERE `entry`= 28490; +-- Lord Thorval (Entry 29196) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 29196; +-- Knight of the Ebon Blade (Entry 31094) (part of the DK questline, is phased, but here nonetheless) +UPDATE `creature_template` SET `faction`= 2050 WHERE `entry`= 31094; +-- Knight of the Ebon Blade (Entry 29202) - Command Center floor - weapon sheathing +DELETE FROM `creature_addon` WHERE `guid` IN (125719, 125727, 125730, 125733, 125738, 125743, 125749, 125753); +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125719, 0, 0, 0, 0, 0, 0, NULL), +(125727, 0, 0, 0, 0, 0, 0, NULL), +(125730, 0, 0, 0, 0, 0, 0, NULL), +(125733, 0, 0, 0, 0, 0, 0, NULL), +(125738, 0, 0, 0, 0, 0, 0, NULL), +(125743, 0, 0, 0, 0, 0, 0, NULL), +(125749, 0, 0, 0, 0, 0, 0, NULL), +(125753, 0, 0, 0, 0, 0, 0, NULL); +-- Knight of the Ebon Blade (Entry 29202, GUIDs 125728-729) - Command Center OUTER ring patrol +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125728; +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 0 WHERE `guid`= 125729; + +DELETE FROM `creature_addon` WHERE `guid`= 125728; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125728, 1257280, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257280; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257280, 1, 2453.019, -5659.009, 376.963, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 2, 2436.503, -5654.292, 377.010, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 3, 2421.870, -5646.072, 377.085, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 4, 2409.462, -5633.826, 377.061, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 5, 2401.033, -5619.471, 377.059, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 6, 2396.028, -5602.503, 377.042, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 7, 2395.134, -5584.744, 377.032, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 8, 2400.252, -5568.880, 377.175, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 9, 2408.467, -5554.637, 377.087, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 10, 2422.047, -5542.582, 377.077, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 11, 2436.357, -5533.727, 377.048, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 12, 2453.023, -5528.724, 377.021, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 13, 2470.619, -5528.724, 376.969, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 14, 2486.896, -5534.060, 377.074, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 15, 2500.929, -5542.755, 377.070, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 16, 2511.579, -5556.004, 377.029, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 17, 2520.007, -5569.621, 377.088, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 18, 2525.179, -5586.072, 377.027, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 19, 2525.346, -5603.292, 376.955, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 20, 2521.025, -5618.274, 377.021, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 21, 2512.638, -5633.113, 377.022, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 22, 2500.469, -5644.090, 377.130, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 23, 2485.997, -5652.299, 377.080, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257280, 24, 2470.307, -5658.005, 377.048, NULL, 0, 0, 0, 0, 0, 100, 0); +-- Follower 125729 +DELETE FROM `creature_formations` WHERE `leaderGUID`= 125728 OR `memberGUID` IN (125728, 125729); +INSERT INTO `creature_formations` (`leaderGUID`, `memberGUID`, `dist`, `angle`, `groupAI`, `point_1`, `point_2`) VALUES +(125728, 125728, 0, 0, 7, 0, 0), +(125728, 125729, 3, 270, 519, 0, 0); +-- Knight of the Ebon Blade (Entry 29202, GUID 125746) - Command Center INNER ring patrol +UPDATE `creature` SET `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125746; + +DELETE FROM `creature_addon` WHERE `guid`= 125746; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125746, 1257460, 25280, 0, 1, 0, 0, NULL); + +DELETE FROM `waypoint_data` WHERE `id`= 1257460; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257460, 1, 2476.547, -5569.105, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 2, 2465.888, -5564.635, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 3, 2454.430, -5564.898, 366.996, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 4, 2439.936, -5573.309, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 5, 2431.440, -5587.982, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 6, 2431.727, -5599.489, 367.319, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 7, 2436.017, -5610.170, 367.033, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 8, 2442.707, -5616.534, 367.034, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 9, 2446.977, -5619.203, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 10, 2456.497, -5622.578, 366.988, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 11, 2465.452, -5622.340, 367.009, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 12, 2468.838, -5621.344, 367.016, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 13, 2476.673, -5618.066, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 14, 2482.256, -5612.358, 366.987, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 15, 2489.440, -5599.080, 367.042, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 16, 2488.702, -5586.393, 367.230, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257460, 17, 2485.971, -5579.744, 367.045, NULL, 0, 0, 0, 0, 0, 100, 0); diff --git a/data/sql/updates/db_world/2026_07_14_02.sql b/data/sql/updates/db_world/2026_07_14_02.sql new file mode 100644 index 000000000..d4a1ab402 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_14_02.sql @@ -0,0 +1,49 @@ +-- DB update 2026_07_14_01 -> 2026_07_14_02 +-- ------------------------------------------- +-- Acherus: The Ebon Hold (Eastern Plaguelands, map 0) +-- Ebon Hold Re-haul Part 3: Lord Thorval (Walking Lecture) +-- NPC Updates, waypoints, etc +-- ------------------------------------------- +-- Lord Thorval (Entry 29196, GUID 125712) +UPDATE `creature` SET `position_x`= 2527.7344, `position_y`= -5549.2075, `position_z`= 377.18475, `orientation`= 3.735, `wander_distance`= 0, `MovementType`= 2 WHERE `guid`= 125712; + +DELETE FROM `creature_addon` WHERE `guid`= 125712; +INSERT INTO `creature_addon` (`guid`, `path_id`, `mount`, `bytes1`, `bytes2`, `emote`, `visibilityDistanceType`, `auras`) VALUES +(125712, 1257120, 0, 0, 1, 0, 0, NULL); +-- Waypoints (22 min between lectures/walking cycle) +DELETE FROM `waypoint_data` WHERE `id`= 1257120; +INSERT INTO `waypoint_data` (`id`, `point`, `position_x`, `position_y`, `position_z`, `orientation`, `velocity`, `delay`, `smoothTransition`, `move_type`, `action`, `action_chance`, `wpguid`) VALUES +(1257120, 1, 2527.7344, -5549.2075, 377.18475, 3.735, 0, 1320000, 0, 0, 0, 100, 0), +(1257120, 2, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 8000, 0, 0, 0, 100, 0), +(1257120, 3, 2530.72, -5555.91, 377.13217, 3.263766, 0, 6500, 0, 0, 0, 100, 0), +(1257120, 4, 2530.4983, -5555.5771, 377.135, 3.263766, 0, 8500, 0, 0, 0, 100, 0), +(1257120, 5, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 6400, 0, 0, 0, 100, 0), +(1257120, 6, 2523.2881, -5544.7523, 377.219, 4.433136, 0, 8500, 0, 0, 0, 100, 0), +(1257120, 7, 2530.72, -5555.91, 377.13217, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 8, 2523.0664, -5544.4194, 377.22162, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 9, 2530.72, -5555.91, 377.13217, NULL, 0, 0, 0, 0, 0, 100, 0), +(1257120, 10, 2523.0664, -5544.4194, 377.22162, 4.433136, 0, 10000, 0, 0, 0, 100, 0); +-- Speech +DELETE FROM `creature_text` WHERE (`CreatureID` = 29196) AND (`GroupID` IN (0, 1, 2, 3, 4, 5, 6, 7, 8)); +INSERT INTO `creature_text` (`CreatureID`, `GroupID`, `ID`, `Text`, `Type`, `Language`, `Probability`, `Emote`, `Duration`, `Sound`, `BroadcastTextId`, `TextRange`, `comment`) VALUES +(29196, 0, 0, 'As disciples of blood, you strive to master the very lifeforce of your enemies.', 12, 0, 100, 1, 0, 0, 29867, 0, 'Lord Thorval Lecture 1'), +(29196, 1, 0, 'Be it by blade or incantation, blood feeds our attacks and weakens our foes.', 12, 0, 100, 1, 0, 0, 29868, 0, 'Lord Thorval Lecture 2'), +(29196, 2, 0, 'True masters learn to make blood serve more than just their strength in battle.', 12, 0, 100, 1, 0, 0, 29869, 0, 'Lord Thorval Lecture 3'), +(29196, 3, 0, 'Stripping energy from our foes, both fighting and fallen, allows us to persevere where lesser beings fall exhausted.', 12, 0, 100, 1, 0, 0, 29870, 0, 'Lord Thorval Lecture 4'), +(29196, 4, 0, 'And every foe that falls, energy sapped and stolen, only further fuels our assault.', 12, 0, 100, 1, 0, 0, 29871, 0, 'Lord Thorval Lecture 5'), +(29196, 5, 0, 'As masters of blood, we know battle without end...', 12, 0, 100, 1, 0, 0, 29872, 0, 'Lord Thorval Lecture 6'), +(29196, 6, 0, 'We know hunger never to be quenched...', 12, 0, 100, 1, 0, 0, 29873, 0, 'Lord Thorval Lecture 7'), +(29196, 7, 0, 'We know power never to be overcome...', 12, 0, 100, 1, 0, 0, 29874, 0, 'Lord Thorval Lecture 8'), +(29196, 8, 0, 'As masters of blood, we are masters of life and death itself. Against us, even hope falls drained and lifeless.', 12, 0, 100, 1, 0, 0, 29875, 0, 'Lord Thorval Lecture 9'); +-- SAI to talk on waypoint reached +DELETE FROM `smart_scripts` WHERE (`entryorguid`= 29196 AND `source_type`= 0) OR (`entryorguid`= 2919600 AND `source_type`= 9); +INSERT INTO `smart_scripts` (`entryorguid`, `source_type`, `id`, `link`, `event_type`, `event_phase_mask`, `event_chance`, `event_flags`, `event_param1`, `event_param2`, `event_param3`, `event_param4`, `event_param5`, `event_param6`, `action_type`, `action_param1`, `action_param2`, `action_param3`, `action_param4`, `action_param5`, `action_param6`, `target_type`, `target_param1`, `target_param2`, `target_param3`, `target_param4`, `target_x`, `target_y`, `target_z`, `target_o`, `comment`) VALUES +(29196, 0, 0, 0, 108, 0, 100, 0, 2, 1257120, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 2 Reached - Say Line 0'), +(29196, 0, 1, 0, 108, 0, 100, 0, 3, 1257120, 0, 0, 0, 0, 1, 1, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 3 Reached - Say Line 1'), +(29196, 0, 2, 0, 108, 0, 100, 0, 4, 1257120, 0, 0, 0, 0, 1, 2, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 4 Reached - Say Line 2'), +(29196, 0, 3, 0, 108, 0, 100, 0, 5, 1257120, 0, 0, 0, 0, 1, 3, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 5 Reached - Say Line 3'), +(29196, 0, 4, 0, 108, 0, 100, 0, 6, 1257120, 0, 0, 0, 0, 1, 4, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 6 Reached - Say Line 4'), +(29196, 0, 5, 0, 108, 0, 100, 0, 7, 1257120, 0, 0, 0, 0, 1, 5, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 7 Reached - Say Line 5'), +(29196, 0, 6, 0, 108, 0, 100, 0, 8, 1257120, 0, 0, 0, 0, 1, 6, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 8 Reached - Say Line 6'), +(29196, 0, 7, 0, 108, 0, 100, 0, 9, 1257120, 0, 0, 0, 0, 1, 7, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 9 Reached - Say Line 7'), +(29196, 0, 8, 0, 108, 0, 100, 0, 10, 1257120, 0, 0, 0, 0, 1, 8, 0, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 'Lord Thorval - On Waypoint 10 Reached - Say Line 8'); diff --git a/data/sql/updates/db_world/2026_07_14_03.sql b/data/sql/updates/db_world/2026_07_14_03.sql new file mode 100644 index 000000000..c58bbc258 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_14_03.sql @@ -0,0 +1,115 @@ +-- DB update 2026_07_14_02 -> 2026_07_14_03 +-- Reusable option sets for creature text groups +CREATE TABLE IF NOT EXISTS `creature_text_option_sets` ( + `SetID` TINYINT UNSIGNED NOT NULL, + `Cooldown` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Group cooldown in ms before it can fire again', + `TriggerChance` TINYINT UNSIGNED NOT NULL DEFAULT 100 COMMENT '0-100 pct chance to fire at all', + `PlayerOnly` TINYINT UNSIGNED NOT NULL DEFAULT 0 COMMENT 'Only fire if target is a player', + `comment` VARCHAR(255) DEFAULT '', + PRIMARY KEY (`SetID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DELETE FROM `creature_text_option_sets`; +INSERT INTO `creature_text_option_sets` (`SetID`, `Cooldown`, `TriggerChance`, `PlayerOnly`, `comment`) VALUES +(1, 6000, 100, 0, 'Standard kill yell: 6s cooldown'), +(2, 6000, 100, 1, 'Player-only kill yell: 6s cooldown'), +(3, 5000, 100, 1, 'Short cooldown player-only kill yell: 5s cooldown'), +(4, 0, 30, 0, 'Chance-based: 30% trigger, no cooldown'), +(5, 5000, 100, 0, 'Standard kill yell: 5s cooldown'), +(6, 0, 25, 1, 'Chance-based: 25% trigger, player-only'), +(7, 0, 50, 1, 'Chance-based: 50% trigger, player-only'); + +-- Assign option sets to creature text groups +CREATE TABLE IF NOT EXISTS `creature_text_options` ( + `CreatureID` INT UNSIGNED NOT NULL, + `GroupID` TINYINT UNSIGNED NOT NULL, + `OptionSetID` TINYINT UNSIGNED NOT NULL, + PRIMARY KEY (`CreatureID`, `GroupID`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +DELETE FROM `creature_text_options`; +INSERT INTO `creature_text_options` (`CreatureID`, `GroupID`, `OptionSetID`) VALUES +-- Black Temple +(22887, 2, 3), -- High Warlord Najentus SAY_SLAY +(22871, 2, 2), -- Teron Gorefiend SAY_SLAY +(22917, 1, 1), -- Illidan Stormrage SAY_ILLIDAN_KILL +(22947, 3, 1), -- Mother Shahraz SAY_SLAY +(22949, 3, 1), -- Gathios the Shatterer SAY_COUNCIL_SLAY +(22950, 3, 1), -- High Nethermancer Zerevor SAY_COUNCIL_SLAY +(22951, 3, 1), -- Lady Malande SAY_COUNCIL_SLAY +(22952, 3, 1), -- Veras Darkshadow SAY_COUNCIL_SLAY + +-- Azjol-Nerub +(28684, 1, 1), -- Krik'thir the Gatewatcher SAY_SLAY +(29120, 1, 1), -- Anub'arak SAY_SLAY + +-- Drak'Tharon Keep +(26631, 1, 1), -- Novos the Summoner SAY_KILL +(26632, 1, 1), -- The Prophet Tharon'ja SAY_KILL +(26630, 1, 1), -- Trollgore SAY_KILL + +-- Gundrak +(29306, 1, 1), -- Gal'darah SAY_SLAY +(29305, 1, 1), -- Moorabi SAY_SLAY +(29304, 1, 1), -- Slad'ran SAY_SLAY + +-- The Nexus +(26723, 1, 1), -- Keristrasza SAY_SLAY +(26731, 1, 1), -- Grand Magus Telestra SAY_KILL +(26794, 5, 1), -- Ormorok the Tree-Shaper SAY_KILL + +-- Ruby Sanctum +(39863, 6, 2), -- Halion SAY_KILL +(39751, 2, 1), -- Baltharus the Warborn SAY_KILL +(39746, 1, 1), -- General Zarithrian SAY_KILL +(39747, 3, 1), -- Saviana Ragefire SAY_KILL + +-- Icecrown Citadel +(36597, 10, 3), -- The Lich King SAY_LK_KILL + +-- Naxxramas +(16061, 1, 4), -- Instructor Razuvious SAY_SLAY + +-- Karazhan +(15687, 2, 3), -- Moroes SAY_KILL + +-- Battle for Mount Hyjal +(17888, 0, 2), -- Kazrogal SAY_ONSLAY +(17842, 1, 2), -- Azgalor SAY_ONSLAY +(17767, 1, 2), -- Rage Winterchill SAY_ONSLAY +(17808, 1, 2), -- Anetheron SAY_ONSLAY + +-- Serpentshrine Cavern +(21212, 6, 1), -- Lady Vashj SAY_SLAY +(21216, 2, 1), -- Hydross the Unstable SAY_CLEAN_SLAY +(21216, 5, 1), -- Hydross the Unstable SAY_CORRUPT_SLAY +(21214, 5, 1), -- Fathom-Lord Karathress SAY_SLAY +(21213, 3, 1), -- Morogrim Tidewalker SAY_SLAY + +-- Gruul's Lair +(19044, 3, 5), -- Gruul the Dragonkiller SAY_SLAY +(18831, 3, 5), -- High King Maulgar SAY_SLAY + +-- Magtheridon's Lair +(17257, 2, 5), -- Magtheridon SAY_SLAY + +-- Tempest Keep: The Eye +(19516, 1, 5), -- Void Reaver SAY_SLAY + +-- Black Temple (additional) +(22948, 1, 1), -- Gurtogg Bloodboil SAY_SLAY +(23418, 2, 1), -- Essence of Suffering SUFF_SAY_SLAY +(23419, 1, 1), -- Essence of Desire DESI_SAY_SLAY +(23420, 1, 1), -- Essence of Anger ANGER_SAY_SLAY + +-- Molten Core +(12018, 2, 6), -- Majordomo Executus SAY_SLAY +(11502, 9, 6), -- Ragnaros SAY_KILL + +-- Sunwell Plateau +(24882, 6, 7), -- Brutallus YELL_KILL +(25038, 1, 7), -- Felmyst YELL_KILL +(24850, 1, 7), -- Kalecgos SAY_EVIL_SLAY + +-- Tempest Keep: The Eye (additional) +(18805, 2, 7); -- High Astromancer Solarian SAY_KILL diff --git a/data/sql/updates/db_world/2026_07_15_00.sql b/data/sql/updates/db_world/2026_07_15_00.sql new file mode 100644 index 000000000..2b94fe60d --- /dev/null +++ b/data/sql/updates/db_world/2026_07_15_00.sql @@ -0,0 +1,4 @@ +-- DB update 2026_07_14_03 -> 2026_07_15_00 +-- Missile Barrage: consume on FINISH instead of CAST; its mods (DURATION, +-- ACTIVATION_TIME, COST) are applied inside handle_immediate, after CAST fires. +UPDATE `spell_proc` SET `SpellPhaseMask` = 4 WHERE `SpellId` = 44401; diff --git a/data/sql/updates/db_world/2026_07_15_01.sql b/data/sql/updates/db_world/2026_07_15_01.sql new file mode 100644 index 000000000..ed08f863c --- /dev/null +++ b/data/sql/updates/db_world/2026_07_15_01.sql @@ -0,0 +1,7 @@ +-- DB update 2026_07_15_00 -> 2026_07_15_01 +-- Watcher Gashra's Enrage (52470) is a self-buff, but its -50% physical damage +-- done effect (EFFECT_2) makes the core classify the whole aura as negative, +-- so it shows as a debuff on the target frame. +-- Mark all three effects positive (SPELL_ATTR0_CU_POSITIVE_EFF0 | EFF1 | EFF2). +DELETE FROM `spell_custom_attr` WHERE `spell_id` = 52470; +INSERT INTO `spell_custom_attr` (`spell_id`, `attributes`) VALUES (52470, 0x0E000000); diff --git a/data/sql/updates/db_world/2026_07_15_02.sql b/data/sql/updates/db_world/2026_07_15_02.sql new file mode 100644 index 000000000..0dcbfa2b3 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_15_02.sql @@ -0,0 +1,5 @@ +-- DB update 2026_07_15_01 -> 2026_07_15_02 +-- +DELETE FROM `gossip_menu_option` WHERE `MenuID` = 6626 AND `OptionID` = 0; +INSERT INTO `gossip_menu_option` (`MenuID`, `OptionID`, `OptionIcon`, `OptionText`, `OptionBroadcastTextID`, `OptionType`, `OptionNpcFlag`, `ActionMenuID`, `ActionPoiID`, `BoxCoded`, `BoxMoney`, `BoxText`, `BoxBroadcastTextID`) VALUES +(6626, 0, 0, 'Please tell me the Phrase..', 0, 0, 0, 0, 0, 0, 0, '', 0); diff --git a/data/sql/updates/db_world/2026_07_15_03.sql b/data/sql/updates/db_world/2026_07_15_03.sql new file mode 100644 index 000000000..c3a7d6396 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_15_03.sql @@ -0,0 +1,6 @@ +-- DB update 2026_07_15_02 -> 2026_07_15_03 +-- Yogg Portal 'Descend Into Madness' +-- Movement flags: DisableGravity + Root +DELETE FROM `creature_template_movement` WHERE (`CreatureId` = 34072); +INSERT INTO `creature_template_movement` (`CreatureId`, `Ground`, `Swim`, `Flight`, `Rooted`, `Chase`, `Random`, `InteractionPauseTimer`) VALUES +(34072, 0, 0, 1, 1, 0, 0, 0); diff --git a/data/sql/updates/db_world/2026_07_15_04.sql b/data/sql/updates/db_world/2026_07_15_04.sql new file mode 100644 index 000000000..ee659d342 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_15_04.sql @@ -0,0 +1,8 @@ +-- DB update 2026_07_15_03 -> 2026_07_15_04 +-- Sara should start friendly, becoming hostile when Yogg-Saron reveals himself (phase 2) +UPDATE `creature_template` SET `faction` = 35 WHERE `entry` IN (33134, 34332); + +-- Sara (1) +DELETE FROM `conditions` WHERE (`SourceTypeOrReferenceId` = 13) AND (`SourceGroup` = 1) AND (`SourceEntry` = 65719) AND (`SourceId` = 0) AND (`ElseGroup` = 1) AND (`ConditionTypeOrReference` = 31) AND (`ConditionTarget` = 0) AND (`ConditionValue1` = 3) AND (`ConditionValue2` = 34332) AND (`ConditionValue3` = 0); +INSERT INTO `conditions` (`SourceTypeOrReferenceId`, `SourceGroup`, `SourceEntry`, `SourceId`, `ElseGroup`, `ConditionTypeOrReference`, `ConditionTarget`, `ConditionValue1`, `ConditionValue2`, `ConditionValue3`, `NegativeCondition`, `ErrorType`, `ErrorTextId`, `ScriptName`, `Comment`) VALUES +(13, 1, 65719, 0, 1, 31, 0, 3, 34332, 0, 0, 0, 0, '', 'Shadow Nova'); diff --git a/data/sql/updates/db_world/2026_07_16_00.sql b/data/sql/updates/db_world/2026_07_16_00.sql new file mode 100644 index 000000000..5efb53892 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_16_00.sql @@ -0,0 +1,2 @@ +-- DB update 2026_07_15_04 -> 2026_07_16_00 +UPDATE `quest_offer_reward` SET `RewardText` = 'You''ve been a tremendous help here today; we now have a solid chance at unlocking the mystery of why these creatures choose to beach themselves on the coast of Darkshore. The idea that they might be fleeing the area around Teldrassil is disturbing. Should you come across others in your travels, please let me know. Our research here is but in its infancy.$B$BOn behalf of the Temple of the Moon, please accept this as thanks for the effort you have given. Thank you, $N!' WHERE `ID` = 4681; diff --git a/data/sql/updates/db_world/2026_07_16_01.sql b/data/sql/updates/db_world/2026_07_16_01.sql new file mode 100644 index 000000000..842fa39b4 --- /dev/null +++ b/data/sql/updates/db_world/2026_07_16_01.sql @@ -0,0 +1,8 @@ +-- DB update 2026_07_16_00 -> 2026_07_16_01 +-- Aerial Command Unit: hover height and flags to match TDB (hover-based chase) +-- 256 = UNIT_FLAG_IMMUNE_TO_PC (cleared by script at phase start), 512 = CREATURE_FLAG_EXTRA_NO_MOVE_FLAGS_UPDATE +UPDATE `creature_template` SET `HoverHeight` = 15, `unit_flags` = `unit_flags`|256, `flags_extra` = `flags_extra`|512 WHERE `entry` IN (33670, 34109); + +-- Values ported from TrinityCore (TrinityCore/TrinityCore@7c13b383); zeroed combat reach +-- made the hovering ACU unreachable for Magnetic Core's 15y check +UPDATE `creature_model_info` SET `BoundingRadius` = 0.31, `CombatReach` = 5 WHERE `DisplayID` = 28979; diff --git a/deps/g3dlite/include/G3D/platform.h b/deps/g3dlite/include/G3D/platform.h index d043f2149..1c660efef 100644 --- a/deps/g3dlite/include/G3D/platform.h +++ b/deps/g3dlite/include/G3D/platform.h @@ -360,7 +360,7 @@ int WINAPI WinMain(HINSTANCE hInst, HINSTANCE hPrev, LPSTR szCmdLine, int sw) {\ // Bring in shared_ptr and weak_ptr #if (defined(__GNUC__) && defined(__APPLE__)) || defined(__linux__) -#include <ciso646> // Defines _LIBCC_VERSION if linking against libc++ or does nothing +#include <version> // Defines _LIBCC_VERSION if linking against libc++ or does nothing #endif #if (!defined(_LIBCPP_VERSION) && defined(__APPLE__)) || (!defined(_LIBCPP_VERSION) && defined(__linux__)) # include <tr1/memory> diff --git a/pull_request_template.md b/pull_request_template.md index ca5745a3e..5a573003e 100644 --- a/pull_request_template.md +++ b/pull_request_template.md @@ -13,11 +13,13 @@ This PR proposes changes to: ### AI-assisted Pull Requests > [!IMPORTANT] -> While the use of AI tools when preparing pull requests is not prohibited, contributors must clearly disclose when such tools have been used and specify the model involved. -> -> Contributors are also expected to fully understand the changes they are submitting and must be able to explain and justify those changes when requested by maintainers. +> Using AI tools to prepare pull requests is allowed, but it must be disclosed and it must follow our AC guidelines for AI Agentic Engineering (link below). +> +> You are expected to fully understand the changes you submit and to be able to explain and justify them when maintainers ask. -- [ ] AI tools (e.g. ChatGPT, Claude, or similar) were used entirely or partially in preparing this pull request. Please specify which tools were used, if any. +- [ ] AI tools (e.g. Claude, ChatGPT, or similar) were used entirely or partially to prepare this pull request. If checked, specify which tools and models below. + - Tools/models used: +- [ ] I have read and understood the [AC guidelines for AI Agentic Engineering](https://www.azerothcore.org/wiki/agentic-engineering) ## Issues Addressed: <!-- If your fix has a relating issue, link it below --> diff --git a/src/common/Common.h b/src/common/Common.h index a14647b1f..b77fe3760 100644 --- a/src/common/Common.h +++ b/src/common/Common.h @@ -93,7 +93,7 @@ enum AccountFlag ACCOUNT_FLAG_EXPANSION2_COLLECTOR = 0x4000000, // NYI WotLK Collector's Edition ACCOUNT_FLAG_OVERMIND_LINKED = 0x8000000, // NYI Linked with Battle.net account ACCOUNT_FLAG_DEMOS = 0x10000000, // NYI UNK - ACCOUNT_FLAG_DEATH_KNIGHT_OK = 0x20000000, // NYI Has level 55 on account? + ACCOUNT_FLAG_DEATH_KNIGHT_OK = 0x20000000, // Allowed to create Death Knight (account met heroic level requirement on WotLK+) // Below might be StarCraft II related ACCOUNT_FLAG_S2_REQUIRE_IGR = 0x40000000, // NYI UNK ACCOUNT_FLAG_S2_TRIAL = 0x80000000, // NYI UNK diff --git a/src/server/apps/authserver/Main.cpp b/src/server/apps/authserver/Main.cpp index 6d351336a..f5c3b80bc 100644 --- a/src/server/apps/authserver/Main.cpp +++ b/src/server/apps/authserver/Main.cpp @@ -129,6 +129,10 @@ int main(int argc, char** argv) std::shared_ptr<void> dbHandle(nullptr, [](void*) { StopDB(); }); + // Mark every realm offline on startup; each worldserver clears this flag for its own realm once it is ready. + // This prevents realms from appearing online in the realm list when no worldserver is actually running. + LoginDatabase.DirectExecute("UPDATE realmlist SET flag = flag | {}", REALM_FLAG_OFFLINE); + std::shared_ptr<Acore::Asio::IoContext> ioContext = std::make_shared<Acore::Asio::IoContext>(); // Get the list of realms for the server diff --git a/src/server/apps/worldserver/worldserver.conf.dist b/src/server/apps/worldserver/worldserver.conf.dist index 2dbe0e423..0ac6aec76 100644 --- a/src/server/apps/worldserver/worldserver.conf.dist +++ b/src/server/apps/worldserver/worldserver.conf.dist @@ -3978,10 +3978,35 @@ Battleground.SpeedBuffRespawn = 150 # # Battleground.Override.LowLevels.MinPlayers # Description: Overrides the minimum number of required players per team for all levels < MaxPlayerLevel +# Used as fallback for any battleground without a per-battleground override (see below) # Default: 0 (Disabled) Battleground.Override.LowLevels.MinPlayers = 0 +# +# Battleground.Override.LowLevels.MinPlayers.AV +# Battleground.Override.LowLevels.MinPlayers.WS +# Battleground.Override.LowLevels.MinPlayers.AB +# Battleground.Override.LowLevels.MinPlayers.EY +# Battleground.Override.LowLevels.MinPlayers.SA +# Battleground.Override.LowLevels.MinPlayers.IC +# Description: Per-battleground overrides of the minimum number of required players per team +# for all levels < MaxPlayerLevel. +# AV = Alterac Valley, WS = Warsong Gulch, AB = Arathi Basin, +# EY = Eye of the Storm, SA = Strand of the Ancients, IC = Isle of Conquest +# Resolution order: per-battleground key, then Battleground.Override.LowLevels.MinPlayers, +# then the battleground_template DB value. +# Matches entered through the Random Battleground queue always use the global +# Battleground.Override.LowLevels.MinPlayers, never the per-battleground keys. +# Default: 0 (Disabled, use Battleground.Override.LowLevels.MinPlayers) + +Battleground.Override.LowLevels.MinPlayers.AV = 0 +Battleground.Override.LowLevels.MinPlayers.WS = 0 +Battleground.Override.LowLevels.MinPlayers.AB = 0 +Battleground.Override.LowLevels.MinPlayers.EY = 0 +Battleground.Override.LowLevels.MinPlayers.SA = 0 +Battleground.Override.LowLevels.MinPlayers.IC = 0 + # # Battleground.Warsong.Flags # Description: Set the number of flags required for a team to win in Warsong battleground diff --git a/src/server/database/Database/Implementation/LoginDatabase.cpp b/src/server/database/Database/Implementation/LoginDatabase.cpp index a82d08026..e593f13d3 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.cpp +++ b/src/server/database/Database/Implementation/LoginDatabase.cpp @@ -92,6 +92,7 @@ void LoginDatabaseConnection::DoPrepareStatements() PrepareStatement(LOGIN_UPD_LAST_ATTEMPT_IP, "UPDATE account SET last_attempt_ip = ? WHERE username = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_ACCOUNT_ONLINE, "UPDATE account SET online = 1 WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_UPD_UPTIME_PLAYERS, "UPDATE uptime SET uptime = ?, maxplayers = ? WHERE realmid = ? AND starttime = ?", CONNECTION_ASYNC); + PrepareStatement(LOGIN_UPD_REALM_ONLINE, "UPDATE realmlist SET flag = flag & ~? WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_OLD_LOGS, "DELETE FROM logs WHERE (time + ?) < ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_ACCOUNT_ACCESS, "DELETE FROM account_access WHERE id = ?", CONNECTION_ASYNC); PrepareStatement(LOGIN_DEL_ACCOUNT_ACCESS_BY_REALM, "DELETE FROM account_access WHERE id = ? AND (RealmID = ? OR RealmID = -1)", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/LoginDatabase.h b/src/server/database/Database/Implementation/LoginDatabase.h index 8dde435f7..305dfb6c6 100644 --- a/src/server/database/Database/Implementation/LoginDatabase.h +++ b/src/server/database/Database/Implementation/LoginDatabase.h @@ -76,6 +76,7 @@ enum LoginDatabaseStatements : uint32 LOGIN_UPD_LAST_ATTEMPT_IP, LOGIN_UPD_ACCOUNT_ONLINE, LOGIN_UPD_UPTIME_PLAYERS, + LOGIN_UPD_REALM_ONLINE, LOGIN_DEL_OLD_LOGS, LOGIN_DEL_ACCOUNT_ACCESS, LOGIN_DEL_ACCOUNT_ACCESS_BY_REALM, diff --git a/src/server/database/Database/Implementation/WorldDatabase.cpp b/src/server/database/Database/Implementation/WorldDatabase.cpp index dfe7b047d..11b8a613b 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.cpp +++ b/src/server/database/Database/Implementation/WorldDatabase.cpp @@ -27,6 +27,7 @@ void WorldDatabaseConnection::DoPrepareStatements() PrepareStatement(WORLD_DEL_CRELINKED_RESPAWN, "DELETE FROM linked_respawn WHERE guid = ?", CONNECTION_ASYNC); PrepareStatement(WORLD_REP_CREATURE_LINKED_RESPAWN, "REPLACE INTO linked_respawn (guid, linkedGuid) VALUES (?, ?)", CONNECTION_ASYNC); PrepareStatement(WORLD_SEL_CREATURE_TEXT, "SELECT CreatureID, GroupID, ID, Text, Type, Language, Probability, Emote, Duration, Sound, BroadcastTextId, TextRange FROM creature_text", CONNECTION_SYNCH); + PrepareStatement(WORLD_SEL_CREATURE_TEXT_OPTIONS, "SELECT o.CreatureID, o.GroupID, s.Cooldown, s.TriggerChance, s.PlayerOnly FROM creature_text_options o JOIN creature_text_option_sets s ON o.OptionSetID = s.SetID", CONNECTION_SYNCH); PrepareStatement(WORLD_SEL_SMART_SCRIPTS, "SELECT entryorguid, source_type, id, link, event_type, event_phase_mask, event_chance, event_flags, event_param1, event_param2, event_param3, event_param4, event_param5, event_param6, action_type, action_param1, action_param2, action_param3, action_param4, action_param5, action_param6, target_type, target_param1, target_param2, target_param3, target_param4, target_x, target_y, target_z, target_o FROM smart_scripts ORDER BY entryorguid, source_type, id, link", CONNECTION_SYNCH); PrepareStatement(WORLD_SEL_SMARTAI_WP, "SELECT entry, pointid, position_x, position_y, position_z, orientation, delay FROM waypoints ORDER BY entry, pointid", CONNECTION_SYNCH); PrepareStatement(WORLD_DEL_GAMEOBJECT, "DELETE FROM gameobject WHERE guid = ?", CONNECTION_ASYNC); diff --git a/src/server/database/Database/Implementation/WorldDatabase.h b/src/server/database/Database/Implementation/WorldDatabase.h index 6874a81d4..c82fd1652 100644 --- a/src/server/database/Database/Implementation/WorldDatabase.h +++ b/src/server/database/Database/Implementation/WorldDatabase.h @@ -32,6 +32,7 @@ enum WorldDatabaseStatements : uint32 WORLD_DEL_CRELINKED_RESPAWN, WORLD_REP_CREATURE_LINKED_RESPAWN, WORLD_SEL_CREATURE_TEXT, + WORLD_SEL_CREATURE_TEXT_OPTIONS, WORLD_SEL_SMART_SCRIPTS, WORLD_SEL_SMARTAI_WP, WORLD_DEL_GAMEOBJECT, diff --git a/src/server/game/Accounts/RBAC.h b/src/server/game/Accounts/RBAC.h index 2989bbf5e..8a2a77585 100644 --- a/src/server/game/Accounts/RBAC.h +++ b/src/server/game/Accounts/RBAC.h @@ -687,6 +687,19 @@ enum RBACPermissions RBAC_PERM_COMMAND_CHATFILTER_LIST = 923, RBAC_PERM_COMMAND_CHATFILTER_ADD = 924, RBAC_PERM_COMMAND_CHATFILTER_REMOVE = 925, + RBAC_PERM_COMMAND_AUTOBROADCAST_LIST = 926, + RBAC_PERM_COMMAND_AUTOBROADCAST_ADD = 927, + RBAC_PERM_COMMAND_AUTOBROADCAST_LOCALE = 928, + RBAC_PERM_COMMAND_AUTOBROADCAST_REMOVE = 929, + RBAC_PERM_COMMAND_MAIL_LIST = 930, + RBAC_PERM_COMMAND_MAIL_RETURN = 931, + RBAC_PERM_COMMAND_NPC_LOAD = 932, + RBAC_PERM_COMMAND_POOL_INFO = 933, + RBAC_PERM_COMMAND_POOL_LOOKUP = 934, + RBAC_PERM_COMMAND_SPELLINFO_ATTRIBUTES = 935, + RBAC_PERM_COMMAND_SPELLINFO_EFFECTS = 936, + RBAC_PERM_COMMAND_SPELLINFO_TARGETS = 937, + RBAC_PERM_COMMAND_SPELLINFO_ALL = 938, // custom permissions 1000+ RBAC_PERM_MAX }; diff --git a/src/server/game/Battlegrounds/Battleground.cpp b/src/server/game/Battlegrounds/Battleground.cpp index b10feb7b8..caa95df99 100644 --- a/src/server/game/Battlegrounds/Battleground.cpp +++ b/src/server/game/Battlegrounds/Battleground.cpp @@ -18,12 +18,8 @@ #include "Battleground.h" #include "ArenaSpectator.h" #include "ArenaTeam.h" -#include "BattlegroundBE.h" -#include "BattlegroundDS.h" #include "BattlegroundMgr.h" -#include "BattlegroundNA.h" -#include "BattlegroundRL.h" -#include "BattlegroundRV.h" +#include "BattlegroundUtils.h" #include "Chat.h" #include "ChatTextBuilder.h" #include "Creature.h" @@ -247,6 +243,12 @@ Battleground::~Battleground() delete itr.second; } +uint32 Battleground::GetMinPlayersPerTeam() const +{ + uint32 lowLevelsOverride = GetLowLevelsMinPlayersOverride(GetBgTypeID()); + return (lowLevelsOverride && !isTemplate() && !isMaxLevel() && !isArena()) ? lowLevelsOverride : m_MinPlayersPerTeam; +} + void Battleground::Update(uint32 diff) { // pussywizard: @@ -533,6 +535,8 @@ inline void Battleground::_ProcessJoin(uint32 diff) // Mark setup as completed m_SetupCompleted = true; + + sScriptMgr->OnBattlegroundSetup(this); } // First announcement at 120s or 60s (Depending on BG or Arena and configured time) @@ -558,33 +562,6 @@ inline void Battleground::_ProcessJoin(uint32 diff) if (StartMessageIds[BG_STARTING_EVENT_THIRD]) SendBroadcastText(StartMessageIds[BG_STARTING_EVENT_THIRD], CHAT_MSG_BG_SYSTEM_NEUTRAL); - - if (isArena()) - switch (GetBgTypeID()) - { - case BATTLEGROUND_NA: - DelObject(BG_NA_OBJECT_READY_MARKER_1); - DelObject(BG_NA_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_BE: - DelObject(BG_BE_OBJECT_READY_MARKER_1); - DelObject(BG_BE_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_RL: - DelObject(BG_RL_OBJECT_READY_MARKER_1); - DelObject(BG_RL_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_DS: - DelObject(BG_DS_OBJECT_READY_MARKER_1); - DelObject(BG_DS_OBJECT_READY_MARKER_2); - break; - case BATTLEGROUND_RV: - DelObject(BG_RV_OBJECT_READY_MARKER_1); - DelObject(BG_RV_OBJECT_READY_MARKER_2); - break; - default: - break; - } } // Delay expired (after configured prep time) else if (GetStartDelayTime() <= 0 && !(m_Events & BG_STARTING_EVENT_4)) @@ -1380,22 +1357,6 @@ void Battleground::SpectatorsSendPacket(WorldPacket& data) (*itr)->SendDirectMessage(&data); } -void Battleground::ReadyMarkerClicked(Player* p) -{ - if (!isArena() || GetStatus() >= STATUS_IN_PROGRESS || GetStartDelayTime() <= BG_START_DELAY_15S || (m_Events & BG_STARTING_EVENT_3) || p->IsSpectator()) - return; - readyMarkerClickedSet.insert(p->GetGUID()); - uint32 count = readyMarkerClickedSet.size(); - uint32 req = ArenaTeam::GetReqPlayersForType(GetArenaType()); - ChatHandler(p->GetSession()).SendNotification("You are marked as ready {}/{}", count, req); - if (count == req) - { - m_Events |= BG_STARTING_EVENT_2; - m_StartTime += GetStartDelayTime() - BG_START_DELAY_15S; - SetStartDelayTime(BG_START_DELAY_15S); - } -} - void Battleground::BuildPvPLogDataPacket(WorldPacket& data) { uint8 type = (isArena() ? 1 : 0); diff --git a/src/server/game/Battlegrounds/Battleground.h b/src/server/game/Battlegrounds/Battleground.h index eb075a346..17e4a27ce 100644 --- a/src/server/game/Battlegrounds/Battleground.h +++ b/src/server/game/Battlegrounds/Battleground.h @@ -284,7 +284,6 @@ enum BGHonorMode }; #define ARENA_TIMELIMIT_POINTS_LOSS -16 -#define ARENA_READY_MARKER_ENTRY 301337 /* This class is used to: @@ -349,11 +348,7 @@ public: } [[nodiscard]] uint32 GetMaxPlayersPerTeam() const { return m_MaxPlayersPerTeam; } - [[nodiscard]] uint32 GetMinPlayersPerTeam() const - { - auto lowLevelsOverride = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); - return (lowLevelsOverride && !isTemplate() && !isMaxLevel() && !isArena()) ? lowLevelsOverride : m_MinPlayersPerTeam; - } + [[nodiscard]] uint32 GetMinPlayersPerTeam() const; [[nodiscard]] int32 GetStartDelayTime() const { return m_StartDelayTime; } [[nodiscard]] uint8 GetArenaType() const { return m_ArenaType; } @@ -388,6 +383,9 @@ public: void ModifyStartDelayTime(int32 diff) { m_StartDelayTime -= diff; } void SetStartDelayTime(int32 Time) { m_StartDelayTime = Time; } + [[nodiscard]] uint8 GetStartingEventFlags() const { return m_Events; } + void AddStartingEventFlag(uint8 flag) { m_Events |= flag; } + void SetMaxPlayersPerTeam(uint32 MaxPlayers) { m_MaxPlayersPerTeam = MaxPlayers; } void SetMinPlayersPerTeam(uint32 MinPlayers) { m_MinPlayersPerTeam = MinPlayers; } @@ -420,9 +418,6 @@ public: [[nodiscard]] BattlegroundPlayerMap const& GetPlayers() const { return m_Players; } [[nodiscard]] uint32 GetPlayersSize() const { return m_Players.size(); } - void ReadyMarkerClicked(Player* p); // pussywizard - GuidSet readyMarkerClickedSet; // pussywizard - typedef std::unordered_map<ObjectGuid::LowType, BattlegroundScore*> BattlegroundScoreMap; typedef std::unordered_map<ObjectGuid, ArenaLogEntryData> ArenaLogEntryDataMap; // pussywizard ArenaLogEntryDataMap ArenaLogEntries; // pussywizard diff --git a/src/server/game/Battlegrounds/BattlegroundMgr.cpp b/src/server/game/Battlegrounds/BattlegroundMgr.cpp index c03323e4b..5c2328e43 100644 --- a/src/server/game/Battlegrounds/BattlegroundMgr.cpp +++ b/src/server/game/Battlegrounds/BattlegroundMgr.cpp @@ -948,7 +948,7 @@ BGFreeSlotQueueContainer& BattlegroundMgr::GetBGFreeSlotQueueStore(BattlegroundT void BattlegroundMgr::AddToBGFreeSlotQueue(BattlegroundTypeId bgTypeId, Battleground* bg) { - bgDataStore[bgTypeId].BGFreeSlotQueue.push_front(bg); + bgDataStore[bgTypeId].BGFreeSlotQueue.push_back(bg); } void BattlegroundMgr::RemoveFromBGFreeSlotQueue(BattlegroundTypeId bgTypeId, uint32 instanceId) diff --git a/src/server/game/Battlegrounds/BattlegroundQueue.cpp b/src/server/game/Battlegrounds/BattlegroundQueue.cpp index 1fac5ad54..ad2c7c95e 100644 --- a/src/server/game/Battlegrounds/BattlegroundQueue.cpp +++ b/src/server/game/Battlegrounds/BattlegroundQueue.cpp @@ -805,8 +805,7 @@ void BattlegroundQueue::BattlegroundQueueUpdate(uint32 diff, BattlegroundTypeId } }; - // battleground with free slot for player should be always in the beggining of the queue - // maybe it would be better to create bgfreeslotqueue for each bracket_id + // fill existing joinable battlegrounds, oldest first, before a new instance may form below BGFreeSlotQueueContainer& bgQueues = sBattlegroundMgr->GetBGFreeSlotQueueStore(bgTypeId); for (BGFreeSlotQueueContainer::iterator itr = bgQueues.begin(); itr != bgQueues.end();) { diff --git a/src/server/game/Battlegrounds/BattlegroundUtils.cpp b/src/server/game/Battlegrounds/BattlegroundUtils.cpp index 54703472b..f0c4b476c 100644 --- a/src/server/game/Battlegrounds/BattlegroundUtils.cpp +++ b/src/server/game/Battlegrounds/BattlegroundUtils.cpp @@ -14,7 +14,23 @@ uint32 GetMinPlayersPerTeam(Battleground* bg, PvPDifficultyEntry const* bracketE auto maxPlayerLevel = sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL); auto isMaxLevel = bracketEntry->minLevel <= maxPlayerLevel && maxPlayerLevel <= bracketEntry->maxLevel; - auto lowLevelsOverride = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); + auto lowLevelsOverride = GetLowLevelsMinPlayersOverride(bg->GetBgTypeID()); return (lowLevelsOverride && !isMaxLevel) ? lowLevelsOverride : bg->GetMinPlayersPerTeam(); } + +uint32 GetLowLevelsMinPlayersOverride(BattlegroundTypeId bgTypeId) +{ + uint32 perBg = 0; + switch (bgTypeId) + { + case BATTLEGROUND_AV: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV); break; + case BATTLEGROUND_WS: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS); break; + case BATTLEGROUND_AB: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB); break; + case BATTLEGROUND_EY: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY); break; + case BATTLEGROUND_SA: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA); break; + case BATTLEGROUND_IC: perBg = sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC); break; + default: break; + } + return perBg ? perBg : sWorld->getIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS); +} diff --git a/src/server/game/Battlegrounds/BattlegroundUtils.h b/src/server/game/Battlegrounds/BattlegroundUtils.h index 9d18cbb82..ed54548f7 100644 --- a/src/server/game/Battlegrounds/BattlegroundUtils.h +++ b/src/server/game/Battlegrounds/BattlegroundUtils.h @@ -5,4 +5,10 @@ uint32 GetMinPlayersPerTeam(Battleground* bg, PvPDifficultyEntry const* bracketEntry); +// Effective low-levels MinPlayersPerTeam override for a BG type: +// the per-BG config if set, else the global one. 0 = no override. +// BGs entered through the Random BG queue have BATTLEGROUND_RB as +// their type, so they always resolve through the global key. +uint32 GetLowLevelsMinPlayersOverride(BattlegroundTypeId bgTypeId); + #endif // BATTLEGROUNDUTILS_H diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp index a467d3032..2a8064439 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundBE.cpp @@ -104,10 +104,7 @@ bool BattlegroundBE::SetupBattleground() || !AddObject(BG_BE_OBJECT_DOOR_4, BG_BE_OBJECT_TYPE_DOOR_4, 6177.708f, 227.3481f, 3.604374f, -2.260201f, 0, 0, 0.9044551f, -0.4265689f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_BE_OBJECT_BUFF_1, BG_BE_OBJECT_TYPE_BUFF_1, 6249.042f, 275.3239f, 11.22033f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_BE_OBJECT_BUFF_2, BG_BE_OBJECT_TYPE_BUFF_2, 6228.26f, 249.566f, 11.21812f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_BE_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 6189.47f, 235.54f, 5.52f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_BE_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 6287.19f, 288.25f, 5.33f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_BE_OBJECT_BUFF_2, BG_BE_OBJECT_TYPE_BUFF_2, 6228.26f, 249.566f, 11.21812f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundBE: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundBE.h b/src/server/game/Battlegrounds/Zones/BattlegroundBE.h index abb16edb2..dceafb225 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundBE.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundBE.h @@ -28,9 +28,7 @@ enum BattlegroundBEObjectTypes BG_BE_OBJECT_DOOR_4 = 3, BG_BE_OBJECT_BUFF_1 = 4, BG_BE_OBJECT_BUFF_2 = 5, - BG_BE_OBJECT_READY_MARKER_1 = 6, - BG_BE_OBJECT_READY_MARKER_2 = 7, - BG_BE_OBJECT_MAX = 8 + BG_BE_OBJECT_MAX = 6 }; enum BattlegroundBEGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp index bd3a61f68..064e36478 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundDS.cpp @@ -195,10 +195,7 @@ bool BattlegroundDS::SetupBattleground() // knockback creatures || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_WATERFALL_KNOCKBACK, 1291.76f, 791.02f, 7.115f, 3.054326f, RESPAWN_IMMEDIATELY) || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_1, 1369.977f, 817.2882f, 16.08718f, 3.106686f, RESPAWN_IMMEDIATELY) - || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_2, 1212.833f, 765.3871f, 16.09484f, 0.0f, RESPAWN_IMMEDIATELY) - // Arena Ready Marker - || !AddObject(BG_DS_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 1229.44f, 759.35f, 17.89f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_DS_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 1352.90f, 822.77f, 17.96f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddCreature(BG_DS_NPC_TYPE_WATER_SPOUT, BG_DS_NPC_PIPE_KNOCKBACK_2, 1212.833f, 765.3871f, 16.09484f, 0.0f, RESPAWN_IMMEDIATELY)) { LOG_ERROR("sql.sql", "BatteGroundDS: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundDS.h b/src/server/game/Battlegrounds/Zones/BattlegroundDS.h index 103ec6cfe..e83beebea 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundDS.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundDS.h @@ -30,9 +30,7 @@ enum BattlegroundDSObjectTypes BG_DS_OBJECT_WATER_2 = 3, BG_DS_OBJECT_BUFF_1 = 4, BG_DS_OBJECT_BUFF_2 = 5, - BG_DS_OBJECT_READY_MARKER_1 = 6, - BG_DS_OBJECT_READY_MARKER_2 = 7, - BG_DS_OBJECT_MAX = 8 + BG_DS_OBJECT_MAX = 6 }; enum BattlegroundDSGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp index fd1ec0bfa..41dc5543a 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundNA.cpp @@ -80,10 +80,7 @@ bool BattlegroundNA::SetupBattleground() || !AddObject(BG_NA_OBJECT_DOOR_4, BG_NA_OBJECT_TYPE_DOOR_4, 4090.064f, 2858.438f, 10.23631f, 0.4928045f, 0, 0, 0.2439165f, 0.9697962f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_NA_OBJECT_BUFF_1, BG_NA_OBJECT_TYPE_BUFF_1, 4009.189941f, 2895.250000f, 13.052700f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_NA_OBJECT_BUFF_2, BG_NA_OBJECT_TYPE_BUFF_2, 4103.330078f, 2946.350098f, 13.051300f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_NA_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 4090.46f, 2875.43f, 12.16f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_NA_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 4022.82f, 2966.61f, 12.17f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_NA_OBJECT_BUFF_2, BG_NA_OBJECT_TYPE_BUFF_2, 4103.330078f, 2946.350098f, 13.051300f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundNA: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundNA.h b/src/server/game/Battlegrounds/Zones/BattlegroundNA.h index 889b9ed0e..0f545c49b 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundNA.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundNA.h @@ -28,9 +28,7 @@ enum BattlegroundNAObjectTypes BG_NA_OBJECT_DOOR_4 = 3, BG_NA_OBJECT_BUFF_1 = 4, BG_NA_OBJECT_BUFF_2 = 5, - BG_NA_OBJECT_READY_MARKER_1 = 6, - BG_NA_OBJECT_READY_MARKER_2 = 7, - BG_NA_OBJECT_MAX = 8 + BG_NA_OBJECT_MAX = 6 }; enum BattlegroundNAGameObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp index ac845386f..e17ddf734 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRL.cpp @@ -105,10 +105,7 @@ bool BattlegroundRL::SetupBattleground() || !AddObject(BG_RL_OBJECT_DOOR_2, BG_RL_OBJECT_TYPE_DOOR_2, 1278.648f, 1730.557f, 31.60557f, 1.684245f, 0, 0, 0.7460582f, 0.6658807f, RESPAWN_IMMEDIATELY) // buffs || !AddObject(BG_RL_OBJECT_BUFF_1, BG_RL_OBJECT_TYPE_BUFF_1, 1328.719971f, 1632.719971f, 36.730400f, -1.448624f, 0, 0, 0.6626201f, -0.7489557f, 120) - || !AddObject(BG_RL_OBJECT_BUFF_2, BG_RL_OBJECT_TYPE_BUFF_2, 1243.300049f, 1699.170044f, 34.872601f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120) - // Arena Ready Marker - || !AddObject(BG_RL_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 1298.61f, 1598.59f, 31.62f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_RL_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 1273.71f, 1734.05f, 31.61f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300)) + || !AddObject(BG_RL_OBJECT_BUFF_2, BG_RL_OBJECT_TYPE_BUFF_2, 1243.300049f, 1699.170044f, 34.872601f, -0.06981307f, 0, 0, 0.03489945f, -0.9993908f, 120)) { LOG_ERROR("sql.sql", "BatteGroundRL: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRL.h b/src/server/game/Battlegrounds/Zones/BattlegroundRL.h index 3e273810c..9a7c63ff3 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRL.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRL.h @@ -26,9 +26,7 @@ enum BattlegroundRLObjectTypes BG_RL_OBJECT_DOOR_2 = 1, BG_RL_OBJECT_BUFF_1 = 2, BG_RL_OBJECT_BUFF_2 = 3, - BG_RL_OBJECT_READY_MARKER_1 = 4, - BG_RL_OBJECT_READY_MARKER_2 = 5, - BG_RL_OBJECT_MAX = 6 + BG_RL_OBJECT_MAX = 4 }; enum BattlegroundRLObjects diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp b/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp index 5b77fc9a3..4120a054c 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRV.cpp @@ -240,12 +240,7 @@ bool BattlegroundRV::SetupBattleground() || !AddObject(BG_RV_OBJECT_PILAR_1, BG_RV_OBJECT_TYPE_PILAR_1, 763.632385f, -306.162384f, 25.909504f, 3.141593f, 0, 0, 0, RESPAWN_IMMEDIATELY) || !AddObject(BG_RV_OBJECT_PILAR_2, BG_RV_OBJECT_TYPE_PILAR_2, 723.644287f, -284.493256f, 24.648525f, 3.141593f, 0, 0, 0, RESPAWN_IMMEDIATELY) || !AddObject(BG_RV_OBJECT_PILAR_3, BG_RV_OBJECT_TYPE_PILAR_3, 763.611145f, -261.856750f, 25.909504f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY) - || !AddObject(BG_RV_OBJECT_PILAR_4, BG_RV_OBJECT_TYPE_PILAR_4, 802.211609f, -284.493256f, 24.648525f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY) - - // Arena Ready Marker - || !AddObject(BG_RV_OBJECT_READY_MARKER_1, ARENA_READY_MARKER_ENTRY, 769.93f, -301.04f, 2.80f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - || !AddObject(BG_RV_OBJECT_READY_MARKER_2, ARENA_READY_MARKER_ENTRY, 757.02f, -267.30f, 2.80f, 0.0f, 0.0f, 0.0f, 0.0f, 0.0f, 300) - ) + || !AddObject(BG_RV_OBJECT_PILAR_4, BG_RV_OBJECT_TYPE_PILAR_4, 802.211609f, -284.493256f, 24.648525f, 0.000000f, 0, 0, 0, RESPAWN_IMMEDIATELY)) { LOG_ERROR("sql.sql", "BatteGroundRV: Failed to spawn some object!"); return false; diff --git a/src/server/game/Battlegrounds/Zones/BattlegroundRV.h b/src/server/game/Battlegrounds/Zones/BattlegroundRV.h index ffa3d5809..38b7c7eb4 100644 --- a/src/server/game/Battlegrounds/Zones/BattlegroundRV.h +++ b/src/server/game/Battlegrounds/Zones/BattlegroundRV.h @@ -22,53 +22,50 @@ enum BattlegroundRVObjectTypes { - BG_RV_OBJECT_BUFF_1, - BG_RV_OBJECT_BUFF_2, - BG_RV_OBJECT_FIRE_1, - BG_RV_OBJECT_FIRE_2, - BG_RV_OBJECT_FIREDOOR_1, - BG_RV_OBJECT_FIREDOOR_2, + BG_RV_OBJECT_BUFF_1 = 0, + BG_RV_OBJECT_BUFF_2 = 1, + BG_RV_OBJECT_FIRE_1 = 2, + BG_RV_OBJECT_FIRE_2 = 3, + BG_RV_OBJECT_FIREDOOR_1 = 4, + BG_RV_OBJECT_FIREDOOR_2 = 5, - BG_RV_OBJECT_PILAR_1, - BG_RV_OBJECT_PILAR_3, - BG_RV_OBJECT_GEAR_1, - BG_RV_OBJECT_GEAR_2, + BG_RV_OBJECT_PILAR_1 = 6, + BG_RV_OBJECT_PILAR_3 = 7, + BG_RV_OBJECT_GEAR_1 = 8, + BG_RV_OBJECT_GEAR_2 = 9, - BG_RV_OBJECT_PILAR_2, - BG_RV_OBJECT_PILAR_4, - BG_RV_OBJECT_PULLEY_1, - BG_RV_OBJECT_PULLEY_2, + BG_RV_OBJECT_PILAR_2 = 10, + BG_RV_OBJECT_PILAR_4 = 11, + BG_RV_OBJECT_PULLEY_1 = 12, + BG_RV_OBJECT_PULLEY_2 = 13, - BG_RV_OBJECT_ELEVATOR_1, - BG_RV_OBJECT_ELEVATOR_2, + BG_RV_OBJECT_ELEVATOR_1 = 14, + BG_RV_OBJECT_ELEVATOR_2 = 15, - BG_RV_OBJECT_READY_MARKER_1, - BG_RV_OBJECT_READY_MARKER_2, - - BG_RV_OBJECT_MAX, + BG_RV_OBJECT_MAX = 16 }; enum BattlegroundRVGameObjects { - BG_RV_OBJECT_TYPE_BUFF_1 = 184663, - BG_RV_OBJECT_TYPE_BUFF_2 = 184664, - BG_RV_OBJECT_TYPE_FIRE_1 = 192704, - BG_RV_OBJECT_TYPE_FIRE_2 = 192705, + BG_RV_OBJECT_TYPE_BUFF_1 = 184663, + BG_RV_OBJECT_TYPE_BUFF_2 = 184664, + BG_RV_OBJECT_TYPE_FIRE_1 = 192704, + BG_RV_OBJECT_TYPE_FIRE_2 = 192705, - BG_RV_OBJECT_TYPE_FIREDOOR_2 = 192387, - BG_RV_OBJECT_TYPE_FIREDOOR_1 = 192388, - BG_RV_OBJECT_TYPE_PULLEY_1 = 192389, - BG_RV_OBJECT_TYPE_PULLEY_2 = 192390, - BG_RV_OBJECT_TYPE_GEAR_1 = 192393, - BG_RV_OBJECT_TYPE_GEAR_2 = 192394, + BG_RV_OBJECT_TYPE_FIREDOOR_2 = 192387, + BG_RV_OBJECT_TYPE_FIREDOOR_1 = 192388, + BG_RV_OBJECT_TYPE_PULLEY_1 = 192389, + BG_RV_OBJECT_TYPE_PULLEY_2 = 192390, + BG_RV_OBJECT_TYPE_GEAR_1 = 192393, + BG_RV_OBJECT_TYPE_GEAR_2 = 192394, - BG_RV_OBJECT_TYPE_ELEVATOR_1 = 194582, - BG_RV_OBJECT_TYPE_ELEVATOR_2 = 194586, + BG_RV_OBJECT_TYPE_ELEVATOR_1 = 194582, + BG_RV_OBJECT_TYPE_ELEVATOR_2 = 194586, - BG_RV_OBJECT_TYPE_PILAR_1 = 194583, // axe - BG_RV_OBJECT_TYPE_PILAR_2 = 194584, // arena - BG_RV_OBJECT_TYPE_PILAR_3 = 194585, // lightning - BG_RV_OBJECT_TYPE_PILAR_4 = 194587, // ivory + BG_RV_OBJECT_TYPE_PILAR_1 = 194583, // axe + BG_RV_OBJECT_TYPE_PILAR_2 = 194584, // arena + BG_RV_OBJECT_TYPE_PILAR_3 = 194585, // lightning + BG_RV_OBJECT_TYPE_PILAR_4 = 194587, // ivory }; enum BattlegroundRVData diff --git a/src/server/game/Calendar/CalendarMgr.cpp b/src/server/game/Calendar/CalendarMgr.cpp index e258e2420..ecf98ef6d 100644 --- a/src/server/game/Calendar/CalendarMgr.cpp +++ b/src/server/game/Calendar/CalendarMgr.cpp @@ -483,7 +483,7 @@ uint32 CalendarMgr::GetPlayerNumPending(ObjectGuid guid) std::string CalendarEvent::BuildCalendarMailSubject(ObjectGuid remover) const { std::ostringstream strm; - strm << remover.ToString() << ':' << _title; + strm << remover.GetRawValue() << ':' << _title; return strm.str(); } diff --git a/src/server/game/Entities/Creature/Creature.cpp b/src/server/game/Entities/Creature/Creature.cpp index df4785545..75308ef23 100644 --- a/src/server/game/Entities/Creature/Creature.cpp +++ b/src/server/game/Entities/Creature/Creature.cpp @@ -3817,6 +3817,21 @@ void Creature::ClearTextRepeatGroup(uint8 textGroup) groupItr->second.clear(); } +bool Creature::IsTextOnCooldown(uint8 textGroup) const +{ + auto itr = m_textCooldowns.find(textGroup); + if (itr == m_textCooldowns.end()) + return false; + + return GameTime::GetGameTime().count() < itr->second; +} + +void Creature::SetTextCooldown(uint8 textGroup, uint32 cooldownMs) +{ + m_textCooldowns[textGroup] = + GameTime::GetGameTime().count() + ((cooldownMs + 999) / 1000); +} + void Creature::SetRespawnTime(uint32 respawn) { m_respawnTime = respawn ? GameTime::GetGameTime().count() + respawn : 0; diff --git a/src/server/game/Entities/Creature/Creature.h b/src/server/game/Entities/Creature/Creature.h index b492bd6c8..6b14d82f2 100644 --- a/src/server/game/Entities/Creature/Creature.h +++ b/src/server/game/Entities/Creature/Creature.h @@ -411,6 +411,9 @@ public: void SetTextRepeatId(uint8 textGroup, uint8 id); void ClearTextRepeatGroup(uint8 textGroup); + bool IsTextOnCooldown(uint8 textGroup) const; + void SetTextCooldown(uint8 textGroup, uint32 cooldownMs); + bool IsFreeToMove(); static constexpr uint32 MOVE_CIRCLE_CHECK_INTERVAL = 3000; static constexpr uint32 MOVE_BACKWARDS_CHECK_INTERVAL = 2000; @@ -548,6 +551,7 @@ private: ObjectGuid _spellFocusTarget; ///> Saved target during spell focus for restoration CreatureTextRepeatGroup m_textRepeat; + std::unordered_map<uint8, uint32> m_textCooldowns; // groupID -> expiry time (s) bool _isMissingSwimmingFlagOutOfCombat; diff --git a/src/server/game/Entities/GameObject/GameObject.cpp b/src/server/game/Entities/GameObject/GameObject.cpp index 523144f2c..955bd32cb 100644 --- a/src/server/game/Entities/GameObject/GameObject.cpp +++ b/src/server/game/Entities/GameObject/GameObject.cpp @@ -1846,14 +1846,19 @@ void GameObject::Use(Unit* user) if (GetUniqueUseCount() == info->summoningRitual.reqParticipants) return; + SpellCastResult castResult; if (info->summoningRitual.animSpell) - player->CastSpell(player, info->summoningRitual.animSpell, true); + castResult = player->CastSpell(player, info->summoningRitual.animSpell, true); else - player->CastSpell(player, GetSpellId(), + castResult = player->CastSpell(player, GetSpellId(), TriggerCastFlags(TRIGGERED_IGNORE_EFFECTS + | TRIGGERED_IGNORE_SPELL_AND_CATEGORY_CD | TRIGGERED_IGNORE_POWER_AND_REAGENT_COST | TRIGGERED_CAST_DIRECTLY)); + if (castResult != SPELL_CAST_OK) + return; + AddUniqueUse(player); // full amount unique participants including original summoner diff --git a/src/server/game/Entities/Player/Player.cpp b/src/server/game/Entities/Player/Player.cpp index 01e41fb2d..e78c12eb6 100644 --- a/src/server/game/Entities/Player/Player.cpp +++ b/src/server/game/Entities/Player/Player.cpp @@ -11682,6 +11682,12 @@ void Player::SendInitialPacketsAfterAddToMap() auraList.front()->HandleEffect(this, AURA_EFFECT_HANDLE_SEND_FOR_CLIENT, true); } + // Explicitly synchronize CAN_FLY state with client on login to prevent + // players from retaining flight ability after disconnecting during a + // teleport from a flyable to non-flyable zone (e.g. entering an instance). + if (!HasIncreaseMountedFlightSpeedAura() && !HasFlyAura()) + SetCanFly(false); + // Fix mount, update block gets messed somewhere { if (!isBeingLoaded() && GetMountBlockId() && !HasMountedAura()) diff --git a/src/server/game/Entities/Player/PlayerStorage.cpp b/src/server/game/Entities/Player/PlayerStorage.cpp index bc9f918bd..18d9b5aa1 100644 --- a/src/server/game/Entities/Player/PlayerStorage.cpp +++ b/src/server/game/Entities/Player/PlayerStorage.cpp @@ -1888,6 +1888,10 @@ InventoryResult Player::CanEquipNewItem(uint8 slot, uint16& dest, uint32 item, b if (pItem) { InventoryResult result = CanEquipItem(slot, dest, pItem, swap); + // Random-property items queue themselves on creation (SetItemRandomProperties + // -> SetState(ITEM_CHANGED, owner)); the probe must leave the update queue + // before deletion or the queue keeps a pointer to freed memory. + pItem->RemoveFromUpdateQueueOf(const_cast<Player*>(this)); delete pItem; return result; } diff --git a/src/server/game/Entities/Unit/Unit.cpp b/src/server/game/Entities/Unit/Unit.cpp index f53e3b05d..320949f31 100644 --- a/src/server/game/Entities/Unit/Unit.cpp +++ b/src/server/game/Entities/Unit/Unit.cpp @@ -384,8 +384,6 @@ Unit::Unit() : WorldObject(), for (uint8 i = 0; i < MAX_GAMEOBJECT_SLOT; ++i) m_ObjectSlot[i].Clear(); - m_auraUpdateIterator = m_ownedAuras.end(); - m_interruptMask = 0; m_transform = 0; m_canModifyStats = false; @@ -4034,13 +4032,16 @@ void Unit::_UpdateSpells(uint32 time) } } - // m_auraUpdateIterator can be updated in indirect called code at aura remove to skip next planned to update but removed auras - for (m_auraUpdateIterator = m_ownedAuras.begin(); m_auraUpdateIterator != m_ownedAuras.end();) - { - Aura* i_aura = m_auraUpdateIterator->second; - ++m_auraUpdateIterator; // need shift to next for allow update if need into aura update - i_aura->UpdateOwner(time, this); - } + // snapshot - UpdateOwner can mutate the map; pointers stay valid (removed auras + // are deleted later in _DeleteRemovedAuras) + m_auraUpdateSnapshot.clear(); + m_auraUpdateSnapshot.reserve(m_ownedAuras.size()); + for (auto& [id, aura] : m_ownedAuras) + m_auraUpdateSnapshot.push_back(aura); + + for (Aura* aura : m_auraUpdateSnapshot) + if (!aura->IsRemoved()) + aura->UpdateOwner(time, this); // remove expired auras - do that after updates(used in scripts?) for (AuraMap::iterator i = m_ownedAuras.begin(); i != m_ownedAuras.end();) @@ -4986,10 +4987,6 @@ void Unit::RemoveOwnedAura(AuraMap::iterator& i, AuraRemoveMode removeMode) Aura* aura = i->second; ASSERT(!aura->IsRemoved()); - // if unit currently update aura list then make safe update iterator shift to next - if (m_auraUpdateIterator == i && m_auraUpdateIterator != m_ownedAuras.end()) - ++m_auraUpdateIterator; - m_ownedAuras.erase(i); m_removedAuras.push_back(aura); @@ -5063,6 +5060,9 @@ void Unit::RemoveAura(AuraApplicationMap::iterator& i, AuraRemoveMode mode) // Remove aura - for Area and Target auras if (aura->GetOwner() == this) aura->Remove(mode); + + // Aura::Remove can cascade into m_appliedAuras mutations - reset again for the caller + i = m_appliedAuras.begin(); } void Unit::RemoveAura(uint32 spellId, ObjectGuid caster, uint8 reqEffMask, AuraRemoveMode removeMode) @@ -5150,7 +5150,8 @@ void Unit::RemoveAppliedAuras(std::function<bool(AuraApplication const*)> const& { for (AuraApplicationMap::iterator iter = m_appliedAuras.begin(); iter != m_appliedAuras.end();) { - if (check(iter->second)) + // RemoveAura no-ops on applications already mid-removal + if (!iter->second->GetRemoveMode() && check(iter->second)) { RemoveAura(iter); continue; @@ -5176,9 +5177,11 @@ void Unit::RemoveAppliedAuras(uint32 spellId, std::function<bool(AuraApplication { for (AuraApplicationMap::iterator iter = m_appliedAuras.lower_bound(spellId); iter != m_appliedAuras.upper_bound(spellId);) { - if (check(iter->second)) + // RemoveAura no-ops on applications already mid-removal + if (!iter->second->GetRemoveMode() && check(iter->second)) { RemoveAura(iter); + iter = m_appliedAuras.lower_bound(spellId); continue; } ++iter; @@ -7428,24 +7431,27 @@ bool Unit::Attack(Unit* victim, bool meleeAttack) else if (victim->IsCreature()) victim->ToCreature()->UpdateLeashExtensionTime(); - // set position before any AI calls/assistance - //if (IsCreature()) - // ToCreature()->SetCombatStartPosition(GetPositionX(), GetPositionY(), GetPositionZ()); - // player-controlled creatures (pets, charms) enter combat on contact instead - // (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked) - if (creature && !IsControlledByPlayer()) + // Player-controlled creatures (pets, charms) enter combat on contact instead + // (melee swing execution or spell launch/hit, see Unit::AtTargetAttacked). + // Non-controllable guardians (e.g. Shaman Elementals, Infernal) have no attack + // command and must engage immediately so their AI can chase and attack. + + if (creature && (!IsControlledByPlayer() || (IsGuardian() && !IsControllableGuardian()))) { EngageWithTarget(victim); - creature->SendAIReaction(AI_REACTION_HOSTILE); + if (!IsControlledByPlayer()) + { + creature->SendAIReaction(AI_REACTION_HOSTILE); - /// @todo: Implement aggro range, detection range and assistance range templates - if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE))) - creature->CallAssistance(); + /// @todo: Implement aggro range, detection range and assistance range templates + if (!(creature->HasFlagsExtra(CREATURE_FLAG_EXTRA_DONT_CALL_ASSISTANCE))) + creature->CallAssistance(); - creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD)); + creature->SetAssistanceTimer(sWorld->getIntConfig(CONFIG_CREATURE_FAMILY_ASSISTANCE_PERIOD)); - SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + } } // delay offhand weapon attack by 50% of the base attack time diff --git a/src/server/game/Entities/Unit/Unit.h b/src/server/game/Entities/Unit/Unit.h index 4f07f6769..97a3fdf5f 100644 --- a/src/server/game/Entities/Unit/Unit.h +++ b/src/server/game/Entities/Unit/Unit.h @@ -31,6 +31,7 @@ #include "ThreatManager.h" #include "UnitDefines.h" #include "UnitUtils.h" +#include <boost/container/flat_map.hpp> #include <functional> #include <utility> @@ -666,15 +667,15 @@ public: typedef std::unordered_set<Unit*> AttackerSet; typedef std::set<Unit*> ControlSet; - typedef std::multimap<uint32, Aura*> AuraMap; + typedef boost::container::flat_multimap<uint32, Aura*> AuraMap; typedef std::pair<AuraMap::const_iterator, AuraMap::const_iterator> AuraMapBounds; typedef std::pair<AuraMap::iterator, AuraMap::iterator> AuraMapBoundsNonConst; - typedef std::multimap<uint32, AuraApplication*> AuraApplicationMap; + typedef boost::container::flat_multimap<uint32, AuraApplication*> AuraApplicationMap; typedef std::pair<AuraApplicationMap::const_iterator, AuraApplicationMap::const_iterator> AuraApplicationMapBounds; typedef std::pair<AuraApplicationMap::iterator, AuraApplicationMap::iterator> AuraApplicationMapBoundsNonConst; - typedef std::multimap<AuraStateType, AuraApplication*> AuraStateAurasMap; + typedef boost::container::flat_multimap<AuraStateType, AuraApplication*> AuraStateAurasMap; typedef std::pair<AuraStateAurasMap::const_iterator, AuraStateAurasMap::const_iterator> AuraStateAurasMapBounds; typedef std::vector<AuraEffect*> AuraEffectList; @@ -2166,7 +2167,7 @@ protected: AuraMap m_ownedAuras; AuraApplicationMap m_appliedAuras; AuraList m_removedAuras; - AuraMap::iterator m_auraUpdateIterator; + std::vector<Aura*> m_auraUpdateSnapshot; // _UpdateSpells scratch buffer uint32 m_removedAurasCount; AuraEffectList m_modAuras[TOTAL_AURAS]; diff --git a/src/server/game/Events/GameEventMgr.cpp b/src/server/game/Events/GameEventMgr.cpp index ee987592e..fb1137076 100644 --- a/src/server/game/Events/GameEventMgr.cpp +++ b/src/server/game/Events/GameEventMgr.cpp @@ -1960,6 +1960,17 @@ void GameEventMgr::SetHolidayEventTime(GameEventData& event) time_t curTime = GameTime::GetGameTime().count(); + if (holiday->Looping) + { + // Looping events (Battleground Call to Arms) carry a single past anchor in the DBC and + // recur via event.Occurence. Anchor to that DBC date so the window keeps the correct phase + // instead of inheriting the wrong time of day from a stale game_event.start_time. + if (time_t start = HolidayDateCalculator::FindLoopingStartTime(holiday->Date[0], stageOffset, event.Occurence, curTime)) + event.Start = start; + + return; + } + if (!singleDate) { time_t start = HolidayDateCalculator::FindStartTimeForStage( diff --git a/src/server/game/Events/HolidayDateCalculator.cpp b/src/server/game/Events/HolidayDateCalculator.cpp index c507f5413..367fd7056 100644 --- a/src/server/game/Events/HolidayDateCalculator.cpp +++ b/src/server/game/Events/HolidayDateCalculator.cpp @@ -671,3 +671,28 @@ time_t HolidayDateCalculator::FindStartTimeForStage(const uint32_t* packedDates, return 0; } + +time_t HolidayDateCalculator::FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, time_t curTime) +{ + if (!packedAnchor) + return 0; + + std::tm timeInfo = {}; + timeInfo.tm_year = static_cast<int>((packedAnchor >> 24) & 0x1F) + 100; // years since 1900 (2000-2031) + timeInfo.tm_mon = static_cast<int>((packedAnchor >> 20) & 0xF); + timeInfo.tm_mday = static_cast<int>((packedAnchor >> 14) & 0x3F) + 1; + timeInfo.tm_hour = static_cast<int>((packedAnchor >> 6) & 0x1F); + timeInfo.tm_min = static_cast<int>(packedAnchor & 0x3F); + timeInfo.tm_sec = 0; + timeInfo.tm_isdst = -1; + + time_t anchor = mktime(&timeInfo) + stageOffset; + + // Roll forward by whole periods to the most recent occurrence, preserving phase. + time_t const period = static_cast<time_t>(occurenceMinutes) * 60; + if (period > 0 && anchor < curTime) + anchor += ((curTime - anchor) / period) * period; + + return anchor; +} diff --git a/src/server/game/Events/HolidayDateCalculator.h b/src/server/game/Events/HolidayDateCalculator.h index 6ed4f6c67..50b9eb909 100644 --- a/src/server/game/Events/HolidayDateCalculator.h +++ b/src/server/game/Events/HolidayDateCalculator.h @@ -107,6 +107,11 @@ public: static time_t FindStartTimeForStage(const uint32_t* packedDates, uint8_t numDates, time_t stageOffset, uint32_t stageLengthMinutes, time_t curTime); + // Start time for a looping holiday event (Battleground Call to Arms): rolls the packed anchor + // forward by whole occurenceMinutes periods to the most recent occurrence. Returns 0 if anchor is 0. + static time_t FindLoopingStartTime(uint32_t packedAnchor, time_t stageOffset, + uint32_t occurenceMinutes, time_t curTime); + private: // Julian Date conversions for lunar calculations static double DateToJulianDay(int year, int month, double day); diff --git a/src/server/game/Handlers/BattleGroundHandler.cpp b/src/server/game/Handlers/BattleGroundHandler.cpp index 9051801e5..07e2863ad 100644 --- a/src/server/game/Handlers/BattleGroundHandler.cpp +++ b/src/server/game/Handlers/BattleGroundHandler.cpp @@ -554,6 +554,8 @@ void WorldSession::HandleBattleFieldPortOpcode(WorldPacket& recvData) // leaks forever, permanently skewing team selection and blocking the // empty instance's cleanup. bg->DecreaseInvitedCount(teamId); + if (bg->HasFreeSlots()) + bg->AddToBGFreeSlotQueue(); _player->RemoveBattlegroundQueueId(bgQueueTypeId); _player->SetBattlegroundId(0, BATTLEGROUND_TYPE_NONE, PLAYER_MAX_BATTLEGROUND_QUEUES, false, false, TEAM_NEUTRAL); diff --git a/src/server/game/Handlers/CharacterHandler.cpp b/src/server/game/Handlers/CharacterHandler.cpp index d7dca9bb4..8125d12db 100644 --- a/src/server/game/Handlers/CharacterHandler.cpp +++ b/src/server/game/Handlers/CharacterHandler.cpp @@ -368,7 +368,8 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) // speedup check for heroic class disabled case uint32 req_level_for_heroic = sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_MIN_LEVEL_FOR_HEROIC_CHARACTER); - if (!HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT && req_level_for_heroic > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + if (!HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT + && req_level_for_heroic > sWorld->getIntConfig(CONFIG_MAX_PLAYER_LEVEL) && !HasAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK)) { SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); return; @@ -435,7 +436,8 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) } bool haveSameRace = false; uint32 heroicReqLevel = sWorld->getIntConfig(CONFIG_CHARACTER_CREATING_MIN_LEVEL_FOR_HEROIC_CHARACTER); - bool hasHeroicReqLevel = (heroicReqLevel == 0); + bool const deathKnightFlagAlreadySet = HasAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK); // Account flag is superior to level requirement. + bool hasHeroicReqLevel = (heroicReqLevel == 0) || deathKnightFlagAlreadySet; bool allowTwoSideAccounts = !sWorld->IsPvPRealm() || sWorld->getBoolConfig(CONFIG_ALLOW_TWO_SIDE_ACCOUNTS) || HasPermission(rbac::RBAC_PERM_TWO_SIDE_CHARACTER_CREATION); uint32 skipCinematics = sWorld->getIntConfig(CONFIG_SKIP_CINEMATICS); bool checkDeathKnightReqs = !HasPermission(rbac::RBAC_PERM_SKIP_CHECK_CHARACTER_CREATION_DEATH_KNIGHT) && createInfo->Class == CLASS_DEATH_KNIGHT; @@ -524,10 +526,16 @@ void WorldSession::HandleCharCreateOpcode(WorldPacket& recvData) } } - if (checkDeathKnightReqs && !hasHeroicReqLevel) + if (checkDeathKnightReqs) { - SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); - return; + if (!deathKnightFlagAlreadySet && hasHeroicReqLevel) + UpdateAccountFlag(ACCOUNT_FLAG_DEATH_KNIGHT_OK); + + if (!hasHeroicReqLevel) + { + SendCharCreate(CHAR_CREATE_LEVEL_REQUIREMENT); + return; + } } // Check name uniqueness in the same step as saving to database diff --git a/src/server/game/Handlers/NPCHandler.cpp b/src/server/game/Handlers/NPCHandler.cpp index d2c0383cc..62f117b9d 100644 --- a/src/server/game/Handlers/NPCHandler.cpp +++ b/src/server/game/Handlers/NPCHandler.cpp @@ -21,7 +21,6 @@ #include "Creature.h" #include "DatabaseEnv.h" #include "GameGraveyard.h" -#include "Language.h" #include "NPCPackets.h" #include "ObjectMgr.h" #include "Opcodes.h" diff --git a/src/server/game/Instances/InstanceScript.cpp b/src/server/game/Instances/InstanceScript.cpp index f4dec4454..ab0813234 100644 --- a/src/server/game/Instances/InstanceScript.cpp +++ b/src/server/game/Instances/InstanceScript.cpp @@ -398,6 +398,8 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) if (id < bosses.size()) { BossInfo* bossInfo = &bosses[id]; + MinionSet minions = bossInfo->minion; + sScriptMgr->OnBeforeSetBossState(id, state, bossInfo->state, instance); if (bossInfo->state == TO_BE_DECIDED) // loading { @@ -410,8 +412,8 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) return false; if (state == DONE) - for (MinionSet::iterator i = bossInfo->minion.begin(); i != bossInfo->minion.end(); ++i) - if ((*i)->isWorldBoss() && (*i)->IsAlive()) + for (Creature* minion : minions) + if (minion && minion->isWorldBoss() && minion->IsAlive()) return false; bossInfo->state = state; @@ -422,8 +424,9 @@ bool InstanceScript::SetBossState(uint32 id, EncounterState state) for (DoorSet::iterator i = bossInfo->door[type].begin(); i != bossInfo->door[type].end(); ++i) UpdateDoorState(*i); - for (MinionSet::iterator i = bossInfo->minion.begin(); i != bossInfo->minion.end(); ++i) - UpdateMinionState(*i, state); + for (Creature* minion : minions) + if (minion) + UpdateMinionState(minion, state); return true; } diff --git a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp index 9881a25f0..3d9213ed8 100644 --- a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp +++ b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.cpp @@ -104,6 +104,11 @@ void ScriptMgr::OnBattlegroundCreate(Battleground* bg) CALL_ENABLED_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_CREATE, script->OnBattlegroundCreate(bg)); } +void ScriptMgr::OnBattlegroundSetup(Battleground* bg) +{ + CALL_ENABLED_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_SETUP, script->OnBattlegroundSetup(bg)); +} + bool ScriptMgr::CanAddGroupToMatchingPool(BattlegroundQueue* queue, GroupQueueInfo* group, uint32 poolPlayerCount, Battleground* bg, BattlegroundBracketId bracketId) { CALL_ENABLED_BOOLEAN_HOOKS(AllBattlegroundScript, ALLBATTLEGROUNDHOOK_CAN_ADD_GROUP_TO_MATCHING_POOL, !script->CanAddGroupToMatchingPool(queue, group, poolPlayerCount, bg, bracketId)); diff --git a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h index 108f3cf7f..f1aaca7b3 100644 --- a/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h +++ b/src/server/game/Scripting/ScriptDefines/AllBattlegroundScript.h @@ -41,6 +41,7 @@ enum AllBattlegroundHook ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_END, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_DESTROY, ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_CREATE, + ALLBATTLEGROUNDHOOK_ON_BATTLEGROUND_SETUP, ALLBATTLEGROUNDHOOK_CAN_ADD_GROUP_TO_MATCHING_POOL, ALLBATTLEGROUNDHOOK_GET_PLAYER_MATCHMAKING_RATING, ALLBATTLEGROUNDHOOK_END @@ -139,6 +140,13 @@ public: */ virtual void OnBattlegroundCreate(Battleground* /*bg*/) { } + /** + * @brief This hook runs after the Battleground setup is completed (map is created and initial objects are spawned) + * + * @param bg Contains information about the Battleground + */ + virtual void OnBattlegroundSetup(Battleground* /*bg*/) { } + /** * @brief This hook runs before adding a group to the battleground matching pool * diff --git a/src/server/game/Scripting/ScriptMgr.h b/src/server/game/Scripting/ScriptMgr.h index 45aebb963..35eb7d642 100644 --- a/src/server/game/Scripting/ScriptMgr.h +++ b/src/server/game/Scripting/ScriptMgr.h @@ -634,6 +634,7 @@ public: /* BGScript */ void OnBattlegroundEnd(Battleground* bg, TeamId winnerTeamId); void OnBattlegroundDestroy(Battleground* bg); void OnBattlegroundCreate(Battleground* bg); + void OnBattlegroundSetup(Battleground* bg); bool CanAddGroupToMatchingPool(BattlegroundQueue* queue, GroupQueueInfo* group, uint32 poolPlayerCount, Battleground* bg, BattlegroundBracketId bracketId); bool GetPlayerMatchmakingRating(ObjectGuid playerGuid, BattlegroundTypeId bgTypeId, float& outRating); diff --git a/src/server/game/Spells/Auras/SpellAuraEffects.cpp b/src/server/game/Spells/Auras/SpellAuraEffects.cpp index 9c4fa8fe8..2ea95ae4b 100644 --- a/src/server/game/Spells/Auras/SpellAuraEffects.cpp +++ b/src/server/game/Spells/Auras/SpellAuraEffects.cpp @@ -2989,7 +2989,7 @@ void AuraEffect::HandleFeignDeath(AuraApplication const* aurApp, uint8 mode, boo for (auto& pair : target->GetThreatMgr().GetThreatenedByMeList()) pair.second->ScaleThreat(0.0f); - if (target->GetMap()->IsDungeon()) // feign death does not remove combat in dungeons + if (target->GetInstanceScript() && target->GetInstanceScript()->IsEncounterInProgress()) { target->AttackStop(); if (Player* targetPlayer = target->ToPlayer()) diff --git a/src/server/game/Spells/Auras/SpellAuras.cpp b/src/server/game/Spells/Auras/SpellAuras.cpp index 5f2b887f0..d539ea79f 100644 --- a/src/server/game/Spells/Auras/SpellAuras.cpp +++ b/src/server/game/Spells/Auras/SpellAuras.cpp @@ -201,6 +201,11 @@ void AuraApplication::BuildUpdatePacket(ByteBuffer& data, bool remove) const uint32 flags = _flags; if (aura->GetMaxDuration() > 0 && !aura->GetSpellInfo()->HasAttribute(SPELL_ATTR5_DO_NOT_DISPLAY_DURATION)) flags |= AFLAG_DURATION; + + // paladin auras are positive for self-cast only + if (!IsSelfcasted() && aura->GetSpellInfo()->GetSpellSpecific() == SPELL_SPECIFIC_AURA) + flags &= ~AFLAG_POSITIVE; + data << uint8(flags); data << uint8(aura->GetCasterLevel()); // send stack amount for aura which could be stacked (never 0 - causes incorrect display) or charges diff --git a/src/server/game/Spells/Spell.cpp b/src/server/game/Spells/Spell.cpp index a5f8bcc1d..4609a074d 100644 --- a/src/server/game/Spells/Spell.cpp +++ b/src/server/game/Spells/Spell.cpp @@ -3951,6 +3951,44 @@ void Spell::_cast(bool skipCheck) } } + // CAST -> HIT -> FINISH ordering: fire CAST before handle_immediate so an aura + // applied during HIT (e.g. Arcane Potency from Clearcasting) isn't consumed by + // the same cast. Triggered spells skip this so periodic ticks (Blizzard etc.) + // don't burn cast-charge buffs. + if (m_originalCaster && !IsTriggered()) + { + uint32 procAttacker = m_procAttacker; + if (!procAttacker) + { + bool IsPositive = m_spellInfo->IsPositive(); + if (m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MAGIC) + { + procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG; + } + else + { + procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG; + } + } + + uint32 hitMask = PROC_HIT_NORMAL; + + for (std::list<TargetInfo>::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit) + { + if (ihit->missCondition != SPELL_MISS_NONE) + continue; + + if (!ihit->crit) + continue; + + hitMask |= PROC_HIT_CRITICAL; + break; + } + + Unit::ProcSkillsAndAuras(m_originalCaster, nullptr, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo, + m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST); + } + // Okay, everything is prepared. Now we need to distinguish between immediate and evented delayed spells if ((m_spellInfo->Speed > 0.0f && !m_spellInfo->IsChanneled())/* xinef: we dont need this || m_spellInfo->Id == 14157*/) { @@ -4008,46 +4046,6 @@ void Spell::_cast(bool skipCheck) if (modOwner) modOwner->SetSpellModTakingSpell(this, false); - // Handle procs on cast - only for non-triggered spells - // Triggered spells (from auras, items, etc.) should not fire CAST phase procs - // as they are not player-initiated casts. This prevents issues like Arcane Potency - // charges being consumed by periodic damage effects (e.g., Blizzard ticks). - // Must be called AFTER handle_immediate() so spell mods (like Missile Barrage's - // duration reduction) are applied before the aura is consumed by the proc. - if (m_originalCaster && !IsTriggered()) - { - uint32 procAttacker = m_procAttacker; - if (!procAttacker) - { - bool IsPositive = m_spellInfo->IsPositive(); - if (m_spellInfo->DmgClass == SPELL_DAMAGE_CLASS_MAGIC) - { - procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_MAGIC_DMG_CLASS_NEG; - } - else - { - procAttacker = IsPositive ? PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_POS : PROC_FLAG_DONE_SPELL_NONE_DMG_CLASS_NEG; - } - } - - uint32 hitMask = PROC_HIT_NORMAL; - - for (std::list<TargetInfo>::iterator ihit = m_UniqueTargetInfo.begin(); ihit != m_UniqueTargetInfo.end(); ++ihit) - { - if (ihit->missCondition != SPELL_MISS_NONE) - continue; - - if (!ihit->crit) - continue; - - hitMask |= PROC_HIT_CRITICAL; - break; - } - - Unit::ProcSkillsAndAuras(m_originalCaster, nullptr, procAttacker, PROC_FLAG_NONE, hitMask, 1, BASE_ATTACK, m_spellInfo, m_triggeredByAuraSpell.spellInfo, - m_triggeredByAuraSpell.effectIndex, this, nullptr, nullptr, PROC_SPELL_PHASE_CAST); - } - if (std::vector<int32> const* spell_triggered = sSpellMgr->GetSpellLinked(m_spellInfo->Id)) { for (int32 id : *spell_triggered) diff --git a/src/server/game/Spells/SpellInfoCorrections.cpp b/src/server/game/Spells/SpellInfoCorrections.cpp index f5e22058c..e155138f6 100644 --- a/src/server/game/Spells/SpellInfoCorrections.cpp +++ b/src/server/game/Spells/SpellInfoCorrections.cpp @@ -2057,12 +2057,6 @@ void SpellMgr::LoadSpellInfoCorrections() spellInfo->AttributesEx6 |= SPELL_ATTR6_IGNORE_PHASE_SHIFT; }); - // Shadow Nova - ApplySpellFix({ 62714, 65209 }, [](SpellInfo* spellInfo) - { - spellInfo->AttributesEx3 |= SPELL_ATTR3_ALWAYS_HIT; - }); - // Cosmic Smash (Algalon the Observer) ApplySpellFix({ 62293 }, [](SpellInfo* spellInfo) { @@ -5345,6 +5339,13 @@ void SpellMgr::LoadSpellInfoCorrections() vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(1520)); // Wyrmrest Vanquisher vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + // Salvaged Siege Engine (Ulduar) extra passenger seats: the only salvaged vehicle seats + // without this flag, leaving those passengers targetable by Flame Leviathan's AoE + vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(4026)); + vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(4027)); + vse->m_flags |= VEHICLE_SEAT_FLAG_PASSENGER_NOT_SELECTABLE; + // pussywizard: fix z offset for some vehicles: vse = const_cast<VehicleSeatEntry*>(sVehicleSeatStore.LookupEntry(6206)); // Marrowgar - Bone Spike vse->m_attachmentOffsetZ = 4.0f; diff --git a/src/server/game/Spells/SpellScript.h b/src/server/game/Spells/SpellScript.h index 0ca94edb2..e13ce56cb 100644 --- a/src/server/game/Spells/SpellScript.h +++ b/src/server/game/Spells/SpellScript.h @@ -823,12 +823,14 @@ public: #define AuraEffectSplitFn(F, I) EffectSplitFunction(&F, I) // executed when aura checks if it can proc + // do not use this hook for triggering spellcasts/removing auras etc - runs mid proc-iteration and may be unsafe // example: DoCheckProc += AuraCheckProcFn(class::function); // where function is: bool function (ProcEventInfo& eventInfo); HookList<CheckProcHandler> DoCheckProc; #define AuraCheckProcFn(F) CheckProcHandlerFunction(&F) // executed when aura effect checks if it can proc the aura + // do not use this hook for triggering spellcasts/removing auras etc - runs mid proc-iteration and may be unsafe // example: DoCheckEffectProc += AuraCheckEffectProcFn(class::function, EffectIndexSpecifier, EffectAuraNameSpecifier); // where function is: bool function (AuraEffect const* aurEff, ProcEventInfo& eventInfo); HookList<CheckEffectProcHandler> DoCheckEffectProc; diff --git a/src/server/game/Texts/CreatureTextMgr.cpp b/src/server/game/Texts/CreatureTextMgr.cpp index 501bea861..7ecb8153f 100644 --- a/src/server/game/Texts/CreatureTextMgr.cpp +++ b/src/server/game/Texts/CreatureTextMgr.cpp @@ -21,9 +21,11 @@ #include "Chat.h" #include "Common.h" #include "DatabaseEnv.h" +#include "GameTime.h" #include "GridNotifiers.h" #include "MiscPackets.h" #include "ObjectMgr.h" +#include "Random.h" class CreatureTextBuilder { @@ -196,6 +198,62 @@ void CreatureTextMgr::LoadCreatureTextLocales() LOG_INFO("server.loading", " "); } +void CreatureTextMgr::LoadCreatureTextOptions() +{ + uint32 oldMSTime = getMSTime(); + + mTextOptionsMap.clear(); + + WorldDatabasePreparedStatement* stmt = WorldDatabase.GetPreparedStatement(WORLD_SEL_CREATURE_TEXT_OPTIONS); + PreparedQueryResult result = WorldDatabase.Query(stmt); + + if (!result) + { + LOG_WARN("server.loading", ">> Loaded 0 creature text options. DB table `creature_text_options` is empty."); + LOG_INFO("server.loading", " "); + return; + } + + uint32 count = 0; + + do + { + Field* fields = result->Fetch(); + uint32 creatureId = fields[0].Get<uint32>(); + uint8 groupId = fields[1].Get<uint8>(); + + CreatureTextOptions options; + options.cooldown = fields[2].Get<uint32>(); + options.triggerChance = fields[3].Get<uint8>(); + options.playerOnly = fields[4].Get<bool>(); + + if (options.triggerChance > 100) + { + LOG_ERROR("sql.sql", "CreatureTextMgr: Entry {}, Group {} in table `creature_text_options` has TriggerChance {} > 100. Setting to 100.", creatureId, groupId, options.triggerChance); + options.triggerChance = 100; + } + + mTextOptionsMap[creatureId][groupId] = options; + ++count; + } while (result->NextRow()); + + LOG_INFO("server.loading", ">> Loaded {} Creature Text Options in {} ms", count, GetMSTimeDiffToNow(oldMSTime)); + LOG_INFO("server.loading", " "); +} + +CreatureTextOptions const* CreatureTextMgr::GetTextOptions(uint32 entry, uint8 group) const +{ + auto entryItr = mTextOptionsMap.find(entry); + if (entryItr == mTextOptionsMap.end()) + return nullptr; + + auto groupItr = entryItr->second.find(group); + if (groupItr == entryItr->second.end()) + return nullptr; + + return &groupItr->second; +} + uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject const* target /*= nullptr*/, ChatMsg msgType /*= CHAT_MSG_ADDON*/, Language language /*= LANG_ADDON*/, CreatureTextRange range /*= TEXT_RANGE_NORMAL*/, uint32 sound /*= 0*/, TeamId teamId /*= TEAM_NEUTRAL*/, bool gmOnly /*= false*/, Player* srcPlr /*= nullptr*/) { if (!source) @@ -218,6 +276,21 @@ uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject } CreatureTextGroup const& textGroupContainer = itr->second; //has all texts in the group + + // Check creature_text_options for this group + CreatureTextOptions const* options = GetTextOptions(source->GetEntry(), textGroup); + if (options) + { + if (options->playerOnly && (!target || !target->IsPlayer())) + return 0; + + if (options->triggerChance < 100 && !roll_chance_i(options->triggerChance)) + return 0; + + if (options->cooldown > 0 && source->IsTextOnCooldown(textGroup)) + return 0; + } + CreatureTextRepeatIds repeatGroup = source->GetTextRepeatGroup(textGroup);//has all textIDs from the group that were already said CreatureTextGroup tempGroup;//will use this to talk after sorting repeatGroup @@ -265,6 +338,11 @@ uint32 CreatureTextMgr::SendChat(Creature* source, uint8 textGroup, WorldObject } source->SetTextRepeatId(textGroup, iter->id); + + // Set cooldown if creature_text_options specifies one + if (options && options->cooldown > 0) + source->SetTextCooldown(textGroup, options->cooldown); + return iter->duration; } diff --git a/src/server/game/Texts/CreatureTextMgr.h b/src/server/game/Texts/CreatureTextMgr.h index 37a393496..22ca7dbee 100644 --- a/src/server/game/Texts/CreatureTextMgr.h +++ b/src/server/game/Texts/CreatureTextMgr.h @@ -71,10 +71,20 @@ struct CreatureTextId uint32 textId; }; +struct CreatureTextOptions +{ + uint32 cooldown; // ms before this group can fire again + uint8 triggerChance; // 0-100 chance to fire at all + bool playerOnly; // only fire if target is a player +}; + typedef std::vector<CreatureTextEntry> CreatureTextGroup; // texts in a group typedef std::unordered_map<uint8, CreatureTextGroup> CreatureTextHolder; // groups for a creature by groupid typedef std::unordered_map<uint32, CreatureTextHolder> CreatureTextMap; // all creatures by entry +typedef std::unordered_map<uint8, CreatureTextOptions> CreatureTextOptionsMap; // options per group +typedef std::unordered_map<uint32, CreatureTextOptionsMap> CreatureTextOptionsContainer; // per creature entry + typedef std::map<CreatureTextId, CreatureTextLocale> LocaleCreatureTextMap; class CreatureTextMgr @@ -87,7 +97,9 @@ public: ~CreatureTextMgr() { } void LoadCreatureTexts(); void LoadCreatureTextLocales(); + void LoadCreatureTextOptions(); CreatureTextMap const& GetTextMap() const { return mTextMap; } + CreatureTextOptions const* GetTextOptions(uint32 entry, uint8 group) const; void SendSound(Creature* source, uint32 sound, ChatMsg msgType, WorldObject const* target, CreatureTextRange range, TeamId teamId, bool gmOnly); void SendEmote(Unit* source, uint32 emote); @@ -105,6 +117,7 @@ private: CreatureTextMap mTextMap; LocaleCreatureTextMap mLocaleTextMap; + CreatureTextOptionsContainer mTextOptionsMap; }; #define sCreatureTextMgr CreatureTextMgr::instance() diff --git a/src/server/game/World/World.cpp b/src/server/game/World/World.cpp index 7d22bee97..07403604e 100644 --- a/src/server/game/World/World.cpp +++ b/src/server/game/World/World.cpp @@ -870,6 +870,9 @@ void World::SetInitialWorldSettings() LOG_INFO("server.loading", "Loading Creature Texts..."); sCreatureTextMgr->LoadCreatureTexts(); + LOG_INFO("server.loading", "Loading Creature Text Options..."); + sCreatureTextMgr->LoadCreatureTextOptions(); + LOG_INFO("server.loading", "Loading Creature Text Locales..."); sCreatureTextMgr->LoadCreatureTextLocales(); @@ -1286,6 +1289,12 @@ void World::Update(uint32 diff) stmt->SetData(2, realm.Id.Realm); stmt->SetData(3, uint32(GameTime::GetStartTime().count())); LoginDatabase.Execute(stmt); + + // Re-assert this realm as online in case the offline flag was set externally (e.g. an authserver restart). + LoginDatabasePreparedStatement* onlineStmt = LoginDatabase.GetPreparedStatement(LOGIN_UPD_REALM_ONLINE); + onlineStmt->SetData(0, uint8(REALM_FLAG_OFFLINE)); + onlineStmt->SetData(1, realm.Id.Realm); + LoginDatabase.Execute(onlineStmt); } ///- Process Game events when necessary diff --git a/src/server/game/World/WorldConfig.cpp b/src/server/game/World/WorldConfig.cpp index 5233d6e48..26ca792c0 100644 --- a/src/server/game/World/WorldConfig.cpp +++ b/src/server/game/World/WorldConfig.cpp @@ -426,6 +426,12 @@ void WorldConfig::BuildConfigCache() SetConfigValue<uint32>(CONFIG_BATTLEGROUND_PREP_TIME, "Battleground.PrepTime", 120); SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, "Battleground.Override.LowLevels.MinPlayers", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, "Battleground.Override.LowLevels.MinPlayers.AV", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, "Battleground.Override.LowLevels.MinPlayers.WS", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, "Battleground.Override.LowLevels.MinPlayers.AB", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, "Battleground.Override.LowLevels.MinPlayers.EY", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, "Battleground.Override.LowLevels.MinPlayers.SA", 0); + SetConfigValue<uint32>(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, "Battleground.Override.LowLevels.MinPlayers.IC", 0); SetConfigValue<bool>(CONFIG_BATTLEGROUND_DISABLE_QUEST_SHARE_IN_BG, "Battleground.DisableQuestShareInBG", false); SetConfigValue<bool>(CONFIG_BATTLEGROUND_DISABLE_READY_CHECK_IN_BG, "Battleground.DisableReadyCheckInBG", false); SetConfigValue<bool>(CONFIG_BATTLEGROUND_CAST_DESERTER, "Battleground.CastDeserter", true); diff --git a/src/server/game/World/WorldConfig.h b/src/server/game/World/WorldConfig.h index 7e3ef1319..e869d8b3b 100644 --- a/src/server/game/World/WorldConfig.h +++ b/src/server/game/World/WorldConfig.h @@ -264,6 +264,12 @@ enum ServerConfigs CONFIG_DISABLE_BREATHING, CONFIG_BATTLEGROUND_PREP_TIME, CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, + CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_SPAM_DELAY, CONFIG_BATTLEGROUND_QUEUE_ANNOUNCER_TIMER, CONFIG_BATTLEGROUND_PREMATURE_FINISH_TIMER, diff --git a/src/server/scripts/Commands/cs_autobroadcast.cpp b/src/server/scripts/Commands/cs_autobroadcast.cpp index 27b5a8c20..3173b3b4c 100644 --- a/src/server/scripts/Commands/cs_autobroadcast.cpp +++ b/src/server/scripts/Commands/cs_autobroadcast.cpp @@ -20,6 +20,7 @@ #include "CommandScript.h" #include "Config.h" #include "Language.h" +#include "RBAC.h" using namespace Acore::ChatCommands; @@ -32,10 +33,10 @@ public: { static ChatCommandTable autobroadcastCommandTable = { - { "list", HandleAutobroadcastListCommand, SEC_GAMEMASTER, Console::Yes }, - { "add", HandleAutobroadcastAddCommand, SEC_ADMINISTRATOR, Console::Yes }, - { "locale", HandleAutobroadcastLocaleCommand, SEC_ADMINISTRATOR, Console::Yes }, - { "remove", HandleAutobroadcastRemoveCommand, SEC_ADMINISTRATOR, Console::Yes } + { "list", HandleAutobroadcastListCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_LIST, Console::Yes }, + { "add", HandleAutobroadcastAddCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_ADD, Console::Yes }, + { "locale", HandleAutobroadcastLocaleCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_LOCALE, Console::Yes }, + { "remove", HandleAutobroadcastRemoveCommand, rbac::RBAC_PERM_COMMAND_AUTOBROADCAST_REMOVE, Console::Yes } }; static ChatCommandTable commandTable = diff --git a/src/server/scripts/Commands/cs_go.cpp b/src/server/scripts/Commands/cs_go.cpp index 03e657aa1..dd7d0dcbe 100644 --- a/src/server/scripts/Commands/cs_go.cpp +++ b/src/server/scripts/Commands/cs_go.cpp @@ -223,7 +223,7 @@ public: std::string str = name.data(); // Making subtractions to the last character does not with in string_view WorldDatabase.EscapeString(str); - QueryResult result = WorldDatabase.Query("SELECT entry FROM creature_template WHERE name = \"{}\" LIMIT 1", str); + QueryResult result = WorldDatabase.Query("SELECT entry FROM creature_template WHERE name = '{}' LIMIT 1", str); if (!result) { handler->SendErrorMessage(LANG_COMMAND_GOCREATNOTFOUND); diff --git a/src/server/scripts/Commands/cs_mail.cpp b/src/server/scripts/Commands/cs_mail.cpp index 3fcf3a2c8..d8f09f85b 100644 --- a/src/server/scripts/Commands/cs_mail.cpp +++ b/src/server/scripts/Commands/cs_mail.cpp @@ -26,6 +26,7 @@ #include "ObjectAccessor.h" #include "ObjectMgr.h" #include "Player.h" +#include "RBAC.h" #include "ScriptMgr.h" #include "Timer.h" @@ -40,8 +41,8 @@ public: { static ChatCommandTable mailCommandTable = { - { "list", HandleMailListCommand, SEC_GAMEMASTER, Console::Yes }, - { "return", HandleMailReturnCommand, SEC_GAMEMASTER, Console::Yes } + { "list", HandleMailListCommand, rbac::RBAC_PERM_COMMAND_MAIL_LIST, Console::Yes }, + { "return", HandleMailReturnCommand, rbac::RBAC_PERM_COMMAND_MAIL_RETURN, Console::Yes } }; static ChatCommandTable commandTable = diff --git a/src/server/scripts/Commands/cs_npc.cpp b/src/server/scripts/Commands/cs_npc.cpp index 28da07703..d44fcfb81 100644 --- a/src/server/scripts/Commands/cs_npc.cpp +++ b/src/server/scripts/Commands/cs_npc.cpp @@ -200,11 +200,11 @@ public: { "add", npcAddCommandTable }, { "delete", npcDeleteCommandTable }, { "follow", npcFollowCommandTable }, - { "load", HandleNpcLoadCommand, SEC_ADMINISTRATOR, Console::Yes }, + { "load", HandleNpcLoadCommand, rbac::RBAC_PERM_COMMAND_NPC_LOAD, Console::Yes }, { "set", npcSetCommandTable }, - { "showloot", HandleNpcShowLootCommand, rbac::RBAC_PERM_COMMAND_NPC_SHOWLOOT, Console::No }, - { "spawngroup", HandleNpcSpawnGroupCommand, SEC_ADMINISTRATOR, Console::No }, - { "despawngroup", HandleNpcDespawnGroupCommand, SEC_ADMINISTRATOR, Console::No } + { "showloot", HandleNpcShowLootCommand, rbac::RBAC_PERM_COMMAND_NPC_SHOWLOOT, Console::No }, + { "spawngroup", HandleNpcSpawnGroupCommand, rbac::RBAC_PERM_COMMAND_NPC_SPAWNGROUP, Console::No }, + { "despawngroup", HandleNpcDespawnGroupCommand, rbac::RBAC_PERM_COMMAND_NPC_DESPAWNGROUP, Console::No } }; static ChatCommandTable commandTable = { diff --git a/src/server/scripts/Commands/cs_pool.cpp b/src/server/scripts/Commands/cs_pool.cpp index 8f56e7063..44661a23a 100644 --- a/src/server/scripts/Commands/cs_pool.cpp +++ b/src/server/scripts/Commands/cs_pool.cpp @@ -25,6 +25,7 @@ #include "ObjectMgr.h" #include "Player.h" #include "PoolMgr.h" +#include "RBAC.h" using namespace Acore::ChatCommands; @@ -37,8 +38,8 @@ public: { static ChatCommandTable poolCommandTable = { - { "info", HandlePoolInfoCommand, SEC_GAMEMASTER, Console::Yes }, - { "lookup", HandlePoolLookupCommand, SEC_GAMEMASTER, Console::No }, + { "info", HandlePoolInfoCommand, rbac::RBAC_PERM_COMMAND_POOL_INFO, Console::Yes }, + { "lookup", HandlePoolLookupCommand, rbac::RBAC_PERM_COMMAND_POOL_LOOKUP, Console::No }, }; static ChatCommandTable commandTable = { diff --git a/src/server/scripts/Commands/cs_spellinfo.cpp b/src/server/scripts/Commands/cs_spellinfo.cpp index f0a1014d9..5b2523ce3 100644 --- a/src/server/scripts/Commands/cs_spellinfo.cpp +++ b/src/server/scripts/Commands/cs_spellinfo.cpp @@ -19,6 +19,7 @@ #include "CommandScript.h" #include "DBCStructure.h" #include "Language.h" +#include "RBAC.h" #include "SmartEnum.h" #include "SpellAuraDefines.h" #include "SpellInfo.h" @@ -35,10 +36,10 @@ public: { static ChatCommandTable spellinfoCommandTable = { - { "attributes", HandleSpellInfoAttributesCommand, SEC_GAMEMASTER, Console::Yes }, - { "effects", HandleSpellInfoEffectsCommand, SEC_GAMEMASTER, Console::Yes }, - { "targets", HandleSpellInfoTargetsCommand, SEC_GAMEMASTER, Console::Yes }, - { "all", HandleSpellInfoAllCommand, SEC_GAMEMASTER, Console::Yes } + { "attributes", HandleSpellInfoAttributesCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_ATTRIBUTES, Console::Yes }, + { "effects", HandleSpellInfoEffectsCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_EFFECTS, Console::Yes }, + { "targets", HandleSpellInfoTargetsCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_TARGETS, Console::Yes }, + { "all", HandleSpellInfoAllCommand, rbac::RBAC_PERM_COMMAND_SPELLINFO_ALL, Console::Yes } }; static ChatCommandTable commandTable = diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp index bb563de0a..ea9267357 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_majordomo_executus.cpp @@ -219,8 +219,7 @@ struct boss_majordomo : public BossAI void KilledUnit(Unit* victim) override { - if (roll_chance_i(25) && victim->IsPlayer()) - Talk(SAY_SLAY); + Talk(SAY_SLAY, victim); } void JustEngagedWith(Unit* /*attacker*/) override diff --git a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp index 6538947e3..73eab62c6 100644 --- a/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp +++ b/src/server/scripts/EasternKingdoms/BlackrockMountain/MoltenCore/boss_ragnaros.cpp @@ -209,8 +209,7 @@ struct boss_ragnaros : public BossAI void KilledUnit(Unit* victim) override { - if (roll_chance_i(25) && victim->IsPlayer()) - Talk(SAY_KILL); + Talk(SAY_KILL, victim); } void AttackStart(Unit* target) override diff --git a/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp b/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp index 168037e7a..97bd59505 100644 --- a/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp +++ b/src/server/scripts/EasternKingdoms/Karazhan/boss_moroes.cpp @@ -117,7 +117,6 @@ struct boss_moroes : public BossAI { BossAI::Reset(); DoCastSelf(SPELL_DUAL_WIELD, true); - _recentlySpoken = false; _vanished = false; me->SetImmuneToAll(false); me->SetReactState(REACT_AGGRESSIVE); @@ -170,15 +169,7 @@ struct boss_moroes : public BossAI void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer()) - { - Talk(SAY_KILL); - _recentlySpoken = true; - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override @@ -243,7 +234,6 @@ struct boss_moroes : public BossAI private: EventMap _events2; uint8 _activeGuests; - bool _recentlySpoken; bool _vanished; }; diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp index 0e55c9d1d..5e8570136 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_brutallus.cpp @@ -104,8 +104,7 @@ struct boss_brutallus : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(YELL_KILL); + Talk(YELL_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp index 6aee91f41..d3ce0fa71 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_felmyst.cpp @@ -186,8 +186,7 @@ struct boss_felmyst : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(YELL_KILL); + Talk(YELL_KILL, victim); } void SpellHitTarget(Unit* target, const SpellInfo* spell) override diff --git a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp index c23569e4e..1249a90d7 100644 --- a/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp +++ b/src/server/scripts/EasternKingdoms/SunwellPlateau/boss_kalecgos.cpp @@ -260,8 +260,7 @@ struct boss_kalecgos : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - Talk(SAY_EVIL_SLAY); + Talk(SAY_EVIL_SLAY, victim); } private: diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp index 0ac933683..0e0e923ef 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_anetheron.cpp @@ -47,10 +47,7 @@ enum Texts struct boss_anetheron : public BossAI { public: - boss_anetheron(Creature* creature) : BossAI(creature, DATA_ANETHERON) - { - _recentlySpoken = false; - } + boss_anetheron(Creature* creature) : BossAI(creature, DATA_ANETHERON) { } void JustEngagedWith(Unit * who) override { @@ -114,16 +111,8 @@ public: void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -132,8 +121,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_anetheron_sleep : public SpellScript diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp index 22024068e..abbd9374d 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_azgalor.cpp @@ -44,10 +44,7 @@ enum Texts struct boss_azgalor : public BossAI { public: - boss_azgalor(Creature* creature) : BossAI(creature, DATA_AZGALOR) - { - _recentlySpoken = false; - } + boss_azgalor(Creature* creature) : BossAI(creature, DATA_AZGALOR) { } void JustEngagedWith(Unit * who) override { @@ -85,18 +82,10 @@ public: me->GetMotionMaster()->MoveWaypoint(HORDE_BOSS_PATH, false); } - void KilledUnit(Unit * victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -111,8 +100,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_azgalor_doom : public SpellScript diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp index 889ccc178..42cfb545d 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_kazrogal.cpp @@ -52,7 +52,6 @@ public: void Reset() override { - _recentlySpoken = false; _markCounter = 0; BossAI::Reset(); } @@ -104,18 +103,10 @@ public: me->GetMotionMaster()->MoveWaypoint(HORDE_BOSS_PATH, false); } - void KilledUnit(Unit * victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit * killer) override @@ -125,7 +116,6 @@ public: } private: - bool _recentlySpoken; uint8 _markCounter; }; diff --git a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp index 2bdf44abf..9e2a8a26c 100644 --- a/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp +++ b/src/server/scripts/Kalimdor/CavernsOfTime/BattleForMountHyjal/boss_rage_winterchill.cpp @@ -47,10 +47,7 @@ enum Misc struct boss_rage_winterchill : public BossAI { public: - boss_rage_winterchill(Creature* creature) : BossAI(creature, DATA_WINTERCHILL) - { - _recentlySpoken = false; - } + boss_rage_winterchill(Creature* creature) : BossAI(creature, DATA_WINTERCHILL) { } void JustEngagedWith(Unit* who) override { @@ -117,16 +114,8 @@ public: void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer() && me->IsAlive()) - { - Talk(SAY_ONSLAY); - _recentlySpoken = true; - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + if (me->IsAlive()) + Talk(SAY_ONSLAY, victim); } void JustDied(Unit* killer) override @@ -135,8 +124,6 @@ public: BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; void AddSC_boss_rage_winterchill() diff --git a/src/server/scripts/Kalimdor/zone_thousand_needles.cpp b/src/server/scripts/Kalimdor/zone_thousand_needles.cpp index 9c60689c5..c76108dac 100644 --- a/src/server/scripts/Kalimdor/zone_thousand_needles.cpp +++ b/src/server/scripts/Kalimdor/zone_thousand_needles.cpp @@ -202,13 +202,14 @@ public: # npc_plucky ######*/ -#define GOSSIP_P "Please tell me the Phrase.." - enum Plucky { QUEST_SCOOP = 1950, SPELL_PLUCKY_HUMAN = 9192, - SPELL_PLUCKY_CHICKEN = 9220 + SPELL_PLUCKY_CHICKEN = 9220, + + GOSSIP_MENU_PLUCKY = 6626, + GOSSIP_OPTION_PHRASE = 0 }; class npc_plucky : public CreatureScript @@ -232,7 +233,7 @@ public: bool OnGossipHello(Player* player, Creature* creature) override { if (player->GetQuestStatus(QUEST_SCOOP) == QUEST_STATUS_INCOMPLETE) - AddGossipItemFor(player, GOSSIP_ICON_CHAT, GOSSIP_P, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); + AddGossipItemFor(player, GOSSIP_MENU_PLUCKY, GOSSIP_OPTION_PHRASE, GOSSIP_SENDER_MAIN, GOSSIP_ACTION_INFO_DEF + 1); SendGossipMenuFor(player, 738, creature->GetGUID()); diff --git a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp index 640bb9c98..4d06c331d 100644 --- a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp +++ b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_anubarak.cpp @@ -69,8 +69,7 @@ enum Events EVENT_SUMMON_GUARDIAN = 8, EVENT_SUMMON_VENOMANCER = 9, EVENT_SUMMON_DARTER = 10, - EVENT_SUMMON_ASSASSINS = 11, - EVENT_KILL_TALK = 12 + EVENT_SUMMON_ASSASSINS = 11 }; enum CreatureIds @@ -262,11 +261,7 @@ struct boss_anub_arak : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void SummonedCreatureDies(Creature* summon, Unit* /*killer*/) override diff --git a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp index be33fad5a..90e24370f 100644 --- a/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp +++ b/src/server/scripts/Northrend/AzjolNerub/AzjolNerub/boss_krikthir_the_gatewatcher.cpp @@ -58,7 +58,6 @@ public: boss_krik_thirAI(Creature* creature) : BossAI(creature, DATA_KRIKTHIR) { _initTalk = false; - _canTalk = true; _minionInCombat = false; } @@ -79,7 +78,6 @@ public: }); }); - _canTalk = true; _minionInCombat = false; _firstCall = true; _minionsEngaged = 0; @@ -236,14 +234,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - _canTalk = false; - Talk(SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _canTalk = true; - }, 6s); - } + Talk(SAY_SLAY); } void SummonedCreatureDies(Creature* summon, Unit*) override @@ -253,7 +244,6 @@ public: private: bool _initTalk; - bool _canTalk; bool _minionInCombat; uint8 _minionsEngaged; bool _firstCall; diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp index 5a8cc49b1..802aced27 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_baltharus_the_warborn.cpp @@ -66,7 +66,6 @@ enum Events EVENT_CHECK_HEALTH1 = 4, EVENT_CHECK_HEALTH2 = 5, EVENT_CHECK_HEALTH3 = 6, - EVENT_KILL_TALK = 7, EVENT_SUMMON_CLONE = 8, EVENT_XERESTRASZA_EVENT_0 = 1, @@ -191,11 +190,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp index faf314e6f..fe0ea6de9 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_general_zarithrian.cpp @@ -47,7 +47,6 @@ enum Misc EVENT_INTIDMDATING_ROAR = 2, EVENT_SUMMON_ADDS1 = 3, EVENT_SUMMON_ADDS2 = 4, - EVENT_KILL_TALK = 5, // Onyx Flamecaller EVENT_BLAST_NOVA = 6, @@ -136,11 +135,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp index 5a53b1bdd..d289520f6 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_halion.cpp @@ -131,7 +131,6 @@ enum Events EVENT_FIERY_COMBUSTION = 5, EVENT_TAIL_LASH = 6, EVENT_CHECK_HEALTH = 7, - EVENT_KILL_TALK = 8, EVENT_TRIGGER_BERSERK = 9, EVENT_HALION_VISIBILITY = 10, @@ -310,11 +309,7 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override @@ -477,11 +472,7 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !_events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - _events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp index 2bebd1502..7792dfeb1 100644 --- a/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp +++ b/src/server/scripts/Northrend/ChamberOfAspects/RubySanctum/boss_saviana_ragefire.cpp @@ -46,8 +46,7 @@ enum Events EVENT_CONFLAGRATION = 4, EVENT_LAND_GROUND = 5, EVENT_AIR_MOVEMENT = 6, - EVENT_LAND_BACK = 7, - EVENT_KILL_TALK = 8 + EVENT_LAND_BACK = 7 }; enum Misc @@ -121,11 +120,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp index 36f8b555c..dd1842cd6 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_novos.cpp @@ -56,8 +56,6 @@ enum Misc NPC_CRYSTAL_HANDLER = 26627, NPC_SUMMON_CRYSTAL_HANDLER_TARGET = 27583, - EVENT_KILL_TALK = 1, - ROOM_RIGHT = 0, ROOM_LEFT = 1, ROOM_STAIRS = 2 @@ -206,11 +204,7 @@ struct boss_novos : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp index 1a240ce15..43fc5e74d 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_tharon_ja.cpp @@ -60,8 +60,7 @@ enum Misc EVENT_SPELL_LIGHTNING_BREATH = 5, EVENT_SPELL_POISON_CLOUD = 6, EVENT_TURN_FLESH_REAL = 9, - EVENT_TURN_BONES_REAL = 10, - EVENT_KILL_TALK = 11 + EVENT_TURN_BONES_REAL = 10 }; class boss_tharon_ja : public CreatureScript @@ -106,11 +105,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void DoAction(int32 param) override diff --git a/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp b/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp index 8380f1caa..6d71ac5cd 100644 --- a/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp +++ b/src/server/scripts/Northrend/DraktharonKeep/boss_trollgore.cpp @@ -55,8 +55,7 @@ enum Events EVENT_SPELL_CRUSH = 2, EVENT_SPELL_CONSUME = 3, EVENT_SPELL_CORPSE_EXPLODE = 4, - EVENT_SPAWN_INVADERS = 5, - EVENT_KILL_TALK = 6 + EVENT_SPAWN_INVADERS = 5 }; class boss_trollgore : public CreatureScript @@ -102,11 +101,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp b/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp index f44da0ccb..8f4af9474 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_gal_darah.cpp @@ -47,11 +47,6 @@ enum Yells SAY_IMPALE = 5 }; -enum Events -{ - EVENT_KILL_TALK = 1 -}; - struct boss_gal_darah : public BossAI { boss_gal_darah(Creature* creature) : BossAI(creature, DATA_GAL_DARAH) { } @@ -160,11 +155,7 @@ struct boss_gal_darah : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } private: diff --git a/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp b/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp index 2d405fe5d..d369dc0f6 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_moorabi.cpp @@ -55,7 +55,6 @@ enum Events EVENT_NUMBLING_SHOUT = 2, EVENT_DETERMINED_STAB = 3, EVENT_TRANSFORMATION = 4, - EVENT_KILL_TALK = 5, EVENT_PHANTOM = 10 }; @@ -125,11 +124,7 @@ public: void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void UpdateAI(uint32 diff) override diff --git a/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp b/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp index 2d8b6ad56..2d3eaf0b0 100644 --- a/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp +++ b/src/server/scripts/Northrend/Gundrak/boss_slad_ran.cpp @@ -49,9 +49,7 @@ enum Misc MAX_VIPER = 2, MAX_CONSTRICTOR = 3, - MAX_SUMMONS = 5, - - EVENT_KILL_TALK = 1 + MAX_SUMMONS = 5 }; const Position SpawnLoc[] = @@ -137,11 +135,7 @@ public: void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } private: diff --git a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp index b27e9008b..05c6838d6 100644 --- a/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp +++ b/src/server/scripts/Northrend/IcecrownCitadel/boss_the_lich_king.cpp @@ -638,7 +638,6 @@ public: uint32 _necroticPlagueStack; uint32 _vileSpiritExplosions; uint16 _positionCheckTimer; - uint32 _lastTalkTimeKill; uint32 _lastTalkTimeBuff; bool _bFrostmournePhase; bool _bFordringMustFallYell; @@ -649,7 +648,6 @@ public: _necroticPlagueStack = 0; _vileSpiritExplosions = 0; _positionCheckTimer = 5000; - _lastTalkTimeKill = 0; _lastTalkTimeBuff = 0; _bFrostmournePhase = false; _bFordringMustFallYell = false; @@ -721,11 +719,8 @@ public: void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && !me->IsInEvadeMode() && _phase != PHASE_OUTRO && _lastTalkTimeKill + 5 < GameTime::GetGameTime().count()) - { - _lastTalkTimeKill = GameTime::GetGameTime().count(); - Talk(SAY_LK_KILL); - } + if (!me->IsInEvadeMode() && _phase != PHASE_OUTRO) + Talk(SAY_LK_KILL, victim); } void DoAction(int32 action) override diff --git a/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp b/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp index d6fbd906a..f0e7cf6a6 100644 --- a/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp +++ b/src/server/scripts/Northrend/Naxxramas/boss_razuvious.cpp @@ -205,8 +205,7 @@ public: void KilledUnit(Unit* who) override { - if (roll_chance_i(30)) - Talk(SAY_SLAY); + Talk(SAY_SLAY); if (who->IsPlayer()) instance->StorePersistentData(PERSISTENT_DATA_IMMORTAL_FAIL, 1); diff --git a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp index ce575923c..89d474bb3 100644 --- a/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp +++ b/src/server/scripts/Northrend/Nexus/EyeOfEternity/boss_malygos.cpp @@ -389,7 +389,10 @@ struct boss_malygos : public BossAI case EVENT_START_FIGHT: { instance->SetData(DATA_HIDE_IRIS_AND_PORTAL, 0); + + instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_YOU_DONT_HAVE_AN_ENTERNITY_EVENT); // just in case at reset some players already left the instance instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, ACHIEV_YOU_DONT_HAVE_AN_ENTERNITY_EVENT); + me->RemoveUnitFlag(UNIT_FLAG_NON_ATTACKABLE | UNIT_FLAG_PACIFIED); if (Unit* target = me->SelectNearestTarget(250.0f)) { diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp index 23cd3f12a..e6e92f8ce 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_keristrasza.cpp @@ -51,8 +51,7 @@ enum Events EVENT_CRYSTAL_CHAINS = 2, EVENT_TAIL_SWEEP = 3, EVENT_HEALTH_CHECK = 4, - EVENT_ACHIEVEMENT_CHECK = 5, - EVENT_KILL_TALK = 6 + EVENT_ACHIEVEMENT_CHECK = 5 }; struct boss_keristrasza : public BossAI @@ -89,11 +88,7 @@ struct boss_keristrasza : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_SLAY); } void SetData(uint32 type, uint32) override diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp index cb8705696..a410d7212 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_magus_telestra.cpp @@ -76,8 +76,7 @@ enum Events EVENT_MAGUS_HEALTH2 = 5, EVENT_MAGUS_FAIL_ACHIEVEMENT = 6, EVENT_MAGUS_MERGED = 7, - EVENT_MAGUS_RELOCATE = 8, - EVENT_KILL_TALK = 9 + EVENT_MAGUS_RELOCATE = 8 }; struct boss_magus_telestra : public BossAI @@ -131,11 +130,7 @@ struct boss_magus_telestra : public BossAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp b/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp index 397f68f44..65b0a8440 100644 --- a/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp +++ b/src/server/scripts/Northrend/Nexus/Nexus/boss_ormorok.cpp @@ -49,8 +49,7 @@ enum Events EVENT_ORMOROK_SPELL_REFLECTION = 3, EVENT_ORMOROK_SUMMON = 4, EVENT_ORMOROK_HEALTH = 5, - EVENT_ORMOROK_SUMMON_SPIKES = 6, - EVENT_KILL_TALK = 7 + EVENT_ORMOROK_SUMMON_SPIKES = 6 }; enum Misc @@ -93,11 +92,7 @@ struct boss_ormorok : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_KILL); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_KILL); } void JustSummoned(Creature* summon) override diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp index 0af5863dc..5e4f66eac 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_algalon_the_observer.cpp @@ -364,7 +364,7 @@ struct boss_algalon_the_observer : public ScriptedAI } if (_instance) - _instance->SetData(BOSS_ALGALON, FAIL); + _instance->SetBossState(BOSS_ALGALON, FAIL); ScriptedAI::EnterEvadeMode(why); } @@ -391,7 +391,7 @@ struct boss_algalon_the_observer : public ScriptedAI } if (_instance) - _instance->SetData(BOSS_ALGALON, NOT_STARTED); + _instance->SetBossState(BOSS_ALGALON, NOT_STARTED); } void KilledUnit(Unit* victim) override @@ -446,7 +446,7 @@ struct boss_algalon_the_observer : public ScriptedAI me->SetUnitFlag(UNIT_FLAG_NOT_SELECTABLE); me->InterruptNonMeleeSpells(false); if (_instance) - _instance->SetData(BOSS_ALGALON, NOT_STARTED); + _instance->SetBossState(BOSS_ALGALON, NOT_STARTED); break; case ACTION_INIT_ALGALON: _firstPull = false; @@ -622,7 +622,7 @@ struct boss_algalon_the_observer : public ScriptedAI bool IsInRoom() { - if (me->GetExactDist2d(&me->GetHomePosition()) > 45.f || me->GetPositionZ() < 410.f) + if (me->GetExactDist2d(&me->GetHomePosition()) > 47.f || me->GetPositionZ() < 410.f) { DoAction(ACTION_ASCEND); return false; @@ -660,7 +660,7 @@ struct boss_algalon_the_observer : public ScriptedAI brann->AI()->DoAction(ACTION_FINISH_INTRO); break; case EVENT_START_COMBAT: - _instance->SetData(BOSS_ALGALON, IN_PROGRESS); + _instance->SetBossState(BOSS_ALGALON, IN_PROGRESS); Talk(SAY_ALGALON_AGGRO); break; case EVENT_REMOVE_UNNATTACKABLE: @@ -690,12 +690,17 @@ struct boss_algalon_the_observer : public ScriptedAI events.Repeat(15s + 500ms); break; case EVENT_SUMMON_COLLAPSING_STAR: + { Talk(SAY_ALGALON_COLLAPSING_STAR); Talk(EMOTE_ALGALON_COLLAPSING_STAR); - for (uint8 i = 0; i < COLLAPSING_STAR_COUNT; ++i) - me->SummonCreature(NPC_COLLAPSING_STAR, CollapsingStarPos[i], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2000); + uint8 activeStars = summons.GetEntryCount(NPC_COLLAPSING_STAR); + if (activeStars < COLLAPSING_STAR_COUNT) + for (uint8 i = activeStars; i < COLLAPSING_STAR_COUNT; ++i) + me->SummonCreature(NPC_COLLAPSING_STAR, CollapsingStarPos[i], TEMPSUMMON_CORPSE_TIMED_DESPAWN, 2000); + events.Repeat(1min); break; + } case EVENT_COSMIC_SMASH: Talk(EMOTE_ALGALON_COSMIC_SMASH); me->CastCustomSpell(SPELL_COSMIC_SMASH, SPELLVALUE_MAX_TARGETS, RAID_MODE(1, 3), (Unit*)nullptr); @@ -737,7 +742,7 @@ struct boss_algalon_the_observer : public ScriptedAI case EVENT_OUTRO_START: if (_instance) { - _instance->SetData(BOSS_ALGALON, DONE); + _instance->SetBossState(BOSS_ALGALON, DONE); _instance->SetData(DATA_ALGALON_DEFEATED, 1); } break; diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp index 216aa247b..fec84d23f 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_freya.cpp @@ -37,6 +37,9 @@ enum FreyaSpells SPELL_TOUCH_OF_EONAR = 62528, SPELL_ATTUNED_TO_NATURE = 62519, SPELL_SUMMON_LIFEBINDER = 62870, + SPELL_SUMMON_WAVE_1 = 62685, // Summon Ancient Conservator + SPELL_SUMMON_WAVE_3 = 62686, // Summon Trio (Water Spirit, Storm Lasher, Snaplasher) + SPELL_SUMMON_WAVE_10 = 62687, // Summon Detonating Lashers SPELL_SUNBEAM = 62623, SPELL_NATURE_BOMB_FLIGHT = 64648, SPELL_NATURE_BOMB_DAMAGE = 64587, @@ -212,6 +215,13 @@ enum Misc WAYPOINT_BLUE = 18, }; +enum AlliesGroup +{ + GROUP_TRIO = 0, + GROUP_CONSERVATOR = 1, + GROUP_LASHERS = 2, +}; + struct boss_freya : public BossAI { boss_freya(Creature* pCreature) : BossAI(pCreature, BOSS_FREYA) @@ -220,9 +230,9 @@ struct boss_freya : public BossAI instance->SetBossState(BOSS_FREYA, DONE); } - uint8 _waveNumber; uint8 _trioKilled; uint8 _spawnedAmount; + uint8 _setPermutation; uint8 _lumberjacked; bool _respawningTrio; bool _backToNature; @@ -248,7 +258,7 @@ struct boss_freya : public BossAI _lumberjacked = 0; _spawnedAmount = 0; _trioKilled = 0; - _waveNumber = urand(1, 3); + _setPermutation = urand(0, 5); _respawningTrio = false; _backToNature = true; _deforestation = 0; @@ -329,37 +339,32 @@ struct boss_freya : public BossAI void SpawnWave() { - _waveNumber = _waveNumber == 1 ? 3 : _waveNumber - 1; Talk(EMOTE_ALLIES_OF_NATURE); - // Wave of three - if (_waveNumber == 1) + static constexpr uint8 permTable[6][3] = { + {GROUP_TRIO, GROUP_CONSERVATOR, GROUP_LASHERS}, + {GROUP_TRIO, GROUP_LASHERS, GROUP_CONSERVATOR}, + {GROUP_CONSERVATOR, GROUP_TRIO, GROUP_LASHERS}, + {GROUP_CONSERVATOR, GROUP_LASHERS, GROUP_TRIO}, + {GROUP_LASHERS, GROUP_TRIO, GROUP_CONSERVATOR}, + {GROUP_LASHERS, GROUP_CONSERVATOR, GROUP_TRIO} + }; + + switch (permTable[_setPermutation][_spawnedAmount % 3]) { - Talk(SAY_SUMMON_TRIO); - me->SummonCreature(NPC_ANCIENT_WATER_SPIRIT, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_STORM_LASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - me->SummonCreature(NPC_SNAPLASHER, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ())); - } - // Ancient Conservator - else if (_waveNumber == 2) - { - Talk(SAY_SUMMON_CONSERVATOR); - me->SummonCreature(NPC_ANCIENT_CONSERVATOR, me->GetPositionX() + urand(5, 15), me->GetPositionY() + urand(5, 15), me->GetMapHeight(me->GetPositionX(), me->GetPositionY(), me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); - } - // Detonating Lashers - else if (_waveNumber == 3) - { - Talk(SAY_SUMMON_LASHERS); - // Spread the lashers evenly in a ring around Freya instead of - // clustering them in one spot, matching retail. - for (uint8 i = 0; i < 10; ++i) - { - float angle = i * 2.0f * float(M_PI) / 10.0f + frand(-0.35f, 0.35f); - float dist = frand(18.0f, 28.0f); - float x = me->GetPositionX() + dist * std::cos(angle); - float y = me->GetPositionY() + dist * std::sin(angle); - me->SummonCreature(NPC_DETONATING_LASHER, x, y, me->GetMapHeight(x, y, me->GetPositionZ()), 0, TEMPSUMMON_CORPSE_DESPAWN); - } + case GROUP_TRIO: + Talk(SAY_SUMMON_TRIO); + DoCast(SPELL_SUMMON_WAVE_3); + break; + case GROUP_CONSERVATOR: + Talk(SAY_SUMMON_CONSERVATOR); + DoCast(SPELL_SUMMON_WAVE_1); + break; + case GROUP_LASHERS: + Talk(SAY_SUMMON_LASHERS); + for (uint8 i = 0; i < 10; ++i) + DoCast(SPELL_SUMMON_WAVE_10); + break; } } @@ -586,7 +591,7 @@ struct boss_freya : public BossAI break; case EVENT_FREYA_IRON_ROOT: Talk(EMOTE_IRON_ROOTS); - me->CastCustomSpell(SPELL_IRON_ROOTS_FREYA, SPELLVALUE_MAX_TARGETS, 1, me, false); + me->CastCustomSpell(SPELL_IRON_ROOTS_FREYA, SPELLVALUE_MAX_TARGETS, me->GetMap()->Is25ManRaid() ? 3 : 1, me, false); events.Repeat(45s, 55s); break; case EVENT_FREYA_UNSTABLE_SUN_BEAM: diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp index de0677dd4..c41b28006 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_general_vezax.cpp @@ -47,6 +47,7 @@ enum VezaxSpellData SPELL_CORRUPTED_WISDOM = 64646, SPELL_SHAMANISTIC_RAGE = 30823, SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 = 31876, + SPELL_DRUID_CLEARCASTING = 16870, SPELL_SUMMON_SARONITE_VAPORS = 63081, NPC_SARONITE_VAPORS = 33488, @@ -404,7 +405,7 @@ class spell_aura_of_despair_aura : public AuraScript bool Validate(SpellInfo const* /*spellInfo*/) override { - return ValidateSpellInfo({ SPELL_AURA_OF_DESPAIR_2, SPELL_CORRUPTED_RAGE, SPELL_CORRUPTED_WISDOM }); + return ValidateSpellInfo({ SPELL_AURA_OF_DESPAIR_2, SPELL_CORRUPTED_RAGE, SPELL_CORRUPTED_WISDOM, SPELL_DRUID_CLEARCASTING }); } void OnApply(AuraEffect const* /*aurEff*/, AuraEffectHandleModes /*mode*/) @@ -416,6 +417,11 @@ class spell_aura_of_despair_aura : public AuraScript return; target->CastSpell(target, SPELL_AURA_OF_DESPAIR_2, true); + + // Resto druids: Omen of Clarity can no longer trigger Clearcasting (Patch 3.2.0) + if (target->ToPlayer()->GetSpec() == TALENT_TREE_DRUID_RESTORATION) + target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, true); + if (target->HasSpell(SPELL_SHAMANISTIC_RAGE)) caster->CastSpell(target, SPELL_CORRUPTED_RAGE, true); else if (target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1) || target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 + 1) || target->HasSpell(SPELL_JUDGEMENTS_OF_THE_WISDOM_RANK_1 + 2)) @@ -430,6 +436,8 @@ class spell_aura_of_despair_aura : public AuraScript target->RemoveAurasDueToSpell(SPELL_AURA_OF_DESPAIR_2); target->RemoveAurasDueToSpell(SPELL_CORRUPTED_RAGE); target->RemoveAurasDueToSpell(SPELL_CORRUPTED_WISDOM); + + target->ApplySpellImmune(SPELL_AURA_OF_DESPAIR_2, IMMUNITY_ID, SPELL_DRUID_CLEARCASTING, false); } } diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp index 46f9a3367..f8f406e3b 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_hodir.cpp @@ -255,6 +255,12 @@ struct boss_hodir : public BossAI events.ScheduleEvent(EVENT_HARD_MODE_MISSED, 3min); Talk(TEXT_AGGRO); + // Helpers spawn IMMUNE_TO_NPC so idle bosses (e.g. Freya's Ground Tremor, #26330) can't tag + // them; drop it on engage. TODO: confirm timing - sniffs may clear it per-helper on unfreeze. + for (uint8 i = 0; i < 8; ++i) + if (Creature* helper = GetHelper(i)) + helper->RemoveUnitFlag(UNIT_FLAG_IMMUNE_TO_NPC); + if (instance->GetBossState(BOSS_HODIR) != DONE) instance->SetBossState(BOSS_HODIR, IN_PROGRESS); } diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp index 2965bc4d4..3710026d8 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_kologarn.cpp @@ -161,8 +161,12 @@ struct boss_kologarn : public BossAI void AttachLeftArm() { - if (Unit* arm = ObjectAccessor::GetCreature(*me, _left)) + if (Creature* arm = ObjectAccessor::GetCreature(*me, _left)) + { arm->SetHealth(arm->GetMaxHealth()); + // Clear the arm's combat-start state so it can re-pull the boss on a subsequent attempt. + arm->AI()->Reset(); + } else if (Creature* accessory = me->SummonCreature(NPC_LEFT_ARM, *me, TEMPSUMMON_MANUAL_DESPAWN)) { accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); @@ -179,8 +183,12 @@ struct boss_kologarn : public BossAI void AttachRightArm() { - if (Unit* arm = ObjectAccessor::GetCreature(*me, _right)) + if (Creature* arm = ObjectAccessor::GetCreature(*me, _right)) + { arm->SetHealth(arm->GetMaxHealth()); + // Clear the arm's combat-start state so it can re-pull the boss on a subsequent attempt. + arm->AI()->Reset(); + } else if (Creature* accessory = me->SummonCreature(NPC_RIGHT_ARM, *me, TEMPSUMMON_MANUAL_DESPAWN)) { accessory->AddUnitTypeMask(UNIT_MASK_ACCESSORY); diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp index c0f979d77..54102ba97 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_mimiron.cpp @@ -264,7 +264,8 @@ enum Texts #define GetVX001() instance->GetCreature(DATA_MIMIRON_VX001) #define GetACU() instance->GetCreature(DATA_MIMIRON_ACU) -Position const ACUSummonPos = { 2742.6265f, 2568.0571f, 377.22076f, 0.0f }; /// @todo: replace with sniffed position +// Z is above hover height (15y) so that activating hover does not relocate the ACU upwards +Position const ACUSummonPos = { 2744.650f, 2569.460f, 380.0f, 3.141593f }; struct boss_mimiron : public BossAI { @@ -952,6 +953,14 @@ struct npc_ulduar_leviathan_mkii : public ScriptedAI _events.Reset(); } + void AttackStart(Unit* who) override + { + ScriptedAI::AttackStart(who); + // Unit::Attack clears the emote state on target switch, which would retract VX-001's arms + if (_phase == 4) + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); + } + void SetData(uint32 id, uint32 value) override { if (id == 1) // setting phase to start fighting @@ -985,6 +994,8 @@ struct npc_ulduar_leviathan_mkii : public ScriptedAI if (Unit* target = SelectTargetFromPlayerList(75.0f)) AttackStart(target); DoZoneInCombat(); + // The assembled V0-L7R-ON animates through its vehicle base; this state keeps VX-001's arms deployed + me->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); _events.Reset(); _events.ScheduleEvent(EVENT_SPELL_SHOCK_BLAST, 20s); _events.ScheduleEvent(EVENT_PROXIMITY_MINES_1, 6s); @@ -1380,7 +1391,8 @@ struct npc_ulduar_vx001 : public ScriptedAI if (Unit* vb = me->GetVehicleBase()) { vb->SendMeleeAttackStop(); - vb->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_ONESHOT_NONE); + // Keep the arms-deployed state so the model returns to it after the one-shot pulse animation + vb->SetUInt32Value(UNIT_NPC_EMOTESTATE, EMOTE_STATE_CUSTOM_SPELL_01); if (!_leftArm) { @@ -1502,7 +1514,8 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI _isDefeated = false; _events.Reset(); _summons.DespawnAll(); - me->SetCombatMovement(false); /// @todo: research ACU behaviour + me->SetHover(false); + me->SetDisableGravity(true); } void SetData(uint32 id, uint32 value) override @@ -1517,7 +1530,12 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI break; case 3: _phase = 3; + // Hover instead of disabled gravity: chase then keeps the ACU at + // HoverHeight above ground instead of dragging it to ground level. + me->SetDisableGravity(false); + me->SetHover(true); me->RemoveUnitFlag(UNIT_FLAG_NOT_SELECTABLE); + me->SetImmuneToPC(false); DoZoneInCombat(); _events.Reset(); _events.ScheduleEvent(EVENT_SUMMON_BOMB_BOT, 15s); @@ -1549,14 +1567,24 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI me->CastStop(); me->AttackStop(); me->SetReactState(REACT_PASSIVE); - me->SetDisableGravity(false); + // Raw flag removal first: MoveFall lands at ground + hover height, which would + // keep the ACU airborne, and SetHover(false) would relocate it without a spline. + // SetHover(false) afterwards is relocation-free and tells the client to stop + // hovering, else it keeps floating the grounded unit back up + me->RemoveUnitMovementFlag(MOVEMENTFLAG_HOVER); me->GetMotionMaster()->MoveFall(); + me->SetHover(false); _events.DelayEvents(25s); break; case DO_ENABLE_AERIAL: + if (_isDefeated) + break; me->SetDisableGravity(true); - me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ() + 10.0f); + // 16y: above hover height (15y) so SetHover does not relocate it further up + me->GetMotionMaster()->MovePoint(0, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ() + 16.0f); me->m_Events.AddEventAtOffset([&] { + me->SetDisableGravity(false); + me->SetHover(true); me->SetReactState(REACT_AGGRESSIVE); }, 2s); break; @@ -1588,6 +1616,7 @@ struct npc_ulduar_aerial_command_unit : public ScriptedAI me->InterruptNonMeleeSpells(false); me->RemoveAllAurasExceptType(SPELL_AURA_CONTROL_VEHICLE); + me->SetDisableGravity(true); me->GetMotionMaster()->MovePoint(0, 2744.65f, 2569.46f, 381.34f); if (Creature* c = GetMimiron()) diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp index 157873af3..bec73c9d7 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_thorim.cpp @@ -550,10 +550,12 @@ struct boss_thorim : public BossAI if (_hardMode) chestId += 1; // hard mode offset - if ((go = me->SummonGameObject(chestId, 2134.73f, -286.32f, 419.51f, 4.65f, 0, 0, 0, 0, 0))) + // Summoned by the map, not Thorim, so the chest survives his despawn during the outro. + if ((go = me->GetMap()->SummonGameObject(chestId, 2134.73f, -286.32f, 419.51f, 4.65f, 0, 0, 0, 0, 0))) { go->ReplaceAllGameObjectFlags((GameObjectFlags)0); go->SetLootRecipient(me->GetMap()); + go->SetRespawnTime(7 * DAY); } // Defeat credit diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp index dff9c61ca..812c72143 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/boss_yoggsaron.cpp @@ -49,6 +49,8 @@ enum YoggSpells SPELL_TITANIC_STORM_PASSIVE = 64171, SPELL_WEAKENED = 64162, + SPELL_TELEPORT_PORTAL_VISUAL = 64416, + // GLOBAL SPELL_SANITY_BASE = 63786, SPELL_SANITY = 63050, @@ -70,6 +72,7 @@ enum YoggSpells // GUARDIANS OF YOGG-SARON SPELL_SHADOW_NOVA = 62714, + SPELL_SHADOW_NOVA_SARA = 65719, SPELL_DARK_VOLLEY = 63038, // SARA P2 @@ -290,16 +293,16 @@ const uint32 TABLE_GOSSIP_ENTRY[4] = {NPC_FREYA_GOSSIP, NPC_HODIR_GOSSIP, NPC_MI static LocationsXY yoggPortalLoc[] = { - {1970.48f, -9.75f, 325.5f}, - {1992.76f, -10.21f, 325.5f}, - {1995.53f, -39.78f, 325.5f}, - {1969.25f, -42.00f, 325.5f}, - {1960.62f, -32.00f, 325.5f}, - {1981.98f, -5.69f, 325.5f}, - {1982.78f, -45.73f, 325.5f}, - {2000.66f, -29.68f, 325.5f}, - {1999.88f, -19.61f, 325.5f}, - {1961.37f, -19.54f, 325.5f} + {1964.60f, -42.71f, 325.08f}, + {1986.94f, -46.21f, 324.98f}, + {1989.50f, -6.70f, 325.08f}, + {1965.52f, -8.09f, 324.95f}, + {2000.84f, -25.40f, 325.19f}, + {1960.22f, -26.14f, 325.01f}, + {1976.30f, -47.83f, 325.11f}, + {1997.69f, -37.46f, 325.04f}, + {1998.07f, -13.36f, 325.17f}, + {1976.99f, -3.96f, 325.17f} }; enum Texts @@ -452,6 +455,8 @@ struct boss_yoggsaron_sara : public ScriptedAI me->SetVisible(true); me->SetDisplayId(me->GetNativeDisplayId()); me->SetDisableGravity(true); + me->SetFaction(FACTION_FRIENDLY); + me->ClearUnitState(UNIT_STATE_EVADE); EnableSara(false); SpawnClouds(); @@ -469,7 +474,7 @@ struct boss_yoggsaron_sara : public ScriptedAI { _instance->DoStopTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, CRITERIA_NOT_GETTING_OLDER); _instance->DoRemoveAurasDueToSpellOnPlayers(SPELL_SANITY); - _instance->SetData(BOSS_YOGGSARON, NOT_STARTED); + _instance->SetBossState(BOSS_YOGGSARON, NOT_STARTED); if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) go->SetGoState(GO_STATE_ACTIVE); } @@ -480,12 +485,11 @@ struct boss_yoggsaron_sara : public ScriptedAI if (!_instance) return; - // some simple hack checks - if (_instance->GetBossState(BOSS_VEZAX) != DONE || _instance->GetBossState(BOSS_XT002) != DONE) + if (_instance->GetBossState(BOSS_VEZAX) != DONE) return; _instance->DoStartTimedAchievement(ACHIEVEMENT_TIMED_TYPE_EVENT, CRITERIA_NOT_GETTING_OLDER); - _instance->SetData(BOSS_YOGGSARON, IN_PROGRESS); + _instance->SetBossState(BOSS_YOGGSARON, IN_PROGRESS); me->SetInCombatWithZone(); AttackStart(target); @@ -493,8 +497,6 @@ struct boss_yoggsaron_sara : public ScriptedAI // Engage Keepers summons.DoZoneInCombat(); - me->CastSpell(me, SPELL_SANITY_BASE, true); - events.ScheduleEvent(EVENT_SARA_P1_DOORS_CLOSE, 15s, 0, EVENT_PHASE_ONE); events.ScheduleEvent(EVENT_SARA_P1_BERSERK, 15min, 0, 0); events.ScheduleEvent(EVENT_SARA_P1_SUMMON, 0ms, 0, EVENT_PHASE_ONE); @@ -580,15 +582,15 @@ struct boss_yoggsaron_sara : public ScriptedAI void AddPortals() { _summonSpeed -= 0.1f; - Creature* cr = nullptr; + Creature* creature = nullptr; // Spawn Portals for (uint8 i = 0; i < RAID_MODE(4, 10); ++i) { - if ((cr = me->SummonCreature(NPC_DESCEND_INTO_MADNESS, yoggPortalLoc[i].x, yoggPortalLoc[i].y, yoggPortalLoc[i].z, 0, TEMPSUMMON_TIMED_DESPAWN, 25000))) + if ((creature = me->SummonCreature(NPC_DESCEND_INTO_MADNESS, yoggPortalLoc[i].x, yoggPortalLoc[i].y, yoggPortalLoc[i].z, 0, TEMPSUMMON_TIMED_DESPAWN, 25000))) { - cr->SetUnitFlag(UNIT_FLAG_DISABLE_MOVE | UNIT_FLAG_NON_ATTACKABLE); - cr->SetArmor(_currentIllusion); + creature->SetArmor(_currentIllusion); + creature->CastSpell(creature, SPELL_TELEPORT_PORTAL_VISUAL, true); } } @@ -700,31 +702,33 @@ struct boss_yoggsaron_sara : public ScriptedAI me->CastSpell(me, SPELL_SHATTERED_ILLUSION, true); } - void DamageTaken(Unit* who, uint32& damage, DamageEffectType, SpellSchoolMask) override + void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType /*damagetype*/, SpellSchoolMask /*damageSchoolMask*/) override { - if (who && who->GetEntry() == NPC_GUARDIAN_OF_YS && !_secondPhase) + // Guardians can be spawned by walking into Ominous Clouds even when InitFight + // never ran (e.g. Vezax not defeated); their novas must not start phase 2 then. + if (!_instance || _instance->GetBossState(BOSS_YOGGSARON) != IN_PROGRESS || !attacker || attacker->GetEntry() != NPC_GUARDIAN_OF_YS || _secondPhase) { - damage = 25000; - - // START PHASE 2 - if (me->GetHealth() <= damage) - { - _secondPhase = true; - damage = 0; - - events.SetPhase(EVENT_PHASE_TWO); - me->SetHealth(me->GetMaxHealth()); - - if (Creature* cr = me->SummonCreature(NPC_YOGG_SARON, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), M_PI)) - cr->SetVisible(false); - - _p2TalkTimer++; - Talk(SAY_SARA_TRANSFORM_1); - } + damage = 0; return; } - damage = 0; + // START PHASE 2 + if (me->GetHealth() <= damage) + { + _secondPhase = true; + damage = 0; + + events.SetPhase(EVENT_PHASE_TWO); + me->SetHealth(me->GetMaxHealth()); + me->SetInCombatWithZone(); + me->SetFaction(FACTION_MONSTER_2); + + if (Creature* creature = me->SummonCreature(NPC_YOGG_SARON, me->GetPositionX(), me->GetPositionY(), me->GetPositionZ(), M_PI)) + creature->SetVisible(false); + + _p2TalkTimer++; + Talk(SAY_SARA_TRANSFORM_1); + } } // Players who descend into a portal are teleported ~100 yd below to the illusion @@ -1033,7 +1037,8 @@ struct boss_yoggsaron_guardian_of_ys : public ScriptedAI void JustDied(Unit*) override { - me->CastSpell((Unit*)nullptr, SPELL_SHADOW_NOVA, true); + DoCastAOE(SPELL_SHADOW_NOVA, true); + DoCastAOE(SPELL_SHADOW_NOVA_SARA, true); } void UpdateAI(uint32 diff) override @@ -1104,7 +1109,7 @@ struct boss_yoggsaron : public ScriptedAI if (_instance) { - _instance->SetData(BOSS_YOGGSARON, DONE); + _instance->SetBossState(BOSS_YOGGSARON, DONE); if (Creature* sara = _instance->GetCreature(DATA_SARA)) sara->AI()->DoAction(ACTION_YOGG_SARON_DEATH); if (GameObject* go = _instance->GetGameObject(DATA_YOGG_SARON_DOORS)) @@ -2094,7 +2099,8 @@ struct boss_yoggsaron_voice : public NullCreatureAI void Reset() override { - me->CastSpell(me, SPELL_INSANE_PERIODIC, true); + DoCastSelf(SPELL_INSANE_PERIODIC, true); + DoCastSelf(SPELL_SANITY_BASE, true); } void SpellHitTarget(Unit* target, SpellInfo const* spellInfo) override diff --git a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp index 9daa556ff..c2d28b23d 100644 --- a/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp +++ b/src/server/scripts/Northrend/Ulduar/Ulduar/instance_ulduar.cpp @@ -47,7 +47,8 @@ DoorData const doorData[] = { GO_KEEPERS_GATE, BOSS_THORIM, DOOR_TYPE_PASSAGE }, { GO_KEEPERS_GATE, BOSS_FREYA, DOOR_TYPE_PASSAGE }, { GO_VEZAX_DOOR, BOSS_VEZAX, DOOR_TYPE_PASSAGE }, - { GO_YOGG_SARON_DOORS, BOSS_YOGGSARON, DOOR_TYPE_ROOM }, + // GO_YOGG_SARON_DOORS is handled by boss_yoggsaron_sara: it closes 15 seconds + // after the pull, not immediately when the encounter enters IN_PROGRESS. { GO_DOODAD_UL_SIGILDOOR_03, BOSS_ALGALON, DOOR_TYPE_ROOM }, { GO_DOODAD_UL_UNIVERSEFLOOR_01, BOSS_ALGALON, DOOR_TYPE_ROOM }, { GO_DOODAD_UL_UNIVERSEFLOOR_02, BOSS_ALGALON, DOOR_TYPE_SPAWN_HOLE }, @@ -623,6 +624,17 @@ public: if (GetBossState(BOSS_FREYA) == DONE) gameObject->SetRespawnTime(7 * DAY); break; + // Freya Loot + case 194324: + case 194325: + case 194326: + case 194327: + case 194328: + case 194329: + case 194330: + case 194331: + gameObject->SetLootRecipient(instance); + break; } } diff --git a/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp b/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp index 6e667c6b9..7fec8c37e 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_bloodboil.cpp @@ -66,12 +66,11 @@ enum Misc struct boss_gurtogg_bloodboil : public BossAI { - boss_gurtogg_bloodboil(Creature* creature) : BossAI(creature, DATA_GURTOGG_BLOODBOIL), _recentlySpoken(false) { } + boss_gurtogg_bloodboil(Creature* creature) : BossAI(creature, DATA_GURTOGG_BLOODBOIL) { } void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } void JustEngagedWith(Unit* who) override @@ -145,13 +144,7 @@ struct boss_gurtogg_bloodboil : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(SAY_SLAY); } void JustSummoned(Creature* summon) override @@ -171,8 +164,6 @@ struct boss_gurtogg_bloodboil : public BossAI return me->GetHomePosition().GetExactDist2d(me) > 105.0f; } -private: - bool _recentlySpoken; }; class spell_gurtogg_bloodboil : public SpellScript diff --git a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp index 2e0cfe9f0..8a79fd4bb 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_illidan.cpp @@ -196,7 +196,7 @@ public: struct boss_illidan_stormrage : public BossAI { - boss_illidan_stormrage(Creature* creature) : BossAI(creature, DATA_ILLIDAN_STORMRAGE), _canTalk(true), _dying(false), _inCutscene(false), beamPosId(0) { } + boss_illidan_stormrage(Creature* creature) : BossAI(creature, DATA_ILLIDAN_STORMRAGE), _dying(false), _inCutscene(false), beamPosId(0) { } void Reset() override { @@ -204,7 +204,6 @@ struct boss_illidan_stormrage : public BossAI me->m_Events.CancelEventGroup(GROUP_BERSERK); me->m_Events.CancelEventGroup(GROUP_PHASE_FLYING); me->m_Events.CancelEventGroup(GROUP_DEMON_FORM); - _canTalk = true; _dying = false; _inCutscene = false; beamPosId = urand(0, MAX_EYE_BEAM_POS - 1); @@ -652,15 +651,7 @@ struct boss_illidan_stormrage : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - Talk(SAY_ILLIDAN_KILL); - _canTalk = false; - - me->m_Events.AddEventAtOffset([&] { - _canTalk = true; - }, 6s); // 3590ms - } + Talk(SAY_ILLIDAN_KILL); } void DamageTaken(Unit* attacker, uint32& damage, DamageEffectType damagetype, SpellSchoolMask damageSchoolMask) override @@ -689,7 +680,6 @@ struct boss_illidan_stormrage : public BossAI } private: - bool _canTalk; bool _dying; bool _inCutscene; uint8 beamPosId; diff --git a/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp b/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp index 31c16831a..73f6504b6 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_illidari_council.cpp @@ -98,7 +98,6 @@ enum Misc EVENT_SPELL_VANISH_OUT = 31, EVENT_SPELL_ENRAGE = 32, - EVENT_KILL_TALK = 100 }; class VerasEnvenom : public BasicEvent @@ -262,11 +261,7 @@ struct boss_illidari_council_memberAI : public ScriptedAI void KilledUnit(Unit*) override { - if (!events.HasTimeUntilEvent(EVENT_KILL_TALK)) - { - Talk(SAY_COUNCIL_SLAY); - events.ScheduleEvent(EVENT_KILL_TALK, 6s); - } + Talk(SAY_COUNCIL_SLAY); } void JustDied(Unit*) override diff --git a/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp b/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp index 5cfb86a64..40024dc6e 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_mother_shahraz.cpp @@ -64,13 +64,12 @@ enum Misc struct boss_mother_shahraz : public BossAI { - boss_mother_shahraz(Creature* creature) : BossAI(creature, DATA_MOTHER_SHAHRAZ), _canTalk(true) { } + boss_mother_shahraz(Creature* creature) : BossAI(creature, DATA_MOTHER_SHAHRAZ) { } void Reset() override { _Reset(); me->m_Events.CancelEventGroup(GROUP_ENRAGE); - _canTalk = true; ScheduleHealthCheckEvent(10, [&] { Talk(SAY_EMOTE_FRENZY); @@ -113,15 +112,7 @@ struct boss_mother_shahraz : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (_canTalk) - { - Talk(SAY_SLAY); - _canTalk = false; - - ScheduleUniqueTimedEvent(6s, [&] { - _canTalk = true; - }, 1); - } + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -131,8 +122,6 @@ struct boss_mother_shahraz : public BossAI Talk(SAY_DEATH); } - private: - bool _canTalk; }; class spell_mother_shahraz_random_periodic_aura : public AuraScript diff --git a/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp b/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp index 0827a0e63..f4e9d95bd 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_reliquary_of_souls.cpp @@ -263,11 +263,10 @@ public: struct boss_essence_of_sufferingAI : public ScriptedAI { - boss_essence_of_sufferingAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_sufferingAI(Creature* creature) : ScriptedAI(creature) { } void Reset() override { - _recentlySpoken = false; scheduler.CancelAll(); } @@ -311,13 +310,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SUFF_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(SUFF_SAY_SLAY); } void JustEngagedWith(Unit* /*who*/) override @@ -354,8 +347,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; @@ -371,11 +362,10 @@ public: struct boss_essence_of_desireAI : public ScriptedAI { - boss_essence_of_desireAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_desireAI(Creature* creature) : ScriptedAI(creature) { } void Reset() override { - _recentlySpoken = false; scheduler.CancelAll(); } @@ -418,13 +408,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(DESI_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(DESI_SAY_SLAY); } void JustEngagedWith(Unit* /*who*/) override @@ -467,8 +451,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; @@ -484,13 +466,12 @@ public: struct boss_essence_of_angerAI : public ScriptedAI { - boss_essence_of_angerAI(Creature* creature) : ScriptedAI(creature), _recentlySpoken(false) { } + boss_essence_of_angerAI(Creature* creature) : ScriptedAI(creature) { } ObjectGuid targetGUID; void Reset() override { - _recentlySpoken = false; targetGUID.Clear(); scheduler.CancelAll(); } @@ -507,13 +488,7 @@ public: void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(ANGER_SAY_SLAY); - me->m_Events.AddEventAtOffset([&] { - _recentlySpoken = false; - }, 6s); - } + Talk(ANGER_SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -563,8 +538,6 @@ public: DoMeleeAttackIfReady(); } - private: - bool _recentlySpoken; }; }; diff --git a/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp b/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp index 2cd31e17e..b944ca351 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_teron_gorefiend.cpp @@ -77,7 +77,6 @@ struct boss_teron_gorefiend : public BossAI { boss_teron_gorefiend(Creature* creature) : BossAI(creature, DATA_TERON_GOREFIEND) { - _recentlySpoken = false; _intro = false; } @@ -127,18 +126,9 @@ struct boss_teron_gorefiend : public BossAI BossAI::JustEngagedWith(who); } - void KilledUnit(Unit* victim) override + void KilledUnit(Unit* victim) override { - if (!_recentlySpoken && victim->IsPlayer()) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - - ScheduleUniqueTimedEvent(6s, [&] - { - _recentlySpoken = false; - }, 1); - } + Talk(SAY_SLAY, victim); } void SetData(uint32 type, uint32 id) override @@ -175,7 +165,6 @@ struct boss_teron_gorefiend : public BossAI } private: - bool _recentlySpoken; bool _intro; }; diff --git a/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp b/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp index 2791c7f04..f817fed19 100644 --- a/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp +++ b/src/server/scripts/Outland/BlackTemple/boss_warlord_najentus.cpp @@ -46,7 +46,6 @@ enum Spells enum Events { - EVENT_TALK_CHECK = 1, EVENT_ENRAGE = 2 }; @@ -57,7 +56,7 @@ enum Misc struct boss_najentus : public BossAI { - boss_najentus(Creature* creature) : BossAI(creature, DATA_HIGH_WARLORD_NAJENTUS), _canTalk(true) { } + boss_najentus(Creature* creature) : BossAI(creature, DATA_HIGH_WARLORD_NAJENTUS) { } void Reset() override { @@ -68,8 +67,6 @@ struct boss_najentus : public BossAI void JustEngagedWith(Unit* who) override { - _canTalk = true; - BossAI::JustEngagedWith(who); Talk(SAY_AGGRO); DoCastSelf(SPELL_REMOVE_SPINES); @@ -108,15 +105,7 @@ struct boss_najentus : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && _canTalk) - { - Talk(SAY_SLAY); - _canTalk = false; - ScheduleUniqueTimedEvent(5s, [&] - { - _canTalk = true; - }, EVENT_TALK_CHECK); - } + Talk(SAY_SLAY, victim); } void JustDied(Unit* killer) override @@ -126,8 +115,6 @@ struct boss_najentus : public BossAI Talk(SAY_DEATH); } -private: - bool _canTalk; }; class spell_najentus_needle_spine : public SpellScript diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp index 353e357cf..36f0d3c4b 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_fathomlord_karathress.cpp @@ -92,7 +92,6 @@ struct boss_fathomlord_karathress : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; ScheduleHealthCheckEvent(75, [&]{ instance->DoForAllMinions(DATA_FATHOM_LORD_KARATHRESS, [&](Creature* fathomguard) { @@ -117,15 +116,7 @@ struct boss_fathomlord_karathress : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -182,8 +173,6 @@ struct boss_fathomlord_karathress : public BossAI }); } } -private: - bool _recentlySpoken; }; struct LeechingThrowSelector diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp index d24ca089c..d289b8e17 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_hydross_the_unstable.cpp @@ -87,12 +87,11 @@ enum WaterElementalPathIds struct boss_hydross_the_unstable : public BossAI { - boss_hydross_the_unstable(Creature* creature) : BossAI(creature, DATA_HYDROSS_THE_UNSTABLE), _recentlySpoken(false) { } + boss_hydross_the_unstable(Creature* creature) : BossAI(creature, DATA_HYDROSS_THE_UNSTABLE) { } void Reset() override { _Reset(); - _recentlySpoken = false; SummonTaintedElementalOOC(); } @@ -249,15 +248,7 @@ struct boss_hydross_the_unstable : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_SLAY : SAY_CLEAN_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_SLAY : SAY_CLEAN_SLAY); } void JustSummoned(Creature* summon) override @@ -291,8 +282,6 @@ struct boss_hydross_the_unstable : public BossAI Talk(me->HasAura(SPELL_CORRUPTION) ? SAY_CORRUPT_DEATH : SAY_CLEAN_DEATH); BossAI::JustDied(killer); } -private: - bool _recentlySpoken; }; class spell_hydross_cleansing_field_aura : public AuraScript diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp index 265572584..8851ee068 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_lady_vashj.cpp @@ -78,7 +78,6 @@ struct boss_lady_vashj : public BossAI void Reset() override { _count = 0; - _recentlySpoken = false; _batTimer = 20s; _playerAngle = 0.0f; BossAI::Reset(); @@ -94,15 +93,7 @@ struct boss_lady_vashj : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -258,7 +249,6 @@ struct boss_lady_vashj : public BossAI private: float _playerAngle; - bool _recentlySpoken; bool _intro; int32 _count; std::chrono::seconds _batTimer; diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp index be599ef83..1b94f747b 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_leotheras_the_blind.cpp @@ -83,7 +83,6 @@ struct boss_leotheras_the_blind : public BossAI DoCastSelf(SPELL_CLEAR_CONSUMING_MADNESS, true); DoCastSelf(SPELL_DUAL_WIELD, true); me->SetReactState(REACT_PASSIVE); - _recentlySpoken = false; ScheduleHealthCheckEvent(15, [&]{ me->RemoveAurasDueToSpell(SPELL_WHIRLWIND); @@ -242,8 +241,6 @@ struct boss_leotheras_the_blind : public BossAI me->setAttackTimer(BASE_ATTACK, 2000); } } -private: - bool _recentlySpoken; }; struct npc_inner_demon : public ScriptedAI diff --git a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp index a923b81a9..240dc4444 100644 --- a/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp +++ b/src/server/scripts/Outland/CoilfangReservoir/SerpentShrine/boss_morogrim_tidewalker.cpp @@ -60,21 +60,11 @@ struct boss_morogrim_tidewalker : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } - void KilledUnit(Unit*) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(6s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustSummoned(Creature* summon) override @@ -131,8 +121,6 @@ struct boss_morogrim_tidewalker : public BossAI context.Repeat(45s, 60s); }); } -private: - bool _recentlySpoken; }; class spell_morogrim_tidewalker_watery_grave : public SpellScript diff --git a/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp b/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp index 9c0dfcd9c..0dd93ac90 100644 --- a/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp +++ b/src/server/scripts/Outland/GruulsLair/boss_gruul.cpp @@ -67,7 +67,6 @@ struct boss_gruul : public BossAI void Reset() override { _Reset(); - _recentlySpoken = false; _caveInTimer = 29s; } @@ -119,18 +118,9 @@ struct boss_gruul : public BossAI }); } - void KilledUnit(Unit* /*who*/) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -154,7 +144,6 @@ struct boss_gruul : public BossAI private: std::chrono::milliseconds _caveInTimer; - bool _recentlySpoken; }; struct npc_invisible_tractor_beam_source : public NullCreatureAI diff --git a/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp b/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp index ac24a919d..57c4895e1 100644 --- a/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp +++ b/src/server/scripts/Outland/GruulsLair/boss_high_king_maulgar.cpp @@ -69,7 +69,6 @@ struct boss_high_king_maulgar : public BossAI void Reset() override { _Reset(); - _recentlySpoken = false; me->SetLootMode(0); ScheduleHealthCheckEvent(50, [&]{ Talk(SAY_ENRAGE); @@ -87,18 +86,9 @@ struct boss_high_king_maulgar : public BossAI }); } - void KilledUnit(Unit* /*victim*/) override + void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* /*killer*/) override @@ -158,8 +148,6 @@ struct boss_high_king_maulgar : public BossAI DoMeleeAttackIfReady(); } -private: - bool _recentlySpoken; }; struct boss_olm_the_summoner : public ScriptedAI diff --git a/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp b/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp index edb20d541..f97519f04 100644 --- a/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp +++ b/src/server/scripts/Outland/HellfireCitadel/MagtheridonsLair/boss_magtheridon.cpp @@ -80,7 +80,6 @@ struct boss_magtheridon : public BossAI BossAI::Reset(); _currentPhase = 0; _castingQuake = false; - _recentlySpoken = false; _magReleased = false; _interruptScheduler.CancelAll(); scheduler.Schedule(90s, [this](TaskContext context) @@ -123,16 +122,7 @@ struct boss_magtheridon : public BossAI void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - } - - scheduler.Schedule(5s, [this](TaskContext /*context*/) - { - _recentlySpoken = false; - }); + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -244,7 +234,6 @@ struct boss_magtheridon : public BossAI private: bool _castingQuake; - bool _recentlySpoken; bool _magReleased; uint8 _currentPhase; TaskScheduler _interruptScheduler; diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp index 54235dda0..8e82904e3 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_astromancer.cpp @@ -104,10 +104,7 @@ struct boss_high_astromancer_solarian : public BossAI void KilledUnit(Unit* victim) override { - if (victim->IsPlayer() && roll_chance_i(50)) - { - Talk(SAY_KILL); - } + Talk(SAY_KILL, victim); } void JustDied(Unit* killer) override diff --git a/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp b/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp index 3c4582d46..6b47324c8 100644 --- a/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp +++ b/src/server/scripts/Outland/TempestKeep/Eye/boss_void_reaver.cpp @@ -52,20 +52,11 @@ struct boss_void_reaver : public BossAI void Reset() override { BossAI::Reset(); - _recentlySpoken = false; } void KilledUnit(Unit* /*victim*/) override { - if (!_recentlySpoken) - { - Talk(SAY_SLAY); - _recentlySpoken = true; - scheduler.Schedule(5s, [this](TaskContext) - { - _recentlySpoken = false; - }); - } + Talk(SAY_SLAY); } void JustDied(Unit* killer) override @@ -102,8 +93,6 @@ struct boss_void_reaver : public BossAI }); } - private: - bool _recentlySpoken; }; void AddSC_boss_void_reaver() diff --git a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp index ad0f03273..12d8c8431 100644 --- a/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp +++ b/src/server/scripts/Outland/TempestKeep/botanica/instance_the_botanica.cpp @@ -127,9 +127,19 @@ class spell_botanica_shift_form_aura : public AuraScript { _swapTime = GameTime::GetGameTime().count() + 6; _lastSchool = spellInfo->GetSchoolMask(); - GetUnitOwner()->RemoveAurasDueToSpell(_lastForm); + + // Runs from Aura::GetProcEffectMask while the proc engine iterates the owner's + // applied-aura map. Swapping forms mutates that map (RemoveAura + CastSpell), + // which invalidates the live iterator now that aura containers are flat_multimaps. + // Defer the swap to the owner's event queue so it runs outside the proc walk. + Unit* owner = GetUnitOwner(); + uint32 const oldForm = _lastForm; _lastForm = form; - GetUnitOwner()->CastSpell(GetUnitOwner(), _lastForm, true); + owner->m_Events.AddEventAtOffset([owner, oldForm, form]() + { + owner->RemoveAurasDueToSpell(oldForm); + owner->CastSpell(owner, form, true); + }, 1ms); } } diff --git a/src/server/scripts/Spells/spell_mage.cpp b/src/server/scripts/Spells/spell_mage.cpp index b6a5cf812..ef3b49e36 100644 --- a/src/server/scripts/Spells/spell_mage.cpp +++ b/src/server/scripts/Spells/spell_mage.cpp @@ -123,16 +123,21 @@ class spell_mage_burning_determination : public AuraScript if (!(eventInfo.GetSpellInfo()->GetAllEffectsMechanicMask() & ((1ULL << MECHANIC_INTERRUPT) | (1ULL << MECHANIC_SILENCE)))) return false; + Unit* target = eventInfo.GetActionTarget(); + + // This hook runs while the proc engine iterates the target's aura map; removing an + // aura inline would invalidate the live iterator (aura containers are flat_multimaps). + // Defer removals to the target's event queue so they run outside the proc walk. // Xinef: immuned effect should just eat charge if (eventInfo.GetHitMask() & PROC_EX_IMMUNE) { - eventInfo.GetActionTarget()->RemoveAurasDueToSpell(54748); + target->m_Events.AddEventAtOffset([target]() { target->RemoveAurasDueToSpell(54748); }, 1ms); return false; } - if (Aura* aura = eventInfo.GetActionTarget()->GetAura(54748)) + if (Aura* aura = target->GetAura(54748)) { if (aura->GetDuration() < aura->GetMaxDuration()) - eventInfo.GetActionTarget()->RemoveAurasDueToSpell(54748); + target->m_Events.AddEventAtOffset([target]() { target->RemoveAurasDueToSpell(54748); }, 1ms); return false; } @@ -1045,7 +1050,14 @@ class spell_mage_combustion : public AuraScript // Do not take charges, add a stack of crit buff if (!(eventInfo.GetHitMask() & PROC_HIT_CRITICAL)) { - eventInfo.GetActor()->CastSpell(static_cast<Unit*>(nullptr), SPELL_MAGE_COMBUSTION_PROC, true); + // Applying the stack mutates the actor's aura map while the proc engine iterates + // it (this hook runs from Aura::GetProcEffectMask); defer it so the insert can't + // invalidate the live iterator (aura containers are flat_multimaps). + Unit* actor = eventInfo.GetActor(); + actor->m_Events.AddEventAtOffset([actor]() + { + actor->CastSpell(static_cast<Unit*>(nullptr), SPELL_MAGE_COMBUSTION_PROC, true); + }, 1ms); return false; } diff --git a/src/server/scripts/World/go_scripts.cpp b/src/server/scripts/World/go_scripts.cpp index cfcf56121..ac8c897e6 100644 --- a/src/server/scripts/World/go_scripts.cpp +++ b/src/server/scripts/World/go_scripts.cpp @@ -75,20 +75,6 @@ public: } }; -class go_arena_ready_marker : public GameObjectScript -{ -public: - go_arena_ready_marker() : GameObjectScript("go_arena_ready_marker") { } - - bool OnGossipHello(Player* player, GameObject* /*go*/) override - { - if (Battleground* bg = player->GetBattleground()) - bg->ReadyMarkerClicked(player); - - return false; - } -}; - /*###### ## go_ethereum_prison ######*/ @@ -1889,7 +1875,6 @@ void AddSC_go_scripts() { new go_seer_of_zebhalak(); new go_witherbark_totem_bundle(); - new go_arena_ready_marker(); new go_ethereum_prison(); new go_ethereum_stasis(); new go_resonite_cask(); diff --git a/src/test/mocks/SpellInfoTestHelper.h b/src/test/mocks/SpellInfoTestHelper.h index 2b5a14eca..436135be1 100644 --- a/src/test/mocks/SpellInfoTestHelper.h +++ b/src/test/mocks/SpellInfoTestHelper.h @@ -32,9 +32,6 @@ class TestSpellEntryHelper public: TestSpellEntryHelper() { - // Zero initialize all fields - std::memset(&_entry, 0, sizeof(_entry)); - // Set safe defaults _entry.EquippedItemClass = -1; _entry.SchoolMask = SPELL_SCHOOL_MASK_NORMAL; @@ -154,7 +151,7 @@ public: } private: - SpellEntry _entry; + SpellEntry _entry{}; }; /** diff --git a/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp b/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp new file mode 100644 index 000000000..3073f1374 --- /dev/null +++ b/src/test/server/game/Battlegrounds/LowLevelsMinPlayersOverrideTest.cpp @@ -0,0 +1,204 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "Battleground.h" +#include "BattlegroundUtils.h" +#include "DBCStructure.h" +#include "ScriptMgr.h" +#include "ScriptDefines/AllBattlegroundScript.h" +#include "WorldMock.h" +#include "gtest/gtest.h" + +/** + * Tests the resolution order of the low-levels MinPlayersPerTeam override: + * per-BG config key > global config key > battleground_template value, + * and the guards that keep it away from arenas, templates (member function) + * and max-level brackets. + */ +class LowLevelsMinPlayersOverrideTest : public ::testing::Test +{ +protected: + static constexpr uint32 TemplateMinPlayers = 5; + + void SetUp() override + { + // Battleground's destructor fires OnBattlegroundDestroy; the hook + // registry must be sized before any hook is invoked. + ScriptRegistry<BGScript>::InitEnabledHooksIfNeeded(ALLBATTLEGROUNDHOOK_END); + + previousWorld_ = std::move(sWorld); + worldMock_ = new ::testing::NiceMock<WorldMock>(); + ON_CALL(*worldMock_, getIntConfig(::testing::_)) + .WillByDefault(::testing::Return(0)); + ON_CALL(*worldMock_, getIntConfig(CONFIG_MAX_PLAYER_LEVEL)) + .WillByDefault(::testing::Return(80)); + sWorld.reset(worldMock_); + } + + void TearDown() override + { + sWorld = std::move(previousWorld_); + } + + void SetIntConfig(ServerConfigs index, uint32 value) + { + ON_CALL(*worldMock_, getIntConfig(index)) + .WillByDefault(::testing::Return(value)); + } + + static PvPDifficultyEntry MakeBracket(uint32 minLevel, uint32 maxLevel) + { + return PvPDifficultyEntry(/*mapId*/ 0, /*bracketId*/ 0, minLevel, maxLevel, /*difficulty*/ 0); + } + + // A template BG as loaded from battleground_template + static void SetupTemplate(Battleground& bg, BattlegroundTypeId bgTypeId) + { + bg.SetBgTypeID(bgTypeId); + bg.SetMinPlayersPerTeam(TemplateMinPlayers); + } + + // A real BG instance bound to a level bracket + static void SetupRealBg(Battleground& bg, BattlegroundTypeId bgTypeId, PvPDifficultyEntry const& bracket) + { + SetupTemplate(bg, bgTypeId); + bg.SetBracket(&bracket); + } + + ::testing::NiceMock<WorldMock>* worldMock_ = nullptr; + std::unique_ptr<IWorld> previousWorld_; +}; + +TEST_F(LowLevelsMinPlayersOverrideTest, PerBgKeyWinsOverGlobalAndTemplate) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), 2u); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), 2u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, GlobalKeyAppliesWhenPerBgUnset) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), 4u); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), 4u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, TemplateValueAppliesWhenNoOverrideSet) +{ + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, MaxLevelBracketIgnoresOverride) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(71, 80); + SetupRealBg(bg, BATTLEGROUND_WS, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); + + // same via the template path of the free helper + Battleground templateBg; + SetupTemplate(templateBg, BATTLEGROUND_WS); + EXPECT_EQ(GetMinPlayersPerTeam(&templateBg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, ArenaIgnoresOverride) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_NA, bracket); + bg.SetArenaorBGType(true); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); + EXPECT_EQ(GetMinPlayersPerTeam(&bg, &bracket), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, PerBgKeyDoesNotLeakToOtherBgs) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_WS), 2u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AB), 0u); + + Battleground bg; + PvPDifficultyEntry bracket = MakeBracket(10, 19); + SetupRealBg(bg, BATTLEGROUND_AB, bracket); + + EXPECT_EQ(bg.GetMinPlayersPerTeam(), TemplateMinPlayers); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, EachPerBgKeyResolvesIndependently) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AV, 10); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_AB, 3); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_EY, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_SA, 5); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_IC, 6); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AV), 10u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_WS), 2u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_AB), 3u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_EY), 4u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_SA), 5u); + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_IC), 6u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, UnmappedBgTypeFallsBackToGlobal) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS, 4); + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + EXPECT_EQ(GetLowLevelsMinPlayersOverride(BATTLEGROUND_RB), 4u); +} + +TEST_F(LowLevelsMinPlayersOverrideTest, FreeHelperOnTemplateUsesPerBgValue) +{ + SetIntConfig(CONFIG_BATTLEGROUND_OVERRIDE_LOWLEVELS_MINPLAYERS_WS, 2); + + Battleground templateBg; + SetupTemplate(templateBg, BATTLEGROUND_WS); + PvPDifficultyEntry bracket = MakeBracket(10, 19); + + // the free helper resolves via the bracket entry for templates + EXPECT_EQ(GetMinPlayersPerTeam(&templateBg, &bracket), 2u); + + // the member function never applies overrides to templates + EXPECT_EQ(templateBg.GetMinPlayersPerTeam(), TemplateMinPlayers); +} diff --git a/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp b/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp new file mode 100644 index 000000000..86c7208ff --- /dev/null +++ b/src/test/server/game/Entities/FlatMultimapAuraPatternTest.cpp @@ -0,0 +1,611 @@ +/* + * This file is part of the AzerothCore Project. See AUTHORS file for Copyright information + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, but WITHOUT + * ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or + * FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for + * more details. + * + * You should have received a copy of the GNU General Public License along + * with this program. If not, see <http://www.gnu.org/licenses/>. + */ + +#include "gtest/gtest.h" +#include <boost/container/flat_map.hpp> +#include <cstdint> +#include <set> +#include <vector> + +// Lightweight fake aura — just enough state for pattern testing. +struct FakeAura +{ + uint32_t spellId; + bool removed = false; + bool expired = false; + bool updated = false; + + explicit FakeAura(uint32_t id) : spellId(id) {} + bool IsRemoved() const { return removed; } + bool IsExpired() const { return expired; } +}; + +using AuraMap = boost::container::flat_multimap<uint32_t, FakeAura*>; + +// ----------------------------------------------------------------------- +// 1. Snapshot-update pattern (mirrors _UpdateSpells snapshot loop) +// +// Snapshot pointers to a vector, then iterate the vector. +// Entries marked removed during iteration are skipped. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SnapshotUpdate) +{ + FakeAura a1(100), a2(100), a3(200), a4(300); + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(200, &a3); + map.emplace(300, &a4); + + // Snapshot pointers (mirrors Unit::_UpdateSpells) + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + ASSERT_EQ(snapshot.size(), 4u); + + // Mark a2 as removed mid-iteration (simulates cascading removal) + a2.removed = true; + + // Process snapshot — skip removed entries + for (FakeAura* aura : snapshot) + { + if (!aura->IsRemoved()) + aura->updated = true; + } + + EXPECT_TRUE(a1.updated); + EXPECT_FALSE(a2.updated); // skipped — was removed + EXPECT_TRUE(a3.updated); + EXPECT_TRUE(a4.updated); +} + +// ----------------------------------------------------------------------- +// 2. Erase-and-reset-to-begin (mirrors RemoveOwnedAura / expire loop) +// +// for (auto i = map.begin(); i != map.end();) +// if (should_remove) { erase(i); i = map.begin(); } +// else ++i; +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseResetToBegin) +{ + FakeAura a1(100), a2(200), a3(300), a4(400), a5(500); + a2.expired = true; + a4.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + map.emplace(500, &a5); + + std::vector<FakeAura*> removed; + + // Mirrors the expire loop in _UpdateSpells / RemoveOwnedAuras + for (AuraMap::iterator i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + FakeAura* aura = i->second; + map.erase(i); + i = map.begin(); // reset — flat_multimap invalidates all + removed.push_back(aura); + } + else + ++i; + } + + // a2 and a4 should have been removed + ASSERT_EQ(removed.size(), 2u); + EXPECT_EQ(removed[0]->spellId, 200u); + EXPECT_EQ(removed[1]->spellId, 400u); + + // Remaining entries + ASSERT_EQ(map.size(), 3u); + std::set<uint32_t> remaining; + for (auto& [id, aura] : map) + remaining.insert(id); + EXPECT_TRUE(remaining.count(100)); + EXPECT_TRUE(remaining.count(300)); + EXPECT_TRUE(remaining.count(500)); +} + +// ----------------------------------------------------------------------- +// 3. lower_bound / upper_bound erase (mirrors RemoveOwnedAura by spellId +// and RemoveAurasDueToSpell) +// +// Iterate a key range, erase matching entries, reset iterator to +// lower_bound after each erase. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, LowerUpperBoundErase) +{ + FakeAura a1(100), a2(100), a3(100), a4(200), a5(200); + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(200, &a4); + map.emplace(200, &a5); + + // Remove all entries with key 100 — mirrors RemoveOwnedAura(spellId) + uint32_t targetKey = 100; + for (auto itr = map.lower_bound(targetKey); + itr != map.upper_bound(targetKey);) + { + map.erase(itr); + itr = map.lower_bound(targetKey); // reset after erase + } + + // All key-100 entries gone + EXPECT_EQ(map.count(100), 0u); + + // Key-200 entries untouched + EXPECT_EQ(map.count(200), 2u); + ASSERT_EQ(map.size(), 2u); +} + +// ----------------------------------------------------------------------- +// 3b. Selective erase within a key range +// (mirrors RemoveOwnedAura with casterGUID / effectMask filter) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SelectiveLowerBoundErase) +{ + FakeAura a1(100), a2(100), a3(100); + // Only remove entries whose spellId matches AND which are expired + a1.expired = false; + a2.expired = true; + a3.expired = false; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + + uint32_t targetKey = 100; + for (auto itr = map.lower_bound(targetKey); + itr != map.upper_bound(targetKey);) + { + if (itr->second->IsExpired()) + { + map.erase(itr); + itr = map.lower_bound(targetKey); + } + else + ++itr; + } + + // Only a2 removed + EXPECT_EQ(map.count(100), 2u); + + // Verify the right ones survived + std::set<FakeAura*> survivors; + for (auto itr = map.lower_bound(100); itr != map.upper_bound(100); ++itr) + survivors.insert(itr->second); + EXPECT_TRUE(survivors.count(&a1)); + EXPECT_FALSE(survivors.count(&a2)); + EXPECT_TRUE(survivors.count(&a3)); +} + +// ----------------------------------------------------------------------- +// 4. Erase during snapshot (mirrors cascading removal — snapshot loop +// processes pointers while the map is mutated underneath) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseDuringSnapshot) +{ + FakeAura a1(100), a2(200), a3(300), a4(400); + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + + // Snapshot pointers + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + // Process snapshot; during processing, erase entries from the map + std::vector<uint32_t> processed; + for (FakeAura* aura : snapshot) + { + if (aura->IsRemoved()) + continue; + + processed.push_back(aura->spellId); + + // Simulate cascading removal: processing a1 causes a3 to be + // removed from the map and marked removed + if (aura == &a1) + { + a3.removed = true; + // Erase a3 from map by finding it + for (auto it = map.begin(); it != map.end(); ++it) + { + if (it->second == &a3) + { + map.erase(it); + break; + } + } + } + } + + // a1, a2, a4 processed; a3 skipped due to IsRemoved() + ASSERT_EQ(processed.size(), 3u); + EXPECT_EQ(processed[0], 100u); + EXPECT_EQ(processed[1], 200u); + EXPECT_EQ(processed[2], 400u); + + // Map should have 3 entries (a3 was erased) + EXPECT_EQ(map.size(), 3u); +} + +// ----------------------------------------------------------------------- +// 5. Insert during snapshot iteration +// Snapshot loop must NOT see newly inserted entries. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, InsertDuringSnapshot) +{ + FakeAura a1(100), a2(200); + FakeAura a3(300); // will be inserted during snapshot iteration + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + + // Snapshot before iteration + std::vector<FakeAura*> snapshot; + snapshot.reserve(map.size()); + for (auto& [id, aura] : map) + snapshot.push_back(aura); + + ASSERT_EQ(snapshot.size(), 2u); + + // During iteration, insert a new entry + std::vector<uint32_t> processed; + for (FakeAura* aura : snapshot) + { + if (!aura->IsRemoved()) + { + processed.push_back(aura->spellId); + aura->updated = true; + } + + // Insert a3 while iterating the snapshot + if (aura == &a1) + map.emplace(300, &a3); + } + + // Only the original 2 were processed + ASSERT_EQ(processed.size(), 2u); + EXPECT_EQ(processed[0], 100u); + EXPECT_EQ(processed[1], 200u); + + // a3 was NOT processed (not in snapshot) + EXPECT_FALSE(a3.updated); + + // But it IS in the map for future iterations + EXPECT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(300), 1u); +} + +// ----------------------------------------------------------------------- +// 6. Predicate-based removal with reset-to-begin +// (mirrors RemoveOwnedAuras(std::function<bool(Aura const*)>)) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, PredicateRemovalResetBegin) +{ + FakeAura a1(100), a2(200), a3(300), a4(400), a5(500); + // Remove even-numbered spell IDs + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.emplace(400, &a4); + map.emplace(500, &a5); + + auto shouldRemove = [](FakeAura const* a) { + return (a->spellId % 200) == 0; + }; + + for (AuraMap::iterator iter = map.begin(); iter != map.end();) + { + if (shouldRemove(iter->second)) + { + map.erase(iter); + iter = map.begin(); // reset — mirrors RemoveOwnedAuras + continue; + } + ++iter; + } + + ASSERT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(100), 1u); + EXPECT_EQ(map.count(200), 0u); + EXPECT_EQ(map.count(300), 1u); + EXPECT_EQ(map.count(400), 0u); + EXPECT_EQ(map.count(500), 1u); +} + +// ----------------------------------------------------------------------- +// 7. Predicate removal within a key range with reset-to-lower_bound +// (mirrors RemoveOwnedAuras(spellId, check)) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, PredicateRemovalInKeyRange) +{ + FakeAura a1(100), a2(100), a3(100), a4(100); + a1.expired = false; + a2.expired = true; + a3.expired = false; + a4.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(100, &a4); + map.emplace(200, new FakeAura(200)); // different key, untouched + + uint32_t spellId = 100; + for (auto iter = map.lower_bound(spellId); + iter != map.upper_bound(spellId);) + { + if (iter->second->IsExpired()) + { + map.erase(iter); + iter = map.lower_bound(spellId); + continue; + } + ++iter; + } + + // 2 expired removed, 2 non-expired remain under key 100 + EXPECT_EQ(map.count(100), 2u); + // Key 200 untouched + EXPECT_EQ(map.count(200), 1u); + + // Clean up heap-allocated entry + for (auto& [id, aura] : map) + if (id == 200) + delete aura; +} + +// ----------------------------------------------------------------------- +// 8. AuraStateAuras erase-and-reset-to-lower_bound +// (mirrors _UnapplyAura's m_auraStateAuras cleanup) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, AuraStateMapErasePattern) +{ + // Uses an enum-like key (AuraStateType is an enum in the real code) + using AuraStateMap = boost::container::flat_multimap<uint32_t, FakeAura*>; + + FakeAura a1(10), a2(20), a3(30), a4(40); + AuraStateMap map; + // State 1 has multiple entries + map.emplace(1, &a1); + map.emplace(1, &a2); + map.emplace(1, &a3); + // State 2 has one entry + map.emplace(2, &a4); + + // Remove a2 from state 1 — mirrors _UnapplyAura pattern + uint32_t auraState = 1; + FakeAura* target = &a2; + for (auto itr = map.lower_bound(auraState); + itr != map.upper_bound(auraState);) + { + if (itr->second == target) + { + map.erase(itr); + itr = map.lower_bound(auraState); + continue; + } + ++itr; + } + + EXPECT_EQ(map.count(1), 2u); + EXPECT_EQ(map.count(2), 1u); + + // Verify a2 is gone, a1 and a3 remain + std::set<FakeAura*> stateOnes; + for (auto itr = map.lower_bound(1); itr != map.upper_bound(1); ++itr) + stateOnes.insert(itr->second); + EXPECT_TRUE(stateOnes.count(&a1)); + EXPECT_FALSE(stateOnes.count(&a2)); + EXPECT_TRUE(stateOnes.count(&a3)); +} + +// ----------------------------------------------------------------------- +// 9. Empty map edge cases — all patterns should be no-ops on empty maps +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EmptyMapPatterns) +{ + AuraMap map; + + // Snapshot of empty map + std::vector<FakeAura*> snapshot; + for (auto& [id, aura] : map) + snapshot.push_back(aura); + EXPECT_TRUE(snapshot.empty()); + + // Erase-reset-to-begin on empty map + for (auto i = map.begin(); i != map.end();) + { + map.erase(i); + i = map.begin(); + } + EXPECT_TRUE(map.empty()); + + // lower_bound/upper_bound on empty map + for (auto itr = map.lower_bound(100); itr != map.upper_bound(100);) + { + map.erase(itr); + itr = map.lower_bound(100); + } + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 10. Single-element map — boundary case for erase patterns +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, SingleElementPatterns) +{ + FakeAura a1(100); + a1.expired = true; + + AuraMap map; + map.emplace(100, &a1); + + // Erase-reset-to-begin with single element + for (auto i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + map.erase(i); + i = map.begin(); + } + else + ++i; + } + + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 11. Erase all via reset-to-begin (stress test) +// Every entry matches removal criteria. +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, EraseAllViaResetBegin) +{ + constexpr int N = 50; + std::vector<FakeAura> auras; + auras.reserve(N); + + AuraMap map; + for (int i = 0; i < N; ++i) + { + auras.emplace_back(static_cast<uint32_t>(i * 10)); + map.emplace(auras.back().spellId, &auras.back()); + } + + ASSERT_EQ(map.size(), static_cast<size_t>(N)); + + // Remove everything — every entry triggers erase + reset + for (auto i = map.begin(); i != map.end();) + { + map.erase(i); + i = map.begin(); + } + + EXPECT_TRUE(map.empty()); +} + +// ----------------------------------------------------------------------- +// 12. Duplicate keys with interleaved erase (multiple auras per spellId) +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, DuplicateKeysInterleavedErase) +{ + FakeAura a1(100), a2(100), a3(100), a4(200), a5(200); + a2.expired = true; + a5.expired = true; + + AuraMap map; + map.emplace(100, &a1); + map.emplace(100, &a2); + map.emplace(100, &a3); + map.emplace(200, &a4); + map.emplace(200, &a5); + + // Global erase-reset-to-begin for expired entries + for (auto i = map.begin(); i != map.end();) + { + if (i->second->IsExpired()) + { + map.erase(i); + i = map.begin(); + } + else + ++i; + } + + EXPECT_EQ(map.size(), 3u); + EXPECT_EQ(map.count(100), 2u); + EXPECT_EQ(map.count(200), 1u); +} + +// ----------------------------------------------------------------------- +// 13. Cascade-after-reset (Unit::RemoveAura contract): Aura::Remove can +// mutate the map after _UnapplyAura reset the iterator. The DISABLED_ +// test is deliberate UB - run it only under ASAN, where it reports a +// deterministic heap-use-after-free: +// ./unit_tests --gtest_also_run_disabled_tests --gtest_filter='*StaleIteratorAfterCascade*' +// ----------------------------------------------------------------------- +TEST(FlatMultimapAuraPattern, DISABLED_StaleIteratorAfterCascade_AsanOnly) +{ + FakeAura a1(100), a2(200), a3(300), cascade1(999), cascade2(998); + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.shrink_to_fit(); + + // caller loop holds an iterator + AuraMap::iterator iter = map.begin(); + + // RemoveAura: erase + reset, then cascades grow past capacity -> realloc + map.erase(iter); + iter = map.begin(); + map.emplace(999, &cascade1); + map.emplace(998, &cascade2); + + // resume with the pre-cascade iterator: heap-use-after-free under ASAN + EXPECT_NE(iter->second->spellId, 0u); +} + +// The fixed contract: reset after all cascade mutations (mirrors Unit::RemoveAura). +TEST(FlatMultimapAuraPattern, IteratorResetAfterCascade) +{ + FakeAura a1(100), a2(200), a3(300), cascade1(999), cascade2(998); + + AuraMap map; + map.emplace(100, &a1); + map.emplace(200, &a2); + map.emplace(300, &a3); + map.shrink_to_fit(); + + AuraMap::iterator iter = map.begin(); + + map.erase(iter); + map.emplace(999, &cascade1); + map.emplace(998, &cascade2); + iter = map.begin(); // reset AFTER the cascades + + std::set<uint32_t> seen; + for (; iter != map.end(); ++iter) + seen.insert(iter->second->spellId); + + EXPECT_EQ(seen.size(), 4u); + EXPECT_FALSE(seen.count(100)); // removed + EXPECT_TRUE(seen.count(200)); + EXPECT_TRUE(seen.count(300)); + EXPECT_TRUE(seen.count(999)); // cascade-applied + EXPECT_TRUE(seen.count(998)); +} diff --git a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp index 19084eb6f..fb16fce9f 100644 --- a/src/test/server/game/Events/HolidayDateCalculatorTest.cpp +++ b/src/test/server/game/Events/HolidayDateCalculatorTest.cpp @@ -1428,3 +1428,86 @@ TEST_F(FindStartTimeForStageTest, AllDatesPast_ReturnsZero) time_t result = HolidayDateCalculator::FindStartTimeForStage(dates, 26, stageOffset, stageLengthMin, curTime); EXPECT_EQ(result, 0); } + +// ============================================================================ +// FindLoopingStartTime tests (Battleground Call to Arms) +// ============================================================================ + +class FindLoopingStartTimeTest : public ::testing::Test +{ +protected: + static time_t MakeTime(int year, int month, int day, int hour = 0) + { + std::tm t = {}; + t.tm_year = year - 1900; + t.tm_mon = month - 1; + t.tm_mday = day; + t.tm_hour = hour; + t.tm_isdst = -1; + return mktime(&t); + } + + static uint32_t PackAnchor(int year, int month, int day, int hour, int minute) + { + uint32_t packed = 0; + packed |= (static_cast<uint32_t>(year - 2000) & 0x1F) << 24; + packed |= (static_cast<uint32_t>(month - 1) & 0xF) << 20; + packed |= (static_cast<uint32_t>(day - 1) & 0x3F) << 14; + packed |= (static_cast<uint32_t>(hour) & 0x1F) << 6; + packed |= (static_cast<uint32_t>(minute) & 0x3F); + return packed; + } + + static constexpr uint32_t OCCURENCE_MIN = 60480; // 42 day cycle (AV Call to Arms) +}; + +TEST_F(FindLoopingStartTimeTest, ZeroAnchor_ReturnsZero) +{ + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(0, 0, OCCURENCE_MIN, curTime), 0); +} + +TEST_F(FindLoopingStartTimeTest, FutureAnchor_NotRolled) +{ + uint32_t anchor = PackAnchor(2026, 10, 30, 0, 0); + time_t curTime = MakeTime(2026, 6, 1); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime), + MakeTime(2026, 10, 30)); +} + +// The bug: an ancient midnight anchor must roll forward preserving phase, not fall back to a stale start_time. +TEST_F(FindLoopingStartTimeTest, AncientAnchor_RollsToMostRecentOccurrence) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, 0, OCCURENCE_MIN, curTime); + + time_t const period = static_cast<time_t>(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26); + + EXPECT_LE(result, curTime); + EXPECT_GT(result + period, curTime); + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, StageOffsetApplied) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t stageOffset = 96 * 3600; + time_t curTime = MakeTime(2026, 2, 6, 12); + + time_t result = HolidayDateCalculator::FindLoopingStartTime(anchor, stageOffset, OCCURENCE_MIN, curTime); + + time_t const period = static_cast<time_t>(OCCURENCE_MIN) * 60; + time_t const base = MakeTime(2007, 10, 26) + stageOffset; + EXPECT_EQ((result - base) % period, 0); +} + +TEST_F(FindLoopingStartTimeTest, ZeroOccurence_ReturnsAnchor) +{ + uint32_t anchor = PackAnchor(2007, 10, 26, 0, 0); + time_t curTime = MakeTime(2026, 2, 6, 12); + EXPECT_EQ(HolidayDateCalculator::FindLoopingStartTime(anchor, 0, 0, curTime), + MakeTime(2007, 10, 26)); +}