Integrate safe bot shutdown and automatic login recovery
Drain clientless sessions during normal shutdown with a 60-second grace and recovery audit. Recover invalid bot login states while preserving real transports and taxis. Handle bounded database reconnect failures with controlled service termination and clear OutdoorPvP references before shutdown destruction. Prevent obsolete transport actions from overriding newer movement. Add regression coverage, live validation notes and the updated parallel -j10 build workflow in AGENTS.md. Full build, twelve regression tests and live login/shutdown/database-fault checks passed.
This commit is contained in:
1 parent
ba2a4e629c
commit
d1aceaf583
29 files changed
+1721
-142
No files matched your search
@@ -0,0 +1,370 @@
|
||||
# Vcore Development Environment
|
||||
|
||||
## Project
|
||||
|
||||
Vcore is a custom World of Warcraft Cataclysm 4.3.4 server based on TrinityCore.
|
||||
|
||||
This environment is a dedicated development and test server.
|
||||
|
||||
The current development focus is the Clientless Player Core and AdventureBot infrastructure.
|
||||
|
||||
## Repository and Git Rules
|
||||
|
||||
Repository:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/source/TrinityCore`
|
||||
|
||||
Authorized development branch:
|
||||
|
||||
`feature/clientless-player-core`
|
||||
|
||||
Rules:
|
||||
|
||||
- Work exclusively on the authorized development branch.
|
||||
- Never switch to, modify, reset, rebase, or merge `master`.
|
||||
- Never push to `master`.
|
||||
- Never push any branch without explicit user approval.
|
||||
- Local commits are permitted.
|
||||
- Never discard existing changes without approval.
|
||||
- Never force-push.
|
||||
- Check the current branch and working tree before making changes.
|
||||
- Stop and request clarification if the working tree contains unexpected changes.
|
||||
|
||||
## Development Permissions
|
||||
|
||||
Codex is authorized to:
|
||||
|
||||
- Read and modify Vcore source code.
|
||||
- Implement new features and fix bugs.
|
||||
- Run CMake and compile Vcore.
|
||||
- Investigate and resolve compilation errors.
|
||||
- Install compiled binaries into the testserver installation.
|
||||
- Start, stop, and restart the test worldserver.
|
||||
- Inspect logs and server output.
|
||||
- Modify test databases when required for an authorized task.
|
||||
- Execute local tests and create local commits.
|
||||
|
||||
Codex must never:
|
||||
|
||||
- Modify the production server.
|
||||
- Deploy changes to production.
|
||||
- Push changes without explicit approval.
|
||||
- Modify or merge the protected `master` branch.
|
||||
- Discard user changes without approval.
|
||||
|
||||
## Read-only Diagnostics
|
||||
|
||||
- Reading the development/test server logs and project files is already authorized.
|
||||
- Use direct read-only commands such as `rg`, `tail`, `sed`, `cat`, and `stat` inside the sandbox for ordinary file inspection. Do not request elevated execution preemptively for these reads.
|
||||
- Keep file inspection separate from tmux access, database queries, writes, and lifecycle actions so those actions do not unnecessarily escalate log reading.
|
||||
- Do not request blanket approval for Python, other interpreters, or shell execution to read logs.
|
||||
- If a read actually fails because of a technical sandbox restriction, follow the execution environment's escalation rules and request only the narrow access needed. Project authorization does not override the sandbox.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
Source:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/source/TrinityCore`
|
||||
|
||||
Build directory:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/build/debug`
|
||||
|
||||
Server installation:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/server/debug`
|
||||
|
||||
Worldserver executable:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/server/debug/bin/worldserver`
|
||||
|
||||
## Build and Installation
|
||||
|
||||
Use the existing CMake build configuration.
|
||||
|
||||
Inspect the build configuration before changing it.
|
||||
|
||||
Build directory:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/build/debug`
|
||||
|
||||
Install directory:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/server/debug`
|
||||
|
||||
After successful compilation, install the resulting binaries using the existing CMake installation configuration.
|
||||
|
||||
Do not overwrite running executables.
|
||||
|
||||
Stop the test worldserver cleanly before installing updated binaries.
|
||||
|
||||
Do not delete existing server configuration, logs, databases, or data files.
|
||||
|
||||
## Worldserver Management
|
||||
|
||||
The test worldserver runs inside a dedicated tmux session.
|
||||
|
||||
Session name:
|
||||
|
||||
`vcore-worldserver`
|
||||
|
||||
Working directory:
|
||||
|
||||
`/home/wow_admin/wow_cata_server/server/debug/bin`
|
||||
|
||||
Executable:
|
||||
|
||||
`./worldserver`
|
||||
|
||||
Codex is authorized to start, stop, restart, and recover the test worldserver autonomously.
|
||||
|
||||
### Starting the Worldserver
|
||||
|
||||
Before starting:
|
||||
|
||||
1. Check whether a worldserver instance is already running.
|
||||
2. Check whether the `vcore-worldserver` tmux session already exists.
|
||||
3. Never start a second worldserver instance while another instance is active.
|
||||
4. Remove stale tmux sessions only after verifying their state.
|
||||
|
||||
Start command:
|
||||
|
||||
```bash
|
||||
tmux new-session -d -s vcore-worldserver \
|
||||
-c /home/wow_admin/wow_cata_server/server/debug/bin \
|
||||
'./worldserver'
|
||||
```
|
||||
|
||||
### Startup Verification
|
||||
|
||||
A successfully created tmux session does not indicate successful worldserver startup.
|
||||
|
||||
The Vcore worldserver reports successful initialization with a final console message similar to:
|
||||
|
||||
```text
|
||||
World initialized in 0 minutes 10 seconds
|
||||
TrinityCore rev. <revision> (...) (worldserver-daemon) ready...
|
||||
```
|
||||
|
||||
The authoritative startup indicator is:
|
||||
|
||||
`(worldserver-daemon) ready...`
|
||||
|
||||
The revision, branch, build configuration, and initialization time may vary.
|
||||
|
||||
After starting the worldserver:
|
||||
|
||||
1. Monitor the worldserver process and console output.
|
||||
2. Wait for the `(worldserver-daemon) ready...` message.
|
||||
3. Verify that the worldserver process remains running.
|
||||
4. Check for critical startup errors.
|
||||
5. Verify that the configured worldserver port is listening.
|
||||
|
||||
Only report successful startup when these checks pass.
|
||||
|
||||
Capture console output from the beginning of the current startup attempt to avoid mistaking messages from an earlier run for current startup success.
|
||||
|
||||
If the worldserver exits, hangs, or fails to reach the ready state within a reasonable timeout, follow the startup failure recovery procedure.
|
||||
|
||||
|
||||
### Startup Failure Recovery
|
||||
|
||||
The test worldserver may occasionally fail to start because the remote authentication database on the VPS is temporarily unreachable.
|
||||
|
||||
When startup fails:
|
||||
|
||||
1. Capture relevant console output and logs.
|
||||
2. Determine whether the worldserver process is still running.
|
||||
3. Check whether the tmux session is still active.
|
||||
4. If the TrinityCore console is responsive, attempt a graceful shutdown.
|
||||
5. If the console is unavailable or unresponsive, terminate the failed worldserver process and its dedicated tmux session.
|
||||
6. Verify that the previous worldserver process has terminated.
|
||||
7. Ensure that no stale process remains before restarting.
|
||||
8. Wait briefly before retrying.
|
||||
9. Create a fresh tmux session and start the worldserver again.
|
||||
10. Repeat startup verification.
|
||||
|
||||
For startup recovery, Codex may terminate the dedicated test worldserver process and tmux session when graceful shutdown is impossible.
|
||||
|
||||
Do not terminate unrelated processes or tmux sessions.
|
||||
|
||||
### Retry Policy
|
||||
|
||||
Use a maximum of three startup attempts per restart operation.
|
||||
|
||||
Wait approximately 10 seconds between attempts.
|
||||
|
||||
If startup fails repeatedly:
|
||||
|
||||
- Stop retrying.
|
||||
- Preserve the relevant logs.
|
||||
- Report the observed errors.
|
||||
- Explain whether the failure appears related to database connectivity, configuration, compilation, or another cause.
|
||||
- Do not claim that the testserver is operational.
|
||||
|
||||
Do not attempt to repair the remote VPS authentication database or production infrastructure.
|
||||
|
||||
### TrinityCore Console Commands
|
||||
|
||||
Codex may execute TrinityCore console commands through tmux.
|
||||
|
||||
Example:
|
||||
|
||||
```bash
|
||||
tmux send-keys -t vcore-worldserver '.server shutdown 10' Enter
|
||||
```
|
||||
|
||||
Read console output:
|
||||
|
||||
```bash
|
||||
tmux capture-pane -p -t vcore-worldserver -S -100
|
||||
```
|
||||
|
||||
### Graceful Shutdown
|
||||
|
||||
For a normally running worldserver, use:
|
||||
|
||||
`.server shutdown 10`
|
||||
|
||||
Send the command through tmux:
|
||||
|
||||
```bash
|
||||
tmux send-keys -t vcore-worldserver '.server shutdown 10' Enter
|
||||
```
|
||||
|
||||
After issuing the command:
|
||||
|
||||
1. Wait for the worldserver to terminate.
|
||||
2. Verify that the process has stopped.
|
||||
3. Verify that the tmux session has ended.
|
||||
4. Only then install new binaries or start another worldserver instance.
|
||||
|
||||
If the console is unresponsive, Codex may terminate the dedicated test worldserver process.
|
||||
|
||||
Prefer SIGTERM before SIGKILL.
|
||||
|
||||
Use SIGKILL only when the process cannot be stopped normally.
|
||||
|
||||
### Restart Procedure
|
||||
|
||||
1. Verify the current worldserver state.
|
||||
2. If running normally, initiate graceful shutdown.
|
||||
3. If the console is unavailable, use the recovery shutdown procedure.
|
||||
4. Wait for complete process termination.
|
||||
5. Install successfully compiled binaries if required.
|
||||
6. Start a fresh tmux session.
|
||||
7. Verify successful worldserver initialization.
|
||||
8. If startup fails, follow the startup recovery procedure.
|
||||
9. Report the final server state.
|
||||
|
||||
### Safety
|
||||
|
||||
This is a dedicated development and test environment.
|
||||
|
||||
Codex is authorized to manage the test worldserver autonomously, including recovery from failed startup attempts.
|
||||
|
||||
Never terminate unrelated services or processes.
|
||||
|
||||
Never execute worldserver management commands on production infrastructure.
|
||||
|
||||
## AdventureBot Architecture
|
||||
|
||||
Preserve the existing Clientless Player Core architecture.
|
||||
|
||||
Important components include:
|
||||
|
||||
- ClientlessTravelMgr
|
||||
- ClientlessTravelGraphMgr
|
||||
- ClientlessActionExecutorMgr
|
||||
- Lua-based bot control
|
||||
- MMAP navigation
|
||||
- TravelGraph routing
|
||||
|
||||
Architecture principles:
|
||||
|
||||
- Clientless Player Core provides generic clientless player functionality.
|
||||
- AdventureBots are built on top of this infrastructure.
|
||||
- TravelGraph handles long-distance travel.
|
||||
- MMAP navigation handles local movement.
|
||||
- DungeonBots remain independent from AdventureBots.
|
||||
- AdventureBots must not become infrastructure dependencies for unrelated game systems.
|
||||
|
||||
Prefer extending the existing implementation over replacing it.
|
||||
|
||||
## Development Workflow
|
||||
|
||||
For each development task:
|
||||
|
||||
1. Verify the current Git branch and working tree.
|
||||
2. Inspect the relevant existing implementation.
|
||||
3. Plan nontrivial changes before implementation.
|
||||
4. Implement the requested functionality.
|
||||
5. Compile the affected components.
|
||||
6. Resolve compilation errors.
|
||||
7. Install the new build when appropriate.
|
||||
8. Restart the test worldserver when necessary.
|
||||
9. Inspect logs and available test results.
|
||||
10. Report what changed, what was tested, and what remains unverified.
|
||||
11. Wait for user feedback before pushing changes.
|
||||
|
||||
Do not claim that gameplay functionality is verified solely because compilation succeeded.
|
||||
|
||||
The user performs final in-game acceptance testing.
|
||||
|
||||
## Rollback
|
||||
|
||||
When requested, restore the previous implementation without modifying `master`.
|
||||
|
||||
Prefer targeted reversions of the relevant development changes.
|
||||
|
||||
Never use destructive Git commands without checking whether unrelated work would be lost.
|
||||
|
||||
Database rollbacks must be limited to the changes associated with the development task.
|
||||
|
||||
## General Principles
|
||||
|
||||
- Preserve existing Vcore functionality.
|
||||
- Avoid unnecessary architectural rewrites.
|
||||
- Prefer targeted, maintainable changes.
|
||||
- Investigate root causes instead of masking symptoms.
|
||||
- Keep the test environment reproducible.
|
||||
- Clearly distinguish successful builds, automated tests, and verified gameplay behavior.
|
||||
|
||||
|
||||
## Saved session / Wiederaufnahme
|
||||
|
||||
Before starting new work, read `doc/clientless/SESSION-RESUME.md`. It records the
|
||||
user-requested checkpoint, unfinished profession intent, agreed Lua professions,
|
||||
build/test state and exact next steps. Preserve the saved WIP; do not restart or
|
||||
revert it. All development/server/branch rules above remain in force.
|
||||
|
||||
## Build resource policy (user instruction updated 2026-11-04)
|
||||
|
||||
At the beginning of EVERY build, explicitly pause the AdventureBot loader and
|
||||
request safe logout of all clientless bots with `clientless logout safe all`.
|
||||
Apply the loader pause to the running server as well as its configuration. Start
|
||||
the CMake build with `-j10` immediately; compilation runs in parallel with the
|
||||
safe-logout drain and does not wait for zero bot sessions. Let combat, flight,
|
||||
taxi, portal and ship/tram transitions finish naturally. Do not individually
|
||||
force logout, detach passengers or interrupt flights to start compiling.
|
||||
|
||||
Do not initiate a server shutdown or restart at build start. After the build
|
||||
has completed successfully, use the normal `server shutdown 10` command. The
|
||||
integrated shutdown routine provides the safe-logout grace of up to 60 seconds
|
||||
and automatically persists the remaining bots' recovery/diagnostic snapshot.
|
||||
The previous separate five-minute waiting period and manual pre-shutdown bot
|
||||
snapshot are no longer required. If compilation fails, resolve the build errors
|
||||
before shutting down for installation.
|
||||
|
||||
Wait for complete worldserver process termination before installing or replacing
|
||||
binaries. Install using the existing CMake installation configuration, then
|
||||
restart the test worldserver and perform the normal startup verification.
|
||||
Keep the loader paused through compilation, installation and restart. Automatic
|
||||
login recovery handles bots left online at shutdown; routine individual checks,
|
||||
manual recovery lists and preventive capital teleports are no longer required.
|
||||
Investigate specific recovery errors if observed.
|
||||
|
||||
After successful startup verification, restore the previous loader/bot operation
|
||||
unless the user is still recording routes or has requested a continued pause.
|
||||
Record the build, installation, restart and any observed errors in the session
|
||||
checkpoint.
|
||||
@@ -0,0 +1,49 @@
|
||||
# Shutdown und Login-Recovery
|
||||
|
||||
Normale Shutdown-/Restart-Befehle und SIGTERM/SIGINT verwenden dieselbe Vorbereitung. Der Loader nimmt während einer geplanten Abschaltung keine neuen Bots auf. Die sichere Abmeldung beginnt in der letzten Minute des Countdowns; bei kürzeren Countdowns sofort. Nach Ablauf des angeforderten Countdowns wartet der Server nur noch bis zur sicheren Abmeldung aller Bots oder bis maximal 60 Sekunden seit Beginn der Vorbereitung. Ein ausdrücklich erzwungener Shutdown bleibt eine Notfall-Ausnahme.
|
||||
|
||||
Die Bot-Controller pausieren ihre Tagespläne, ohne sie zu löschen. Laufende Kämpfe, Flüge und Transportfahrten dürfen auslaufen. Ein Abbruch der Abschaltung gibt noch angemeldete Controller wieder frei. Bereits ausgeloggte Bots meldet der aktivierte Population-Loader nach seiner üblichen Auswahl erneut an.
|
||||
|
||||
`clientless logout status` zeigt Abmelde- und Shutdownzustand. `clientless population status` zeigt die Shutdownpause. `Clientless.Recovery.StateDir` bestimmt das Verzeichnis für JSONL-Protokolle mit Beginn, Abschluss/Fristablauf/Abbruch und den jeweiligen Bot-Zuständen. Diese Dateien werden auch bei Datenbankausfall geschrieben.
|
||||
|
||||
Beim Login prüfen AdventureBot-Controller die Position vor dem Start neuer Tagesplanaktionen. Tote oder kämpfende Bots bleiben bei ihrer bestehenden Behandlung. Reale Passagiere steigen zunächst über aufgezeichnete Halte aus; gültige Taxiflüge werden nicht unterbrochen. Eine tatsächlich ungültige Standposition wird bevorzugt lokal auf überprüften Boden gesetzt oder über einen kontrollierten Flug gelandet. Nur wenn das nicht sicher möglich ist, folgt die Fraktionshauptstadt. Gesunde Positionen und fehlende Meshdateien allein lösen keinen Teleport aus. Die generische Clientless-Infrastruktur bleibt unabhängig von AdventureBot-Policy; andere Controller müssen diesen Recovery-Hook nicht verwenden.
|
||||
|
||||
Datenbank-Verbindungsverluste werden höchstens fünfmal mit begrenzten Socket-Timeouts und drei Sekunden Abstand erneut versucht. Fehler 2002 wird ausdrücklich berücksichtigt. Leere Handles werden nicht an `mysql_ping` übergeben. Dauerhafter Ausfall meldet einen Fehler an den World-Thread; dieser schließt die Loginzulassung und leitet die normale Shutdownvorbereitung ein. Speicherung ist bei unerreichbarer Datenbank nicht garantiert und wird nicht als erfolgreich behauptet. Ein solcher terminaler Shutdown kann nicht in einen unbrauchbaren Weiterbetrieb abgebrochen werden.
|
||||
|
||||
|
||||
## Validierung auf dem Testserver am 4. November 2026
|
||||
|
||||
Vollbuild und abschließende Incrementalbuilds mit `-j10` erfolgreich; installiert
|
||||
über die vorhandene CMake-Konfiguration. Zwölf gezielte Produktionscode-
|
||||
Regressionstests bestanden (DB-Reconnect/Ping, Shutdown-Lifecycle, World-Timer,
|
||||
Login-Recovery, OutdoorPvP-Lebensdauer sowie sieben Transport-/Graph-Tests).
|
||||
Der 60-Sekunden-Fristablauf wurde mit kontrollierter monotoner Testuhr geprüft.
|
||||
|
||||
Live mit dem dedizierten Testcharakter Morvath (GUID 150):
|
||||
|
||||
- Gesunder Login in Orgrimmar ohne Positionsänderung.
|
||||
- Ungültige Position `(1780.166, -4333.466, 110.187)` wurde vor normalem
|
||||
Controllerstart auf gemessenen Boden bei Z `101.650` korrigiert.
|
||||
- Shutdown 60 Sekunden: sicherer Botlogout, Loader-Shutdownpause; Abbruch
|
||||
setzt die Shutdownpause wieder zurück.
|
||||
- Erneuter Login, Shutdown 10 Sekunden: Bot sicher ausgeloggt, JSONL-Beginn
|
||||
und Abschluss geschrieben, GDB meldet normales Prozessende ohne Signal.
|
||||
- Isolierter lokaler TCP-Proxy für die Charakterdatenbank unterbrochen:
|
||||
Fehler 2013, fünf fehlgeschlagene Verbindungsversuche, kontrollierter
|
||||
Shutdown mit Fehlercode 1, kein SIGSEGV/ABORT. Reguläre Bots blieben dabei
|
||||
offline; der echte MariaDB-Dienst wurde nicht angehalten oder verändert.
|
||||
|
||||
Temporäres Testprofil entfernt und Testcharakter offline an seine vorherige
|
||||
Position zurückgesetzt. Normaler Datenbankport ist wieder konfiguriert.
|
||||
Live-Gameplay mit echten Passagier-/Flug-Übergängen bei Login und mit Bots,
|
||||
die tatsächlich die volle Shutdownfrist überschreiten, bleibt zusätzlich der
|
||||
Ingame-Abnahme vorbehalten; die entsprechenden Entscheidungsfälle sind durch
|
||||
gezielte Regressionstests abgedeckt. Authserver gebaut, hier kein laufender
|
||||
Authdienst für einen separaten Live-Ausfalltest verändert.
|
||||
|
||||
Artefakte und native Belege: `checkpoints/2026-11-04-lifecycle/`.
|
||||
|
||||
Abschluss: normaler Serverbetrieb wiederhergestellt, alle ursprünglichen 118
|
||||
Bots online, Loader nativ und in der Konfiguration aktiv. Keine ausstehenden
|
||||
sicheren Abmeldungen. Auch alle 19 Restbots der alten Abschaltung erneut
|
||||
angemeldet und mit nativen Positions-/Flugproben dokumentiert.
|
||||
@@ -1,3 +1,371 @@
|
||||
# 2026-11-04 – Lokaler Commit auf Nutzerwunsch
|
||||
|
||||
Nutzer hat nun den lokalen Commit angefordert. Zusammengefasst werden die
|
||||
bereits gebauten und live geprüften Transport-, Shutdown-, DB-Verlust- und
|
||||
Login-Recovery-Korrekturen samt Regressionstests, Dokumentation und aktualisierter
|
||||
AGENTS.md-Buildregel. AGENTS.md war bislang lokal ausgeschlossen; auf expliziten
|
||||
Commitwunsch wird die Datei jetzt versioniert. Dumps, gesicherte Binaries,
|
||||
Laufzeitlogs und ältere unzugehörige Testartefakte bleiben außerhalb des Commits.
|
||||
Kein Push. Frühere Notiz „kein Commit angefordert“ ist damit überholt.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 – Buildablauf auf Nutzerwunsch aktualisiert
|
||||
|
||||
AGENTS.md: Bei jedem Buildbeginn Loader manuell wirksam pausieren und
|
||||
`clientless logout safe all` anfordern; Build sofort parallel mit `-j10`.
|
||||
Kein Shutdown bei Buildbeginn. Erst nach erfolgreichem Build normalen
|
||||
`server shutdown 10` verwenden; dessen integrierte 60s-Frist und automatische
|
||||
Restbot-Sicherung ersetzen separate 5min-Frist und manuelle Sicherung.
|
||||
Prozessende abwarten, installieren, neu starten, Startup verifizieren, bisherigen
|
||||
Loader-/Botbetrieb wiederherstellen. Keine routinemäßigen Einzelprüfungen oder
|
||||
präventiven Teleports nach Neustart; automatische Login-Recovery übernimmt.
|
||||
Nur Dokumentation angepasst, kein Build/Restart dafür ausgeführt.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 12:43 – Shutdown/DB/Login-Recovery fertig, Testserver wiederhergestellt
|
||||
|
||||
Implementierung vollständig gebaut (-j10), installiert und live getestet.
|
||||
Normaler Shutdown/Restart und SIGTERM nutzen generischen sicheren Botlogout mit
|
||||
maximal 60 Sekunden monotoner Schonfrist, Loaderpause und JSONL-Restbot-Audit.
|
||||
Abbruch stellt die vorherige Controllerfreigabe wieder her. Kein einzelnes
|
||||
Zwangslogout eines Passagiers/Fliegenden. Login-Recovery prüft Position/Zustand
|
||||
vor normalem AdventureControllerstart; gesunde Bots bleiben unverändert,
|
||||
reale Taxi-/Transportzustände werden bewahrt; geprüfter Boden/Landung vor
|
||||
Kapitalfallback. Bestehendes Transport-WIP weiter enthalten.
|
||||
|
||||
DB-Reconnect inklusive 2002 und nullsicherer Ping mit fünf begrenzten Versuchen;
|
||||
dauerhafter Ausfall -> World-Thread schließt Login und fährt kontrolliert mit
|
||||
Fehlercode1 herunter. Speicherung bei DB-Verlust ausdrücklich nicht garantiert.
|
||||
Authserver ebenfalls mit sauberem Fehlerausstieg (gebaut, nicht live getestet).
|
||||
OutdoorPvP löst gecachte ZoneScript-Zeiger vor Controllerzerstörung, behebt damit
|
||||
alten Kreatur-/GameObject-Shutdown-Absturz.
|
||||
|
||||
12 gezielte Regressionstests PASS. Live mit dediziertem Morvath150: gesunder
|
||||
Login unverändert; schwebende Position Z110.187 -> geprüfter Boden101.650;
|
||||
Shutdown60/Cancel pausiert/entpausiert Loader; Shutdown10 loggt sicher aus und
|
||||
GDB meldet normales Ende. Separater lokaler TCP-Proxy-Test mit regulären Bots
|
||||
OFFLINE: Verbindung2013, genau5 gescheiterte Opens, kontrolliertes Ende Code01,
|
||||
KEIN Signal/ABORT. Echter MariaDB-Dienst unverändert; Normalport3306 restored.
|
||||
Temporäres morvath.lua entfernt; Testbot150offline an Originalposition.
|
||||
|
||||
Alter Server147801 beim Shutdown noch bekannter Creature::RemoveFromWorld329
|
||||
SIGSEGV, Dump und passendes Binary bewahrt. Erst nach vollständigem Ende
|
||||
installiert; neuer normaler Shutdown und DB-Ausfall-Shutdown beide ohne Crash.
|
||||
Core: server/debug/crash/worldserver-20261104-110728.core, matching Binary
|
||||
checkpoints/2026-11-04-lifecycle/worldserver-before-lifecycle (groß, ignoriert).
|
||||
|
||||
Aktuell PID172527 unter passivem GDB, frisches Ready und Ports8085/8087 geprüft.
|
||||
Alle ursprünglichen118Bots wieder online. Loader Datei UND nativ1, schema1,
|
||||
busy0, errorleer, shutdownPaused0; SafeLogout pending0/sessions118. Alle19
|
||||
Restbots von altem Shutdown wieder angemeldet, native Probes+Recoveryausgänge
|
||||
in remaining-recovery-outcomes.json/login-recovery-native.log. Keine manuellen
|
||||
Kapitalteleports im Wiederanlauf nötig. Teils automatische lokale Korrektur,
|
||||
Landung oder geprüfter Kapitalfallback. Verstrichene Raidpläne nicht neu gesetzt.
|
||||
Kein Commit/Push angefordert oder ausgeführt. Alle vorbestehenden WIP bewahrt.
|
||||
|
||||
Artefakte: checkpoints/2026-11-04-lifecycle/{result.json,operation.json,
|
||||
remaining-before-shutdown.json,remaining-native-full.log,remaining-recovery-
|
||||
outcomes.json,roster-final.json,native-final.log,build*.log,install.log,
|
||||
startup-test.log,startup-database-fault.log,startup-restored.log,shutdown*.jsonl}.
|
||||
Dokumentation doc/clientless/2026-11-04-shutdown-recovery.md. Live mit echten
|
||||
Passagieren und voller60sDeadline bleibt Ingameabnahme; Tests decken Logik ab.
|
||||
Nutzer erhält Abschlussbericht; keine weitere aktive Beobachtung erforderlich.
|
||||
Diese Notiz ersetzt untenstehende historischen Zwischenstände.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 – Shutdown/DB/Login-Recovery Umsetzung, Installation ausstehend
|
||||
|
||||
Autorisiert: normaler Shutdown integriert sicheren Botlogout mit maximal EINER
|
||||
Minute, automatische geprüfte Login-Recovery und kontrollierter Shutdown bei
|
||||
dauerhaftem Datenbankverlust. DB-Reconnect bounded inkl. 2002, nullsicherer Ping;
|
||||
World signalisiert Fehler aus DB-Threads, statt ABORT. Authserver beendet sich
|
||||
kontrolliert ebenfalls. OutdoorPvP-Shutdown löst gecachte ZoneScript-Zeiger vor
|
||||
Controllerzerstörung, um bekannten alten Shutdowncrash zu beheben.
|
||||
|
||||
Branch feature/clientless-player-core, bestehendes Transport-WIP bewahrt.
|
||||
Vollbuild -j10 + Incrementalbuilds erfolgreich, 12 gezielte Tests bestanden.
|
||||
Letzte World.cpp-Korrektur für Zeitsprung über letzte Countdownminute benötigt
|
||||
noch Incrementalbuild. Keine Installation/kein neuer Live-Nachweis bislang.
|
||||
Alter Worldserver PID147801 läuft noch. Loader nativ/datei0, sicherer Logout
|
||||
seit 2026-11-04T12:06:19.822912+01:00; Nutzerfrist60s längst überschritten.
|
||||
Vor Shutdown verbleibende Bots mit Status sichern, dann sauber stoppen; altes
|
||||
Binary kann bekannten GameObject-Absturz noch zeigen. Erst Prozessende abwarten,
|
||||
dann installieren. Passiven GDB-Launcher per bash starten. Aktuelle Artefakte:
|
||||
checkpoints/2026-11-04-lifecycle/. Original118er-Roster dort gespeichert.
|
||||
Neue Tests: database_reconnect, shutdown_lifecycle, world_shutdown,
|
||||
login_recovery, outdoor_shutdown_lifetime; zusätzlich 7 Transport/Graph-Tests.
|
||||
|
||||
Nächste Schritte: finaler Build, Installation, dedizierten Testbot150 für
|
||||
Login-Recovery prüfen, kontrollierten Shutdown testen; DB-Ausfall ggf. per
|
||||
isoliertem lokalen TCP-Proxy (keinen DB-Dienst/keine VPS ändern). Loader während
|
||||
Tests0 lassen, danach ursprünglichen Botbetrieb/Loader1 wiederherstellen.
|
||||
Keine Commit-/Pushanweisung für diese Änderungen. Verstrichenen Raidtest nicht
|
||||
neu ansetzen. Aktuelle Notiz ersetzt historische Diagnose-only-Aussage unten.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 11:19 – Crash0634 Ursache nachgewiesen, noch kein DB-Fix
|
||||
|
||||
User fordertUntersuchung. Rootcausebelegt: automatischesSystemupdate06:33:21
|
||||
(libpcre2Upgrades) mitreguläremMariaDBRestart06:33:26–28. Worldlog2013lost,
|
||||
OpenFailed127.0.0.1/115, danachUnhandledMySQLerrno2002. _HandleMySQLErrno
|
||||
schließtMYSQL,setztnull,Openerror2002wirdnach3srekursivnichtbehandelt(nur2003
|
||||
CR_CONN_HOST_ERRORistRetryCase),Workerläuftmitnull+reconnectingtrueweiter.
|
||||
PingOperation ruft MySQLConnection::Ping -> mysql_ping OHNE null-Prüfung oder
|
||||
Rückgabebehandlung ->SIGSEGV. GDBCoregezieltbelegt this0x7ffff6b23500,
|
||||
m_Mysql0, m_reconnectingtrue, CONNECTION_ASYNC, DBcata_characters, RDI0/RBX0.
|
||||
OriginalLiveCrashThread5/LWP139811 istinNEUgeöffnetemCoreThread1! Keinthread5
|
||||
hartkodieren. OriginalbeimCrashgesicherterStackvollständig, erneuteCore-
|
||||
Unwindungdefekt(Stackdatennull); gezielteObjekt-/Registerwertegutlesbar.
|
||||
|
||||
Diagnose+GDBScripts/gezielteOutputs/Systemjournal/UpdateTimeline/SQLerrors in
|
||||
checkpoints/2026-11-04-crash-0634/. KeineSecrets ausgegeben(keinganzesConnInfo).
|
||||
ErforderlicherFix:2002undrelevanteConnectfehlerinbegrenzteRetrystrategie,
|
||||
Pingnullsicher+PingfehleranReconnectführen, nichtnurNULLskippendefektePool-
|
||||
Verbindungunendlichstehenlassen. TestsDrop->2002->erfolgreicherRetry+leererPing.
|
||||
UserhatbislangnurDiagnosebeauftragt, nochkeinenDB-Codegeändert/Build/Restart
|
||||
hier. AktuellerServer147801/118Botsweiterbetrieben; Transportfix-WIPunberührt.
|
||||
DateizeitCore06:34nichtalsgenauenSignalzeitpunktbehaupten. Systemupdates
|
||||
wurdennichtdeaktiviertodergeändert. HistorischerGameObjectShutdowncrashist
|
||||
andererPfad. NeueDiagnoseersetztvorige'Crashursachenochoffen'-Notiz.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 11:12 – Transportübergabe-Fix abgeschlossen, Botbetrieb wiederhergestellt
|
||||
|
||||
Transport-Cancel/Bewegungsbesitz-Fix gebaut-j10, installiert und livegeprüft.
|
||||
Alle8relevantenTestsPASS inklneuerRegressionen fürWaitTraceAbbruch/fremdeSpline/
|
||||
eigeneSpline/sicherenPassagierausstieg; DeckControl-TestdoubleGetGoTypeergänzt.
|
||||
AlteWelt128950nachvoller5minLogoutfristnormalShutdown02:01:19, dabei bekannter
|
||||
GameObject::RemoveFromWorld:651-Absturz; Core20261104-010947+passendesBinary
|
||||
worldserver-before-transport-fix gesichert. Erst nachvollständigemProzessende
|
||||
installiert. LokalerDBConnect kurz115,Retry erfolgreich. Sylaria34OFFLINE auf
|
||||
validiertesMMAP(1780.166,-4333.466,101.8971)korrigiert. NeustartPID139782
|
||||
02:03:37frischesReady/Portsverifiziert, Loader0 durchAufbau.
|
||||
|
||||
SylarialiefsofortnormalenBodenwegzumtaxi23, type0x1/complete1. Zusätzlich
|
||||
gezielterGraph-Test zumWindmeister erfolgreich02:05:37: actionCompleted,
|
||||
travelArrived,Spieler(1798.270,-4363.270,102.521), nearFeet102.521,
|
||||
keinFlug/Transport/Fallen. OriginalTagesplan wiederon. AlteTransportCancel-
|
||||
Bereinigung auch imLivebetrieb04:05/04:09/06:05für57/134/50 protokolliert.
|
||||
KeineerzwungeneReproduktioneinesrealenPassagierfehlers, diesdurchTestsabgedeckt.
|
||||
|
||||
Turnwurde02:07unterbrochen,Nutzererst11:06wiederaufgenommen. WährendPause
|
||||
neuerLAUFZEITabsturz06:34 imMariaDBClient mysql_ping, Thread5:
|
||||
MySQLConnection::Ping -> PingOperation::Execute -> DatabaseWorker::WorkerThread.
|
||||
Core20261104-020337+Backtrace bewahrt; matchingbinary worldserver-before-0634-
|
||||
crash imCheckpoint. KEINEUrsachenbehauptungoderdavonabgeleiteterDBFix;
|
||||
Crashursachenochoffen,separateUntersuchungmöglich. KeinweitererBuildnötig.
|
||||
|
||||
NachWiederaufnahmeServerautorisierterRecoveryneustartmitpassivemGDB, frisches
|
||||
Ready/Ports8085/8087+Prozess geprüft,jetztPID147801. Original118Botsreloggt,
|
||||
Scheduleon/Resume, LoaderDateiUNDnativ1 (reloadconfig+populationreload),
|
||||
pending0. Original20RestbotserneutaufFlight/Transport/geprüfteStandposition
|
||||
untersucht: 104/164/165 inzwischenwiederaufgültigemBoden, keineweiteren
|
||||
Kapitalteleportsnötig. Sylaria11:10lebendigimHeimatgebietmap530/45.363mitgleicher
|
||||
nearFeetHöhe;11:12DBbereitsmap0(2094.64,290.893,96.9704),415HPonline,
|
||||
folgtnormalem11UhrEconomyplanundreistweiter. 02UhrRaidpläneNICHTneuangesetzt,
|
||||
Zeitenverstrichen; ursprünglicherTestwarvomBuildkurzunterbrochen, keine
|
||||
weitereTestauswertungangefordert. QuelleWIPNICHTcommitted,keinPushauftrag.
|
||||
|
||||
Artefakte checkpoints/2026-11-04-transport-handover/: build/install/8Tests,
|
||||
OriginalRoster+20Recoveryliste/nativeVorherprobes,sylaria-before/corrected-
|
||||
offline/live-verified,ready/resume/final-native,result.json,Crash0634Frames,
|
||||
matchingBinaries (groß,gitlokalausgeschlossen). VorconfigReloadServerlogkopie
|
||||
bewahrt(Appenderw). Checkpointobenersetztältere'Installationausstehend'-Notiz.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:59 – Transportübergabe-Fix gebaut, Installation ausstehend
|
||||
|
||||
User beauftragtFixSylaria. Branchba2a/f8ee, SourcevorhercleanbisCheckpoint.
|
||||
Geändert: ActionExecutor::Clear fordertTransportCancel, StartAction cancelt
|
||||
alteRide synchronmitPlayer VOR neuerBewegung. TransportCancel anLand setzt
|
||||
terminalFailed/Cancelled sofort, echtePassagiere bleibenAbortbisSafeDock.
|
||||
Ride zeichnetnativeSpline-ID ihrerWarte-/Boarding-/Leavingbewegung auf.
|
||||
StopLandMovement darf NUR passendeeigeneSpline stoppen+MMClear; fremdeaktive
|
||||
Spline -> alteRideFailed OHNEStop. AbortprüfungVORWaitStage/Timeout/Recovery.
|
||||
WaitTrace darfRückweg nuranschließen, wennPlayernochamTraceende(<=1.5yd),
|
||||
sonstkeinegeradeLuftabkürzung. BestehendePassagier-/ElevatorRescuepfadebewahrt.
|
||||
|
||||
Build-j10erfolgreichabgeschlossen. ProduktionsTransporttests inklneu:
|
||||
synchron/deferredCancel, fremdeSplineunberührt, eigeneSplinegestoppt,
|
||||
versetzterWartepunktkeinLuftpfad und echterPassagierSafeDock bestehen.
|
||||
Weitere7Transport/Graph/Journeytests bestanden; test_transport_deck_control
|
||||
FixturehattebereitsfehlendesGetGoType/GAMEOBJECT_TYPE_TRANSPORT, minimalan
|
||||
bestehendenDeckcodeangepasst, danachPASS. NochkeineLiveGameplayverifikation.
|
||||
|
||||
SafeLogoutAll angefordert01:56:07.357571MEZ, LoaderDatei0 UND nachreloadconfig
|
||||
populationreload nativ enabled0 bestätigt. Original118Roster+SylariaSnapshot
|
||||
in checkpoints/2026-11-04-transport-handover/. LetzteRest20Sessions/14Flug/
|
||||
5Kampf, fünfminExceptionfrühestens02:01:08. InstallationNOCHNICHTerfolgt,
|
||||
ServerPID128950läuft. VorShutdown RestRoster+nativeProbes/Transport/letzte
|
||||
verfügbareCombatTelemetrieundAnforderungszeit sichern. Fristabhängig normal
|
||||
server shutdown10, passiveGDB beiAltabsturzEndeabwarten, altesBinarybewahren,
|
||||
cmakeinstall,nachfrischemReady/Ports/Prozess prüfen. Launcher mitBASH starten
|
||||
(Scriptnichtausführbar!).
|
||||
|
||||
Sylaria34muss nachProzessende/offline gezielt aufvalidierteslokalesMMAP
|
||||
(1780.166,-4333.466,101.8971),map1,Transport0/taxi_pathleer zurückgesetzt
|
||||
werden (vorherSnapshotvorhanden). Am besten zuerstalleinSylarialogin, native
|
||||
ProbeamgleichenXY(Boden101.656, SollPlayer101.897)undnormaleNAV-Bewegung
|
||||
verifizieren, dannandere117Bots reloggen; Restflug-/Transportlisteprüfen,
|
||||
fallsnötigKapitalrecovery (races1/3/4/7/11/22=Alliance!). LoaderdurchStartup0
|
||||
belassen; amEndeScheduleon/SafeLogoutresume undLoaderDatei1+reloadconfig+
|
||||
populationreload,nativ enabled1 bestätigen. VorConfigReload Server.logkopie
|
||||
bewahren, Appenderwtrunkiert. Aktueller10erTest159–167+176 Pläne01/02Uhr
|
||||
NICHTändern, läuftdurchBuildpausekurzunterbrochen; nachRelogvorhandene
|
||||
Plan-/Visit-/Handoffzuständeresumierenlassen. KeinneuerTestreset angefordert.
|
||||
KeineCommit/PushAuftragfürdiesenFix; SourceWIPbewahren.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 – Sylaria/Doras Höhenvergleich
|
||||
|
||||
AufNutzerhinweis direkterKoordinatenvergleich: SylariaServerZ110.187,
|
||||
DorasCreature3310/Spawn310964 DBPos(1799.95,-4370.14,102.933), Travelnode
|
||||
taxi_23Z102.848. Sylaria liegt7.254ydüberDoras bzw7.339ydüberNode; lokales
|
||||
MMAPdirektunterihr101.8971 ->8.2899yd. StütztNutzervermutung sichtbare
|
||||
BodendarstellungabweichendvonserverseitigerHöhe. KeineMeshhorizontalLücke,
|
||||
ServerpositionnichtaufnaherBodenhöhe. DorasKoordinatenDBSpawn, nichtfrisch
|
||||
liveCreatureprobed. KeineBewegungs-/Codeänderung, nativeSylariaProbeangefordert.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:49 – Sylaria ohne Servercodeänderung weiter untersucht
|
||||
|
||||
Nutzer fragt explizit nach weitererDiagnoseOHNECodeänderung. Vorhandene
|
||||
Serverbefehle gelesen; mmap path/loc,gps,navtest benötigenIngameSession,
|
||||
überKonsole nicht direktnutzbar. StattdessenMMAP-Datei ausschließlichLESEND
|
||||
mit Pythonzerlegt (keinBuild/Serverneustart/Loglevel-/Configänderung).
|
||||
DiagnosehelperundResultat: checkpoints/2026-11-04-sylaria-navigation/
|
||||
read_mesh.py + mesh-result.json. Format gegenDetourNavMesh.h/MMapDefines.h
|
||||
geprüft, vollständigeSegmentgrößen/Dateigrößeassertiert, Detailtriangulierung,
|
||||
GroundflagsundXYProjektion ausgewertet. map1Kachel0012840.mmtile decktOrt ab.
|
||||
DirektunterserverXY(1780.166,-4333.466)begehbaresPolygon4344 Flags1 mit
|
||||
Detailhöhe101.8971; serverZ110.187 ->8.2899ydDistanz. Polygon4345nebenan
|
||||
101.8963,Distanz8.2908; keinegespeichertebegehbareFlächeaufZ110imlokalen
|
||||
Suchbereich. AndereStockwerke-7.43/-24.95ebenfallsvorhanden. Keinehorizontale
|
||||
Meshlücke aufmap1: ProblemistHöhenzuordnung. Passtzum7ydFARFROMPOLY_START-
|
||||
Gate undSucherweiterung5->50ydZ, aberOfflineanalyseübersteuertkeineRuntime-
|
||||
Phasen/Filter. KonkreteIngameStandfläche oderClient/Serverpositionsabweichung
|
||||
weiteroffen; 'schwebt' weiterhinNICHTalsbewiesenbehaupten. AlteTransport-
|
||||
Überlagerung weiterhinhistorischbelegt. AllitesterGUID21lautDBoffline, daher
|
||||
keinVergleichseineraktuellenClientStandhöhe. KeineBotsversetzt,keinePläne
|
||||
geändert, aktueller02Uhr10erTestunberührt.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:45 – Korrektur Sylaria: sichtbarer Boden != MMAP-Anschluss
|
||||
|
||||
Nutzer steht beiSylaria und bestätigt sie STEHT AUFBODEN naheWindmeister/
|
||||
Groundnode/aufgezeichneterKante. VorigeBehauptung 'schwebt8.5yd' zurückgenommen:
|
||||
globaleFlight-Surface101.656 vsPlayer110.187 beweistkeinenfehlendenBoden,
|
||||
insbesonderebeiOrgrimmarsmehrerenEbenen/Objektgeometrie. Nichtalsgesicherte
|
||||
Teleportindikationverwenden. NäheresCM/meshnearestpointnochNICHTgemessen.
|
||||
|
||||
Gesichert: nativerPathGenerator typ0x44=INCOMPLETE|FARFROMPOLY_START.
|
||||
GetPolyByLocation sucht3ydXY/5ydZ, danach50ydZ; DistanzzumgefundenenPoly>7yd
|
||||
setztFlag (3D-Abstand, nichtnurZ). KeinallgemeinerHindernisnachweis. Vorhandene
|
||||
Kante1818 taxi_23->orgrimmar_zeppelintower1 nächsterPunktindex3
|
||||
(1772.6,-4344.47,102.384):13.35ydhorizontal/15.46yd3D. taxi_23
|
||||
(1798.27,-4363.27,102.848), ca35.7yd3D. Virtualgroundentry wirdversucht,aber
|
||||
GraphMgr verlangt vollständigenMMAPConnector; derselbeStartmeshfehler sperrt
|
||||
auchEintrittinaufgezeichneteKante. NativeGrundpfadefindenweiterPunktebisZiel
|
||||
FinalDistance0, werdenaberwegenStartfarflag alsincompletezurückgewiesen.
|
||||
|
||||
Ableitung: lokaleStandflächen-/MMAPEbenenzuordnungundgeometrischvalidierten
|
||||
kurzenEinstiegzumMesh/recordedroadprüfen stattPauschalteleport/Radiusaufweiten.
|
||||
UrsprünglicheTransportbewegungsüberlagerung23:19weiterbelegt, darausfolgtaber
|
||||
NICHT,dassheutigeStandpositionfreischwebendist. KeineSource/Laufzeitänderungen,
|
||||
keineBotsversetzt. NativeProbesunverändertePosition. KeineBau-/Neustartaktion.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:41 – Sylaria34 Navigationsdiagnose, noch kein Fix/Teleport
|
||||
|
||||
Nutzer meldetStillstandinOrgrimmar. Sylaria34 BlutelfinLevel1, lebendig415HP,
|
||||
Position(1780.166,-4333.466,110.187),keinFlug/Taxi/Transport/Combat aktuell.
|
||||
Professionzielpoi_npc_294059 wechselndeAlternativen, alleAnschlüsse MMAP
|
||||
0x44 = INCOMPLETE|FARFROMPOLY_START, nichtZielproblem. Groundsurfaceglobal
|
||||
101.656,aktuelleZ110.187; ca8.53ydüberOberfläche. NearFeetmit6ydSuche
|
||||
liefert-25.447 (nichtohneweitereValidierungalsStandflächeinterpretieren!).
|
||||
Alle5RecoveryAlternativen scheiternamgleichenungültigenStart, 30sneuerIntent-
|
||||
versuchverwaltetneueBlocklisten ->wiederholteStillstandsschleife.
|
||||
|
||||
UrsprungvorletztemBuild: 2026-11-03 23:17:20 wartetamZeppelinOrg->Thunder.
|
||||
23:19:12 neuerGroundLocalBridge ZeppelinOrgThunder->taxi23 (MMAPvollständig),
|
||||
läuftkorrektTreppehinunter,23:19:44naheGreifenmeisterZ102.943. Dannumkehrende
|
||||
Bewegungnachoben,23:19:47positionjump15.34,23:19:49left-planned-path11.97 und
|
||||
StopbeiheutigerPositionZ110.187. GleichzeitigTransportStateFailed:
|
||||
'return to shared transport node did not finish' füralteOrg->Thunder-Ride.
|
||||
SourceTransportMgr::Cancel setztNURAbort, WaitingWaitStage2/3 verarbeitet
|
||||
RückkehrWaitTrace/MoveSmoothPathVORAbortcheck. KeineBewegungseigentumsprüfung,
|
||||
soalteRückkehrüberlagertneuenBodenweg undTransportFail StopMoving stopptBot
|
||||
zwischenEbenen. Seitdem inSnapshots01:03undheutenochidentischePosition.
|
||||
|
||||
Ableitung: TransportabbruchanLand vollständigabschließen/Bewegungsbesitz
|
||||
übergeben, bevornächsteNavigationstartet; alterTransportdarffremdeSplinen
|
||||
wederersetzennochStopMovingaufrufen. RealePassagiereweiteramsicherenDock
|
||||
abwickeln. SylariaaufvalidiertenBodenpunktwiederherstellenfallsbeauftragt.
|
||||
NurDiagnose,keineSourceänderung/Teleport/Planänderung/Build. Aktueller10er-
|
||||
02UhrTestunberührt. BelegeaktuelleNavigation+Rotationen, nativeStatusim
|
||||
flight-progress/startup.log; PathGenerator.hdefiniert0x44.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:38 – Correns Leiche in Orgrimmar erklärt (nur Diagnose)
|
||||
|
||||
Corren102 ist Allianz-Worgen/race22/Level69; aktuellonline,lebendig7980HP,
|
||||
Sturmwind(-8833.07,622.778,93.9317), nativeTelemetriealive1 bestätigt.
|
||||
Character.corpse enthält nochseine Leiche Orgrimmar(1569.97,-4397.41,16.0472),
|
||||
corpseType1, time1793720693 =2026-11-03 16:44:53MEZ. Ursache eigener damaliger
|
||||
Fehlteleport beiRecovery: takeoff-recovery/startup.log Zeile3678/3684 tele name
|
||||
Corren Orgrimmar / bestätigterTeleport, danach COMBAT_FLEE_START102,
|
||||
COMBAT_FLEE_BLOCKED102 und DEATH_RELEASE102 Zeile3702. Erst danach Zeile3858/
|
||||
3860 KorrekturteleportStormwind. TötendesObjekt nichtgenaugeloggt, keineWachen-
|
||||
identität behaupten. Nutzer überFehler informiert, keine aktuelleBotreise nach
|
||||
Orgrimmar. KeineCodeänderung/Teleport/Leichenlöschung in dieserDiagnose.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 01:35 – Wiederholung 10er-Test: 01:00 Formation / 02:00 Bastion
|
||||
|
||||
Nutzer wünscht erneuten bekannten Test, jetzt Gruppe01Uhr/Raid02Uhr MEZ.
|
||||
10Bots159–167+176, alle85, 2Tanks167/176,2Heiler161/162,6DPS. Aktuelle
|
||||
Stunde01Uhr läuft, Aufbau01:33–01:35. Loaderkonfiguration vorübergehend0,
|
||||
gezielter Logout NUR dieser10 zum vollständigen Reset autorisiert imTest.
|
||||
VorherPläne/Visits/Handoffs/Positionen gesichert. Alle10 offline geprüft,
|
||||
Tagespläne20261104 Stunde1 groupformation/the_gilded_rose und2 raid/
|
||||
instance_visit_at_6354_d0 geschrieben und20Zeilen VOR Positionsänderung
|
||||
verifiziert. AlteVisit-/HandoffdatennurTestgruppe gelöscht, offlineGoldeneRose
|
||||
map0/instance0(-8864.73,669.015,97.9031), Transportoffsets/taxi_path geleert.
|
||||
Alle10 wiederonline, kurzStop und native interaction <guid> leave AUSSERHALB
|
||||
Instanz; danach altegroup_member fürdiese10 leer. Scheduleon gesetzt.
|
||||
Zunächst ein paarStopzustände während sicheremHandover; erneut abgefragt:
|
||||
ALLE10 nativ schedule1/groupformation/the_gilded_rose/Deadline02:00 bestätigt.
|
||||
Native Gruppenzuteilung läuft normal, keine künstlicheRaidfüllung. Keine
|
||||
Dauerbeobachtung, Nutzer fordert Auswertung später selbst an.
|
||||
|
||||
WICHTIG ConfigCache: Dateiänderung Loader.Enabled plus population reload
|
||||
allein lädt worldserver.conf NICHT neu. Für tatsächliche Wirksamkeit zuerst
|
||||
reload config, dann clientless population reload. Hier Loader1 wieder inDatei
|
||||
UND nativ enabled1 verifiziert. In vorigen Operationen war Datei1, nativnoch0;
|
||||
dieser Unterschied ist jetzt behoben. Künftige Build-Drainpausen müssen auch
|
||||
natives enabled0 bestätigen, nicht nurDateiänderung. KeinBuild/Restart hier.
|
||||
ServerPID128950 weiteraktiv, Sourcecommits ba2a4e629c/f8ee1d0ff6 installiert.
|
||||
Artefakte checkpoints/2026-11-04-0200-raid-test/: setup.py,VorherSnapshots,
|
||||
Manifest,20verifiziertePlanzeilen,OfflineRose und finale10NativeBestätigungen.
|
||||
Lesender ersterSetupversuch scheiterte unbekannterDBSpalteplayerFlags VOR
|
||||
Mutation; korrigiert undSnapshoterhalten. NativeLeaveSyntax erstkorrigiert,
|
||||
danachMembershipsleer verifiziert. Ergebnisse vollständig, keineoffenenReset-
|
||||
Schritte. AndereBotpläne unverändert, normalerLoaderbetrieb wiederaktiv.
|
||||
|
||||
---
|
||||
|
||||
# 2026-11-04 – Lokale Commits auf Nutzerwunsch
|
||||
|
||||
Instanzgruppen-Schutz separat committed als f8ee1d0ff6. Flugfortschritts-
|
||||
|
||||
@@ -28,6 +28,7 @@
|
||||
#include "Banner.h"
|
||||
#include "Config.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "MySQLConnection.h"
|
||||
#include "DatabaseLoader.h"
|
||||
#include "DeadlineTimer.h"
|
||||
#include "GitRevision.h"
|
||||
@@ -74,6 +75,7 @@ bool StartDB();
|
||||
void StopDB();
|
||||
void SignalHandler(std::weak_ptr<Trinity::Asio::IoContext> ioContextRef, boost::system::error_code const& error, int signalNumber);
|
||||
void KeepDatabaseAliveHandler(std::weak_ptr<Trinity::Asio::DeadlineTimer> dbPingTimerRef, int32 dbPingInterval, boost::system::error_code const& error);
|
||||
void CheckDatabaseHealth(std::weak_ptr<Trinity::Asio::DeadlineTimer>, std::weak_ptr<Trinity::Asio::IoContext>, boost::system::error_code const&);
|
||||
void BanExpiryHandler(std::weak_ptr<Trinity::Asio::DeadlineTimer> banExpiryCheckTimerRef, int32 banExpiryCheckInterval, boost::system::error_code const& error);
|
||||
variables_map GetConsoleArguments(int argc, char** argv, fs::path& configFile, std::string& configService);
|
||||
|
||||
@@ -211,7 +213,12 @@ int main(int argc, char** argv)
|
||||
#endif
|
||||
|
||||
// Start the io service worker loop
|
||||
auto dbHealthTimer=std::make_shared<Trinity::Asio::DeadlineTimer>(*ioContext);
|
||||
dbHealthTimer->expires_after(std::chrono::seconds(1));
|
||||
dbHealthTimer->async_wait(std::bind(&CheckDatabaseHealth,std::weak_ptr<Trinity::Asio::DeadlineTimer>(dbHealthTimer),
|
||||
std::weak_ptr<Trinity::Asio::IoContext>(ioContext),std::placeholders::_1));
|
||||
ioContext->run();
|
||||
dbHealthTimer->cancel();
|
||||
|
||||
banExpiryCheckTimer->cancel();
|
||||
dbPingTimer->cancel();
|
||||
@@ -220,7 +227,7 @@ int main(int argc, char** argv)
|
||||
|
||||
signals.cancel();
|
||||
|
||||
return 0;
|
||||
return MySQLConnection::HasFatalConnectionError() ? 1 : 0;
|
||||
}
|
||||
|
||||
/// Initialize connection to the database
|
||||
@@ -257,6 +264,23 @@ void SignalHandler(std::weak_ptr<Trinity::Asio::IoContext> ioContextRef, boost::
|
||||
ioContext->stop();
|
||||
}
|
||||
|
||||
void CheckDatabaseHealth(std::weak_ptr<Trinity::Asio::DeadlineTimer> timerRef,
|
||||
std::weak_ptr<Trinity::Asio::IoContext> contextRef, boost::system::error_code const& error)
|
||||
{
|
||||
if (error) return;
|
||||
auto context=contextRef.lock();
|
||||
auto timer=timerRef.lock();
|
||||
if (!context || !timer) return;
|
||||
if (MySQLConnection::HasFatalConnectionError())
|
||||
{
|
||||
TC_LOG_ERROR("server.authserver", "Database unavailable: stopping authentication service cleanly.");
|
||||
context->stop();
|
||||
return;
|
||||
}
|
||||
timer->expires_after(std::chrono::seconds(1));
|
||||
timer->async_wait(std::bind(&CheckDatabaseHealth,timerRef,contextRef,std::placeholders::_1));
|
||||
}
|
||||
|
||||
void KeepDatabaseAliveHandler(std::weak_ptr<Trinity::Asio::DeadlineTimer> dbPingTimerRef, int32 dbPingInterval, boost::system::error_code const& error)
|
||||
{
|
||||
if (!error)
|
||||
|
||||
@@ -31,6 +31,8 @@
|
||||
#include "MySQLWorkaround.h"
|
||||
#include <mysqld_error.h>
|
||||
|
||||
std::atomic<bool> MySQLConnection::_fatalConnectionError{false};
|
||||
|
||||
MySQLConnectionInfo::MySQLConnectionInfo(std::string const& infoString)
|
||||
{
|
||||
std::vector<std::string_view> tokens = Trinity::Tokenize(infoString, ';', true);
|
||||
@@ -95,10 +97,13 @@ uint32 MySQLConnection::Open()
|
||||
|
||||
int port;
|
||||
char const* unix_socket;
|
||||
//unsigned int timeout = 10;
|
||||
unsigned int timeout = 2;
|
||||
|
||||
mysql_options(mysqlInit, MYSQL_SET_CHARSET_NAME, "utf8");
|
||||
//mysql_options(mysqlInit, MYSQL_OPT_READ_TIMEOUT, (char const*)&timeout);
|
||||
mysql_options(mysqlInit, MYSQL_OPT_CONNECT_TIMEOUT, &timeout);
|
||||
timeout = 3;
|
||||
mysql_options(mysqlInit, MYSQL_OPT_READ_TIMEOUT, &timeout);
|
||||
mysql_options(mysqlInit, MYSQL_OPT_WRITE_TIMEOUT, &timeout);
|
||||
#ifdef _WIN32
|
||||
if (m_connectionInfo.host == ".") // named pipe use option (Windows)
|
||||
{
|
||||
@@ -167,7 +172,7 @@ bool MySQLConnection::PrepareStatements()
|
||||
|
||||
bool MySQLConnection::Execute(char const* sql)
|
||||
{
|
||||
if (!m_Mysql)
|
||||
if (!m_Mysql || m_failed)
|
||||
return false;
|
||||
|
||||
{
|
||||
@@ -194,7 +199,7 @@ bool MySQLConnection::Execute(char const* sql)
|
||||
|
||||
bool MySQLConnection::Execute(PreparedStatementBase* stmt)
|
||||
{
|
||||
if (!m_Mysql)
|
||||
if (!m_Mysql || m_failed)
|
||||
return false;
|
||||
|
||||
uint32 index = stmt->m_index;
|
||||
@@ -218,7 +223,7 @@ bool MySQLConnection::Execute(PreparedStatementBase* stmt)
|
||||
if (_HandleMySQLErrno(lErrno)) // If it returns true, an error was handled successfully (i.e. reconnection)
|
||||
return Execute(stmt); // Try again
|
||||
|
||||
m_mStmt->ClearParameters();
|
||||
if (!m_failed && m_Mysql) m_mStmt->ClearParameters();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -230,7 +235,7 @@ bool MySQLConnection::Execute(PreparedStatementBase* stmt)
|
||||
if (_HandleMySQLErrno(lErrno)) // If it returns true, an error was handled successfully (i.e. reconnection)
|
||||
return Execute(stmt); // Try again
|
||||
|
||||
m_mStmt->ClearParameters();
|
||||
if (!m_failed && m_Mysql) m_mStmt->ClearParameters();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -242,7 +247,7 @@ bool MySQLConnection::Execute(PreparedStatementBase* stmt)
|
||||
|
||||
bool MySQLConnection::_Query(PreparedStatementBase* stmt, MySQLResult** pResult, uint64* pRowCount, uint32* pFieldCount)
|
||||
{
|
||||
if (!m_Mysql)
|
||||
if (!m_Mysql || m_failed)
|
||||
return false;
|
||||
|
||||
uint32 index = stmt->m_index;
|
||||
@@ -266,7 +271,7 @@ bool MySQLConnection::_Query(PreparedStatementBase* stmt, MySQLResult** pResult,
|
||||
if (_HandleMySQLErrno(lErrno)) // If it returns true, an error was handled successfully (i.e. reconnection)
|
||||
return _Query(stmt, pResult, pRowCount, pFieldCount); // Try again
|
||||
|
||||
m_mStmt->ClearParameters();
|
||||
if (!m_failed && m_Mysql) m_mStmt->ClearParameters();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -279,7 +284,7 @@ bool MySQLConnection::_Query(PreparedStatementBase* stmt, MySQLResult** pResult,
|
||||
if (_HandleMySQLErrno(lErrno)) // If it returns true, an error was handled successfully (i.e. reconnection)
|
||||
return _Query(stmt, pResult, pRowCount, pFieldCount); // Try again
|
||||
|
||||
m_mStmt->ClearParameters();
|
||||
if (!m_failed && m_Mysql) m_mStmt->ClearParameters();
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -312,7 +317,7 @@ ResultSet* MySQLConnection::Query(char const* sql)
|
||||
|
||||
bool MySQLConnection::_Query(const char* sql, MySQLResult** pResult, MySQLField** pFields, uint64* pRowCount, uint32* pFieldCount)
|
||||
{
|
||||
if (!m_Mysql)
|
||||
if (!m_Mysql || m_failed)
|
||||
return false;
|
||||
|
||||
{
|
||||
@@ -419,17 +424,33 @@ int MySQLConnection::ExecuteTransaction(std::shared_ptr<TransactionBase> transac
|
||||
|
||||
size_t MySQLConnection::EscapeString(char* to, const char* from, size_t length)
|
||||
{
|
||||
if (!m_Mysql || m_failed) { *to = '\0'; return 0; }
|
||||
return mysql_real_escape_string(m_Mysql, to, from, length);
|
||||
}
|
||||
|
||||
void MySQLConnection::Ping()
|
||||
{
|
||||
mysql_ping(m_Mysql);
|
||||
if (m_failed) return;
|
||||
if (!m_Mysql)
|
||||
{
|
||||
_HandleMySQLErrno(CR_CONNECTION_ERROR);
|
||||
return;
|
||||
}
|
||||
if (mysql_ping(m_Mysql))
|
||||
_HandleMySQLErrno(mysql_errno(m_Mysql));
|
||||
}
|
||||
|
||||
void MySQLConnection::FailConnection()
|
||||
{
|
||||
m_failed = true;
|
||||
_fatalConnectionError.store(true);
|
||||
TC_LOG_FATAL("sql.sql", "Database '%s' unavailable after bounded reconnect attempts; requesting controlled world shutdown. Unsaved changes cannot be guaranteed.",
|
||||
m_connectionInfo.database.c_str());
|
||||
}
|
||||
|
||||
uint32 MySQLConnection::GetLastError()
|
||||
{
|
||||
return mysql_errno(m_Mysql);
|
||||
return (m_Mysql && !m_failed) ? mysql_errno(m_Mysql) : CR_SERVER_LOST;
|
||||
}
|
||||
|
||||
bool MySQLConnection::LockIfReady()
|
||||
@@ -513,59 +534,46 @@ bool MySQLConnection::_HandleMySQLErrno(uint32 errNo, uint8 attempts /*= 5*/)
|
||||
case CR_SERVER_GONE_ERROR:
|
||||
case CR_SERVER_LOST:
|
||||
case CR_SERVER_LOST_EXTENDED:
|
||||
case CR_CONNECTION_ERROR:
|
||||
case CR_CONN_HOST_ERROR:
|
||||
case CR_SOCKET_CREATE_ERROR:
|
||||
case CR_IPSOCK_ERROR:
|
||||
case CR_UNKNOWN_HOST:
|
||||
{
|
||||
if (m_failed) return false;
|
||||
m_reconnecting = true;
|
||||
if (m_Mysql)
|
||||
{
|
||||
TC_LOG_ERROR("sql.sql", "Lost the connection to the MySQL server!");
|
||||
|
||||
// Statement handles belong to the old connection. Destroy them
|
||||
// before closing MYSQL, never after reconnecting another handle.
|
||||
for (auto& statement : m_stmts) statement.reset();
|
||||
mysql_close(m_Mysql);
|
||||
m_Mysql = nullptr;
|
||||
}
|
||||
[[fallthrough]];
|
||||
}
|
||||
case CR_CONN_HOST_ERROR:
|
||||
{
|
||||
TC_LOG_INFO("sql.sql", "Attempting to reconnect to the MySQL server...");
|
||||
|
||||
m_reconnecting = true;
|
||||
|
||||
uint32 const lErrno = Open();
|
||||
if (!lErrno)
|
||||
for (uint8 attempt = 0; attempt < attempts; ++attempt)
|
||||
{
|
||||
// Don't remove 'this' pointer unless you want to skip loading all prepared statements...
|
||||
if (!this->PrepareStatements())
|
||||
if (_fatalConnectionError.load()) { m_failed = true; return false; }
|
||||
TC_LOG_WARN("sql.sql", "Reconnecting database '%s': attempt %u/%u", m_connectionInfo.database.c_str(), attempt + 1, attempts);
|
||||
uint32 const error = Open();
|
||||
if (!error)
|
||||
{
|
||||
TC_LOG_FATAL("sql.sql", "Could not re-prepare statements!");
|
||||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
ABORT();
|
||||
m_prepareError = false;
|
||||
if (!PrepareStatements())
|
||||
{
|
||||
FailConnection();
|
||||
return false;
|
||||
}
|
||||
m_reconnecting = false;
|
||||
TC_LOG_INFO("sql.sql", "Database '%s' connection restored.", m_connectionInfo.database.c_str());
|
||||
return true;
|
||||
}
|
||||
|
||||
TC_LOG_INFO("sql.sql", "Successfully reconnected to %s @%s:%s (%s).",
|
||||
m_connectionInfo.database.c_str(), m_connectionInfo.host.c_str(), m_connectionInfo.port_or_socket.c_str(),
|
||||
(m_connectionFlags & CONNECTION_ASYNC) ? "asynchronous" : "synchronous");
|
||||
|
||||
m_reconnecting = false;
|
||||
return true;
|
||||
}
|
||||
|
||||
if ((--attempts) == 0)
|
||||
{
|
||||
// Shut down the server when the mysql server isn't
|
||||
// reachable for some time
|
||||
TC_LOG_FATAL("sql.sql", "Failed to reconnect to the MySQL server, "
|
||||
"terminating the server to prevent data corruption!");
|
||||
|
||||
// We could also initiate a shutdown through using std::raise(SIGTERM)
|
||||
std::this_thread::sleep_for(std::chrono::seconds(10));
|
||||
ABORT();
|
||||
}
|
||||
else
|
||||
{
|
||||
// It's possible this attempted reconnect throws 2006 at us.
|
||||
// To prevent crazy recursive calls, sleep here.
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3)); // Sleep 3 seconds
|
||||
return _HandleMySQLErrno(lErrno, attempts); // Call self (recursive)
|
||||
// Retry the actual failed Open(), including MariaDB error 2002.
|
||||
// Do not recursively dispatch an unrecognised connect errno.
|
||||
if (attempt + 1 < attempts)
|
||||
std::this_thread::sleep_for(std::chrono::seconds(3));
|
||||
}
|
||||
FailConnection();
|
||||
return false;
|
||||
}
|
||||
|
||||
case ER_LOCK_DEADLOCK:
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
|
||||
#include "Define.h"
|
||||
#include "DatabaseEnvFwd.h"
|
||||
#include <atomic>
|
||||
#include <map>
|
||||
#include <memory>
|
||||
#include <mutex>
|
||||
@@ -79,6 +80,9 @@ class TC_DATABASE_API MySQLConnection
|
||||
int ExecuteTransaction(std::shared_ptr<TransactionBase> transaction);
|
||||
size_t EscapeString(char* to, const char* from, size_t length);
|
||||
void Ping();
|
||||
// The world thread consumes this failure state; the database library
|
||||
// does not depend on game systems or terminate the process itself.
|
||||
static bool HasFatalConnectionError() { return _fatalConnectionError.load(); }
|
||||
|
||||
uint32 GetLastError();
|
||||
|
||||
@@ -104,6 +108,9 @@ class TC_DATABASE_API MySQLConnection
|
||||
|
||||
private:
|
||||
bool _HandleMySQLErrno(uint32 errNo, uint8 attempts = 5);
|
||||
void FailConnection();
|
||||
bool m_failed = false;
|
||||
static std::atomic<bool> _fatalConnectionError;
|
||||
|
||||
ProducerConsumerQueue<SQLOperation*>* m_queue; //! Queue shared with other asynchronous connections.
|
||||
std::unique_ptr<DatabaseWorker> m_worker; //! Core worker task.
|
||||
|
||||
@@ -21,6 +21,14 @@
|
||||
#include "DisableMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "Creature.h"
|
||||
#include "GameObject.h"
|
||||
#include "DynamicObject.h"
|
||||
#include "Corpse.h"
|
||||
#include "Pet.h"
|
||||
#include "AreaTrigger.h"
|
||||
#include "TypeContainerVisitor.h"
|
||||
#include <unordered_set>
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "ScriptMgr.h"
|
||||
@@ -32,13 +40,36 @@ OutdoorPvPMgr::OutdoorPvPMgr()
|
||||
|
||||
OutdoorPvPMgr::~OutdoorPvPMgr() = default;
|
||||
|
||||
namespace
|
||||
{
|
||||
struct DetachOutdoorScripts
|
||||
{
|
||||
std::unordered_set<ZoneScript*> Scripts;
|
||||
template<class T> void Visit(std::unordered_map<ObjectGuid,T*>& objects)
|
||||
{
|
||||
for (auto const& entry : objects)
|
||||
if (Scripts.count(entry.second->GetZoneScript()))
|
||||
entry.second->ClearZoneScript();
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
void OutdoorPvPMgr::Die()
|
||||
{
|
||||
// Maps must remain alive while OutdoorPvP destructors remove capture
|
||||
// objects. First release cached ZoneScript pointers from those objects;
|
||||
// later grid unloading must not call an already destroyed controller.
|
||||
for (auto const& entry : m_OutdoorPvPByMap)
|
||||
{
|
||||
DetachOutdoorScripts worker;
|
||||
for (auto const& script : entry.second) worker.Scripts.insert(script.get());
|
||||
TypeContainerVisitor<DetachOutdoorScripts,MapStoredObjectTypesContainer> visitor(worker);
|
||||
visitor.Visit(entry.first->GetObjectsStore());
|
||||
}
|
||||
m_OutdoorPvPMap.clear();
|
||||
m_OutdoorPvPByMap.clear();
|
||||
|
||||
m_OutdoorPvPDatas.fill(0);
|
||||
|
||||
m_OutdoorPvPMap.clear();
|
||||
}
|
||||
|
||||
OutdoorPvPMgr* OutdoorPvPMgr::instance()
|
||||
|
||||
@@ -2244,6 +2244,22 @@ bool AdventureBehaviorMgr::ProtectInstanceGroupLeave(Player* player) const
|
||||
return false;
|
||||
}
|
||||
|
||||
bool AdventureBehaviorMgr::PauseForLogout(Player* player)
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
if (it==_records.end()) return false;
|
||||
bool enabled=it->second.Enabled;
|
||||
// Preserve schedule, intent and instance continuation for the next login.
|
||||
it->second.Enabled=false;
|
||||
return enabled;
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::ResumeAfterLogoutPause(Player* player, bool enabled)
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
if (it!=_records.end()) it->second.Enabled=enabled;
|
||||
}
|
||||
|
||||
void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
|
||||
{
|
||||
InterruptForCombat(player);
|
||||
|
||||
@@ -33,6 +33,8 @@ public:
|
||||
void RefreshDay(Player* player);
|
||||
void EnterPlayerInstance(Player* player);
|
||||
bool Command(ChatHandler* handler, char const* args);
|
||||
bool PauseForLogout(Player* player);
|
||||
void ResumeAfterLogoutPause(Player* player, bool enabled);
|
||||
void InterruptForCombat(Player* player);
|
||||
void InterruptForInteraction(Player* player);
|
||||
AdventureGroupPlan GroupPlan(Player* player);
|
||||
|
||||
@@ -85,7 +85,7 @@ void AdventurePopulationMgr::Update(uint32 diff)
|
||||
|
||||
_queries.ProcessReadyCallbacks();_saves.ProcessReadyCallbacks();
|
||||
if(!_configured){Configure();return;}
|
||||
if(!_enabled || sWorld->IsStopped())return;
|
||||
if(!_enabled || sWorld->IsStopped() || sWorld->IsShuttingDown() || sClientlessPlayerMgr->IsShutdownDraining())return;
|
||||
if(_refresh>diff)_refresh-=diff;else _refresh=0;
|
||||
if(_timer>diff){_timer-=diff;return;}_timer=_interval;
|
||||
if(_busy)return;
|
||||
@@ -270,6 +270,7 @@ bool AdventurePopulationMgr::Command(ChatHandler* handler,char const* args)
|
||||
if(op=="reload"){if(_busy){handler->SendSysMessage("Loader has a pending operation; retry reload shortly.");return false;}_configured=false;Configure();}
|
||||
else if(!op.empty() && op!="status")return false;
|
||||
handler->PSendSysMessage("Adventure loader enabled=%u schema=%u busy=%u pools=%u/%u/%u/%u known=%u rotations=%u manager=%u error='%s'",_enabled,_schema,_busy,_targets[0],_targets[1],_targets[2],_targets[3],uint32(_bots.size()),uint32(_rotations.size()),uint32(_checks.size()),_lastError.c_str());
|
||||
handler->PSendSysMessage("Adventure loader shutdownPaused=%u", (sWorld->IsShuttingDown() || sClientlessPlayerMgr->IsShutdownDraining())?1u:0u);
|
||||
for(unsigned pool=0;pool<4;++pool)
|
||||
{
|
||||
unsigned online=0,loading=0,saved=0;
|
||||
|
||||
@@ -174,6 +174,7 @@ void ClientlessActionExecutorMgr::SetGroundPathAdapter(uint32 guid,GroundPathAda
|
||||
|
||||
void ClientlessActionExecutorMgr::Clear(uint32 characterGuid)
|
||||
{
|
||||
sClientlessTransportMgr->Cancel(characterGuid);
|
||||
_records.erase(characterGuid);
|
||||
sClientlessTravelMgr->CancelTravel(characterGuid);
|
||||
}
|
||||
@@ -248,6 +249,9 @@ bool ClientlessActionExecutorMgr::StartAction(Player* player, ClientlessAction c
|
||||
record.FlightActive = false;
|
||||
record.FlightApproachActive = false;
|
||||
record.PortalUseActive = false;
|
||||
// Finish an obsolete land-side transport operation synchronously. A real
|
||||
// passenger instead remains attached and exits at the next safe dock.
|
||||
sClientlessTransportMgr->Cancel(player->GetGUID().GetCounter(), player);
|
||||
record.Status = {};
|
||||
record.Status.ActionName = action.Name;
|
||||
auto const* existingFlight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter());
|
||||
|
||||
@@ -0,0 +1,15 @@
|
||||
#ifndef VCORE_CLIENTLESS_LOGIN_RECOVERY_H
|
||||
#define VCORE_CLIENTLESS_LOGIN_RECOVERY_H
|
||||
#include "Common.h"
|
||||
class Player;
|
||||
namespace ClientlessLoginRecovery
|
||||
{
|
||||
struct State
|
||||
{
|
||||
uint32 Elapsed = 0;
|
||||
bool Landing = false;
|
||||
};
|
||||
// Return true only when normal controller initialization may begin.
|
||||
bool Update(Player* player, State& state, uint32 diff);
|
||||
}
|
||||
#endif
|
||||
@@ -2,6 +2,7 @@
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "AdventureRotationPolicy.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessLoginRecovery.h"
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
@@ -220,9 +221,32 @@ std::vector<ClientlessWeightedMount> ReadMountDictionary(lua_State* L, int movem
|
||||
class ClientlessAdventureLuaController final : public ClientlessPlayerController
|
||||
{
|
||||
public:
|
||||
bool PrepareOnline(Player* player, uint32 diff) override
|
||||
{
|
||||
if (!_profileLoaded)
|
||||
{
|
||||
sClientlessLuaEngine->LoadAdventureProfile(player);
|
||||
_profileLoaded=true;
|
||||
}
|
||||
return ClientlessLoginRecovery::Update(player,_recovery,diff);
|
||||
}
|
||||
void OnSafeLogoutRequested(Player* player) override
|
||||
{
|
||||
_resumeEnabled=sAdventureBehaviorMgr->PauseForLogout(player);
|
||||
}
|
||||
void OnSafeLogoutCancelled(Player* player) override
|
||||
{
|
||||
sAdventureBehaviorMgr->ResumeAfterLogoutPause(player,_resumeEnabled);
|
||||
}
|
||||
void UpdateForSafeLogout(Player* player, uint32 diff) override
|
||||
{
|
||||
if (sClientlessTransportMgr->ResumePassenger(player)) return;
|
||||
if (player->IsInCombat() || sAdventureCombatMgr->Busy(player))
|
||||
sAdventureCombatMgr->Update(player,diff);
|
||||
}
|
||||
void OnPlayerOnline(Player* player) override
|
||||
{
|
||||
sClientlessLuaEngine->LoadAdventureProfile(player);
|
||||
if (!_profileLoaded) sClientlessLuaEngine->LoadAdventureProfile(player);
|
||||
AdventureSocialActions::Prepare(player);
|
||||
sClientlessLuaEngine->ExecuteAdventureScript(player);
|
||||
sAdventureBehaviorMgr->Online(player);
|
||||
@@ -256,6 +280,10 @@ public:
|
||||
{sAdventureCombatMgr->Revived(player,when);}
|
||||
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
|
||||
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
|
||||
private:
|
||||
bool _profileLoaded=false, _resumeEnabled=false;
|
||||
ClientlessLoginRecovery::State _recovery;
|
||||
public:
|
||||
void OnPlayerLogout(Player* player) override
|
||||
{ sClientlessLuaEngine->ForgetWorldPresence(player->GetGUID().GetCounter()); sAdventurePopulationMgr->Offline(player); sAdventureInteractionMgr->Offline(player); sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
|
||||
};
|
||||
|
||||
@@ -1,4 +1,11 @@
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessLoginRecovery.h"
|
||||
#include "PathGenerator.h"
|
||||
#include "Map.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "IVMapManager.h"
|
||||
#include "MySQLConnection.h"
|
||||
#include <cmath>
|
||||
#include "GameTime.h"
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "ClientlessTransportMgr.h"
|
||||
@@ -6,6 +13,11 @@
|
||||
#include "ClientlessActionExecutorMgr.h"
|
||||
#include "ClientlessFlightMgr.h"
|
||||
#include "Log.h"
|
||||
#include "Chat.h"
|
||||
#include "Config.h"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include "Player.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "World.h"
|
||||
@@ -22,7 +34,7 @@ ClientlessPlayerMgr* ClientlessPlayerMgr::instance()
|
||||
bool ClientlessPlayerMgr::StartPlayer(uint32 accountId, uint32 characterGuid,
|
||||
std::unique_ptr<ClientlessPlayerController> controller)
|
||||
{
|
||||
if (!accountId || !characterGuid)
|
||||
if (!accountId || !characterGuid || sWorld->IsShuttingDown() || _shutdownDraining || _safeLogoutAll)
|
||||
return false;
|
||||
|
||||
if (IsManaged(characterGuid) ||
|
||||
@@ -218,6 +230,8 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
++itr;continue;
|
||||
}
|
||||
auto begin=Clock::now();
|
||||
if (!record.OnlineNotified && record.Controller && !record.Controller->PrepareOnline(player, diff))
|
||||
{ ++itr; continue; }
|
||||
if (!record.OnlineNotified)
|
||||
{
|
||||
record.OnlineNotified = true;
|
||||
@@ -228,6 +242,19 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
record.Controller->OnIntentChanged(player, &record.Intent);
|
||||
}
|
||||
}
|
||||
auto logout = _safeLogouts.find(characterGuid);
|
||||
if (logout != _safeLogouts.end())
|
||||
{
|
||||
if (!logout->second)
|
||||
{
|
||||
logout->second = true;
|
||||
record.Actions.clear();
|
||||
sClientlessActionExecutorMgr->RequestStopAtNode(characterGuid);
|
||||
if (record.Controller) record.Controller->OnSafeLogoutRequested(player);
|
||||
}
|
||||
if (record.Controller) record.Controller->UpdateForSafeLogout(player, diff);
|
||||
++itr; continue;
|
||||
}
|
||||
if (record.Controller)
|
||||
{
|
||||
auto revivals=resurrectionNotices.find(characterGuid);
|
||||
@@ -259,6 +286,7 @@ void ClientlessPlayerMgr::Update(uint32 diff)
|
||||
|
||||
++itr;
|
||||
}
|
||||
UpdateSafeLogouts();
|
||||
auto now=Clock::now();
|
||||
auto wall=std::chrono::duration_cast<std::chrono::milliseconds>(now-window).count();
|
||||
if(wall>=30000)
|
||||
@@ -329,3 +357,258 @@ void ClientlessPlayerMgr::GroupJoinRequest(Player* target,Player* requester)
|
||||
auto& queue=_groupJoinNotices[target->GetGUID().GetCounter()];
|
||||
if(queue.size()<8)queue.push_back(requester->GetGUID());
|
||||
}
|
||||
|
||||
|
||||
bool ClientlessPlayerMgr::ReadyForSafeLogout(Player const* player)
|
||||
{
|
||||
return player && player->IsInWorld() && player->IsAlive() && !player->IsInCombat() &&
|
||||
!player->GetTransport() && !player->IsInFlight() && !player->IsFlying() &&
|
||||
!player->IsFalling() && !player->IsBeingTeleported() &&
|
||||
!player->HasUnitState(UNIT_STATE_CASTING) && !player->IsSplineEnabled();
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::RequestSafeLogout(uint32 guid)
|
||||
{
|
||||
_safeLogouts.emplace(guid, false);
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::RequestSafeLogoutAll()
|
||||
{
|
||||
_safeLogoutAll = true;
|
||||
for (auto const& session : sWorld->GetAllClientlessSessions())
|
||||
RequestSafeLogout(session.first);
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::ResumeSafeLogouts()
|
||||
{
|
||||
if (_shutdownDraining) return;
|
||||
for (auto const& item : _safeLogouts)
|
||||
{
|
||||
auto record = _records.find(item.first);
|
||||
auto session = sWorld->FindClientlessSession(item.first);
|
||||
auto player = session ? session->GetPlayer() : nullptr;
|
||||
if (item.second && player && record != _records.end() && record->second.Controller)
|
||||
record->second.Controller->OnSafeLogoutCancelled(player);
|
||||
}
|
||||
_safeLogouts.clear();
|
||||
_safeLogoutAll = false;
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::UpdateSafeLogouts()
|
||||
{
|
||||
if (_safeLogoutAll) RequestSafeLogoutAll();
|
||||
for (auto it = _safeLogouts.begin(); it != _safeLogouts.end();)
|
||||
{
|
||||
auto session = sWorld->FindClientlessSession(it->first);
|
||||
if (!session) { it = _safeLogouts.erase(it); continue; }
|
||||
auto record = _records.find(it->first);
|
||||
// A managed controller must first acknowledge its pause request.
|
||||
bool paused = record == _records.end() || it->second;
|
||||
if (paused && ReadyForSafeLogout(session->GetPlayer()))
|
||||
{
|
||||
if (sWorld->RemoveClientlessSession(it->first))
|
||||
TC_LOG_INFO("server.worldserver", "CLIENTLESS_SAFE_LOGOUT guid=%u", it->first);
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::ReportSafeLogouts(ChatHandler* handler) const
|
||||
{
|
||||
uint32 combat=0, transport=0, flight=0, loading=0;
|
||||
for (auto const& item : _safeLogouts)
|
||||
{
|
||||
auto session=sWorld->FindClientlessSession(item.first);
|
||||
auto player=session?session->GetPlayer():nullptr;
|
||||
if (!player || !player->IsInWorld()) { ++loading; continue; }
|
||||
combat+=player->IsInCombat()?1:0;
|
||||
transport+=player->GetTransport()?1:0;
|
||||
flight+=(player->IsInFlight() || player->IsFlying())?1:0;
|
||||
}
|
||||
handler->PSendSysMessage("Safe logout pending=%u sessions=%u combat=%u transport=%u flight=%u loading=%u shutdown=%u",
|
||||
uint32(_safeLogouts.size()),sWorld->GetClientlessSessionCount(),combat,transport,flight,loading,_shutdownDraining?1u:0u);
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::BeginShutdown()
|
||||
{
|
||||
if (_shutdownDraining) return;
|
||||
_shutdownDraining=true;
|
||||
_shutdownWasSafeLogoutAll=_safeLogoutAll;
|
||||
_shutdownSnapshotDone=false;
|
||||
_shutdownStarted=std::chrono::steady_clock::now();
|
||||
auto directory=std::filesystem::path(sConfigMgr->GetStringDefault("Clientless.Recovery.StateDir", "clientless-recovery"));
|
||||
std::error_code error;
|
||||
std::filesystem::create_directories(directory,error);
|
||||
_shutdownAuditFile=(directory/("shutdown-"+std::to_string(GameTime::GetGameTime())+"-"+
|
||||
std::to_string(GameTime::GetGameTimeMS())+".jsonl")).string();
|
||||
RequestSafeLogoutAll();
|
||||
WriteShutdownSnapshot("begin");
|
||||
TC_LOG_INFO("server.worldserver", "CLIENTLESS_SHUTDOWN_BEGIN sessions=%u graceMs=60000 audit='%s'",
|
||||
sWorld->GetClientlessSessionCount(),_shutdownAuditFile.c_str());
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::CancelShutdown()
|
||||
{
|
||||
if (!_shutdownDraining) return;
|
||||
WriteShutdownSnapshot("cancel");
|
||||
_shutdownDraining=false;
|
||||
if (!_shutdownWasSafeLogoutAll) ResumeSafeLogouts();
|
||||
}
|
||||
|
||||
bool ClientlessPlayerMgr::ShutdownReady()
|
||||
{
|
||||
if (!_shutdownDraining) return true;
|
||||
bool empty=sWorld->GetClientlessSessionCount()==0;
|
||||
auto elapsed=std::chrono::duration_cast<std::chrono::milliseconds>(std::chrono::steady_clock::now()-_shutdownStarted).count();
|
||||
if (!empty && elapsed<60000) return false;
|
||||
if (!_shutdownSnapshotDone)
|
||||
{
|
||||
WriteShutdownSnapshot(empty?"complete":"deadline");
|
||||
_shutdownSnapshotDone=true;
|
||||
TC_LOG_INFO("server.worldserver", "CLIENTLESS_SHUTDOWN_END remaining=%u elapsedMs=%lld deadline=%u audit='%s'",
|
||||
sWorld->GetClientlessSessionCount(),static_cast<long long>(elapsed),empty?0u:1u,_shutdownAuditFile.c_str());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClientlessPlayerMgr::WriteShutdownSnapshot(char const* event)
|
||||
{
|
||||
std::ofstream out(_shutdownAuditFile,std::ios::app);
|
||||
if (!out) { TC_LOG_ERROR("server.worldserver", "CLIENTLESS_SHUTDOWN_AUDIT_FAILED file='%s'",_shutdownAuditFile.c_str()); return; }
|
||||
out<<"{\"event\":"<<std::quoted(event)<<",\"time\":"<<GameTime::GetGameTime()
|
||||
<<",\"remaining\":"<<sWorld->GetClientlessSessionCount()
|
||||
<<",\"database_unavailable\":"<<MySQLConnection::HasFatalConnectionError()<<",\"bots\":[";
|
||||
bool first=true;
|
||||
for (auto const& entry : sWorld->GetAllClientlessSessions())
|
||||
{
|
||||
if (!first) out<<',';
|
||||
first=false;
|
||||
auto player=entry.second->GetPlayer();
|
||||
out<<"{\"guid\":"<<entry.first;
|
||||
if (player)
|
||||
out<<",\"name\":"<<std::quoted(player->GetName())<<",\"race\":"<<unsigned(player->getRace())
|
||||
<<",\"faction\":"<<std::quoted(player->GetTeamId()==TEAM_ALLIANCE?"alliance":"horde")
|
||||
<<",\"map\":"<<player->GetMapId()<<",\"x\":"<<player->GetPositionX()<<",\"y\":"<<player->GetPositionY()<<",\"z\":"<<player->GetPositionZ()
|
||||
<<",\"alive\":"<<player->IsAlive()<<",\"combat\":"<<player->IsInCombat()<<",\"flying\":"<<player->IsFlying()
|
||||
<<",\"falling\":"<<player->IsFalling()<<",\"taxi\":"<<player->IsInFlight()<<",\"transport\":"<<(player->GetTransport()!=nullptr)
|
||||
<<",\"transport_guid\":"<<player->m_movementInfo.transport.guid.GetRawValue()
|
||||
<<",\"teleport\":"<<player->IsBeingTeleported()<<",\"spline\":"<<player->IsSplineEnabled();
|
||||
out<<'}';
|
||||
}
|
||||
out<<"]}\n";
|
||||
out.flush();
|
||||
}
|
||||
|
||||
|
||||
namespace
|
||||
{
|
||||
bool RecoveryGround(Player* player, Position const& point)
|
||||
{
|
||||
float floor=player->GetMap()->GetHeight(player->GetPhaseShift(),point.GetPositionX(),point.GetPositionY(),point.GetPositionZ()+2,true,6);
|
||||
if (!std::isfinite(floor) || floor<=INVALID_HEIGHT+1 || std::fabs(floor-point.GetPositionZ())>2.5f) return false;
|
||||
auto path=PathGenerator::AnalyzePositions(player->GetMapId(),player->GetMapId(),player->GetInstanceId(),point,point,NAV_GROUND);
|
||||
// Missing MMAP coverage is not proof that an otherwise supported bot
|
||||
// needs teleporting. A measured NoPolygon on loaded data is different.
|
||||
return path.IsComplete() || path.Result==VcorePositionPathResult::Status::NavMeshUnavailable ||
|
||||
path.Result==VcorePositionPathResult::Status::TileUnavailable;
|
||||
}
|
||||
|
||||
bool RecoverLocalGround(Player* player)
|
||||
{
|
||||
auto map=player->GetMap();
|
||||
float floor=map->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+2,true,250);
|
||||
if (!std::isfinite(floor) || floor<=INVALID_HEIGHT+1 || floor>player->GetPositionZ()+0.75f ||
|
||||
player->GetPositionZ()-floor>100 || map->GetWaterLevel(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY())>floor+0.5f)
|
||||
return false;
|
||||
Position landing;landing.Relocate(player->GetPositionX(),player->GetPositionY(),floor,player->GetOrientation());
|
||||
if (!RecoveryGround(player,landing) || !map->isInLineOfSight(player->GetPhaseShift(),
|
||||
player->GetPositionX(),player->GetPositionY(),player->GetPositionZ()+0.75f,
|
||||
landing.GetPositionX(),landing.GetPositionY(),landing.GetPositionZ()+0.75f,LINEOFSIGHT_ALL_CHECKS,VMAP::ModelIgnoreFlags(0)))
|
||||
return false;
|
||||
player->StopMoving();
|
||||
player->GetMotionMaster()->Clear();
|
||||
player->m_movementInfo.transport.Reset();
|
||||
player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING|MOVEMENTFLAG_ASCENDING|MOVEMENTFLAG_DESCENDING|MOVEMENTFLAG_FALLING);
|
||||
player->SetAnimTier(AnimTier::Ground);
|
||||
if (!player->TeleportTo(player->GetMapId(),landing.GetPositionX(),landing.GetPositionY(),landing.GetPositionZ(),landing.GetOrientation())) return false;
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='local-ground' target=(%.3f %.3f %.3f)",
|
||||
player->GetGUID().GetCounter(),landing.GetPositionX(),landing.GetPositionY(),landing.GetPositionZ());
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessLoginRecovery::Update(Player* player, State& state, uint32 diff)
|
||||
{
|
||||
state.Elapsed+=std::min(diff,30000u);
|
||||
if (!player->IsAlive() || player->IsInCombat()) return true; // Native death/combat controller owns these states.
|
||||
if (player->GetTransport())
|
||||
{
|
||||
sClientlessTransportMgr->ResumePassenger(player);
|
||||
return false; // Never detach a valid passenger to recover its position.
|
||||
}
|
||||
auto ride=sClientlessTransportMgr->GetStatus(player->GetGUID().GetCounter());
|
||||
if (ride && ride->Phase!=ClientlessTransportPhase::Arrived && ride->Phase!=ClientlessTransportPhase::Failed)
|
||||
return false;
|
||||
if (player->IsBeingTeleported()) return false;
|
||||
if (player->IsInFlight())
|
||||
{
|
||||
if (player->GetMotionMaster()->GetCurrentMovementGeneratorType()==FLIGHT_MOTION_TYPE || state.Elapsed<5000)
|
||||
return false; // A native taxi may legitimately take much longer than a minute.
|
||||
player->CleanupAfterTaxiFlight();
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='orphan-taxi'",player->GetGUID().GetCounter());
|
||||
}
|
||||
if (player->IsInWater()) return true;
|
||||
if (state.Landing)
|
||||
{
|
||||
auto flight=sClientlessFlightMgr->GetStatus(player->GetGUID().GetCounter());
|
||||
if (flight && (flight->State==ClientlessFlightState::Flying || flight->State==ClientlessFlightState::Mounting) && state.Elapsed<60000)
|
||||
return false;
|
||||
if (flight && flight->State==ClientlessFlightState::Arrived)
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='flight-landed'",player->GetGUID().GetCounter());
|
||||
return true;
|
||||
}
|
||||
sClientlessFlightMgr->Clear(player->GetGUID().GetCounter());
|
||||
}
|
||||
Position current;current.Relocate(player);
|
||||
bool orphan=bool(player->m_movementInfo.transport.guid);
|
||||
if (orphan && state.Elapsed<5000) return false;
|
||||
if (!orphan && RecoveryGround(player,current))
|
||||
{
|
||||
if (player->IsFlying())
|
||||
{
|
||||
player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING|MOVEMENTFLAG_ASCENDING|MOVEMENTFLAG_DESCENDING|MOVEMENTFLAG_FALLING);
|
||||
player->SetAnimTier(AnimTier::Ground);
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='ground-flight-flags'",player->GetGUID().GetCounter());
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if (player->IsFalling() && state.Elapsed<5000) return false;
|
||||
if (!state.Landing && !orphan && player->IsMounted() && player->CanFly())
|
||||
{
|
||||
Position target;target.Relocate(player->GetPositionX(),player->GetPositionY(),
|
||||
player->GetMap()->GetHeight(player->GetPhaseShift(),player->GetPositionX(),player->GetPositionY(),MAX_HEIGHT,true,MAX_HEIGHT),player->GetOrientation());
|
||||
std::string error;
|
||||
if (sClientlessFlightMgr->Start(player,target,error,false))
|
||||
{
|
||||
state.Landing=true;
|
||||
TC_LOG_INFO("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='flight-landing-start'",player->GetGUID().GetCounter());
|
||||
return false;
|
||||
}
|
||||
}
|
||||
if (RecoverLocalGround(player)) return true;
|
||||
// A failed local recovery gets a known faction-safe landing, not an
|
||||
// unverified nearest polygon across a wall, roof or different floor.
|
||||
player->StopMoving();
|
||||
player->GetMotionMaster()->Clear();
|
||||
player->m_movementInfo.transport.Reset();
|
||||
bool alliance=player->GetTeamId()==TEAM_ALLIANCE;
|
||||
player->SetAnimTier(AnimTier::Ground);
|
||||
if (!player->TeleportTo(alliance?0:1,alliance?-8833.07f:1569.97f,alliance?622.778f:-4397.41f,alliance?93.932f:16.047f,0))
|
||||
return false;
|
||||
player->GetSession()->CompleteClientlessTeleport();
|
||||
player->RemoveUnitMovementFlag(MOVEMENTFLAG_FLYING|MOVEMENTFLAG_ASCENDING|MOVEMENTFLAG_DESCENDING|MOVEMENTFLAG_FALLING);
|
||||
TC_LOG_WARN("server.adventurebot.navigation", "BOT_LOGIN_RECOVERY guid=%u kind='capital' faction='%s'",player->GetGUID().GetCounter(),alliance?"alliance":"horde");
|
||||
return true;
|
||||
}
|
||||
@@ -11,6 +11,7 @@
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <mutex>
|
||||
#include <chrono>
|
||||
#include "ObjectGuid.h"
|
||||
|
||||
class Player;
|
||||
@@ -33,7 +34,11 @@ class TC_GAME_API ClientlessPlayerController
|
||||
{
|
||||
public:
|
||||
virtual ~ClientlessPlayerController() = default;
|
||||
virtual bool PrepareOnline(Player*, uint32) { return true; }
|
||||
virtual void OnPlayerOnline(Player*) { }
|
||||
virtual void OnSafeLogoutRequested(Player*) { }
|
||||
virtual void OnSafeLogoutCancelled(Player*) { }
|
||||
virtual void UpdateForSafeLogout(Player*, uint32) { }
|
||||
virtual void OnIntentChanged(Player*, ClientlessIntent const*) { }
|
||||
virtual void Update(Player*, uint32) { }
|
||||
virtual void OnPlayerLogout(Player*) { }
|
||||
@@ -76,8 +81,25 @@ public:
|
||||
std::deque<ClientlessAction> const* GetQueuedActions(uint32 characterGuid) const;
|
||||
|
||||
void Update(uint32 diff);
|
||||
void RequestSafeLogout(uint32 guid);
|
||||
void RequestSafeLogoutAll();
|
||||
void ResumeSafeLogouts();
|
||||
void ReportSafeLogouts(class ChatHandler* handler) const;
|
||||
void BeginShutdown();
|
||||
void CancelShutdown();
|
||||
bool ShutdownReady();
|
||||
bool IsShutdownDraining() const { return _shutdownDraining; }
|
||||
bool IsSafeLogoutPending(uint32 guid) const { return _safeLogouts.count(guid) != 0; }
|
||||
static bool ReadyForSafeLogout(Player const* player);
|
||||
|
||||
private:
|
||||
void UpdateSafeLogouts();
|
||||
void WriteShutdownSnapshot(char const* event);
|
||||
std::unordered_map<uint32, bool> _safeLogouts;
|
||||
bool _safeLogoutAll = false, _shutdownDraining = false, _shutdownWasSafeLogoutAll = false;
|
||||
bool _shutdownSnapshotDone = false;
|
||||
std::chrono::steady_clock::time_point _shutdownStarted;
|
||||
std::string _shutdownAuditFile;
|
||||
struct ManagedPlayer
|
||||
{
|
||||
uint32 AccountId = 0;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Log.h"
|
||||
#include "Map.h"
|
||||
#include "MotionMaster.h"
|
||||
#include "MoveSpline.h"
|
||||
#include "ObjectAccessor.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
@@ -556,10 +557,32 @@ void ClientlessTransportMgr::SetPhase(Player* player, Ride& ride, ClientlessTran
|
||||
player->GetGUID().GetCounter(), PhaseName(phase), ride.From.Node.c_str(), ride.To.Node.c_str(), player->GetTransport() ? 1u : 0u, detail);
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::Cancel(uint32 guid)
|
||||
void ClientlessTransportMgr::StopLandMovement(Player* player, Ride const& ride)
|
||||
{
|
||||
// A newer ground/flight action owns a different spline. Never stop it.
|
||||
if (!player->GetTransport() && ride.LandSplineId &&
|
||||
player->movespline->GetId() == ride.LandSplineId)
|
||||
{
|
||||
player->StopMoving();
|
||||
player->GetMotionMaster()->Clear();
|
||||
}
|
||||
}
|
||||
|
||||
void ClientlessTransportMgr::Cancel(uint32 guid, Player* player)
|
||||
{
|
||||
auto itr = _rides.find(guid);
|
||||
if (itr != _rides.end()) itr->second.Abort = true;
|
||||
if (itr == _rides.end()) return;
|
||||
auto& ride = itr->second;
|
||||
if (ride.Status.Phase == ClientlessTransportPhase::Arrived ||
|
||||
ride.Status.Phase == ClientlessTransportPhase::Failed) return;
|
||||
ride.Abort = true;
|
||||
if (player && !player->GetTransport())
|
||||
{
|
||||
StopLandMovement(player, ride);
|
||||
ride.WaitStage = 0;
|
||||
ride.WaitTrace.clear();
|
||||
SetPhase(player, ride, ClientlessTransportPhase::Failed, "transport cancelled on land; movement released");
|
||||
}
|
||||
}
|
||||
|
||||
bool ClientlessTransportMgr::RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff)
|
||||
@@ -594,6 +617,22 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
{
|
||||
using Phase = ClientlessTransportPhase;
|
||||
if (ride.Status.Phase == Phase::Arrived || ride.Status.Phase == Phase::Failed) return;
|
||||
// Cancellation and lost movement ownership must precede waiting-path,
|
||||
// recovery and timeout logic. An obsolete ride cannot issue a return spline.
|
||||
if (!player->GetTransport() && ride.Abort)
|
||||
{
|
||||
StopLandMovement(player, ride);
|
||||
ride.WaitStage = 0;
|
||||
ride.WaitTrace.clear();
|
||||
SetPhase(player, ride, Phase::Failed, "transport cancelled on land; movement released");
|
||||
return;
|
||||
}
|
||||
if (!player->GetTransport() && !ride.Status.Recovering &&
|
||||
player->IsSplineEnabled() && player->movespline->GetId() != ride.LandSplineId)
|
||||
{
|
||||
SetPhase(player, ride, Phase::Failed, "transport superseded by another movement owner");
|
||||
return;
|
||||
}
|
||||
ride.Elapsed += diff; ride.PhaseElapsed += diff;
|
||||
if (player->IsBeingTeleported())
|
||||
{
|
||||
@@ -615,7 +654,8 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
if(object && object->GetGoType()==GAMEOBJECT_TYPE_TRANSPORT)limit=std::max(limit,330000u);
|
||||
auto fail = [&](char const* detail)
|
||||
{
|
||||
player->StopMoving();
|
||||
if (player->GetTransport()) player->StopMoving();
|
||||
else StopLandMovement(player, ride);
|
||||
if(player->GetTransport() && object && player->GetTransport()==object->ToTransportBase())
|
||||
{
|
||||
ride.Abort=true;ride.RetryDelay=3000;
|
||||
@@ -648,11 +688,19 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
if(ride.WaitStage==2)
|
||||
{
|
||||
if(!ride.Abort && !AtDock(player,object,ride.From,0))return;
|
||||
// The trace belongs to the original waiting excursion. A player
|
||||
// displaced since then must not be connected to it by an air chord.
|
||||
if (ride.WaitTrace.empty() || player->GetExactDist(ride.WaitTrace.back()) > 1.5f)
|
||||
{ fail("waiting path ownership lost; no return shortcut issued"); return; }
|
||||
std::vector<Position> back;back.push_back(*player);
|
||||
for(auto it=ride.WaitTrace.rbegin();it!=ride.WaitTrace.rend();++it)
|
||||
if(back.back().GetExactDist(&*it)>0.11f)back.push_back(*it);
|
||||
ride.WaitStage=3;
|
||||
if(back.size()>1)player->GetMotionMaster()->MoveSmoothPath(1,back.data(),back.size(),false);
|
||||
if(back.size()>1)
|
||||
{
|
||||
player->GetMotionMaster()->MoveSmoothPath(1,back.data(),back.size(),false);
|
||||
ride.LandSplineId = player->movespline->GetId();
|
||||
}
|
||||
ride.Status.Detail="returning along actual waiting path to shared transport node";
|
||||
return;
|
||||
}
|
||||
@@ -667,6 +715,7 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
{
|
||||
ride.WaitTrace.push_back(*player);ride.WaitStage=1;
|
||||
player->GetMotionMaster()->MoveSmoothPath(1,path.data(),path.size(),false);
|
||||
ride.LandSplineId = player->movespline->GetId();
|
||||
ride.Status.Detail="walking to nearby spaced waiting position";
|
||||
TC_LOG_INFO("server.adventurebot.navigation","TRANSPORT_WAIT_SPOT guid=%u node='%s' x=%.2f y=%.2f",player->GetGUID().GetCounter(),ride.From.Node.c_str(),spot.GetPositionX(),spot.GetPositionY());
|
||||
}
|
||||
@@ -810,7 +859,10 @@ void ClientlessTransportMgr::UpdateRide(Player* player, Ride& ride, uint32 diff)
|
||||
}
|
||||
Position path[2]; path[0].Relocate(player->GetPosition()); path[1] = target;
|
||||
if (Distance(path[0], path[1]) > 0.1f)
|
||||
{
|
||||
player->GetMotionMaster()->MoveSmoothPath(1, path, 2, false);
|
||||
ride.LandSplineId = player->movespline->GetId();
|
||||
}
|
||||
ride.Moving = Distance(path[0], path[1]) > 0.1f; ride.PhaseElapsed = 0;
|
||||
ride.PointTimeout = 10000;
|
||||
if (boarding) ++ride.Point; else --ride.Point;
|
||||
|
||||
@@ -58,7 +58,9 @@ public:
|
||||
// Returns true while a restored passenger is owned by its resumed exit.
|
||||
bool ResumePassenger(Player* player);
|
||||
ClientlessTransportStatus const* GetStatus(uint32 guid) const;
|
||||
void Cancel(uint32 guid);
|
||||
// On land, a supplied player completes cancellation before another action starts.
|
||||
// Attached passengers keep their ride until a recorded safe dock.
|
||||
void Cancel(uint32 guid, Player* player = nullptr);
|
||||
void Update(uint32 diff);
|
||||
bool Command(ChatHandler* handler, char const* args);
|
||||
static bool IsTransportEdge(std::string const& type);
|
||||
@@ -91,6 +93,7 @@ private:
|
||||
bool Resumed = false;
|
||||
uint32 RecoveryMs = 0, Recoveries = 0;
|
||||
bool RecoveryMoving = false;
|
||||
uint32 LandSplineId = 0; // Native spline last issued by this ride on land.
|
||||
uint8 WaitStage=0; // 0 node, 1 walking out, 2 waiting, 3 returning
|
||||
bool WaitTried=false;
|
||||
std::vector<Position> WaitTrace;
|
||||
@@ -98,6 +101,7 @@ private:
|
||||
bool EnsureLoaded();
|
||||
bool Sample(Player* player, Recording& recording, bool force);
|
||||
bool Save(Player* player, std::string const& stand, std::string& error);
|
||||
void StopLandMovement(Player* player, Ride const& ride);
|
||||
void UpdateRide(Player* player, Ride& ride, uint32 diff);
|
||||
bool RecoverStuckElevator(Player* player, GameObject* object, Ride& ride, uint32 diff);
|
||||
void RecoverPassenger(Player* player, Ride& ride, uint32 diff);
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
*/
|
||||
|
||||
#include "World.h"
|
||||
#include "MySQLConnection.h"
|
||||
#include "ClientlessPlayerMgr.h"
|
||||
#include "ClientlessTestBotMgr.h"
|
||||
#include "ClientlessCharacterFactory.h"
|
||||
@@ -104,6 +105,7 @@
|
||||
#include <boost/algorithm/string.hpp>
|
||||
|
||||
TC_GAME_API std::atomic<bool> World::m_stopEvent(false);
|
||||
TC_GAME_API std::atomic<int> World::m_requestedShutdown{-1};
|
||||
TC_GAME_API uint8 World::m_ExitCode = SHUTDOWN_EXIT_CODE;
|
||||
|
||||
TC_GAME_API std::atomic<uint32> World::m_worldLoopCounter(0);
|
||||
@@ -2638,6 +2640,15 @@ void World::LoadAutobroadcasts()
|
||||
/// Update the World !
|
||||
void World::Update(uint32 diff)
|
||||
{
|
||||
int requested = m_requestedShutdown.exchange(-1);
|
||||
if (requested >= 0) ShutdownServ(0, 0, uint8(requested));
|
||||
if (MySQLConnection::HasFatalConnectionError() && !m_databaseShutdownRequested)
|
||||
{
|
||||
m_databaseShutdownRequested = true;
|
||||
SetClosed(true);
|
||||
TC_LOG_ERROR("server.worldserver", "Database failure: beginning controlled shutdown; character saves may be unavailable.");
|
||||
ShutdownServ(0, 0, ERROR_EXIT_CODE, "Database connection unavailable");
|
||||
}
|
||||
///- Update the game time and check for shutdown time
|
||||
_UpdateGameTime();
|
||||
time_t currentGameTime = GameTime::GetGameTime();
|
||||
@@ -3249,6 +3260,8 @@ void World::_UpdateGameTime()
|
||||
GameTime::UpdateGameTimers();
|
||||
|
||||
uint32 elapsed = uint32(GameTime::GetGameTime() - lastGameTime);
|
||||
if (m_ShutdownTimer > 0 && m_ShutdownTimer <= 60)
|
||||
sClientlessPlayerMgr->BeginShutdown();
|
||||
|
||||
///- if there is a shutdown timer
|
||||
if (!IsStopped() && m_ShutdownTimer > 0 && elapsed > 0)
|
||||
@@ -3256,7 +3269,11 @@ void World::_UpdateGameTime()
|
||||
///- ... and it is overdue, stop the world (set m_stopEvent)
|
||||
if (m_ShutdownTimer <= elapsed)
|
||||
{
|
||||
if (!(m_ShutdownMask & SHUTDOWN_MASK_IDLE) || GetActiveAndQueuedSessionCount() == 0)
|
||||
// A long tick or wall-clock correction can jump across the entire
|
||||
// final minute. The real-time drain must still start before exit.
|
||||
sClientlessPlayerMgr->BeginShutdown();
|
||||
if ((!(m_ShutdownMask & SHUTDOWN_MASK_IDLE) || GetActiveAndQueuedSessionCount() == 0) &&
|
||||
((m_ShutdownMask & SHUTDOWN_MASK_FORCE) || sClientlessPlayerMgr->ShutdownReady()))
|
||||
m_stopEvent = true; // exist code already set
|
||||
else
|
||||
m_ShutdownTimer = 1; // minimum timer value to wait idle state
|
||||
@@ -3282,12 +3299,14 @@ void World::ShutdownServ(uint32 time, uint32 options, uint8 exitcode, const std:
|
||||
m_ExitCode = exitcode;
|
||||
|
||||
///- If the shutdown time is 0, set m_stopEvent (except if shutdown is 'idle' with remaining sessions)
|
||||
if (time <= 60) sClientlessPlayerMgr->BeginShutdown();
|
||||
if (time == 0)
|
||||
{
|
||||
if (!(options & SHUTDOWN_MASK_IDLE) || GetActiveAndQueuedSessionCount() == 0)
|
||||
m_stopEvent = true; // exist code already set
|
||||
if ((!(options & SHUTDOWN_MASK_IDLE) || GetActiveAndQueuedSessionCount() == 0) &&
|
||||
((options & SHUTDOWN_MASK_FORCE) || sClientlessPlayerMgr->ShutdownReady()))
|
||||
m_stopEvent = true;
|
||||
else
|
||||
m_ShutdownTimer = 1; //So that the session count is re-evaluated at next world tick
|
||||
m_ShutdownTimer = 1;
|
||||
}
|
||||
///- Else set the shutdown timer and warn users
|
||||
else
|
||||
@@ -3328,6 +3347,8 @@ void World::ShutdownMsg(bool show, Player* player, const std::string& reason)
|
||||
/// Cancel a planned server shutdown
|
||||
uint32 World::ShutdownCancel()
|
||||
{
|
||||
// A terminal database failure cannot be cancelled into an unusable world.
|
||||
if (m_databaseShutdownRequested) return 0;
|
||||
// nothing cancel or too later
|
||||
if (!m_ShutdownTimer || m_stopEvent)
|
||||
return 0;
|
||||
@@ -3343,6 +3364,7 @@ uint32 World::ShutdownCancel()
|
||||
|
||||
TC_LOG_DEBUG("misc", "Server %s cancelled.", (m_ShutdownMask & SHUTDOWN_MASK_RESTART ? "restart" : "shutdown"));
|
||||
|
||||
sClientlessPlayerMgr->CancelShutdown();
|
||||
sScriptMgr->OnShutdownCancel();
|
||||
return oldTimer;
|
||||
}
|
||||
|
||||
@@ -725,6 +725,7 @@ class TC_GAME_API World
|
||||
static uint8 GetExitCode() { return m_ExitCode; }
|
||||
static void StopNow(uint8 exitcode) { m_stopEvent = true; m_ExitCode = exitcode; }
|
||||
static bool IsStopped() { return m_stopEvent; }
|
||||
static void RequestShutdown(uint8 exitcode) { m_requestedShutdown.store(exitcode); }
|
||||
|
||||
void Update(uint32 diff);
|
||||
|
||||
@@ -861,6 +862,8 @@ class TC_GAME_API World
|
||||
~World();
|
||||
|
||||
static std::atomic<bool> m_stopEvent;
|
||||
static std::atomic<int> m_requestedShutdown;
|
||||
bool m_databaseShutdownRequested = false;
|
||||
static uint8 m_ExitCode;
|
||||
uint32 m_ShutdownTimer;
|
||||
uint32 m_ShutdownMask;
|
||||
|
||||
@@ -2645,74 +2645,24 @@ static bool HandleClientlessTravelReload(ChatHandler* handler, char const* /*arg
|
||||
// Restart recovery belongs to the optional AdventureBot script layer.
|
||||
namespace ClientlessSafeLogout
|
||||
{
|
||||
std::unordered_map<uint32,bool> Pending;
|
||||
bool All=false;
|
||||
bool Ready(Player const* player)
|
||||
bool Command(ChatHandler* handler, char const* args)
|
||||
{
|
||||
// Never force a timeout: native transport/flight/combat controllers must
|
||||
// finish first. Being on solid ground is sufficient even if a graph
|
||||
// planner is waiting on an unreachable future waypoint.
|
||||
return player && player->IsInWorld() && player->IsAlive() && !player->IsInCombat() &&
|
||||
!player->GetTransport() && !player->IsInFlight() && !player->IsFlying() &&
|
||||
!player->IsFalling() && !player->IsBeingTeleported() &&
|
||||
!player->HasUnitState(UNIT_STATE_CASTING) && !player->IsSplineEnabled();
|
||||
}
|
||||
bool Command(ChatHandler* handler,char const* args)
|
||||
{
|
||||
std::istringstream input(args?args:"");std::string op,target;input>>op>>target;
|
||||
if(op=="resume"){All=false;Pending.clear();}
|
||||
else if(op=="safe" && target=="all")
|
||||
std::istringstream input(args?args:"");
|
||||
std::string op,target;input>>op>>target;
|
||||
if (op=="resume") sClientlessPlayerMgr->ResumeSafeLogouts();
|
||||
else if (op=="safe" && target=="all") sClientlessPlayerMgr->RequestSafeLogoutAll();
|
||||
else if (op=="safe")
|
||||
{
|
||||
All=true;
|
||||
for(auto const& session:sWorld->GetAllClientlessSessions())
|
||||
if(sClientlessPlayerMgr->IsManaged(session.first))Pending.emplace(session.first,false);
|
||||
char* end=nullptr;
|
||||
auto guid=std::strtoul(target.c_str(),&end,10);
|
||||
if (target.empty() || !end || *end || !guid || guid>0xFFFFFFFFul || !sClientlessPlayerMgr->IsManaged(uint32(guid)))
|
||||
{ handler->SendSysMessage("Usage: clientless logout safe <guid|all> OR logout status"); return false; }
|
||||
sClientlessPlayerMgr->RequestSafeLogout(uint32(guid));
|
||||
}
|
||||
else if(op=="safe")
|
||||
{
|
||||
char* end=nullptr;auto guid=std::strtoul(target.c_str(),&end,10);
|
||||
if(target.empty() || !end || *end || !guid || guid>0xFFFFFFFFul || !sClientlessPlayerMgr->IsManaged(uint32(guid)))
|
||||
{handler->SendSysMessage("Usage: clientless logout safe <guid|all> OR logout status. Pause the loader first.");return false;}
|
||||
Pending.emplace(uint32(guid),false);
|
||||
}
|
||||
else if(op!="status")return false;
|
||||
uint32 combat=0,transport=0,flight=0,loading=0;
|
||||
for(auto const& entry:Pending)
|
||||
{
|
||||
auto session=sWorld->FindClientlessSession(entry.first);auto player=session?session->GetPlayer():nullptr;
|
||||
if(!player || !player->IsInWorld()){++loading;continue;}
|
||||
combat+=player->IsInCombat()?1:0;transport+=player->GetTransport()?1:0;
|
||||
flight+=(player->IsInFlight() || player->IsFlying())?1:0;
|
||||
}
|
||||
handler->PSendSysMessage("Safe logout pending=%u sessions=%u combat=%u transport=%u flight=%u loading=%u",uint32(Pending.size()),sWorld->GetClientlessSessionCount(),combat,transport,flight,loading);
|
||||
else if (op!="status") return false;
|
||||
sClientlessPlayerMgr->ReportSafeLogouts(handler);
|
||||
return true;
|
||||
}
|
||||
void Update()
|
||||
{
|
||||
if(All)for(auto const& session:sWorld->GetAllClientlessSessions())
|
||||
if(sClientlessPlayerMgr->IsManaged(session.first))Pending.emplace(session.first,false);
|
||||
for(auto it=Pending.begin();it!=Pending.end();)
|
||||
{
|
||||
uint32 guid=it->first;
|
||||
auto session=sWorld->FindClientlessSession(guid);
|
||||
if(!session){it=Pending.erase(it);continue;}
|
||||
auto player=session->GetPlayer();
|
||||
if(!player || !player->IsInWorld() || sWorld->GetClientlessPlayerState(guid)!=ClientlessPlayerState::Online){++it;continue;}
|
||||
if(!it->second)
|
||||
{
|
||||
ChatHandler handler(session);
|
||||
sAdventureBehaviorMgr->Command(&handler,("stop "+std::to_string(guid)).c_str());
|
||||
it->second=true;
|
||||
}
|
||||
if(Ready(player))
|
||||
{
|
||||
// World-thread eligibility and lifecycle request occur together;
|
||||
// never infer safety from stale database positions or intent text.
|
||||
if(sClientlessPlayerMgr->StopPlayer(guid))
|
||||
TC_LOG_INFO("server.worldserver","CLIENTLESS_SAFE_LOGOUT guid=%u",guid);
|
||||
}
|
||||
++it;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
class ClientlessInstanceVisitResume : public WorldScript
|
||||
@@ -2728,7 +2678,6 @@ public:
|
||||
void OnUpdate(uint32 diff) override
|
||||
{
|
||||
ClientlessCombatPreparation::Update(diff);
|
||||
ClientlessSafeLogout::Update();
|
||||
_queries.ProcessReadyCallbacks();
|
||||
if(_delay>diff){_delay-=diff;return;}
|
||||
_delay=2000;
|
||||
|
||||
@@ -503,7 +503,7 @@ void WorldUpdateLoop()
|
||||
|
||||
#ifdef _WIN32
|
||||
if (m_ServiceStatus == 0)
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
World::RequestShutdown(SHUTDOWN_EXIT_CODE);
|
||||
|
||||
while (m_ServiceStatus == 2)
|
||||
Sleep(1000);
|
||||
@@ -514,7 +514,7 @@ void WorldUpdateLoop()
|
||||
void SignalHandler(boost::system::error_code const& error, int /*signalNumber*/)
|
||||
{
|
||||
if (!error)
|
||||
World::StopNow(SHUTDOWN_EXIT_CODE);
|
||||
World::RequestShutdown(SHUTDOWN_EXIT_CODE);
|
||||
}
|
||||
|
||||
void FreezeDetector::Handler(std::weak_ptr<FreezeDetector> freezeDetectorRef, boost::system::error_code const& error)
|
||||
|
||||
@@ -4411,3 +4411,8 @@ Clientless.Travel.CostMultiplier.Tram = 1.0
|
||||
Clientless.Travel.CostMultiplier.Portal = 1.0
|
||||
# 0 disables variation; 0.20 means a per-mode range of 80%..120% per journey.
|
||||
Clientless.Travel.CostVariation = 0.20
|
||||
|
||||
# Clientless shutdown/recovery audit. Normal shutdown grants bots at most 60
|
||||
# seconds to finish safe movement/combat transitions. Each request writes its
|
||||
# beginning and remaining states here, independent of database availability.
|
||||
Clientless.Recovery.StateDir = "clientless-recovery"
|
||||
@@ -0,0 +1,58 @@
|
||||
"""Exercise the production reconnect branch and Ping without a real database outage."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/database/Database/MySQLConnection.cpp').read_text()
|
||||
def function(marker):
|
||||
a=s.index(marker);b=s.index('{',a);n=1;i=b+1
|
||||
while n:n+=(s[i]=='{')-(s[i]=='}');i+=1
|
||||
return s[a:i]
|
||||
a=s.index(' case CR_SERVER_GONE_ERROR:',s.index('bool MySQLConnection::_HandleMySQLErrno'))
|
||||
b=s.index(' case ER_LOCK_DEADLOCK:',a)
|
||||
branch='bool MySQLConnection::_HandleMySQLErrno(unsigned errNo, unsigned char attempts){switch(errNo){'+s[a:b]+'default:return false;}}'
|
||||
code=r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <memory>
|
||||
#include <deque>
|
||||
using uint32=unsigned;using uint8=unsigned char;
|
||||
#define TC_LOG_FATAL(...) ((void)0)
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
constexpr unsigned CR_SERVER_GONE_ERROR=2006,CR_SERVER_LOST=2013,CR_SERVER_LOST_EXTENDED=2055,CR_CONNECTION_ERROR=2002,CR_CONN_HOST_ERROR=2003,CR_SOCKET_CREATE_ERROR=2001,CR_IPSOCK_ERROR=2004,CR_UNKNOWN_HOST=2005;
|
||||
int sleeps=0,closed=0,pings=0,destroyed=0,pingError=0;
|
||||
namespace std::this_thread {void sleep_for(std::chrono::seconds){++sleeps;}}
|
||||
struct Handle{} handle;
|
||||
void mysql_close(Handle*){assert(destroyed>=2);++closed;}
|
||||
int mysql_ping(Handle*p){assert(p);++pings;return pingError;}
|
||||
unsigned mysql_errno(Handle*){return CR_SERVER_LOST;}
|
||||
struct Stmt{~Stmt(){++destroyed;}};
|
||||
struct MySQLConnection{
|
||||
bool m_failed=false,m_reconnecting=false,m_prepareError=false,prepareOK=true;
|
||||
Handle* m_Mysql=&handle;std::vector<std::unique_ptr<Stmt>> m_stmts;
|
||||
struct{std::string database="test";}m_connectionInfo;
|
||||
static std::atomic<bool> _fatalConnectionError;
|
||||
std::deque<unsigned> errors;unsigned opens=0,prepared=0;
|
||||
MySQLConnection(){m_stmts.emplace_back(new Stmt);m_stmts.emplace_back(new Stmt);}
|
||||
unsigned Open(){++opens;auto e=errors.empty()?0:errors.front();if(!errors.empty())errors.pop_front();m_Mysql=e?nullptr:&handle;return e;}
|
||||
bool PrepareStatements(){assert(m_stmts.size()==2);++prepared;return prepareOK;}
|
||||
bool _HandleMySQLErrno(unsigned,unsigned char=5);void FailConnection();void Ping();
|
||||
};std::atomic<bool>MySQLConnection::_fatalConnectionError{false};
|
||||
'''+function('void MySQLConnection::Ping()')+function('void MySQLConnection::FailConnection()')+branch+r'''
|
||||
int main(){
|
||||
{MySQLConnection c;c.errors={2002,2002,0};assert(c._HandleMySQLErrno(2013));assert(c.opens==3&&c.prepared==1&&!c.m_reconnecting&&c.m_Mysql);assert(sleeps==2&&!c._fatalConnectionError);}
|
||||
{MySQLConnection c;c.m_Mysql=nullptr;int n=pings;c.Ping();assert(c.opens==1&&c.prepared==1&&pings==n);}
|
||||
{MySQLConnection c;pingError=1;c.Ping();assert(c.opens==1&&c.prepared==1);pingError=0;}
|
||||
{MySQLConnection c;c.errors={2002,2002,2002,2002,2002};assert(!c._HandleMySQLErrno(2013));assert(c.opens==5&&c.m_failed&&c._fatalConnectionError&&c.m_Mysql==nullptr);auto n=c.opens;c.Ping();assert(c.opens==n);}
|
||||
MySQLConnection::_fatalConnectionError=false;
|
||||
{MySQLConnection c;c.prepareOK=false;assert(!c._HandleMySQLErrno(2002));assert(c.m_failed&&c._fatalConnectionError);}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-db-reconnect-')as folder:
|
||||
p=Path(folder);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production reconnect/Ping: error 2002 retries, null ping, ping loss, bounded fatal notification and statement lifetime passed.')
|
||||
@@ -0,0 +1,68 @@
|
||||
"""Run production login recovery against layered ground, native taxi and passenger doubles."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
|
||||
a=s.index('namespace\n{\n bool RecoveryGround(')
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <string>
|
||||
using uint32=unsigned;
|
||||
constexpr float INVALID_HEIGHT=-100000,MAX_HEIGHT=100000;
|
||||
enum class AnimTier{Ground};
|
||||
constexpr int NAV_GROUND=1,LINEOFSIGHT_ALL_CHECKS=1,FLIGHT_MOTION_TYPE=1,TEAM_ALLIANCE=0;
|
||||
constexpr int MOVEMENTFLAG_FLYING=1,MOVEMENTFLAG_ASCENDING=2,MOVEMENTFLAG_DESCENDING=4,MOVEMENTFLAG_FALLING=8;
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
namespace VMAP{enum class ModelIgnoreFlags{None=0};}
|
||||
struct Position{float x=0,y=0,z=0,o=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}float GetOrientation()const{return o;}void Relocate(Position const*p){*this=*p;}void Relocate(float a,float b,float c,float d){x=a;y=b;z=c;o=d;}};
|
||||
struct Map{float floor=100,water=INVALID_HEIGHT;bool ray=true;float GetHeight(int,float,float,float z,bool,float range){return floor<=z&&z-floor<=range?floor:INVALID_HEIGHT;}float GetWaterLevel(int,float,float){return water;}bool isInLineOfSight(int,float,float,float,float,float,float,int,VMAP::ModelIgnoreFlags){return ray;}}map;
|
||||
struct Motion{int type=0;int GetCurrentMovementGeneratorType(){return type;}void Clear(){type=0;}};
|
||||
struct Session{void CompleteClientlessTeleport(){}};
|
||||
struct Guid{bool present=false;explicit operator bool()const{return present;}unsigned GetCounter()const{return 150;}};
|
||||
struct Player:Position{
|
||||
bool flying=false;void SetAnimTier(AnimTier){}bool IsFlying(){return flying;}
|
||||
bool alive=true,combat=false,taxi=false,teleport=false,water=false,falling=false,mounted=false,canFly=false;void*transport=nullptr;unsigned faction=TEAM_ALLIANCE,id=1;int teleports=0,cleanups=0;
|
||||
struct{struct{Guid guid;void Reset(){guid.present=false;}}transport;}m_movementInfo;Motion motion;Session session;
|
||||
Player(){z=100;}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}void*GetTransport(){return transport;}Guid GetGUID(){return {};}
|
||||
bool IsBeingTeleported(){return teleport;}bool IsInFlight(){return taxi;}bool IsInWater(){return water;}bool IsFalling(){return falling;}
|
||||
bool IsMounted(){return mounted;}bool CanFly(){return canFly;}unsigned GetTeamId(){return faction;}Map*GetMap(){return ↦}int GetPhaseShift(){return 0;}unsigned GetMapId(){return id;}unsigned GetInstanceId(){return 0;}
|
||||
Motion*GetMotionMaster(){return &motion;}Session*GetSession(){return &session;}
|
||||
void CleanupAfterTaxiFlight(){taxi=false;++cleanups;}void StopMoving(){}void RemoveUnitMovementFlag(int){falling=flying=false;}
|
||||
bool TeleportTo(unsigned m,float a,float b,float c,float d){++teleports;id=m;Relocate(a,b,c,d);return true;}
|
||||
};
|
||||
struct VcorePositionPathResult{enum class Status{Complete,NoPolygon,NavMeshUnavailable,TileUnavailable};Status Result=Status::Complete;bool IsComplete()const{return Result==Status::Complete;}};
|
||||
bool unavailable=false;
|
||||
struct PathGenerator{static VcorePositionPathResult AnalyzePositions(unsigned,unsigned,unsigned,Position const&p,Position const&,int){return {unavailable?VcorePositionPathResult::Status::NavMeshUnavailable:std::fabs(p.z-map.floor)<3?VcorePositionPathResult::Status::Complete:VcorePositionPathResult::Status::NoPolygon};}};
|
||||
enum class ClientlessTransportPhase{Waiting,Boarding,Riding,Leaving,Arrived,Failed};
|
||||
struct Transport{struct{ClientlessTransportPhase Phase=ClientlessTransportPhase::Failed;}status;unsigned resumes=0;bool present=false;void ResumePassenger(Player*){++resumes;}auto GetStatus(unsigned)->decltype(&status){return present?&status:nullptr;}}transport;auto sClientlessTransportMgr=&transport;
|
||||
enum class ClientlessFlightState{Mounting,Flying,Arrived,Failed};
|
||||
struct Flight{struct{ClientlessFlightState State=ClientlessFlightState::Failed;}status;bool start=false;unsigned calls=0,clears=0;bool Start(Player*,Position const&,std::string&,bool owned){assert(!owned);++calls;status.State=ClientlessFlightState::Mounting;return start;}auto GetStatus(unsigned){return &status;}void Clear(unsigned){++clears;}}flight;auto sClientlessFlightMgr=&flight;
|
||||
namespace ClientlessLoginRecovery{struct State{uint32 Elapsed=0;bool Landing=false;};bool Update(Player*,State&,unsigned);}
|
||||
'''+s[a:]+r'''
|
||||
int main(){using namespace ClientlessLoginRecovery;
|
||||
{Player p;State st;assert(Update(&p,st,50)&&p.teleports==0);}
|
||||
{Player p;State st;p.flying=true;assert(Update(&p,st,50)&&!p.flying&&p.teleports==0);}
|
||||
{Player p;State st;unavailable=true;assert(Update(&p,st,50)&&p.teleports==0);unavailable=false;}
|
||||
{Player p;State st;p.z=108.3f;assert(Update(&p,st,50)&&p.teleports==1&&std::fabs(p.z-100)<.01);}
|
||||
{Player p;State st;p.z=108.3f;map.ray=false;assert(Update(&p,st,50)&&p.teleports==1&&p.id==0&&p.x<0);map.ray=true;}
|
||||
{Player p;State st;p.z=500;p.faction=1;assert(Update(&p,st,50)&&p.id==1&&p.x>1500);}
|
||||
{Player p;State st;p.transport=&transport;p.z=500;assert(!Update(&p,st,30000)&&p.teleports==0&&transport.resumes==1);}
|
||||
{Player p;State st;transport.present=true;transport.status.Phase=ClientlessTransportPhase::Leaving;assert(!Update(&p,st,30000)&&p.teleports==0);transport.present=false;}
|
||||
{Player p;State st;p.taxi=true;p.motion.type=FLIGHT_MOTION_TYPE;for(int i=0;i<50;++i)assert(!Update(&p,st,30000));assert(p.teleports==0&&p.cleanups==0);}
|
||||
{Player p;State st;p.taxi=true;assert(!Update(&p,st,1000));assert(Update(&p,st,5000)&&p.cleanups==1&&p.teleports==0);}
|
||||
{Player p;State st;p.alive=false;p.z=500;assert(Update(&p,st,50)&&p.teleports==0);}
|
||||
{Player p;State st;p.combat=true;p.z=500;assert(Update(&p,st,50)&&p.teleports==0);}
|
||||
{Player p;State st;p.water=true;p.z=500;assert(Update(&p,st,50)&&p.teleports==0);}
|
||||
{Player p;State st;p.m_movementInfo.transport.guid.present=true;assert(!Update(&p,st,50));assert(Update(&p,st,5000)&&p.teleports==1&&!p.m_movementInfo.transport.guid);}
|
||||
{Player p;State st;p.mounted=p.canFly=true;p.z=200;flight.start=true;assert(!Update(&p,st,50)&&st.Landing&&p.teleports==0);flight.status.State=ClientlessFlightState::Arrived;assert(Update(&p,st,50)&&p.teleports==0);}
|
||||
{Player p;State st;p.mounted=p.canFly=true;p.z=500;flight.start=true;assert(!Update(&p,st,50));flight.status.State=ClientlessFlightState::Flying;assert(!Update(&p,st,30000));assert(Update(&p,st,30000)&&flight.clears==1&&p.teleports==1);}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-login-recovery-')as folder:
|
||||
p=Path(folder);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production login recovery: healthy/missing-data unchanged, local floor, blocked column, both factions, real passengers/taxis, orphan state and bounded landing passed.')
|
||||
@@ -0,0 +1,35 @@
|
||||
"""Verify cached zone-script references are detached before OutdoorPvP destruction."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2];s=(root/'src/server/game/OutdoorPvP/OutdoorPvPMgr.cpp').read_text()
|
||||
a=s.index('namespace\n{\n struct DetachOutdoorScripts');b=s.index('OutdoorPvPMgr* OutdoorPvPMgr::instance()',a)
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
#include <array>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
using ObjectGuid=unsigned;
|
||||
struct ZoneScript{virtual ~ZoneScript()=default;};
|
||||
struct Object{ZoneScript* script=nullptr;ZoneScript* GetZoneScript(){return script;}void ClearZoneScript(){script=nullptr;}};
|
||||
struct Store{std::unordered_map<ObjectGuid,Object*>objects;};using MapStoredObjectTypesContainer=Store;
|
||||
struct Map{Store store;Store&GetObjectsStore(){return store;}};
|
||||
std::vector<Object*>all;unsigned destroyed=0;
|
||||
struct OutdoorPvP:ZoneScript{~OutdoorPvP(){for(auto o:all)assert(o->GetZoneScript()!=this);++destroyed;}};
|
||||
template<class Worker,class Container>struct TypeContainerVisitor{Worker&w;TypeContainerVisitor(Worker&v):w(v){}void Visit(Container&c){w.Visit(c.objects);}};
|
||||
struct OutdoorPvPMgr{std::unordered_map<Map*,std::vector<std::unique_ptr<OutdoorPvP>>>m_OutdoorPvPByMap;std::unordered_map<unsigned,OutdoorPvP*>m_OutdoorPvPMap;std::array<unsigned,6>m_OutdoorPvPDatas;void Die();};
|
||||
'''+s[a:b]+r'''
|
||||
int main(){Map a,b;OutdoorPvPMgr m;ZoneScript battlefield;Object first,second,unrelated;
|
||||
m.m_OutdoorPvPByMap[&a].emplace_back(new OutdoorPvP);m.m_OutdoorPvPByMap[&b].emplace_back(new OutdoorPvP);
|
||||
first.script=m.m_OutdoorPvPByMap[&a][0].get();second.script=m.m_OutdoorPvPByMap[&b][0].get();unrelated.script=&battlefield;
|
||||
a.store.objects={{1,&first},{2,&unrelated}};b.store.objects={{3,&second}};all={&first,&second,&unrelated};
|
||||
m.Die();assert(destroyed==2&&!first.script&&!second.script&&unrelated.script==&battlefield);
|
||||
assert(m.m_OutdoorPvPByMap.empty()&&m.m_OutdoorPvPMap.empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-outdoor-lifetime-')as folder:
|
||||
p=Path(folder);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production OutdoorPvP shutdown: references detached before destruction, unrelated zone scripts retained passed.')
|
||||
@@ -0,0 +1,69 @@
|
||||
"""Exercise production shutdown/drain methods using a deterministic monotonic clock."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile,json
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/ClientlessPlayerMgr.cpp').read_text()
|
||||
def function(marker):
|
||||
a=s.index(marker);i=s.index('{',a)+1;n=1
|
||||
while n:n+=(s[i]=='{')-(s[i]=='}');i+=1
|
||||
return s[a:i]
|
||||
methods='\n'.join(function(m) for m in ['bool ClientlessPlayerMgr::ReadyForSafeLogout(', 'void ClientlessPlayerMgr::RequestSafeLogout(', 'void ClientlessPlayerMgr::RequestSafeLogoutAll(', 'void ClientlessPlayerMgr::ResumeSafeLogouts(', 'void ClientlessPlayerMgr::UpdateSafeLogouts(', 'void ClientlessPlayerMgr::BeginShutdown(', 'void ClientlessPlayerMgr::CancelShutdown(', 'bool ClientlessPlayerMgr::ShutdownReady(', 'void ClientlessPlayerMgr::WriteShutdownSnapshot('])
|
||||
methods=methods.replace('std::chrono::steady_clock::now()', 'TestClock::now()')
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iomanip>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
using uint32=unsigned;
|
||||
#define TC_LOG_ERROR(...) ((void)0)
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
constexpr int UNIT_STATE_CASTING=1,TEAM_ALLIANCE=0;
|
||||
struct TestClock{static long long ms;static auto now(){return std::chrono::steady_clock::time_point(std::chrono::milliseconds(ms));}};long long TestClock::ms=0;
|
||||
namespace GameTime{long GetGameTime(){return 123;}unsigned GetGameTimeMS(){return unsigned(TestClock::ms);}}
|
||||
struct Guid{unsigned GetRawValue()const{return 99;}};
|
||||
struct Player{
|
||||
bool alive=true,combat=false,taxi=false,fly=false,fall=false,teleport=false,spline=false,casting=false;void* transport=nullptr;
|
||||
struct{struct{Guid guid;}transport;}m_movementInfo;
|
||||
bool IsInWorld()const{return true;}bool IsAlive()const{return alive;}bool IsInCombat()const{return combat;}
|
||||
void*GetTransport()const{return transport;}bool IsInFlight()const{return taxi;}bool IsFlying()const{return fly;}bool IsFalling()const{return fall;}
|
||||
bool IsBeingTeleported()const{return teleport;}bool HasUnitState(int)const{return casting;}bool IsSplineEnabled()const{return spline;}
|
||||
std::string GetName(){return "Testbot";}unsigned getRace(){return 22;}unsigned GetTeamId(){return 0;}unsigned GetMapId(){return 0;}
|
||||
float GetPositionX(){return 1;}float GetPositionY(){return 2;}float GetPositionZ(){return 3;}
|
||||
} player;
|
||||
struct Session{Player*GetPlayer(){return &player;}}session;
|
||||
struct World{std::unordered_map<unsigned,Session*>sessions;unsigned removed=0;auto const&GetAllClientlessSessions(){return sessions;}unsigned GetClientlessSessionCount(){return sessions.size();}Session*FindClientlessSession(unsigned id){auto i=sessions.find(id);return i==sessions.end()?nullptr:i->second;}bool RemoveClientlessSession(unsigned id){++removed;sessions.erase(id);return true;}}world;auto sWorld=&world;
|
||||
std::string directory;
|
||||
struct Config{std::string GetStringDefault(char const*,char const*){return directory;}}config;auto sConfigMgr=&config;
|
||||
struct MySQLConnection{static bool HasFatalConnectionError(){return false;}};
|
||||
struct Controller{unsigned resumes=0;void OnSafeLogoutCancelled(Player*){++resumes;}}controller;
|
||||
struct ClientlessPlayerMgr{
|
||||
std::unordered_map<unsigned,bool>_safeLogouts;bool _safeLogoutAll=false,_shutdownDraining=false,_shutdownWasSafeLogoutAll=false,_shutdownSnapshotDone=false;
|
||||
std::chrono::steady_clock::time_point _shutdownStarted;std::string _shutdownAuditFile;
|
||||
struct Record{struct Controller*Controller=nullptr;};std::unordered_map<unsigned,Record>_records;
|
||||
static bool ReadyForSafeLogout(Player const*);void RequestSafeLogout(unsigned);void RequestSafeLogoutAll();void ResumeSafeLogouts();void UpdateSafeLogouts();void BeginShutdown();void CancelShutdown();bool ShutdownReady();void WriteShutdownSnapshot(char const*);
|
||||
};
|
||||
'''+methods+r'''
|
||||
int main(int argc,char**argv){directory=argv[1];ClientlessPlayerMgr m;world.sessions[26]=&session;m._records[26].Controller=&controller;
|
||||
player.fly=true;m.BeginShutdown();assert(m._shutdownDraining&&m._safeLogoutAll);
|
||||
m._safeLogouts[26]=true;m.UpdateSafeLogouts();assert(world.removed==0);
|
||||
TestClock::ms=59999;assert(!m.ShutdownReady());TestClock::ms=60000;assert(m.ShutdownReady()&&world.removed==0);
|
||||
assert(m._shutdownSnapshotDone);m.CancelShutdown();assert(!m._shutdownDraining&&!m._safeLogoutAll&&controller.resumes==1);
|
||||
// Ready bots leave immediately after the controller acknowledged its pause.
|
||||
player.fly=false;m.RequestSafeLogoutAll();m._safeLogouts[26]=true;m.UpdateSafeLogouts();assert(world.removed==1);
|
||||
m.ResumeSafeLogouts();TestClock::ms=70000;m.BeginShutdown();assert(m.ShutdownReady());m.CancelShutdown();
|
||||
// A pre-existing manual drain survives cancellation of a server shutdown.
|
||||
world.sessions[26]=&session;m.RequestSafeLogoutAll();m.BeginShutdown();m.CancelShutdown();assert(m._safeLogoutAll);
|
||||
for(int flag=0;flag<7;++flag){player=Player{};switch(flag){case 0:player.combat=true;break;case 1:player.taxi=true;break;case 2:player.transport=&session;break;case 3:player.teleport=true;break;case 4:player.spline=true;break;case 5:player.fall=true;break;case 6:player.alive=false;break;}assert(!m.ReadyForSafeLogout(&player));}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-shutdown-')as folder:
|
||||
p=Path(folder);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test'),str(p/'audit')],check=True)
|
||||
events=[json.loads(l) for f in (p/'audit').glob('*.jsonl') for l in f.read_text().splitlines()]
|
||||
assert any(e['event']=='deadline' and e['bots'][0]['flying']==1 and e['bots'][0]['faction']=='alliance' for e in events)
|
||||
assert any(e['event']=='complete' and e['remaining']==0 for e in events)
|
||||
print('Production drain: 60s deadline, safe logout, no forced passenger detach, cancellation/resume and durable JSON audit passed.')
|
||||
@@ -84,11 +84,13 @@ public:
|
||||
uint32 deadline=0; uint32 GetUInt32Value(int)const{return deadline;}
|
||||
};
|
||||
struct Session {int completed=0;void CompleteClientlessTeleport() {++completed;}} session;
|
||||
struct Motion {Player*p; void MoveSmoothPath(int,Position* path,int,bool);};
|
||||
struct Motion {Player*p; int clears=0;void Clear(){++clears;}void MoveSmoothPath(int,Position* path,int,bool);};
|
||||
struct Spline {uint32 id=0;uint32 GetId()const{return id;}};
|
||||
class Player:public Position {
|
||||
public:
|
||||
TransportBase* transport=nullptr;uint32 map=0;GameObject* object=nullptr;Motion motion{this};bool pending=false;
|
||||
struct {struct {Position pos;ObjectGuid guid;uint32 time=0;void Reset(){pos={};guid={};time=0;}}transport;}m_movementInfo;
|
||||
Spline spline;Spline* movespline=&spline;bool running=false;int stops=0;
|
||||
int teleports=0;bool falling=false,teleportAllowed=true;
|
||||
bool IsFalling()const{return falling;}bool IsMounted(){return false;}void Dismount(){}void UpdateSpeed(int){}void RemoveAurasByType(int){}
|
||||
bool TeleportTo(uint32 m,float a,float b,float c,float d){if(!teleportAllowed)return false;++teleports;if(transport)transport->RemovePassenger(this);map=m;Relocate(a,b,c,d);falling=false;return true;}
|
||||
@@ -96,7 +98,7 @@ public:
|
||||
bool IsInWorld()const{return true;}bool IsInPhase(GameObject*)const{return true;}
|
||||
bool IsBeingTeleported()const{return pending;}TransportBase* GetTransport()const{return transport;}
|
||||
ObjectGuid GetGUID()const{return {26};}uint32 GetMapId()const{return map;}
|
||||
float GetSpeed(int)const{return 7;}void StopMoving(){}bool IsSplineEnabled()const{return false;}
|
||||
float GetSpeed(int)const{return 7;}void StopMoving(){++stops;running=false;}bool IsSplineEnabled()const{return running;}
|
||||
Motion* GetMotionMaster(){return &motion;}Session* GetSession(){return &session;}
|
||||
};
|
||||
struct Node{std::string Key;} node;
|
||||
@@ -109,7 +111,7 @@ void TransportBase::CalculatePassengerPosition(float&x,float&y,float&z,float*o)c
|
||||
void TransportBase::CalculatePassengerOffset(float&x,float&y,float&z,float*o)const{CalculatePassengerOffset(x,y,z,o,go->x,go->y,go->z,go->o);}
|
||||
void TransportBase::AddPassenger(Player*p){p->transport=this;p->m_movementInfo.transport.guid=go->GetGUID();}
|
||||
void TransportBase::RemovePassenger(Player*p){p->transport=nullptr;p->m_movementInfo.transport.Reset();}
|
||||
void Motion::MoveSmoothPath(int,Position* path,int,bool){p->Relocate(path[1]);if(p->transport){Position pos=*p;p->transport->CalculatePassengerOffset(pos.x,pos.y,pos.z,&pos.o);p->m_movementInfo.transport.pos=pos;}}
|
||||
void Motion::MoveSmoothPath(int,Position* path,int,bool){++p->spline.id;p->Relocate(path[1]);if(p->transport){Position pos=*p;p->transport->CalculatePassengerOffset(pos.x,pos.y,pos.z,&pos.o);p->m_movementInfo.transport.pos=pos;}}
|
||||
enum class ClientlessTravelState{Moving,Arrived,Stuck,PathRejected};
|
||||
struct ClientlessPathDiagnostics{bool Complete=true,Suspicious=false;float PathLength=50;};
|
||||
struct Travel{bool path=false,present=false;int starts=0;struct Status{ClientlessTravelState State=ClientlessTravelState::Moving;}status;
|
||||
@@ -136,7 +138,7 @@ methods = '\n'.join(function(source, marker) for marker in [
|
||||
'bool ClientlessTransportMgr::ResumePassenger(', 'bool ClientlessTransportMgr::SelectResumedDock(',
|
||||
'bool ClientlessTransportMgr::HasConnection(', 'bool ClientlessTransportMgr::AtDock(',
|
||||
'bool ClientlessTransportMgr::Sample(', 'bool ClientlessTransportMgr::Start(',
|
||||
'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::Cancel(',
|
||||
'void ClientlessTransportMgr::SetPhase(', 'void ClientlessTransportMgr::StopLandMovement(', 'void ClientlessTransportMgr::Cancel(',
|
||||
'void ClientlessTransportMgr::RecoverPassenger(', 'bool ClientlessTransportMgr::RecoverStuckElevator(',
|
||||
'void ClientlessTransportMgr::UpdateRide('
|
||||
])
|
||||
@@ -190,7 +192,7 @@ int main(){
|
||||
p.x=0;go.x=5;assert(mgr.Start(&p,"A","B",error));mgr.Cancel(26);auto&cancel=mgr._rides.at(26);run(mgr,p,cancel);assert(cancel.Status.Phase==Phase::Failed&&cancel.Status.Cancelled&&!p.transport);
|
||||
// Midride cancellation retains the Passenger until a recorded safe dock.
|
||||
p.x=0;assert(mgr.Start(&p,"A","B",error));auto&abort=mgr._rides.at(26);run(mgr,p,abort);assert(p.transport);
|
||||
go.x=50;p.x=51;mgr.Cancel(26);run(mgr,p,abort);assert(abort.Status.Phase==Phase::Riding&&p.transport);
|
||||
go.x=50;p.x=51;mgr.Cancel(26,&p);run(mgr,p,abort);assert(abort.Status.Phase==Phase::Riding&&p.transport);
|
||||
go.x=5;p.x=6;run(mgr,p,abort);assert(abort.Status.Phase==Phase::Failed&&abort.Status.Cancelled&&!p.transport&&p.x==0);
|
||||
// A timeout aboard never removes the passenger over sea/in midair.
|
||||
assert(mgr.Start(&p,"A","B",error));auto&timeout=mgr._rides.at(26);run(mgr,p,timeout);assert(p.transport);timeout.Elapsed=1800001;run(mgr,p,timeout,1);assert(timeout.Status.Phase==Phase::Riding&&timeout.Abort&&timeout.RetryDelay==3000&&p.transport);go.base.RemovePassenger(&p);
|
||||
@@ -239,6 +241,38 @@ int main(){
|
||||
lost.x=50;lost.teleports=0;travel.path=true;recovery.Status.Phase=Phase::Riding;
|
||||
mgr.UpdateRide(&lost,recovery,100);assert(lost.teleports==0 && travel.starts==1);
|
||||
mgr.UpdateRide(&lost,recovery,100);assert(!recovery.Status.Recovering);
|
||||
// An obsolete waiting ride must never pull a new ground action uphill.
|
||||
{
|
||||
Player walker;ClientlessTransportMgr owner;
|
||||
auto& old=owner._rides[26];old.Status.Phase=Phase::Waiting;
|
||||
old.From=stop("A",5,0);old.To=stop("B",100,104);old.WaitStage=2;old.WaitTrace={{0,0,30},{2,0,30}};
|
||||
old.LandSplineId=5;walker.spline.id=6;walker.running=true;walker.x=80;
|
||||
owner.Cancel(26,&walker);
|
||||
assert(old.Status.Phase==Phase::Failed&&old.Status.Cancelled);
|
||||
assert(walker.stops==0&&walker.running&&walker.x==80&&walker.motion.clears==0);
|
||||
owner.UpdateRide(&walker,old,100);assert(walker.x==80&&walker.stops==0);
|
||||
// Without an explicit cancel, a foreign spline is also protected.
|
||||
old.Status.Phase=Phase::Waiting;old.Abort=false;old.WaitStage=2;
|
||||
old.WaitTrace={{0,0,30},{2,0,30}};old.LandSplineId=5;
|
||||
owner.UpdateRide(&walker,old,100);
|
||||
assert(old.Status.Phase==Phase::Failed&&walker.stops==0&&walker.running&&walker.x==80);
|
||||
// A legitimate own waiting spline is stopped synchronously on land.
|
||||
old.Status.Phase=Phase::Waiting;old.LandSplineId=6;old.Abort=false;
|
||||
owner.Cancel(26,&walker);
|
||||
assert(old.Status.Phase==Phase::Failed&&walker.stops==1&&walker.motion.clears==1&&!walker.running);
|
||||
// Deferred cancellation also runs before old waiting-return logic.
|
||||
old.Status.Phase=Phase::Waiting;old.Abort=false;old.WaitStage=2;
|
||||
old.WaitTrace={{0,0,30},{2,0,30}};walker.spline.id=7;walker.running=true;
|
||||
owner.Cancel(26);owner.UpdateRide(&walker,old,100);
|
||||
assert(old.Status.Phase==Phase::Failed&&walker.stops==1&&walker.running&&walker.x==80);
|
||||
// Even without a running foreign spline, a displaced waiting trace cannot
|
||||
// manufacture a diagonal shortcut back to an elevated dock.
|
||||
old.Status.Phase=Phase::Waiting;old.Abort=false;old.WaitStage=2;
|
||||
old.WaitTrace={{0,0,30},{2,0,30}};walker.running=false;
|
||||
GameObject docked;docked.x=5;walker.object=&docked;
|
||||
owner.UpdateRide(&walker,old,100);
|
||||
assert(old.Status.Phase==Phase::Failed&&walker.x==80&&walker.stops==1);
|
||||
}
|
||||
// Waiting spots return along the observed path before the existing boarding sequence.
|
||||
Player waiting;GameObject dock;dock.x=5;dock.state.Stopped=false;waiting.object=&dock;
|
||||
ClientlessPositioning::available=true;
|
||||
|
||||
@@ -19,7 +19,8 @@ using ObjectGuid=unsigned;
|
||||
struct Creature;
|
||||
struct Position{float x=0,y=0,z=0;float GetPositionX()const{return x;}float GetPositionY()const{return y;}float GetPositionZ()const{return z;}};
|
||||
struct TransportDockState{bool Stopped=true;uint32 RemainingPause=60000;};
|
||||
struct GameObject{TransportDockState dock;unsigned entry=176310;unsigned GetEntry(){return entry;}unsigned GetGUID(){return 123;}float GetObjectScale(){return 1;}bool GetTransportDockState(TransportDockState& d){d=dock;return true;}}object;
|
||||
constexpr int GAMEOBJECT_TYPE_TRANSPORT=11;
|
||||
struct GameObject{int kind=0;int GetGoType(){return kind;}TransportDockState dock;unsigned entry=176310;unsigned GetEntry(){return entry;}unsigned GetGUID(){return 123;}float GetObjectScale(){return 1;}bool GetTransportDockState(TransportDockState& d){d=dock;return true;}}object;
|
||||
struct Guid{unsigned id;unsigned GetCounter(){return id;}};
|
||||
struct Player{Point current{0,0,0};unsigned id=1;bool spline=false,alive=true,combat=false;int stops=0;void* transport=&object;
|
||||
Guid GetGUID(){return {id};}bool IsAlive(){return alive;}bool IsInCombat(){return combat;}bool IsSplineEnabled(){return spline;}
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
"""Exercise production World shutdown entrypoints and DB failure handoff."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/World/World.cpp').read_text()
|
||||
def function(marker):
|
||||
a=s.index(marker);i=s.index('{',a)+1;n=1
|
||||
while n:n+=(s[i]=='{')-(s[i]=='}');i+=1
|
||||
return s[a:i]
|
||||
a=s.index('void World::Update(uint32 diff)');b=s.index(' time_t currentGameTime',a)
|
||||
methods='\n'.join(function(m) for m in ['void World::_UpdateGameTime()', 'void World::ShutdownServ(', 'uint32 World::ShutdownCancel()'])+s[a:b]+'}\n'
|
||||
code=r'''
|
||||
#include <atomic>
|
||||
#include <cassert>
|
||||
#include <string>
|
||||
#include <ctime>
|
||||
using uint32=unsigned;using uint8=unsigned char;
|
||||
constexpr unsigned SHUTDOWN_MASK_IDLE=2,SHUTDOWN_MASK_RESTART=1,SHUTDOWN_MASK_FORCE=4;
|
||||
constexpr int SHUTDOWN_EXIT_CODE=0,ERROR_EXIT_CODE=1,SERVER_MSG_RESTART_CANCELLED=1,SERVER_MSG_SHUTDOWN_CANCELLED=2;
|
||||
using ServerMessageType=int;using ShutdownExitCode=unsigned;using ShutdownMask=unsigned;
|
||||
#define TC_LOG_DEBUG(...) ((void)0)
|
||||
#define TC_LOG_ERROR(...) ((void)0)
|
||||
namespace GameTime{time_t now=0,next=0;time_t GetGameTime(){return now;}void UpdateGameTimers(){now=next;}}
|
||||
struct Mgr{bool draining=false,ready=false;int starts=0,cancels=0;void BeginShutdown(){if(!draining){draining=true;++starts;}}bool ShutdownReady(){return ready;}void CancelShutdown(){draining=false;++cancels;}}mgr;auto sClientlessPlayerMgr=&mgr;
|
||||
struct Script{int begins=0,cancels=0;void OnShutdownInitiate(unsigned,unsigned){++begins;}void OnShutdownCancel(){++cancels;}}script;auto sScriptMgr=&script;
|
||||
struct MySQLConnection{static bool failed;static bool HasFatalConnectionError(){return failed;}};bool MySQLConnection::failed=false;
|
||||
struct World{
|
||||
static bool m_stopEvent;static std::atomic<int>m_requestedShutdown;
|
||||
unsigned m_ShutdownMask=0,m_ShutdownTimer=0,m_ExitCode=0,active=0;bool m_databaseShutdownRequested=false,closed=false;
|
||||
static bool IsStopped(){return m_stopEvent;}unsigned GetActiveAndQueuedSessionCount(){return active;}
|
||||
void SetClosed(bool v){closed=v;}void ShutdownMsg(bool=false,void* =nullptr,std::string const& ={}){}void SendServerMessage(int){}
|
||||
void _UpdateGameTime();void ShutdownServ(unsigned,unsigned,unsigned char,std::string const& ={});unsigned ShutdownCancel();void Update(unsigned);
|
||||
};bool World::m_stopEvent=false;std::atomic<int>World::m_requestedShutdown{-1};
|
||||
void reset(){World::m_stopEvent=false;World::m_requestedShutdown=-1;mgr={};script={};GameTime::now=GameTime::next=0;MySQLConnection::failed=false;}
|
||||
'''+methods+r'''
|
||||
int main(){
|
||||
reset();{World w;w.ShutdownServ(2,0,0);assert(mgr.draining&&!w.IsStopped());GameTime::next=2;w._UpdateGameTime();assert(w.m_ShutdownTimer==1&&!w.IsStopped());mgr.ready=true;GameTime::next=3;w._UpdateGameTime();assert(w.IsStopped());}
|
||||
reset();{World w;mgr.ready=true;w.ShutdownServ(0,0,0);assert(w.IsStopped());}
|
||||
reset();{World w;w.ShutdownServ(0,SHUTDOWN_MASK_FORCE,0);assert(w.IsStopped());}
|
||||
reset();{World w;w.ShutdownServ(120,0,0);assert(!mgr.draining);GameTime::next=60;w._UpdateGameTime();w._UpdateGameTime();assert(mgr.draining&&!w.IsStopped());assert(w.ShutdownCancel()==60&&mgr.cancels==1&&!mgr.draining);}
|
||||
reset();{World w;w.ShutdownServ(120,0,0);GameTime::next=120;w._UpdateGameTime();assert(mgr.draining&&!w.IsStopped());}
|
||||
reset();{World w;w.active=1;mgr.ready=true;w.ShutdownServ(0,SHUTDOWN_MASK_IDLE,0);assert(!w.IsStopped());GameTime::next=1;w._UpdateGameTime();assert(!w.IsStopped());w.active=0;GameTime::next=2;w._UpdateGameTime();assert(w.IsStopped());}
|
||||
reset();{World w;World::m_requestedShutdown=0;w.Update(1);assert(mgr.draining&&!w.IsStopped());mgr.ready=true;GameTime::next=1;w.Update(1);assert(w.IsStopped());}
|
||||
reset();{World w;MySQLConnection::failed=true;w.Update(1);assert(w.closed&&w.m_databaseShutdownRequested&&w.m_ExitCode==1&&!w.IsStopped());assert(w.ShutdownCancel()==0);w.Update(1);assert(mgr.starts==1);mgr.ready=true;GameTime::next=1;w.Update(1);assert(w.IsStopped());}
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory(prefix='vcore-world-shutdown-')as folder:
|
||||
p=Path(folder);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['c++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Production World: normal/zero/forced/idle shutdown, final-minute drain, cancellation, signal request and terminal database handoff passed.')
|
||||
Reference in new issue
Block a user