Implement instance modes, full-group departures and mandatory flight regions

This commit is contained in:
Nico committed 2026-10-30 02:15:06 +01:00
1 parent bc8f171055
commit 373818628d
40 files changed
+1838 -82

No files matched your search

+139
View File
@@ -1,3 +1,142 @@
# ACTIVE USER REQUEST: commit flight-region extension, repeat groups, observe until04:00CET
Current dateOct30; user requires flight not only Northrend but also Outland and
Cataclysm regions. Implemented AreaMgr guard for catalog prefer_flight regions;
keeps map530 racial low-level zones separate. Cold68+ onlyNorthrend; otherflight
regions require60+, actual native flight,90267 outside530/571. Build-j10 and69
suite tests passed, logs copied to checkpoints/2026-10-30-flight-regions. Commit
all pending requested work before starting group test, no push. Still need deploy
this final small AreaMgr change (previous broader feature already installed).
Then repeat group test: current local hour(groupformation) atAllianceStormwind/
HordeOrgrimmar, next hour mixed valid normal/heroic dungeons/raids. Exclude bots
below15 as user previously requested. IncludeArthoran26 plus110readygenerated.
Preparation script/tmp/vcore-repeat-group-test.py prepare running (log
/tmp/vcore-group-test-prepare.log). It builds native map/mode bands, uses exact
server eligibility, logs in offline candidates as needed, falls back from raids
without valid entrances to suitable dungeons. Output/tmp/vcore-group-test-20261030/
state.json. NO test DB/config edits yet; prepare onlynativechecks/logins.
Next steps: ensure prepare completes; run script pause (pauses loader, rotation0,
requests participant logout); shutdown current385679 via dedicated session; wait
full exit (oldknown shutdownSIGSEGVpossible); script apply edits onlytwo current
andnextday slots, teleports offlineparticipants factioncapitals, clearsvisit/ghost/
oldhandoff state, restores loader enabled1 but rotation remains0 during test.
Install compiled finalregionalbinary, startfreshdedicatedtmux, verifyreadyports.
RotationPercent previous15 saved in /tmp/vcore-group-test-20261030/worldserver.conf.before
(contains secrets! Never print/copy into repo). Restore onlyRotationPercent at04:00.
Observe bots+logs+server until **2026-10-30 04:00 Europe/Berlin =03:00UTC**, report
then. Clock atinitialrequest01:06UTC(local02:06). Must actually monitor, not final
prematurely. Currentserver385679/sessionvcore-worldserver console
/tmp/vcore-northrend-flight-final-start-20261030.log. Olduser `vcore` session is
shell only; leave it. No secondworldserver. User insists all builds -j10; own build
processes inherit oom_score_adj1000 to prefer build termination over server if
memory exhausted. No running build now. Currentmanifest prepare needsfollow-up.
# Latest: mandatory flight build INSTALLED, final server READY
User says no ground-only bots in Northrend. New source+previous pending instance
modes/group-handoff WIP installed; worldSQL08 applied. Final helper uses existing
native FlightMgr::Enabled, not a direct mounted-speed aura assumption (initial
live mistake fixed). All builds -j10,69 tests passed with updated fixtures and
production native-flight harness. Final dedicated tmuxvcore-worldserver PID385679,
ready12s,8085+8087 listening. Console
/tmp/vcore-northrend-flight-final-start-20261030.log. Build/install/tests saved
under checkpoints/2026-10-30-northrend-flight. No commit/push requested.
Old manually started371982 sessionvcore retained as user's shell after known
shutdownSIGSEGV. Intermediate383849 stopped before final install. Do not start a
second server. Recording hold released by user earlier. Zeppelin1826 and all
recordings unchanged. Final native area probes PASS:26 Arthoran60 ship_valiance_kraken allowed0;
55Zanzul82 zeppelin_warsong allowed1. DB after saveall:26riding225,license1,
cold0,flightmounts1;55riding225,license1,cold1,flightmounts3. No final
mandatory flight/BOT_MANAGER_CHECK errors observed.35/36not used (offline).
# Active: mandatory Northrend flight; final correction built, deploy/verify next
User forbids ground-only bots anywhere in Northrend; all60+ autonomous bots must
have basic flight,68+ cold weather. Implemented profile defaults (legacy too),
legacy manager maintenance without inventory cleaning, actual native provisioning,
map571 admission guard requiring68+54197+FlightMgr::Enabled. No DungeonBot changes.
See northrend-flight-requirement.md. Added production harness with native
FlightType/Enabled and69 tests (two fixture fixes passed separately).
User finished recording. Explicitly announced install/restart and deployed prior
ready instance-mode/handoff WIP together with flight policy; applied worldSQL08.
Old manual sessionvcore371982 stopped, encountered known shutdown SIGSEGV (console
preserved in vcore); installed, started dedicated vcore-worldserver PID383849,
ready12s +8085/8087 verified. Live Arthoran26 revealed helper's direct-speed-aura
mount validation was wrong for native Cataclysm mount data. Fixed ProvisionFlight
to reuse ClientlessFlightMgr::Enabled. Final rebuild -j10 log
/tmp/vcore-northrend-flight-final-build.log currently linking; wait/check before
redeploy. Do NOT claim final correction live until installed. Old373849 remains
running (actual PID383849), installation must wait for its full shutdown.
Earlier first build log /tmp/vcore-northrend-flight-build.log; install log
/tmp/vcore-northrend-flight-install.log; first-start console
/tmp/vcore-northrend-flight-start-20261030.log. SQL08 already applied, do not lose
user's new Zeppelin edge1826/recordings. No push/commit requested. All builds -j10.
# Latest: recorded Orgrimmar–Warsong Zeppelin activated live, no restart
User finished recording routes and asked to validate/activate new Horde Northrend
zeppelin. Existing manual server371982 runs in tmux session `vcore` (NOT
`vcore-worldserver`). No restart/install performed. Previous source feature build
is successful but still undeployed; migration08 remains unapplied.
Validated native service12 entry186238 The Mighty Wind, period298828ms, two docks
and two map legs. Both recorded stops load natively: zeppelin_org_northrend(map1,
5points,9.45yd) and zeppelin_warsong(map571,10points,16.70yd), same stand
mighty_wind_center; deck endpoints within3yd. Core link command successfully
created enabled bidirectional edge1826, zeppelin,cost100. Added explicit Horde-only
allowed_factions2 to both nodes; adventure catalogs+graph reloaded live.
Original recordings unchanged. Source connected via ground edge1819 to
orgrimmar_zeppelintower1. Destination has no recorded onward ground edge yet;
nearest taxi257 is on another storey, so do not invent an untested straight edge.
Native timetable pauses60s at each dock. Native node permission probes with
36Edwin85 allowed both,35Karggan85 denied both (nodeMask2). No forced bot
relocation, no claimed live passenger ride acceptance.
# Latest user instruction: retain -j10 for builds
User explicitly requires builds with -j10, including after crashes. Do not replace
this with -j1/-j2 as a standing policy. This supersedes the temporary lower-job
recommendations below. Final implementation build already succeeded Oct30
01:45:11; latest BehaviorMgr object is newer than source. All67 Adventure tests
passed; latest intent-state, departure and native-DBC variant tests rechecked.
No migration applied, no install/restart, no commit/push. Existing server371982
remains running with unchanged installed binary. Deployment hold still applies.
# Latest: instance modes / full-group early departure — deployment HOLD
User requested implementation of dungeon upper levels (raid minimum-only), native
normal/heroic dungeon+raid planning and early departure of complete groups.
USER RECORDING ROUTES: DO NOT INSTALL, APPLY SQL OR RESTART until hold released.
Current source WIP is intentional, not unexpected changes; preserve it.
Added logical per-mode destination variants reusing physical BaseKey; native LFG
map/mode bands, max+5 dungeon tolerance, raid min-only, reject zero-capacity native
placeholders, native difficulty session handlers and mode/capacity inheritance.
Added per-bot persistent current-block copy in world migration08, actual-members
full detection (human-led too), min(now+300, original hour end, armed source end),
existing gather micro handling, no kick/vacancy rollback, unchanged next day slot,
restart restore/expiry/tempbot cleanup and explicit admin command cancellation.
See group-departure-and-instance-modes.md and tests/checkpoint.
IMPORTANT INCIDENT: Initial -j10 compilation alongside installed worldserver used
all VM RAM; kernel OOM-killed worldserver362937 at2026-10-29 23:36:38 CET. No
shutdown/restart/install was issued by agent. User informed with cause/apology.
At interruption worldserver was OFFLINE. On Oct30 resumption, found an externally
started worldserver371982 (startedOct29 23:57:44). Do not touch it. Installed
executable remains21:21:03 Oct29 unchanged.
Do not restart under recording hold, even to restore the old binary. Future builds
must follow the user-mandated -j10. The temporary -j1 resumption build is
historical and must not be treated as a new build policy. No unrelated service touched.
Final build log /tmp/vcore-group-difficulty-final-build.log; test results
/tmp/vcore-group-difficulty-tests.json. Completed successfully Oct30 01:45:11, final object/source timestamps checked.
No SQL08 applied, no live gameplay verification, no commit/push requested.
# Commit checkpoint: transport resumption, deck return and capital portals
User requested a local commit of all completed changes since19075b6c75. Includes
@@ -0,0 +1,2 @@
Oct 29 23:36:38 vcore-test kernel: oom-kill:constraint=CONSTRAINT_NONE,nodemask=(null),cpuset=/,mems_allowed=0,global_oom,task_memcg=/user.slice/user-1000.slice/user@1000.service/tmux-spawn-c0aac031-fb9c-46cd-b1a7-eba85fc42191.scope,task=worldserver,pid=362937,uid=1000
Oct 29 23:36:38 vcore-test kernel: Out of memory: Killed process 362937 (worldserver) total-vm:11848452kB, anon-rss:5819912kB, file-rss:3156kB, shmem-rss:0kB, UID:1000 pgtables:19928kB oom_score_adj:0
@@ -0,0 +1,280 @@
{
"adventure_suite": [
[
"test_adventure_supply_catalog.py",
0
],
[
"test_adventure_instance.py",
0
],
[
"test_adventure_encounter.py",
0
],
[
"test_adventure_economy.py",
0
],
[
"test_adventure_world_presence.py",
0
],
[
"test_adventure_supplies.py",
0
],
[
"test_adventure_equipment_maintenance.py",
0
],
[
"test_adventure_pvp.py",
0
],
[
"test_adventure_combat_reactions.py",
0
],
[
"test_adventure_rest_targets.py",
0
],
[
"test_adventure_recovery_policy.py",
0
],
[
"test_adventure_graph_handover.py",
0
],
[
"test_adventure_exploration.py",
0
],
[
"test_adventure_rotation.py",
0
],
[
"test_adventure_interactions.py",
0
],
[
"test_adventure_group_plan.py",
0
],
[
"test_adventure_micro_reset.py",
0
],
[
"test_adventure_rest_space.py",
0
],
[
"test_adventure_destination_level.py",
0
],
[
"test_adventure_combat_exercise.py",
0
],
[
"test_adventure_training_catalog.py",
0
],
[
"test_adventure_native_lfg.py",
0
],
[
"test_adventure_area_policy.py",
0
],
[
"test_adventure_instance_variants.py",
0
],
[
"test_adventure_instance_loader.py",
0
],
[
"test_adventure_ground_avoidance.py",
0
],
[
"test_adventure_repair_catalog.py",
0
],
[
"test_adventure_profession_profile.py",
0
],
[
"test_adventure_navigation_rescue.py",
0
],
[
"test_adventure_generator_ownership.py",
0
],
[
"test_adventure_instance_autoresume.py",
0
],
[
"test_adventure_economy_catalog.py",
0
],
[
"test_adventure_async_database.py",
0
],
[
"test_adventure_profession_targets.py",
0
],
[
"test_adventure_area_segment.py",
0
],
[
"test_adventure_intent_state.py",
0
],
[
"test_adventure_talent_templates.py",
0
],
[
"test_adventure_location_import.py",
0
],
[
"test_adventure_profession_catalog.py",
0
],
[
"test_adventure_walk.py",
0
],
[
"test_adventure_group_gather.py",
0
],
[
"test_adventure_profile_selection.py",
0
],
[
"test_adventure_social.py",
0
],
[
"test_adventure_rest.py",
0
],
[
"test_adventure_exploration_catalog.py",
0
],
[
"test_adventure_supply_items.py",
0
],
[
"test_adventure_instance_persistence.py",
0
],
[
"test_adventure_profession_sightline.py",
0
],
[
"test_adventure_population.py",
0
],
[
"test_adventure_rest_fire.py",
0
],
[
"test_adventure_retry_policy.py",
0
],
[
"test_adventure_manager_inventory.py",
0
],
[
"test_adventure_instance_catalog.py",
0
],
[
"test_adventure_combat_preparation.py",
0
],
[
"test_adventure_social_fire.py",
0
],
[
"test_adventure_leisure.py",
0
],
[
"test_adventure_instance_begin.py",
0
],
[
"test_adventure_day_persistence.py",
0
],
[
"test_adventure_behavior_policy.py",
0
],
[
"test_adventure_groups.py",
0
],
[
"test_adventure_leisure_catalog.py",
0
],
[
"test_adventure_group_departure.py",
0
],
[
"test_adventure_generator_policy.py",
0
],
[
"test_adventure_group_travel.py",
0
],
[
"test_adventure_area_node.py",
0
],
[
"test_adventure_class_rotation.py",
0
],
[
"test_adventure_generator_equipment.py",
0
]
],
"latest_change_rechecked": [
"test_adventure_intent_state.py",
"test_adventure_group_departure.py",
"test_adventure_instance_variants.py"
],
"deployed": false,
"sql_applied": false,
"gameplay_verified": false
}
@@ -0,0 +1,4 @@
[1/4] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug
[2/4] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureAreaMgr.cpp.o
[3/4] Linking CXX static library src/server/game/libgame.a
[4/4] Linking CXX executable src/server/worldserver/worldserver
@@ -0,0 +1,278 @@
[
[
"test_adventure_supply_catalog.py",
0
],
[
"test_adventure_instance.py",
0
],
[
"test_adventure_encounter.py",
0
],
[
"test_adventure_economy.py",
0
],
[
"test_adventure_world_presence.py",
0
],
[
"test_adventure_supplies.py",
0
],
[
"test_adventure_equipment_maintenance.py",
0
],
[
"test_adventure_pvp.py",
0
],
[
"test_adventure_combat_reactions.py",
0
],
[
"test_adventure_rest_targets.py",
0
],
[
"test_adventure_recovery_policy.py",
0
],
[
"test_adventure_graph_handover.py",
0
],
[
"test_adventure_exploration.py",
0
],
[
"test_adventure_rotation.py",
0
],
[
"test_adventure_interactions.py",
0
],
[
"test_adventure_group_plan.py",
0
],
[
"test_adventure_micro_reset.py",
0
],
[
"test_adventure_rest_space.py",
0
],
[
"test_adventure_destination_level.py",
0
],
[
"test_adventure_combat_exercise.py",
0
],
[
"test_adventure_training_catalog.py",
0
],
[
"test_adventure_native_lfg.py",
0
],
[
"test_adventure_area_policy.py",
0
],
[
"test_adventure_instance_variants.py",
0
],
[
"test_adventure_instance_loader.py",
0
],
[
"test_adventure_ground_avoidance.py",
0
],
[
"test_adventure_repair_catalog.py",
0
],
[
"test_adventure_profession_profile.py",
0
],
[
"test_adventure_navigation_rescue.py",
0
],
[
"test_adventure_northrend_flight.py",
0
],
[
"test_adventure_generator_ownership.py",
0
],
[
"test_adventure_instance_autoresume.py",
0
],
[
"test_adventure_economy_catalog.py",
0
],
[
"test_adventure_async_database.py",
0
],
[
"test_adventure_profession_targets.py",
0
],
[
"test_adventure_area_segment.py",
0
],
[
"test_adventure_intent_state.py",
0
],
[
"test_adventure_talent_templates.py",
0
],
[
"test_adventure_location_import.py",
0
],
[
"test_adventure_profession_catalog.py",
0
],
[
"test_adventure_walk.py",
0
],
[
"test_adventure_group_gather.py",
0
],
[
"test_adventure_profile_selection.py",
0
],
[
"test_adventure_social.py",
0
],
[
"test_adventure_rest.py",
0
],
[
"test_adventure_exploration_catalog.py",
0
],
[
"test_adventure_supply_items.py",
0
],
[
"test_adventure_instance_persistence.py",
0
],
[
"test_adventure_profession_sightline.py",
0
],
[
"test_adventure_population.py",
0
],
[
"test_adventure_rest_fire.py",
0
],
[
"test_adventure_retry_policy.py",
0
],
[
"test_adventure_manager_inventory.py",
0
],
[
"test_adventure_instance_catalog.py",
0
],
[
"test_adventure_combat_preparation.py",
0
],
[
"test_adventure_social_fire.py",
0
],
[
"test_adventure_leisure.py",
0
],
[
"test_adventure_instance_begin.py",
0
],
[
"test_adventure_day_persistence.py",
0
],
[
"test_adventure_behavior_policy.py",
0
],
[
"test_adventure_groups.py",
0
],
[
"test_adventure_leisure_catalog.py",
0
],
[
"test_adventure_group_departure.py",
0
],
[
"test_adventure_generator_policy.py",
0
],
[
"test_adventure_group_travel.py",
0
],
[
"test_adventure_area_node.py",
0
],
[
"test_adventure_class_rotation.py",
0
],
[
"test_adventure_generator_equipment.py",
0
],
[
"test_flight_area_query.py",
0
]
]
@@ -0,0 +1,39 @@
{
"pid": 385679,
"ready_seconds": 12,
"ports": [
8085,
8087
],
"area_probes": [
{
"guid": 26,
"level": 60,
"map": 571,
"allowed": false
},
{
"guid": 55,
"level": 82,
"map": 571,
"allowed": true
}
],
"saved_training": [
{
"guid": 26,
"riding": 225,
"license": true,
"cold_weather": false,
"flight_mounts": 1
},
{
"guid": 55,
"riding": 225,
"license": true,
"cold_weather": true,
"flight_mounts": 3
}
],
"full_zeppelin_ride_tested": false
}
@@ -0,0 +1,4 @@
[1/4] cd /home/wow_admin/wow_cata_server/source/TrinityCore && /usr/bin/cmake -DBUILDDIR="/home/wow_admin/wow_cata_server/build/debug" -P /home/wow_admin/wow_cata_server/source/TrinityCore/cmake/genrev.cmake /home/wow_admin/wow_cata_server/build/debug
[2/4] Building CXX object src/server/game/CMakeFiles/game.dir/World/AdventureGeneratorMgr.cpp.o
[3/4] Linking CXX static library src/server/game/libgame.a
[4/4] Linking CXX executable src/server/worldserver/worldserver
@@ -0,0 +1,16 @@
-- Install configuration: "Debug"
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/authserver
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/etc/authserver.conf.dist
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/tests
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/tests/smoke.lua
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/generic.lua
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/overrides
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/overrides/arthoran.lua
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/profiles
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/profiles/generic.lua
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/bin/scripts/clientless/adventure/profiles/arthoran.lua
-- Installing: /home/wow_admin/wow_cata_server/server/debug/bin/worldserver
-- Set non-toolchain portion of runtime path of "/home/wow_admin/wow_cata_server/server/debug/bin/worldserver" to "/home/wow_admin/wow_cata_server/server/debug/lib"
-- Up-to-date: /home/wow_admin/wow_cata_server/server/debug/etc/worldserver.conf.dist
@@ -0,0 +1,278 @@
[
[
"test_adventure_supply_catalog.py",
0
],
[
"test_adventure_instance.py",
0
],
[
"test_adventure_encounter.py",
0
],
[
"test_adventure_economy.py",
0
],
[
"test_adventure_world_presence.py",
0
],
[
"test_adventure_supplies.py",
0
],
[
"test_adventure_equipment_maintenance.py",
0
],
[
"test_adventure_pvp.py",
0
],
[
"test_adventure_combat_reactions.py",
0
],
[
"test_adventure_rest_targets.py",
0
],
[
"test_adventure_recovery_policy.py",
0
],
[
"test_adventure_graph_handover.py",
0
],
[
"test_adventure_exploration.py",
0
],
[
"test_adventure_rotation.py",
0
],
[
"test_adventure_interactions.py",
0
],
[
"test_adventure_group_plan.py",
0
],
[
"test_adventure_micro_reset.py",
0
],
[
"test_adventure_rest_space.py",
0
],
[
"test_adventure_destination_level.py",
0
],
[
"test_adventure_combat_exercise.py",
0
],
[
"test_adventure_training_catalog.py",
0
],
[
"test_adventure_native_lfg.py",
0
],
[
"test_adventure_area_policy.py",
0
],
[
"test_adventure_instance_variants.py",
0
],
[
"test_adventure_instance_loader.py",
0
],
[
"test_adventure_ground_avoidance.py",
0
],
[
"test_adventure_repair_catalog.py",
0
],
[
"test_adventure_profession_profile.py",
0
],
[
"test_adventure_navigation_rescue.py",
0
],
[
"test_adventure_northrend_flight.py",
0
],
[
"test_adventure_generator_ownership.py",
0
],
[
"test_adventure_instance_autoresume.py",
0
],
[
"test_adventure_economy_catalog.py",
0
],
[
"test_adventure_async_database.py",
0
],
[
"test_adventure_profession_targets.py",
0
],
[
"test_adventure_area_segment.py",
0
],
[
"test_adventure_intent_state.py",
0
],
[
"test_adventure_talent_templates.py",
0
],
[
"test_adventure_location_import.py",
0
],
[
"test_adventure_profession_catalog.py",
0
],
[
"test_adventure_walk.py",
0
],
[
"test_adventure_group_gather.py",
0
],
[
"test_adventure_profile_selection.py",
0
],
[
"test_adventure_social.py",
0
],
[
"test_adventure_rest.py",
0
],
[
"test_adventure_exploration_catalog.py",
0
],
[
"test_adventure_supply_items.py",
0
],
[
"test_adventure_instance_persistence.py",
0
],
[
"test_adventure_profession_sightline.py",
0
],
[
"test_adventure_population.py",
0
],
[
"test_adventure_rest_fire.py",
0
],
[
"test_adventure_retry_policy.py",
0
],
[
"test_adventure_manager_inventory.py",
0
],
[
"test_adventure_instance_catalog.py",
0
],
[
"test_adventure_combat_preparation.py",
0
],
[
"test_adventure_social_fire.py",
0
],
[
"test_adventure_leisure.py",
0
],
[
"test_adventure_instance_begin.py",
0
],
[
"test_adventure_day_persistence.py",
0
],
[
"test_adventure_behavior_policy.py",
0
],
[
"test_adventure_groups.py",
0
],
[
"test_adventure_leisure_catalog.py",
0
],
[
"test_adventure_group_departure.py",
0
],
[
"test_adventure_generator_policy.py",
0
],
[
"test_adventure_group_travel.py",
0
],
[
"test_adventure_area_node.py",
0
],
[
"test_adventure_class_rotation.py",
0
],
[
"test_adventure_generator_equipment.py",
0
],
[
"test_flight_area_query.py",
0
]
]
@@ -0,0 +1,59 @@
# Planned instance modes and full-group departure
Instance destinations now use native Cataclysm LFG level bands per map and
supported difficulty, rather than the area level around the exterior entrance.
Dungeons allow at most five levels above the native recommended maximum; raids
only require the native minimum. Normal Shadowfang's obsolete84–85 beta LFG row
must not widen its normal16–26 band. Native access requirements can tighten bands.
Unknown dungeon bands are excluded from automatic selection.
The location catalog exposes logical variants of the same physical entrance:
base key for normal dungeon / preferred normal raid mode, `_dN` for other native
modes. `BaseKey` retains the actual recorded worldpoint and instance mapping.
Both persistent day slots and visit records already store these logical keys,
so selected normal/heroic difficulty survives logout without extra day columns.
Only native modes with nonzero capacity are selectable; zero-capacity placeholder
entries do not manufacture heroic versions of classic raids. Dungeon groups have
five slots;10-player raids ten and larger raids at most twenty, as requested.
Recruitment names include normal/heroic and native size. Bot leaders set the mode
through the native session handler; human leaders retain control of difficulty.
A full group means actual joined members, not pending invitations. Each forming
bot snapshots the recruiting leader's next intent/location. The gather deadline
is the earlier of now+300s, the original formation-hour end and an already armed
leader's deadline. Full14:20 departs14:25; full14:57 still departs15:00. Existing
safe micro completion, return to meeting point and travel/combat safeguards apply.
A vacancy or kick does not revert an already copied current slot. The original
next `vcore_adventure_day` row remains untouched. A detached bot resumes that
original next slot at its boundary; an ongoing group continues following its
leader. The copy retains the next activity's planned end for a continuous visit.
World migration `2026_10_29_08_adventure_group_handoff.sql` stores the temporary
copy and deadlines. Arming requires a successful transaction; missing storage
fails closed. Login restores the copy; expiry and temporary-character deletion
clean it up. Explicit console intent/schedule/stop commands cancel the copy.
Player-led groups are supported and retain the formation AFK exemption. The early
transition has a one-time intent-change leave exemption, including human leaders.
## Release state
Source/build only. User is recording routes: no install, migration application,
or restart is authorized in this step. Apply migration08 before the new binary
and loader run. In-game normal/heroic entry, gathering and departure still require
acceptance testing after deployment.
During the initial -j10 build, VM memory exhaustion caused Linux to kill the
existing worldserver362937 at2026-10-29 23:36:38 CET. This was not a requested
shutdown or an application crash. Kernel evidence is preserved in this step's
checkpoint. The user was informed; no restart was attempted under the recording
hold. The final resumption build used temporary reduced parallelism and succeeded.
The user explicitly requires future builds with -j10 even after crashes; the
temporary reduction must not be treated as a standing policy.
On Oct30 resumption an externally started worldserver371982 was found running
(startOct29 23:57:44). Agent left it untouched. The last source correction is
rebuilt with -j1 to limit memory while it runs. Installed binary remains unchanged.
Final build succeeded Oct30 01:45:11. All67 AdventureBot tests passed; the last
intent-state, departure and native-DBC tests were rechecked after the final change.
No installation, migration application or restart was performed.
@@ -0,0 +1,53 @@
# Mandatory flight for Northrend AdventureBots
User requirement: no ground-only AdventureBots in Northrend. Autonomous bots
receive basic flying at level60, and cold weather flying at level68. This covers
legacy named/manual profiles as well as generated profiles.
Profile loading enables free flight at target level60+ and supplies a faction
appropriate gryphon32235 / wyvern32243 if the flight-mount list is empty. Existing
mount choices and weights are preserved. The manager checks legacy autonomous
bots too, while preserving their manual inventories and equipment.
Reconciliation supplies native riding spells33388/33391/34090, skill225 minimum,
Flight Master's License90267 and (level68+) Cold Weather Flying54197, and learns
profile mounts. The capability check reuses ClientlessFlightMgr::Enabled and its
native mount-type data; Cataclysm mounts must not be classified by assuming a
mounted-flight-speed aura resides directly on the mount spell.
AdventureAreaMgr::AllowsPosition denies all map571 destinations to managed bots
below68, missing54197 or lacking actual learned profile flight capability. This
applies equally to day planning, graph nodes, ships/zeppelins and portals. It
checks capability, not whether the bot currently flies: walking inside buildings,
boarding and safe disembarkation remain possible. Ordinary players and independent
DungeonBots without Adventure profiles remain unaffected. Area/faction bans still
apply in addition. Flight configuration disabled means no AdventureBot admission
to Northrend, rather than a ground-only fallback there.
Compiled and linked with user-required -j10;69 tests including the production
profile/default, training/repair and map571 guard harness passed. Initial live
legacy provision check exposed the incorrect direct-aura classification; this
was corrected to the existing native flight manager before final deployment.
Full Zeppelin passenger travel and terrain flight still require gameplay testing.
Final deployment: world migration08 applied (previous pending group feature),
new binary installed, final server385679 ready12s with8085/8087 listeners.
Native checks:26Arthoran60 denied map571;55Zanzul82 allowed Horde Zeppelin target.
After saveall DB confirms26riding225/license1/cold0/flightmounts1 and
55riding225/license1/cold1/flightmounts3. No mandatory-flight manager errors in
final startup. Current population scan found saved Northrend bots112level73 and
135level85, both riding225 and cold weather learned. No forced relocation used.
## Extension to Outland and Cataclysm flight regions
The subsequent user request applies the same admission prerequisite to all flight
regions declared by the editable area catalog (`prefer_flight`), covering Outland
and the Cataclysm outdoor regions. Require actual learned profile flight and
level60+. Northrend additionally requires68+ and54197; other flight regions outside
maps530/571 also require90267. Area bans (including Vashjir) remain in force.
The low-level blood-elf/draenei regions share map530 but are not catalog flight
regions and remain usable without flying. The extended production test covers
Outland60 without cold weather, denied ground-only arrivals, Cataclysm license
checks and preserved access to the low-level map530 racial areas.
Latest flight-region build completed with -j10 and the69-test suite passed.
@@ -0,0 +1,41 @@
# Orgrimmar–Warsong Hold: The Mighty Wind
Activated2026-10-30 on the testserver without a restart. Native transport
spawn12, entry186238, taxi path712, two-map circuit298828ms. Stops:
| Node | Map | Recorded points | Transition length |
| --- | --- | --- | --- |
| zeppelin_org_northrend | 1 | 5 | 9.45yd |
| zeppelin_warsong | 571 | 10 | 16.70yd |
Both recordings use `mighty_wind_center` and their deck endpoints differ by about
1.39yd, within the core3yd limit. Native reload accepted both recordings; the
normal transport-link validator accepted their service, maps and stand match.
Activated with the existing live command:
```text
clientless transport link zeppelin_org_northrend zeppelin_warsong zeppelin 100 both
```
Result: edge1826, enabled and bidirectional. Both nodes have explicit
`vcore_clientless_node_access.allowed_factions=2` (Horde only), with catalogs and
graph reloaded. Area permissions continue to take precedence over node masks.
Source ground edge1819 joins `orgrimmar_zeppelintower1`. No onward ground edge is
recorded at Warsong yet. Taxi257 is nearby horizontally but another storey, so no
unverified straight ground connection was fabricated. Existing permitted flight
and local navigation remain responsible for onward travel until routes are added.
Recorded points, dock positions and stand coordinates were preserved. A complete
live bot boarding/ride/disembarkation has not been acceptance-tested in this step.
No C++ change, build, install, restart or player teleport was required.
Live native permission checks: Horde36Edwin85 allowed at both nodes; Alliance
35Karggan85 denied at both nodes, all four report nodeMask2. Native timetable
confirms60-second pauses at each recorded dock, matching dock coordinates.
Subsequent user requirement: Northrend has no ground-only AdventureBots. All
autonomous level60+ profiles receive flight training/mount defaults; level68+
receive cold weather54197. Access to map571 additionally requires actual learned
flight capability and cold weather, including transport and portal destinations.
Lack of a recorded onward ground edge does not justify sending non-flyers here.
@@ -0,0 +1,10 @@
-- Persistent current-block copy. Original vcore_adventure_day slots stay unchanged.
CREATE TABLE IF NOT EXISTS `vcore_adventure_group_handoff` (
`character_guid` INT UNSIGNED NOT NULL,
`depart_at` BIGINT UNSIGNED NOT NULL,
`formation_end` BIGINT UNSIGNED NOT NULL,
`activity_end` BIGINT UNSIGNED NOT NULL,
`intent` VARCHAR(32) NOT NULL,
`location_key` VARCHAR(100) NOT NULL,
PRIMARY KEY (`character_guid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
+13 -1
View File
@@ -1,5 +1,6 @@
#include "AdventureAreaMgr.h"
#include "ClientlessLuaEngine.h"
#include "ClientlessFlightMgr.h"
#include "ClientlessTravelGraphMgr.h"
#include "DatabaseEnv.h"
#include "DBCStores.h"
@@ -144,7 +145,18 @@ bool AdventureAreaMgr::AllowsPosition(Player const* player,uint32 map,Position c
{
if(!Applies(player))return true;
if(!std::isfinite(pos.GetPositionX()) || !std::isfinite(pos.GetPositionY()) || !std::isfinite(pos.GetPositionZ()))return false;
return EnsureLoaded() && AllowsArea(player,AreaAt(player,map,pos));
if(!EnsureLoaded())return false;
auto area=AreaAt(player,map,pos);
// The editable catalog declares Outland/Cataclysm flight regions. Keep
// low-level blood-elf/draenei areas on map530 separate from Outland.
bool flightRequired=map==571 || Area(area).PreferFlight;
if(flightRequired)
{
if(player->getLevel()<60 || !sClientlessFlightMgr->Enabled(player))return false;
if(map==571 && (player->getLevel()<68 || !player->HasSpell(54197)))return false;
if(map!=530 && map!=571 && !player->HasSpell(90267))return false;
}
return AllowsArea(player,area);
}
bool AdventureAreaMgr::PreferredFlight(Player const* player,uint32 map,Position const& pos)
{
+160 -15
View File
@@ -200,6 +200,7 @@ void AdventureBehaviorMgr::Online(Player * player)
r.Random.seed(player->GetGUID().GetCounter() ^ uint32(GameTime::GetGameTime()));
auto & record = _records.insert_or_assign(player->GetGUID().GetCounter(), std::move(r)).first->second;
if(record.Schedule)Generate(player,record,DateOf(Local(GameTime::GetGameTime())));
RestoreDeparture(player,record);
if (RestoreInstance(player,record)) return;
if (! record.Schedule && Known(profile->InitialIntent)) SetWanted(player, record, profile->InitialIntent, profile->InitialLocation, GameTime::GetGameTime() + profile->InitialDurationSeconds);
}
@@ -212,7 +213,12 @@ void AdventureBehaviorMgr::EnterPlayerInstance(Player* player)
void AdventureBehaviorMgr::RefreshDay(Player* player)
{
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end() || !it->second.Schedule)return;
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return;
// Maintenance also runs while a human leader's follow controller owns
// movement. Advance the logical copy there without queuing autonomous travel.
if(it->second.Departure.At && !sAdventureEncounterMgr->Active(player))
UpdateDeparture(player,it->second,GameTime::GetGameTime());
if(!it->second.Schedule)return;
auto date=DateOf(Local(GameTime::GetGameTime()));
if(it->second.Date!=date){Generate(player,it->second,date);it->second.Hour=-1;}
}
@@ -234,6 +240,7 @@ void AdventureBehaviorMgr::IntentChanged(Player * player, ClientlessIntent const
if (i == _records.end() || i->second.Publishing) return;
if (! intent)
{
ClearDeparture(player,i->second);
SetWanted(player, i->second, "", "", 0);
return;
}
@@ -242,6 +249,7 @@ void AdventureBehaviorMgr::IntentChanged(Player * player, ClientlessIntent const
auto & r = i->second;
r.Enabled = true;
r.Schedule = false;
ClearDeparture(player,r);
SetWanted(player, r, intent->Name, location == intent->Parameters.end() ? PickLocation(player, r, intent->Name) : location->second, GameTime::GetGameTime() + 3600);
}
@@ -475,7 +483,7 @@ void AdventureBehaviorMgr::Begin(Player * player, Record & r)
if ((r.Intent=="instance" || r.Intent=="raid"))
{
if (!ResolveInstance(player,r)) {Fallback(player,r);return;}
Queue(player,r,"travel_to_location","id",r.Location,true);
Queue(player,r,"travel_to_location","id",l->BaseKey.empty()?r.Location:l->BaseKey,true);
}
else if (l->IsArea)
{
@@ -1446,10 +1454,11 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
}
r.Timer = 1000;
time_t now = GameTime::GetGameTime();
UpdateDeparture(player,r,now);
if (r.Instance.Active && UpdateInstance(player,r,now)) return;
auto local = Local(now);
uint32 date = DateOf(local);
if (r.Schedule)
if (r.Schedule && !r.Departure.Departed(now))
{
if (r.Date != date)
{
@@ -1459,7 +1468,8 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
if (r.Hour != local.tm_hour)
{
auto const & slot = r.Plan[local.tm_hour];
bool changed = r.Hour < 0 || r.Plan[r.Hour].Intent != slot.Intent || r.Plan[r.Hour].Location != slot.Location;
bool changed = (r.Hour < 0 || r.Plan[r.Hour].Intent != slot.Intent || r.Plan[r.Hour].Location != slot.Location) &&
(r.WantedIntent != slot.Intent || r.WantedLocation != slot.Location);
r.Hour = local.tm_hour;
std::tm end = local;
end.tm_min = 0;
@@ -1472,7 +1482,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
else r.Deadline = deadline;
}
}
else if (r.Deadline && now >= r.Deadline)
else if (!r.Schedule && !r.Departure.Departed(now) && r.Deadline && now >= r.Deadline)
{
if(r.Intent=="groupformation" && !r.ManualNextIntent.empty())
{
@@ -1482,6 +1492,7 @@ void AdventureBehaviorMgr::Update(Player * player, uint32 diff)
}
else SetWanted(player,r,"","",0);
}
UpdateDeparture(player,r,now);
if(auto group=player->GetGroup())if(sAdventureGroupMgr->BotLed(player) && group->GetLeaderGUID()!=player->GetGUID())
if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))AdoptGroup(player,leader);
if(UpdateGathering(player,r,now))return;
@@ -1708,6 +1719,7 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
std::string mode;
in >> mode;
if (mode != "on" && mode != "off") return false;
ClearDeparture(player,r);
r.Schedule = mode == "on";
r.Enabled = true;
r.Hour = - 1;
@@ -1722,6 +1734,7 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
auto location=sAdventureLocationMgr->Find(meeting);
if(!AdventureGroupPolicy::Eligible(next) || !location || !Allowed(player,*location,"groupformation"))return false;
auto destination=PickLocation(player,r,next);if(destination.empty())return false;
ClearDeparture(player,r);
r.Schedule=false;r.Enabled=true;r.ManualNextIntent=next;r.ManualNextLocation=destination;
SetWanted(player,r,"groupformation",meeting,GameTime::GetGameTime()+seconds);
handler->SendSysMessage("Manual preparation started; next activity begins at the deadline even if the group is incomplete.");return true;
@@ -1738,6 +1751,7 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
handler->SendSysMessage("Use social/adventure/profession/rest/leisure/economy/instance/raid/groupformation and a matching enabled location.");
return false;
}
ClearDeparture(player,r);
r.Schedule = false;
r.Enabled = true;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
SetWanted(player, r, intent, location, GameTime::GetGameTime() + seconds);
@@ -1746,6 +1760,7 @@ bool AdventureBehaviorMgr::Command(ChatHandler * handler, char const * args)
}
if (op == "stop")
{
ClearDeparture(player,r);
r.Schedule = false;r.ManualNextIntent.clear();r.ManualNextLocation.clear();
SetWanted(player, r, "", "", 0);
handler->SendSysMessage("Behavior stop requested at a safe point.");
@@ -1790,7 +1805,9 @@ bool AdventureBehaviorMgr::RestoreInstance(Player* player, Record& r)
v.Map=f[3].GetUInt32();v.InstanceId=f[4].GetUInt32();v.BlockEnd=time_t(f[5].GetUInt64());
v.PrepareUntil=time_t(f[6].GetUInt64());v.Stage=AdventureInstancePolicy::Phase(f[7].GetUInt8());
v.LossPercent=f[8].GetUInt8();v.InjuryApplied=f[9].GetUInt8()!=0;r.Schedule=f[10].GetUInt8()!=0;
if(auto selected=sAdventureLocationMgr->Find(v.Location))v.Difficulty=selected->InstanceDifficulty;
auto restoredMap=sMapStore.LookupEntry(v.Map);
if(player->GetMapId()==v.Map && restoredMap)v.Difficulty=uint32(player->GetDifficulty(restoredMap->IsRaid()));
r.Intent=r.WantedIntent=restoredMap && restoredMap->IsRaid()?"raid":"instance";r.Location=r.WantedLocation=v.Location;r.Deadline=v.BlockEnd;
r.State=v.Stage==AdventureInstancePolicy::Phase::Travel?Phase::Idle:Phase::OnSite;
r.Dirty=v.Stage==AdventureInstancePolicy::Phase::Travel;
@@ -1822,7 +1839,9 @@ void AdventureBehaviorMgr::FinishInstance(Player* player, Record& r)
bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
{
if (r.Instance.Active) return r.Instance.Location==r.Location;
auto key=r.Location;WorldDatabase.EscapeString(key);
auto selected=sAdventureLocationMgr->Find(r.Location);
if(!selected){r.Detail="instance mode variant missing";return false;}
auto key=selected->BaseKey.empty()?r.Location:selected->BaseKey;WorldDatabase.EscapeString(key);
auto row=WorldDatabase.PQuery("SELECT entry_trigger,exit_trigger FROM vcore_adventure_instance WHERE location_key='%s' AND enabled=1",key.c_str());
if (!row) {r.Detail="instance entrance mapping unavailable";return false;}
uint32 entry=row->Fetch()[0].GetUInt32(),exit=row->Fetch()[1].GetUInt32();
@@ -1856,12 +1875,12 @@ bool AdventureBehaviorMgr::ResolveInstance(Player* player, Record& r)
} while(rows->NextRow());
}
r.Instance.Active=true;r.Instance.Location=r.Location;r.Instance.Entry=entry;r.Instance.Exit=exit;
r.Instance.Map=target->target_mapId;r.Instance.BlockEnd=r.Deadline;
r.Instance.Map=target->target_mapId;r.Instance.Difficulty=selected->InstanceDifficulty;r.Instance.BlockEnd=r.Deadline;
r.Instance.LossPercent=std::uniform_int_distribution<uint32>(0,50)(r.Random);
if (!SaveInstance(player,r)) {r.Instance={};return false;}
TC_LOG_INFO("server.adventurebot","INSTANCE_PLAN guid=%u entry=%u exit=%u map=%u deadline=%llu lossPercent=%u",
TC_LOG_INFO("server.adventurebot","INSTANCE_PLAN guid=%u entry=%u exit=%u map=%u deadline=%llu lossPercent=%u mode=%u",
player->GetGUID().GetCounter(),entry,exit,target->target_mapId,
static_cast<unsigned long long>(r.Deadline),r.Instance.LossPercent);
static_cast<unsigned long long>(r.Deadline),r.Instance.LossPercent,r.Instance.Difficulty);
return true;
}
@@ -1914,9 +1933,9 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
v.InstanceId=player->GetInstanceId();v.Stage=Stage::Inside;
if (!SaveInstance(player,r)) {v.Stage=Stage::Entering;return true;}
r.Detail="waiting just inside private instance entrance";
TC_LOG_INFO("server.adventurebot","INSTANCE_ENTERED guid=%u map=%u instance=%u returnAt=%llu",
TC_LOG_INFO("server.adventurebot","INSTANCE_ENTERED guid=%u map=%u instance=%u returnAt=%llu mode=%u",
player->GetGUID().GetCounter(),v.Map,v.InstanceId,
static_cast<unsigned long long>(AdventureInstancePolicy::ReturnAt(v.BlockEnd)));
static_cast<unsigned long long>(AdventureInstancePolicy::ReturnAt(v.BlockEnd)),v.Difficulty);
return true;
}
if (v.Abort || !AdventureInstancePolicy::MayEnter(now,v.BlockEnd))
@@ -1924,6 +1943,20 @@ bool AdventureBehaviorMgr::UpdateInstance(Player* player, Record& r, time_t now)
else
{
auto map=sMapStore.LookupEntry(v.Map);
auto group=player->GetGroup();
bool humanLeader=false;
if(group)if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))humanLeader=!leader->GetSession()->IsClientless();
if(humanLeader)v.Difficulty=uint32(group->GetDifficulty(map->IsRaid()));
else if(player->GetDifficulty(map->IsRaid())!=Difficulty(v.Difficulty))
{
if(group && group->GetLeaderGUID()!=player->GetGUID())
{r.Detail="waiting for leader instance difficulty";return true;}
WorldPacket mode(map->IsRaid()?MSG_SET_RAID_DIFFICULTY:MSG_SET_DUNGEON_DIFFICULTY,4);mode<<uint32(v.Difficulty);
if(map->IsRaid())player->GetSession()->HandleSetRaidDifficultyOpcode(mode);
else player->GetSession()->HandleSetDungeonDifficultyOpcode(mode);
if(player->GetDifficulty(map->IsRaid())!=Difficulty(v.Difficulty))
{r.Detail="native instance difficulty change rejected";return true;}
}
auto bind=player->GetBoundInstance(v.Map,player->GetDifficulty(map->IsRaid()));
bool shared=false;
if (bind && bind->save)
@@ -2057,6 +2090,7 @@ std::string AdventureBehaviorMgr::InteractionSchedule(Player* player)
auto const& location=r.GroupOverride?r.PersonalLocation:r.WantedLocation;
return (deadline && GameTime::GetGameTime()>=deadline?"expired:":"")+intent+":"+location;
}
if(r.Departure.Departed(GameTime::GetGameTime()))return r.Departure.Intent+":"+r.Departure.Location;
auto const& slot=r.Plan[Local(GameTime::GetGameTime()).tm_hour];
return slot.Intent+":"+slot.Location;
}
@@ -2068,6 +2102,91 @@ void AdventureBehaviorMgr::InterruptForInteraction(Player* player)
if(it!=_records.end())it->second.Detail="social/group interaction; original deadline retained";
}
void AdventureBehaviorMgr::ClearDeparture(Player* player,Record& r)
{
if(!r.Departure.At)return;
r.Departure={};
WorldDatabase.PExecute("DELETE FROM vcore_adventure_group_handoff WHERE character_guid=%u",player->GetGUID().GetCounter());
}
void AdventureBehaviorMgr::RestoreDeparture(Player* player,Record& r)
{
if(!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_group_handoff'"))return;
auto row=WorldDatabase.PQuery("SELECT depart_at,formation_end,activity_end,intent,location_key FROM vcore_adventure_group_handoff WHERE character_guid=%u",player->GetGUID().GetCounter());
if(!row)return;
auto f=row->Fetch();
r.Departure={time_t(f[0].GetUInt64()),time_t(f[1].GetUInt64()),time_t(f[2].GetUInt64()),f[3].GetString(),f[4].GetString()};
auto location=sAdventureLocationMgr->Find(r.Departure.Location);
if(!r.Departure.Active(GameTime::GetGameTime()) || !location || !Allowed(player,*location,r.Departure.Intent))
{
r.Departure={};
WorldDatabase.PExecute("DELETE FROM vcore_adventure_group_handoff WHERE character_guid=%u",player->GetGUID().GetCounter());
}
}
void AdventureBehaviorMgr::ArmGroupDeparture(Player* player,AdventureGroupPlan const& source)
{
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return;
auto& r=it->second;auto now=GameTime::GetGameTime();auto local=Local(now);
auto own=GroupPlan(player);
if(!own.Forming || r.Departure.Active(now) || own.NextIntent!=source.NextIntent || source.NextLocation.empty())return;
auto location=sAdventureLocationMgr->Find(source.NextLocation);
if(!location || !Allowed(player,*location,source.NextIntent))return;
time_t end=own.Deadline;
if(r.Schedule){auto tm=local;tm.tm_min=tm.tm_sec=0;++tm.tm_hour;tm.tm_isdst=-1;end=mktime(&tm);}
if(end<=now)return;
// Snapshot once per bot. Later kicks, leadership changes and vacancies do
// not change this copied current slot or the original following day slot.
auto departure=AdventureGroupPolicy::Departure{AdventureGroupPolicy::DepartureAt(now,end),end,
source.NextDeadline?source.NextDeadline:end+3600,source.NextIntent,source.NextLocation};
if(source.Deadline>now)departure.At=std::min(departure.At,source.Deadline);
if(!WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_group_handoff'"))
{TC_LOG_ERROR("server.adventurebot","GROUP_HANDOFF_UNAVAILABLE apply world migration08 before enabling early departures");return;}
auto key=departure.Location,intent=departure.Intent;WorldDatabase.EscapeString(key);WorldDatabase.EscapeString(intent);
auto tx=WorldDatabase.BeginTransaction();
tx->PAppend("REPLACE INTO vcore_adventure_group_handoff (character_guid,depart_at,formation_end,activity_end,intent,location_key) VALUES (%u,%llu,%llu,%llu,'%s','%s')",
player->GetGUID().GetCounter(),static_cast<unsigned long long>(departure.At),static_cast<unsigned long long>(departure.Until),
static_cast<unsigned long long>(departure.ActivityEnd),intent.c_str(),key.c_str());
auto commit=WorldDatabase.AsyncCommitTransaction(tx);
if(!commit.m_future.get()){TC_LOG_ERROR("server.adventurebot","GROUP_HANDOFF_SAVE_FAILED guid=%u",player->GetGUID().GetCounter());return;}
r.Departure=std::move(departure);r.Deadline=r.Departure.At;
TC_LOG_INFO("server.adventurebot","GROUP_FULL_GATHER guid=%u departAt=%llu intent='%s' goal='%s'",
player->GetGUID().GetCounter(),static_cast<unsigned long long>(r.Departure.At),r.Departure.Intent.c_str(),r.Departure.Location.c_str());
}
bool AdventureBehaviorMgr::UpdateDeparture(Player* player,Record& r,time_t now)
{
if(!r.Departure.Active(now))
{
if(r.Departure.At)
{
r.Departure={};r.Hour=-1;
if(r.Schedule && !r.GroupOverride)
{
auto local=Local(now);
if(r.Date!=DateOf(local))Generate(player,r,DateOf(local));
auto const& slot=r.Plan[local.tm_hour];
if(r.WantedIntent!=slot.Intent || r.WantedLocation!=slot.Location)
{
auto end=local;end.tm_min=end.tm_sec=0;++end.tm_hour;end.tm_isdst=-1;
SetWanted(player,r,slot.Intent,slot.Location,mktime(&end));
}
}
WorldDatabase.PExecute("DELETE FROM vcore_adventure_group_handoff WHERE character_guid=%u",player->GetGUID().GetCounter());
}
return false;
}
if(!r.Departure.Departed(now))
{r.Deadline=r.Departure.At;return false;}
if(r.WantedIntent!=r.Departure.Intent || r.WantedLocation!=r.Departure.Location)
{
SetWanted(player,r,r.Departure.Intent,r.Departure.Location,r.Departure.ActivityEnd);
TC_LOG_INFO("server.adventurebot","GROUP_EARLY_DEPART guid=%u intent='%s' goal='%s'",
player->GetGUID().GetCounter(),r.Departure.Intent.c_str(),r.Departure.Location.c_str());
}
return true;
}
AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player)
{
AdventureGroupPlan plan;
@@ -2082,7 +2201,27 @@ AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(Player* player)
if(local.tm_hour<23){plan.NextIntent=r.Plan[local.tm_hour+1].Intent;plan.NextLocation=r.Plan[local.tm_hour+1].Location;}
}
if(!r.Schedule){plan.NextIntent=r.ManualNextIntent;plan.NextLocation=r.ManualNextLocation;}
if(r.Departure.Active(now))
{
if(r.Departure.Departed(now))
{plan.Intent=r.Departure.Intent;plan.Location=r.Departure.Location;plan.Deadline=r.Departure.ActivityEnd;}
else {plan.Deadline=r.Departure.At;plan.NextIntent=r.Departure.Intent;plan.NextLocation=r.Departure.Location;}
}
if(r.Schedule && local.tm_hour<23)
{
auto end=local;end.tm_min=end.tm_sec=0;end.tm_hour=local.tm_hour+2;
while(end.tm_hour<24 && r.Plan[end.tm_hour].Intent==plan.NextIntent && r.Plan[end.tm_hour].Location==plan.NextLocation)++end.tm_hour;
end.tm_isdst=-1;plan.NextDeadline=mktime(&end);
}
else plan.NextDeadline=plan.Deadline+3600;
if(r.Departure.Active(now))plan.NextDeadline=r.Departure.ActivityEnd;
plan.EarlyDeparture=r.Departure.Departed(now);
plan.Forming=plan.Enabled && plan.Intent=="groupformation";
auto activity=plan.Forming?plan.NextIntent:plan.Intent;
plan.Capacity=AdventureGroupPolicy::Capacity(activity);
if(auto location=sAdventureLocationMgr->Find(plan.Forming?plan.NextLocation:plan.Location))
if(activity=="raid" || activity=="instance")
{plan.Capacity=location->InstanceCapacity;plan.Mode=location->InstanceDifficulty;}
return plan;
}
bool AdventureBehaviorMgr::AdoptGroup(Player* player,Player* leader)
@@ -2095,9 +2234,13 @@ bool AdventureBehaviorMgr::AdoptGroup(Player* player,Player* leader)
r.GroupOverride=true;r.PersonalIntent=r.WantedIntent;r.PersonalLocation=r.WantedLocation;r.PersonalDeadline=r.Deadline;
}
if(r.Instance.Active && r.Instance.Stage!=AdventureInstancePolicy::Phase::Travel)return true;
if(r.WantedIntent!=source.Intent || r.WantedLocation!=source.Location)
SetWanted(player,r,source.Intent,source.Location,source.Deadline);
else r.Deadline=source.Deadline;
auto now=GameTime::GetGameTime();
auto const& intent=source.Departure.Departed(now)?source.Departure.Intent:source.Intent;
auto const& location=source.Departure.Departed(now)?source.Departure.Location:source.Location;
auto deadline=source.Departure.Departed(now)?source.Departure.ActivityEnd:source.Deadline;
if(r.WantedIntent!=intent || r.WantedLocation!=location)
SetWanted(player,r,intent,location,deadline);
else r.Deadline=deadline;
return true;
}
@@ -2106,7 +2249,9 @@ void AdventureBehaviorMgr::ResumePersonalPlan(Player* player)
auto it=_records.find(player->GetGUID().GetCounter());if(it==_records.end())return;
auto& r=it->second;if(!r.GroupOverride)return;r.GroupOverride=false;
auto now=GameTime::GetGameTime();
if(r.Schedule)
if(r.Departure.Departed(now))
SetWanted(player,r,r.Departure.Intent,r.Departure.Location,r.Departure.ActivityEnd);
else if(r.Schedule)
{
auto local=Local(now);if(r.Date!=DateOf(local))Generate(player,r,DateOf(local));
auto const& slot=r.Plan[local.tm_hour];
@@ -2,6 +2,7 @@
#define VCORE_ADVENTURE_BEHAVIOR_MGR_H
#include "Common.h"
#include "AdventureGroupMgr.h"
#include "AdventureGroupPolicy.h"
#include "Position.h"
#include "ObjectGuid.h"
#include "AdventureBehaviorPolicy.h"
@@ -36,6 +37,7 @@ public:
void InterruptForInteraction(Player* player);
AdventureGroupPlan GroupPlan(Player* player);
void ResumePersonalPlan(Player* player);
void ArmGroupDeparture(Player* player,AdventureGroupPlan const& source);
bool AdoptGroup(Player* player,Player* leader);
std::string CombatIntent(Player* player) const;
std::string InteractionIntent(Player* player) const;
@@ -59,6 +61,7 @@ private:
std::string PersonalIntent,PersonalLocation;
time_t PersonalDeadline=0;
time_t Deadline=0;
AdventureGroupPolicy::Departure Departure;
bool Gathering=false;
time_t GatherRetryAt=0;
uint32 AreaRevision=0;
@@ -92,6 +95,9 @@ private:
uint32 WorkEmote=0;
bool WorkPending=false;
};
void ClearDeparture(Player* player,Record& record);
void RestoreDeparture(Player* player,Record& record);
bool UpdateDeparture(Player* player,Record& record,time_t now);
bool ResolveInstance(Player* player, Record& record);
bool UpdateNavigationRescue(Player* player,Record& record);
bool UpdateGathering(Player* player,Record& record,time_t now);
@@ -7,6 +7,7 @@
#include "AdventureProfessionPolicy.h"
#include "AdventureSupplyMgr.h"
#include "ClientlessCompanionMgr.h"
#include "ClientlessFlightMgr.h"
#include "Bag.h"
#include "Group.h"
#include "CharacterCache.h"
@@ -59,6 +60,24 @@ namespace
return info && info->SpellLevel<=p->getLevel() && info->BaseLevel<=p->getLevel() &&
p->IsSpellFitByClassAndRace(id) && SpellMgr::IsSpellValid(info,p,false);
}
bool ProvisionFlight(Player* player,ClientlessAdventureProfile const& profile,std::string& error)
{
if(!profile.AutonomousBehavior || player->getLevel()<60)return true;
for(auto spell:{33388u,33391u,34090u,90267u,54197u})
{
if(spell==54197 && player->getLevel()<68)continue;
if(!player->HasSpell(spell) && Learnable(player,spell))player->LearnSpell(spell,false);
if(!player->HasSpell(spell)){error="mandatory flight training unavailable: "+std::to_string(spell);return false;}
}
if(player->GetSkillValue(SKILL_RIDING)<225)player->SetSkill(SKILL_RIDING,3,225,225);
for(auto const& mount:profile.FlightMounts)
{
if(!player->HasSpell(mount.DisplayId) && Learnable(player,mount.DisplayId))player->LearnSpell(mount.DisplayId,false);
}
if(!sClientlessFlightMgr->Enabled(player))
{error="mandatory profile flying mount unavailable";return false;}
return true;
}
std::vector<uint32> Taught(uint32 id)
{
std::vector<uint32> result;
@@ -576,7 +595,8 @@ bool AdventureGeneratorMgr::PrepareCombat(Player* player,std::string& error)
bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(player->GetGUID().GetCounter());
if(!profile || !profile->Level || !profile->Class || profile->ProfileVersion<2)return true;
if(!profile)return true;
if(!profile->Level || !profile->Class || profile->ProfileVersion<2)return ProvisionFlight(player,*profile,error);
if(profile->Class!=player->getClass()){error="Profile class mismatch; no class conversion performed.";return false;}
auto found=std::find_if(AdventureGeneratorPolicy::Archetypes().begin(),AdventureGeneratorPolicy::Archetypes().end(),
[&](Archetype const& a){return a.Class==profile->Class;});
@@ -621,6 +641,7 @@ bool AdventureGeneratorMgr::Reconcile(Player* player,std::string& error)
if(player->getLevel()>=60 && Learnable(player,90267) && !player->HasSpell(90267))player->LearnSpell(90267,false);
if(player->getLevel()>=68 && Learnable(player,54197) && !player->HasSpell(54197))player->LearnSpell(54197,false);
for(auto const& mount:profile->FlightMounts)if(Learnable(player,mount.DisplayId) && !player->HasSpell(mount.DisplayId))player->LearnSpell(mount.DisplayId,false);
if(!ProvisionFlight(player,*profile,error))return false;
player->SetUInt32Value(PLAYER_XP,0);
return true;
}
+36 -13
View File
@@ -37,6 +37,8 @@ bool Ready(Player* p)
{return p && p->IsInWorld() && p->IsAlive() && !p->IsInCombat() && !sAdventureCombatMgr->Busy(p) &&
!p->IsBeingTeleported() && !p->GetTransport() && !p->IsInFlight() && !p->IsFlying() && !p->IsCharmed() && !p->GetMap()->IsDungeon();}
std::string Activity(AdventureGroupPlan const& plan){return plan.Forming?plan.NextIntent:plan.Intent;}
unsigned Capacity(AdventureGroupPlan const& plan)
{return plan.Capacity?plan.Capacity:AdventureGroupPolicy::Capacity(Activity(plan));}
}
AdventureGroupMgr* AdventureGroupMgr::instance(){static AdventureGroupMgr mgr;return &mgr;}
bool AdventureGroupMgr::BotLed(Player* p) const
@@ -48,12 +50,12 @@ bool AdventureGroupMgr::BotLed(Player* p) const
bool AdventureGroupMgr::Forming(Player* p) const
{
if(auto group=p->GetGroup())if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
return sAdventureBehaviorMgr->GroupPlan(leader).Forming;
if(leader->GetSession()->IsClientless())return sAdventureBehaviorMgr->GroupPlan(leader).Forming;
return sAdventureBehaviorMgr->GroupPlan(p).Forming;
}
bool AdventureGroupMgr::PlannedTransition(Player* p)
{
if(!BotLed(p))return false;
if(!BotLed(p) && !sAdventureBehaviorMgr->GroupPlan(p).EarlyDeparture)return false;
auto it=_records.find(p->GetGUID());
if(it==_records.end() || it->second.FormationIntent.empty())return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
@@ -122,7 +124,7 @@ bool AdventureGroupMgr::Accept(Player* invited,Player* inviter)
if(!a.Enabled || !b.Enabled || a.Forming!=b.Forming ||
!AdventureGroupPolicy::Compatible(intent,Activity(b),inviter->getLevel(),invited->getLevel()))return false;
auto group=inviter->GetGroup();
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<AdventureGroupPolicy::Capacity(intent))) && frand(0,1)<Sociability(invited);
return (!group || (group->GetLeaderGUID()==inviter->GetGUID() && group->GetMembersCount()<Capacity(a))) && frand(0,1)<Sociability(invited);
}
bool AdventureGroupMgr::CanInvite(Player* leader,Player* member)
@@ -139,9 +141,29 @@ bool AdventureGroupMgr::CanInvite(Player* leader,Player* member)
Position goal;goal.Relocate(location->Point.X,location->Point.Y,location->Point.Z);
return sAdventureAreaMgr->AllowsPosition(member,location->Point.MapId,goal);
}
void AdventureGroupMgr::CheckFullGroup(Player* p)
{
auto group=p->GetGroup();if(!group)return;
auto source=ObjectAccessor::FindPlayer(group->GetLeaderGUID());
if(!source)return;
// Human-led groups retain the preparing bot's advertised activity. A real
// player's schedule and difficulty are never changed by this handoff.
if(!source->GetSession()->IsClientless())source=p;
if(source->GetMap()->IsDungeon())return;
auto plan=sAdventureBehaviorMgr->GroupPlan(source);
if(!plan.Forming || !AdventureGroupPolicy::Eligible(plan.NextIntent) || group->GetMembersCount()<Capacity(plan))return;
for(auto ref=group->GetFirstMember();ref;ref=ref->next())
if(auto member=ref->GetSource())if(member->GetSession()->IsClientless())
{
_records[member->GetGUID()].FormationIntent=plan.NextIntent;
sAdventureBehaviorMgr->ArmGroupDeparture(member,plan);
}
}
void AdventureGroupMgr::Update(Player* p)
{
auto now=GameTime::GetGameTime();auto& r=_records[p->GetGUID()];
CheckFullGroup(p);
if(PlayerLeadership(p))return;
if(!Ready(p))return;
if(EnforceCapacity(p))return;
@@ -151,12 +173,13 @@ void AdventureGroupMgr::Update(Player* p)
if(group && BotLed(p))if(auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID()))
{auto leaderPlan=sAdventureBehaviorMgr->GroupPlan(leader);if(leaderPlan.Forming)r.FormationIntent=Activity(leaderPlan);}
if(group && group->GetLeaderGUID()!=p->GetGUID())return;
if(intent=="raid" && p->GetRaidDifficulty()!=RAID_DIFFICULTY_25MAN_NORMAL)
if((intent=="raid" || intent=="instance") && p->GetDifficulty(intent=="raid")!=Difficulty(plan.Mode))
{
WorldPacket mode(MSG_SET_RAID_DIFFICULTY,4);mode<<uint32(RAID_DIFFICULTY_25MAN_NORMAL);
p->GetSession()->HandleSetRaidDifficultyOpcode(mode);
WorldPacket mode(intent=="raid"?MSG_SET_RAID_DIFFICULTY:MSG_SET_DUNGEON_DIFFICULTY,4);mode<<uint32(plan.Mode);
if(intent=="raid")p->GetSession()->HandleSetRaidDifficultyOpcode(mode);
else p->GetSession()->HandleSetDungeonDifficultyOpcode(mode);
}
auto capacity=AdventureGroupPolicy::Capacity(intent);
auto capacity=Capacity(plan);
if(group && capacity>5 && !group->isRaidGroup())group->ConvertToRaid();
if(group && capacity<=5 && group->isRaidGroup() && group->GetMembersCount()<=5)group->ConvertToGroup();
unsigned pending=0;
@@ -200,7 +223,7 @@ void AdventureGroupMgr::Update(Player* p)
if(auto existing=other->GetGroup())
{
if(group || existing->GetLeaderGUID()!=other->GetGUID() ||
existing->GetMembersCount()>=AdventureGroupPolicy::Capacity(intent))continue;
existing->GetMembersCount()>=Capacity(peerPlan))continue;
if(plan.Forming)
{if(!AdventureGroupPolicy::Capital(other->GetZoneId()))continue;}
else if(!p->IsInMap(other) || !p->IsInPhase(other) || !p->CanSeeOrDetect(other) || p->GetExactDist(other)>25 || !p->IsWithinLOSInMap(other))continue;
@@ -222,7 +245,7 @@ void AdventureGroupMgr::Update(Player* p)
void AdventureGroupMgr::Report(ChatHandler* h,Player* p) const
{
auto plan=sAdventureBehaviorMgr->GroupPlan(p);
h->PSendSysMessage("Bot group intent='%s' next='%s' forming=%u botLed=%u capacity=%u meeting='%s'",plan.Intent.c_str(),plan.NextIntent.c_str(),plan.Forming,BotLed(p),AdventureGroupPolicy::Capacity(Activity(plan)),plan.Location.c_str());
h->PSendSysMessage("Bot group intent='%s' next='%s' forming=%u botLed=%u capacity=%u meeting='%s'",plan.Intent.c_str(),plan.NextIntent.c_str(),plan.Forming,BotLed(p),Capacity(plan),plan.Location.c_str());
}
bool AdventureGroupMgr::FollowMember(Player* player)
@@ -241,8 +264,8 @@ void AdventureGroupMgr::Invite(Player* p,Player* other,std::string const& intent
unsigned pending=0;
for(auto const& entry:_records)if(entry.second.Inviter==p->GetGUID() && entry.second.InviteUntil>now)
if(auto target=ObjectAccessor::FindPlayer(entry.first))if(target->GetGroupInvite())++pending;
auto group=p->GetGroup();
if((group?group->GetMembersCount():1)+pending>=AdventureGroupPolicy::Capacity(intent))return;
auto group=p->GetGroup();auto plan=sAdventureBehaviorMgr->GroupPlan(p);
if((group?group->GetMembersCount():1)+pending>=Capacity(plan))return;
peer.Inviter=p->GetGUID();peer.InviteUntil=now+30;
if(forming){peer.FormationIntent=intent;r.FormationIntent=intent;}
peer.Partners[p->GetGUID()]=now+600;r.Partners[other->GetGUID()]=now+600;
@@ -265,7 +288,7 @@ void AdventureGroupMgr::RequestPlayerAdmission(Player* member,Player* requester)
auto now=GameTime::GetGameTime();auto& r=_records[leader->GetGUID()];
if(r.Partners[requester->GetGUID()]>now)return;
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);
if(group->GetMembersCount()>=AdventureGroupPolicy::Capacity(Activity(plan)) || frand(0,1)>Sociability(leader))return;
if(group->GetMembersCount()>=Capacity(plan) || frand(0,1)>Sociability(leader))return;
Invite(leader,requester,Activity(plan),false);
}
bool AdventureGroupMgr::PlayerLeadership(Player* leader)
@@ -288,7 +311,7 @@ bool AdventureGroupMgr::EnforceCapacity(Player* p)
auto leader=ObjectAccessor::FindPlayer(group->GetLeaderGUID());if(!leader)return false;
auto plan=sAdventureBehaviorMgr->GroupPlan(leader);auto intent=Activity(plan);
if(!AdventureGroupPolicy::Eligible(intent))return false;
auto capacity=AdventureGroupPolicy::Capacity(intent);if(group->GetMembersCount()<=capacity)return false;
auto capacity=Capacity(plan);if(group->GetMembersCount()<=capacity)return false;
unsigned rank=1;
for(auto const& member:group->GetMemberSlots())
{
+4 -1
View File
@@ -11,7 +11,9 @@ struct AdventureGroupPlan
{
bool Enabled=false,Forming=false;
std::string Intent,Location,NextIntent,NextLocation;
time_t Deadline=0;
time_t Deadline=0,NextDeadline=0;
unsigned Capacity=0,Mode=0;
bool EarlyDeparture=false;
};
class TC_GAME_API AdventureGroupMgr
{
@@ -38,6 +40,7 @@ private:
time_t InviteUntil=0;
std::unordered_map<ObjectGuid,time_t> Partners;
};
void CheckFullGroup(Player* player);
void Invite(Player* leader,Player* member,std::string const& intent,bool forming);
void Speak(Player* player,std::string const& message);
std::string RecruitmentText(Player* player);
@@ -2,12 +2,23 @@
#define VCORE_ADVENTURE_GROUP_POLICY_H
#include <string>
#include <random>
#include <algorithm>
#include <ctime>
namespace AdventureGroupPolicy
{
inline bool Capital(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 1637:case 1638:case 1497:case 3487:case 3557:case 3703:case 4395:return true;default:return false;}}
inline unsigned CapitalFaction(unsigned zone)
{switch(zone){case 1519:case 1537:case 1657:case 3557:return 1;case 1637:case 1638:case 1497:case 3487:return 2;default:return 0;}}
struct Departure
{
time_t At=0, Until=0, ActivityEnd=0;
std::string Intent,Location;
bool Active(time_t now) const {return At && now<Until;}
bool Departed(time_t now) const {return Active(now) && now>=At;}
};
inline time_t DepartureAt(time_t now,time_t blockEnd)
{return std::min(now+300,blockEnd);}
inline bool Gathering(std::string const& intent,long long deadline,long long now)
{return intent=="groupformation" && deadline>now && deadline-now<=300;}
inline bool Eligible(std::string const& intent)
@@ -13,7 +13,7 @@ namespace AdventureInstancePolicy
bool Active=false, InjuryApplied=false, Abort=false;
Phase Stage=Phase::Travel;
std::string Location;
uint32_t Entry=0, Exit=0, Map=0, InstanceId=0, LossPercent=0;
uint32_t Entry=0, Exit=0, Map=0, InstanceId=0, LossPercent=0, Difficulty=0;
time_t BlockEnd=0, PrepareUntil=0;
};
inline time_t ReturnAt(time_t end) {return end>300?end-300:0;}
+59 -24
View File
@@ -11,6 +11,7 @@
#include "PhaseShift.h"
#include "TerrainMgr.h"
#include <cmath>
#include <algorithm>
#include <limits>
#include <memory>
AdventureLocationMgr * AdventureLocationMgr::instance()
@@ -33,25 +34,16 @@ bool AdventureLocationMgr::Reload()
_actions.clear();
_dialogs.clear();
_loaded = false;
_instanceMinimum.clear();
_instanceBands.clear();
for(auto entry:sLFGDungeonStore)
{
auto map=sMapStore.LookupEntry(entry->MapID);
if(!map || !map->IsDungeon() || !entry->MinLevel)continue;
Difficulty difficulty=map->IsRaid()?RAID_DIFFICULTY_25MAN_NORMAL:DUNGEON_DIFFICULTY_NORMAL;
sDBCManager.GetDownscaledMapDifficultyData(map->ID,difficulty);
if(entry->DifficultyID!=difficulty)continue;
auto& minimum=_instanceMinimum[uint32(entry->MapID)];
minimum=minimum?std::min(minimum,entry->MinLevel):entry->MinLevel;
}
// The native access requirements are authoritative even for maps absent
// from the dungeon finder catalog (and may tighten its recommended band).
for(auto map:sMapStore)if(map->IsDungeon())
{
Difficulty difficulty=map->IsRaid()?RAID_DIFFICULTY_25MAN_NORMAL:DUNGEON_DIFFICULTY_NORMAL;
sDBCManager.GetDownscaledMapDifficultyData(map->ID,difficulty);
if(auto access=sObjectMgr->GetAccessRequirement(map->ID,difficulty))
if(access->levelMin)_instanceMinimum[map->ID]=std::max(_instanceMinimum[map->ID],uint32(access->levelMin));
auto& band=_instanceBands[(uint64(map->ID)<<8)|entry->DifficultyID];
// Prefer the lowest native band when obsolete beta LFG entries share
// map/mode (notably normal Shadowfang16..26 versus a beta84..85 row).
if(!band.Min || entry->MinLevel<band.Min)band={entry->MinLevel,entry->Maxlevel};
else if(entry->MinLevel==band.Min)band.Max=std::max(band.Max,entry->Maxlevel);
}
if (! WorldDatabase.Query("SHOW TABLES LIKE 'vcore_adventure_location'")) return false;
if (auto rows = WorldDatabase.Query("SELECT w.id,w.point_key,w.category,w.map,w.position_x,w.position_y,w.position_z,w.orientation," "COALESCE(a.display_name,w.point_key),COALESCE(a.region_key,''),a.area_id," "a.zone_id,a.is_area,a.faction " "FROM vcore_clientless_world_point w LEFT JOIN vcore_adventure_location a ON a.point_key=w.point_key " "WHERE w.enabled=1 AND w.category NOT LIKE 'travel%' ORDER BY w.point_key"))
@@ -89,6 +81,7 @@ bool AdventureLocationMgr::Reload()
auto f=rows->Fetch();
for(auto& l:_locations)if(l.Point.Id==f[0].GetString())l.InstanceMap=f[1].GetUInt16();
}while(rows->NextRow());
BuildInstanceVariants();
if (auto rows = WorldDatabase.Query("SELECT category,intent,weight FROM vcore_adventure_location_intent WHERE weight>=0")) do
{
auto f = rows->Fetch();
@@ -129,7 +122,7 @@ bool AdventureLocationMgr::Reload()
auto f=rows->Fetch();_workstations[f[0].GetUInt32()].insert(f[1].GetString());
} while(rows->NextRow());
_loaded = true;
TC_LOG_INFO("server.adventurebot", "ADVENTURE_CATALOG locations=%u actions=%u lines=%u instanceBands=%u", uint32(_locations.size()), uint32(_actions.size()), uint32(_dialogs.size()),uint32(_instanceMinimum.size()));
TC_LOG_INFO("server.adventurebot", "ADVENTURE_CATALOG locations=%u actions=%u lines=%u instanceBands=%u", uint32(_locations.size()), uint32(_actions.size()), uint32(_dialogs.size()),uint32(_instanceBands.size()));
return true;
}
@@ -144,8 +137,11 @@ bool AdventureLocationMgr::LevelEligible(Player const* player,AdventureLocation
if(!player)return false;
if(intent=="instance" || intent=="raid")
{
auto it=_instanceMinimum.find(location.InstanceMap);
return it!=_instanceMinimum.end() && it->second && player->getLevel()>=it->second;
auto it=_instanceBands.find((uint64(location.InstanceMap)<<8)|location.InstanceDifficulty);
if(it==_instanceBands.end() || !it->second.Min || player->getLevel()<it->second.Min)return false;
// Raids intentionally retain minimum-only eligibility. For dungeons,
// five levels over the recommended upper edge is the outer tolerance.
return intent=="raid" || (it->second.Max && player->getLevel()<=it->second.Max+5);
}
if(!strictArea)return true;
auto area=sAdventureAreaMgr->Area(location.AreaId?location.AreaId:location.ZoneId);
@@ -163,12 +159,7 @@ float AdventureLocationMgr::Weight(AdventureLocation const & location, std::stri
{
auto map=sMapStore.LookupEntry(location.InstanceMap);
if(!map || !map->IsDungeon() || map->IsRaid()!=(intent=="raid"))return 0;
if(intent=="raid")
{
Difficulty difficulty=RAID_DIFFICULTY_25MAN_NORMAL;
auto data=sDBCManager.GetDownscaledMapDifficultyData(location.InstanceMap,difficulty);
if(!data || data->maxPlayers<20)return 0;
}
if(!location.InstanceCapacity)return 0;
}
if(intent=="groupformation" && !AdventureGroupPolicy::Capital(location.ZoneId?location.ZoneId:location.AreaId))return 0;
auto type = _types.find(location.Point.Category);
@@ -399,3 +390,47 @@ bool AdventureLocationMgr::Import(ChatHandler * handler, bool repair)
handler->PSendSysMessage("Location import: %u seeds, %u area destinations, %u inferred guard POIs, %u unusable coordinates skipped. Existing records preserved. Active semantic locations: %u.", count, areas, inferred, skipped, uint32(_locations.size()));
return true;
}
void AdventureLocationMgr::BuildInstanceVariants()
{
std::vector<AdventureLocation> variants;
for(auto& location:_locations)
{
location.BaseKey=location.Point.Id;
if(!location.InstanceMap)continue;
auto map=sMapStore.LookupEntry(location.InstanceMap);
if(!map || !map->IsDungeon())continue;
auto large=map->IsRaid()?sDBCManager.GetMapDifficultyData(map->ID,RAID_DIFFICULTY_25MAN_NORMAL):nullptr;
uint8 preferred=large && large->maxPlayers?1:0;
AdventureLocation original=location;bool baseValid=false;
for(uint8 mode=0;mode<(map->IsRaid()?4:2);++mode)
{
auto data=sDBCManager.GetMapDifficultyData(map->ID,Difficulty(mode));
if(!data || !data->maxPlayers)continue; // never invent a heroic fallback
auto key=(uint64(map->ID)<<8)|mode;
auto band=_instanceBands[key];
if(map->IsRaid() && !band.Min)
for(uint8 normal=0;normal<2;++normal)
{
auto it=_instanceBands.find((uint64(map->ID)<<8)|normal);
if(it!=_instanceBands.end() && it->second.Min && (!band.Min || it->second.Min<band.Min))band=it->second;
}
if(auto access=sObjectMgr->GetAccessRequirement(map->ID,Difficulty(mode)))
{
band.Min=std::max(band.Min,uint32(access->levelMin));
if(access->levelMax)band.Max=band.Max?std::min(band.Max,uint32(access->levelMax)):access->levelMax;
}
if(!band.Min || (!map->IsRaid() && !band.Max))continue;
_instanceBands[key]=band;
auto variant=original;variant.InstanceDifficulty=mode;
variant.InstanceCapacity=map->IsRaid()?std::min(20u,uint32(data->maxPlayers)):std::min(5u,uint32(data->maxPlayers));
variant.MinLevel=uint8(band.Min);variant.MaxLevel=uint8(band.Max);
variant.Name += " ["+std::string((map->IsRaid()?mode>=2:mode==1)?"heroisch":"normal")+
", "+std::to_string(data->maxPlayers)+"]";
if(mode==preferred){location=std::move(variant);baseValid=true;}
else {variant.Point.Id=original.Point.Id+"_d"+std::to_string(mode);variants.push_back(std::move(variant));}
}
if(!baseValid){location.InstanceCapacity=0;location.MinLevel=location.MaxLevel=0;}
}
_locations.insert(_locations.end(),variants.begin(),variants.end());
}
+6 -2
View File
@@ -11,7 +11,9 @@ class Player;
struct AdventureLocation
{
ClientlessWorldPoint Point;
std::string Name, Region;
std::string Name, Region, BaseKey;
uint8 InstanceDifficulty=0;
uint32 InstanceCapacity=0;
uint32 AreaId = 0, ZoneId = 0, InstanceMap = 0;
bool IsArea = false;
uint8 Faction = 0;
@@ -43,7 +45,9 @@ public:
float Weight(AdventureLocation const& location, std::string const& intent) const;
bool LevelEligible(Player const* player, AdventureLocation const& location, std::string const& intent, bool strictArea = false) const;
private:
std::unordered_map<uint32,uint32> _instanceMinimum;
struct Band {uint32 Min=0,Max=0;};
std::unordered_map<uint64,Band> _instanceBands;
void BuildInstanceVariants();
std::unordered_map<uint32,std::unordered_set<std::string>> _workstations;
bool _loaded = false;
std::vector<AdventureLocation> _locations;
@@ -197,6 +197,7 @@ void AdventurePopulationMgr::DeleteRotated()
if(rows){Failed("Native deletion not complete; cleanup deferred.");return;}
auto world=WorldDatabase.BeginTransaction();
world->PAppend("DELETE FROM vcore_adventure_day WHERE character_guid=%u",guid);
world->PAppend("DELETE FROM vcore_adventure_group_handoff WHERE character_guid=%u",guid);
auto worldSaved=WorldDatabase.AsyncCommitTransaction(world);
worldSaved.AfterComplete([this,guid](bool ok)
{
@@ -221,7 +222,7 @@ void AdventurePopulationMgr::DeleteRotated()
void AdventurePopulationMgr::Online(Player* p)
{
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile && profile->ProfileVersion>=2 && profile->Class && profile->Level)_checks[p->GetGUID().GetCounter()]=0;
if(profile && (profile->AutonomousBehavior || (profile->ProfileVersion>=2 && profile->Class && profile->Level)))_checks[p->GetGUID().GetCounter()]=0;
}
void AdventurePopulationMgr::Offline(Player* p){_checks.erase(p->GetGUID().GetCounter());}
void AdventurePopulationMgr::CleanInventory(Player* p)
@@ -256,7 +257,9 @@ void AdventurePopulationMgr::Maintain(Player* p,uint32 diff)
auto it=_checks.find(p->GetGUID().GetCounter());if(it==_checks.end())return;
if(it->second>diff){it->second-=diff;return;}
if(!p->IsInWorld() || !p->IsAlive() || p->IsInCombat() || p->GetMap()->IsDungeon() || p->GetTransport() || p->IsInFlight() || p->IsFlying() || p->IsBeingTeleported() || p->HasUnitState(UNIT_STATE_CASTING))return;
it->second=_maintenance+urand(0,30000);CleanInventory(p);
it->second=_maintenance+urand(0,30000);
auto profile=sClientlessLuaEngine->GetAdventureProfile(p->GetGUID().GetCounter());
if(profile && profile->ProfileVersion>=2 && profile->Class && profile->Level)CleanInventory(p);
std::string error;
if(!sAdventureGeneratorMgr->Reconcile(p,error))TC_LOG_WARN("server.adventurebot","BOT_MANAGER_CHECK guid=%u detail='%s'",p->GetGUID().GetCounter(),error.c_str());
auto tx=CharacterDatabase.BeginTransaction();p->SaveToDB(tx);CharacterDatabase.CommitTransaction(tx);
@@ -508,6 +508,14 @@ bool ClientlessLuaEngine::LoadAdventureProfile(Player* player)
lua_pop(_state,1);
}
lua_pop(_state,1);
// All autonomous level60+ characters can fly. Legacy/manual profiles get
// the same minimum capability as generated profiles, retaining preferences.
if(profile.AutonomousBehavior && (profile.Level?profile.Level:player->getLevel())>=60)
{
profile.CanFreeFly=true;
if(profile.FlightMounts.empty())
profile.FlightMounts.push_back({player->GetTeamId()==TEAM_ALLIANCE?32235u:32243u,1});
}
uint32 const guid = player->GetGUID().GetCounter();
_profiles[guid] = std::move(profile);
ClientlessAdventureProfile const& stored = _profiles.at(guid);
@@ -8,12 +8,13 @@ code=r'''
#include <cassert>
#include <string>
#include <unordered_map>
using uint64=unsigned long long;
struct Player{unsigned level=60;unsigned getLevel()const{return level;}};
struct AdventureLocation{unsigned InstanceMap=0,AreaId=0,ZoneId=0;};
struct AdventureLocation{unsigned InstanceMap=0,AreaId=0,ZoneId=0,InstanceDifficulty=0;};
struct Area{bool KnownLevel=true;unsigned MinLevel=1;};
struct Areas{auto Area(unsigned id)->::Area{return {id!=0,id};}}areas;auto sAdventureAreaMgr=&areas;
struct AdventureLocationMgr{
std::unordered_map<unsigned,unsigned>_instanceMinimum{{533,80},{36,15}};
struct Band{unsigned Min,Max;};std::unordered_map<uint64,Band>_instanceBands{{533ull<<8,{80,80}},{36ull<<8,{15,21}},{(36ull<<8)|1,{85,85}},{33ull<<8,{16,26}},{(33ull<<8)|1,{85,85}},{34ull<<8,{20,30}}};
bool LevelEligible(Player const*,AdventureLocation const&,std::string const&,bool strictArea=false)const;
};
'''+s[a:b]+r'''
@@ -23,6 +24,12 @@ int main(){
p.level=80;assert(mgr.LevelEligible(&p,naxx,"raid"));
AdventureLocation deadmines{36,0,0};p.level=14;assert(!mgr.LevelEligible(&p,deadmines,"instance"));
p.level=15;assert(mgr.LevelEligible(&p,deadmines,"instance"));
p.level=26;assert(mgr.LevelEligible(&p,deadmines,"instance"));p.level=27;assert(!mgr.LevelEligible(&p,deadmines,"instance"));
AdventureLocation stockade{34};for(auto level:{49,57,85}){p.level=level;assert(!mgr.LevelEligible(&p,stockade,"instance"));}
for(unsigned map:{33,36}){AdventureLocation normal{map},heroic{map,0,0,1};p.level=85;
assert(!mgr.LevelEligible(&p,normal,"instance"));assert(mgr.LevelEligible(&p,heroic,"instance"));
p.level=84;assert(!mgr.LevelEligible(&p,heroic,"instance"));}
p.level=85;assert(mgr.LevelEligible(&p,naxx,"raid"));
AdventureLocation unknown{999,0,0};assert(!mgr.LevelEligible(&p,unknown,"raid"));
AdventureLocation zone{0,60,60};p.level=59;
assert(mgr.LevelEligible(&p,zone,"adventure")); // own area policy retains its separate tolerance
@@ -19,7 +19,7 @@ using uint32=uint32_t;
enum class ClientlessActionExecutionState{Moving,Completed};
struct Player {bool transport=false,taxi=false,flying=false,teleport=false;bool GetTransport(){return transport;}bool IsInFlight(){return taxi;}bool IsFlying(){return flying;}bool IsBeingTeleported(){return teleport;}};
struct Record {
bool Active=true,StopAtNode=false;std::size_t RouteStep=1;
bool Active=true,StopAtNode=false,RescueActive=false;std::size_t RouteStep=1;
struct Step{std::string NodeKey;};std::vector<Step> Route{{"goldshire"}};
struct{ClientlessActionExecutionState State=ClientlessActionExecutionState::Moving;std::string Detail;}Status;
};
@@ -0,0 +1,104 @@
"""Execute persisted production handoffs with an actual clock and transactional DB double."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
def function(sig):
a=s.index(sig);b=s.index('{',a);e=b+1;n=1
while n:n+=(s[e]=='{')-(s[e]=='}');e+=1
return s[a:e]
header=(root/'src/server/game/World/AdventureGroupMgr.h').read_text()
a=header.index('struct AdventureGroupPlan');b=header.index('class TC_GAME_API',a)
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureInstancePolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <memory>
#include <unordered_map>
using uint32=unsigned;using uint64=unsigned long long;
#define TC_LOG_INFO(...) ((void)0)
#define TC_LOG_ERROR(...) ((void)0)
struct Guid{unsigned id=0;unsigned GetCounter(){return id;}};struct Player{Guid guid;Guid GetGUID(){return guid;}};
namespace GameTime{time_t now;time_t GetGameTime(){return now;}}
std::tm Local(time_t now){std::tm tm{};localtime_r(&now,&tm);return tm;}unsigned DateOf(std::tm tm){return (1900+tm.tm_year)*10000+(tm.tm_mon+1)*100+tm.tm_mday;}
time_t Clock(int h,int m=0){std::tm tm{};tm.tm_year=126;tm.tm_mon=9;tm.tm_mday=29;tm.tm_hour=h;tm.tm_min=m;return mktime(&tm);}
struct Field{std::string value;uint64 GetUInt64(){return std::stoull(value);}std::string GetString(){return value;}};
struct Row{Field fields[5];Field*Fetch(){return fields;}};
using Result=std::shared_ptr<Row>;
struct Transaction{std::string sql;template<class...Args>void PAppend(char const*fmt,Args...args){char b[2000];snprintf(b,sizeof(b),fmt,args...);sql=b;}};
using Tx=std::shared_ptr<Transaction>;
struct Database{
bool present=true,fail=false;std::unordered_map<unsigned,Row>rows;
Tx BeginTransaction(){return std::make_shared<Transaction>();}void EscapeString(std::string&){}
struct Future{bool ok;bool get(){return ok;}};struct Commit{Future m_future;};
Commit AsyncCommitTransaction(Tx tx){if(fail)return {{false}};unsigned id;uint64 at,end,activity;char intent[64],location[128];
int count=sscanf(tx->sql.c_str(),"REPLACE INTO vcore_adventure_group_handoff (character_guid,depart_at,formation_end,activity_end,intent,location_key) VALUES (%u,%llu,%llu,%llu,'%63[^']','%127[^']')",&id,&at,&end,&activity,intent,location);
assert(count==6);rows[id]={{{std::to_string(at)},{std::to_string(end)},{std::to_string(activity)},{intent},{location}}};return {{true}};}
Result Query(char const*){return present?std::make_shared<Row>():nullptr;}
Result PQuery(char const*,unsigned id){return rows.count(id)?std::make_shared<Row>(rows.at(id)):nullptr;}
void PExecute(char const*,unsigned id){rows.erase(id);}
}WorldDatabase;
struct Location{unsigned InstanceCapacity=5,InstanceDifficulty=1;}location;
struct Locations{Location*Find(std::string const&key){return key.empty()?nullptr:&location;}}locations;auto sAdventureLocationMgr=&locations;
'''+header[a:b]+r'''
struct Encounters{bool Active(Player*){return false;}}encounters;auto sAdventureEncounterMgr=&encounters;
struct AdventureBehaviorMgr{
struct Record{
bool Enabled=true,Dirty=false,Schedule=true,GroupOverride=false;int Hour=14;unsigned Date=20261029;
std::string Intent="groupformation",Location="capital",WantedIntent="groupformation",WantedLocation="capital",PersonalIntent,PersonalLocation,ManualNextIntent,ManualNextLocation;
time_t Deadline=Clock(15),PersonalDeadline=0;AdventureBehaviorPolicy::Day Plan;
AdventureGroupPolicy::Departure Departure;AdventureInstancePolicy::Visit Instance;
};
std::unordered_map<unsigned,Record>_records;unsigned changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
bool Allowed(Player*,Location const&,std::string const&){return true;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;r.Dirty=true;}
void RefreshDay(Player*);void RestoreDeparture(Player*,Record&);bool UpdateDeparture(Player*,Record&,time_t);void ArmGroupDeparture(Player*,AdventureGroupPlan const&);
AdventureGroupPlan GroupPlan(Player*);void ResumePersonalPlan(Player*);bool AdoptGroup(Player*,Player*);std::string InteractionSchedule(Player*);
};
'''
for sig in ('void AdventureBehaviorMgr::RefreshDay(', 'void AdventureBehaviorMgr::RestoreDeparture(', 'void AdventureBehaviorMgr::ArmGroupDeparture(', 'bool AdventureBehaviorMgr::UpdateDeparture(', 'AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(', 'void AdventureBehaviorMgr::ResumePersonalPlan(', 'bool AdventureBehaviorMgr::AdoptGroup(', 'std::string AdventureBehaviorMgr::InteractionSchedule('):code+=function(sig)+'\n'
code+=r'''
void Prepare(AdventureBehaviorMgr::Record&r,std::string const&goal){for(auto&slot:r.Plan)slot={"rest","inn"};r.Plan[14]={"groupformation","capital"};r.Plan[15]={"instance",goal};}
int main(){setenv("TZ","UTC",1);tzset();GameTime::now=Clock(14,20);AdventureBehaviorMgr mgr;Player leader{{1}},member{{2}};
auto&a=mgr._records[1];auto&b=mgr._records[2];Prepare(a,"deadmines_d1");Prepare(b,"shadowfang_d1");
auto source=mgr.GroupPlan(&leader);assert(source.Capacity==5&&source.Mode==1&&source.NextDeadline==Clock(16));
mgr.ArmGroupDeparture(&leader,source);mgr.ArmGroupDeparture(&member,source);
assert(a.Departure.At==Clock(14,25)&&b.Departure.At==Clock(14,25));
assert(b.Departure.Location=="deadmines_d1"&&b.Plan[15].Location=="shadowfang_d1");
assert(WorldDatabase.rows.size()==2);auto saved=WorldDatabase.rows[2];
GameTime::now=Clock(14,23);mgr.ArmGroupDeparture(&member,source);assert(b.Departure.At==Clock(14,25)); // no extending/rearming
assert(!mgr.UpdateDeparture(&member,b,GameTime::now)&&b.Deadline==Clock(14,25));
assert(mgr.GroupPlan(&member).Forming);
GameTime::now=Clock(14,25);assert(mgr.UpdateDeparture(&member,b,GameTime::now));
assert(b.WantedLocation=="deadmines_d1"&&b.WantedIntent=="instance"&&b.Deadline==Clock(16));
auto plan=mgr.GroupPlan(&member);assert(!plan.Forming&&plan.EarlyDeparture&&plan.Location=="deadmines_d1");
// Kick/incomplete group cannot roll the copied current slot back.
b.GroupOverride=true;mgr.ResumePersonalPlan(&member);assert(!b.GroupOverride&&b.WantedLocation=="deadmines_d1");
assert(mgr.InteractionSchedule(&member)=="instance:deadmines_d1");
// Restore independently of any surviving group and continue the same current slot.
AdventureBehaviorMgr restored;auto&c=restored._records[2];Prepare(c,"shadowfang_d1");
restored.RestoreDeparture(&member,c);assert(c.Departure.At==Clock(14,25));
restored.RefreshDay(&member);assert(c.WantedLocation=="deadmines_d1"); // human follow can skip the movement behavior tick
assert(c.Plan[15].Location=="shadowfang_d1");
// At the original boundary the next, untouched personal slot applies.
GameTime::now=Clock(15);restored.UpdateDeparture(&member,c,GameTime::now);
assert(c.WantedLocation=="shadowfang_d1"&&!c.Departure.At&&!WorldDatabase.rows.count(2));
// Late completion never extends the fixed last-five-minute window.
GameTime::now=Clock(14,57);auto&late=restored._records[3];Player lateBot{{3}};Prepare(late,"deadmines_d1");
restored.ArmGroupDeparture(&lateBot,restored.GroupPlan(&lateBot));assert(late.Departure.At==Clock(15));
assert(AdventureGroupPolicy::Gathering("groupformation",late.Deadline,GameTime::now));
GameTime::now=Clock(15);assert(!restored.UpdateDeparture(&lateBot,late,GameTime::now));assert(late.WantedIntent=="instance");
// No armed in-memory transition when persistence fails.
GameTime::now=Clock(14,20);auto&failed=restored._records[4];Player failedBot{{4}};Prepare(failed,"deadmines_d1");
WorldDatabase.fail=true;restored.ArmGroupDeparture(&failedBot,restored.GroupPlan(&failedBot));assert(!failed.Departure.At&&!WorldDatabase.rows.count(4));
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(p/'test.cpp').write_text(code)
subprocess.run(['c++','-std=c++17','-I'+str(root/'src/server/game/World'),str(p/'test.cpp'),'-o',str(p/'test')],check=True)
subprocess.run([str(p/'test')],check=True)
print('Production handoffs: full-group deadline, late clamp, unchanged next slot, kick, restart persistence, expiry and failed save passed.')
@@ -10,6 +10,7 @@ schedule=s[start:end]
a=s.index('AdventureGroupPlan AdventureBehaviorMgr::GroupPlan(')
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.h"
#include "AdventureInstancePolicy.h"
#include <cassert>
#include <ctime>
@@ -17,11 +18,12 @@ code=r'''
using uint32=unsigned;
struct Guid{unsigned id;unsigned GetCounter(){return id;}};
struct Player{Guid guid;Guid GetGUID(){return guid;}};
struct AdventureGroupPlan{bool Enabled=false,Forming=false;std::string Intent,Location,NextIntent,NextLocation;time_t Deadline=0;};
struct AdventureGroupPlan{bool Enabled=false,Forming=false;std::string Intent,Location,NextIntent,NextLocation;time_t Deadline=0,NextDeadline=0;unsigned Capacity=0,Mode=0;bool EarlyDeparture=false;};
namespace GameTime{time_t GetGameTime(){return 100;}}
std::tm Local(time_t){std::tm tm{};tm.tm_hour=5;return tm;}unsigned DateOf(std::tm){return 1;}
struct Location{unsigned InstanceCapacity=5,InstanceDifficulty=1;};struct Locations{Location l;Location*Find(std::string const&){return &l;}}locations;auto sAdventureLocationMgr=&locations;
struct AdventureBehaviorMgr{
struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;};
struct Record{bool Enabled=true,Schedule=true,GroupOverride=false,Dirty=false;int Hour=-1;unsigned Date=1;std::string Intent,Location,WantedIntent,WantedLocation,ManualNextIntent,ManualNextLocation;std::string PersonalIntent,PersonalLocation;time_t PersonalDeadline=0;time_t Deadline=3600;AdventureBehaviorPolicy::Day Plan;AdventureInstancePolicy::Visit Instance;AdventureGroupPolicy::Departure Departure;};
std::unordered_map<unsigned,Record>_records;int changes=0;
void Generate(Player*,Record&r,unsigned date){r.Date=date;}
void SetWanted(Player*,Record&r,std::string const&i,std::string const&l,time_t end){++changes;r.WantedIntent=i;r.WantedLocation=l;r.Deadline=end;}
+29 -7
View File
@@ -29,11 +29,11 @@ struct Map{bool IsDungeon(){return false;}}map;
struct Member{Player*player=nullptr;Member*nextMember=nullptr;Player*GetSource(){return player;}Member*next(){return nextMember;}};
constexpr int GROUP_REMOVEMETHOD_LEAVE=1;
struct Group{struct Slot{ObjectGuid guid;};std::vector<Slot>slots;auto const&GetMemberSlots(){return slots;}void RemoveMember(ObjectGuid,int);Member*members=nullptr;Member*GetFirstMember(){return members;}void ChangeLeader(ObjectGuid g){leader=g;}void SendUpdate(){}ObjectGuid leader;unsigned count=2;bool raid=false;ObjectGuid GetLeaderGUID(){return leader;}unsigned GetMembersCount(){return count;}bool isRaidGroup(){return raid;}void ConvertToRaid(){raid=true;}void ConvertToGroup(){raid=false;}};
struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int ALLIANCE=0,LANG_COMMON=7,LANG_ORCISH=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
struct WorldPacket{unsigned value=0;WorldPacket(int,int=0){}WorldPacket&operator<<(unsigned n){value=n;return *this;}};using Difficulty=unsigned;constexpr int MSG_SET_DUNGEON_DIFFICULTY=3;constexpr int MSG_SET_RAID_DIFFICULTY=2,RAID_DIFFICULTY_25MAN_NORMAL=1;constexpr int ALLIANCE=0,LANG_COMMON=7,LANG_ORCISH=1;constexpr int CMSG_PARTY_INVITE=1,EMOTE_ONESHOT_WAVE=1;
namespace WorldPackets{namespace Party{struct PartyInviteClient{PartyInviteClient(WorldPacket&&){}std::string TargetName;ObjectGuid TargetGUID;};}}
struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);};
struct Session{Player*owner;bool clientless=true;bool IsClientless(){return clientless;}void HandlePartyInviteOpcode(WorldPackets::Party::PartyInviteClient&);void HandleSetRaidDifficultyOpcode(WorldPacket&);void HandleSetDungeonDifficultyOpcode(WorldPacket&p){HandleSetRaidDifficultyOpcode(p);}};
struct Player{ObjectGuid guid;Group*group=nullptr,*invite=nullptr;Session session{this};bool combat=false;unsigned zone=1519,level=60,team=0;float distance=10;
unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
unsigned difficulty=0;unsigned GetRaidDifficulty(){return difficulty;}unsigned GetDifficulty(bool){return difficulty;}ObjectGuid GetGUID(){return guid;}bool IsInWorld(){return true;}bool IsAlive(){return true;}bool IsInCombat(){return combat;}
bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsFlying(){return false;}bool IsInFlight(){return false;}bool IsCharmed(){return false;}Map*GetMap(){return &map;}
unsigned GetZoneId(){return zone;}unsigned getLevel(){return level;}unsigned GetTeam(){return team;}
Group*GetGroup(){return group;}Group*GetGroupInvite(){return invite;}Session*GetSession(){return &session;}std::string GetName(){return std::to_string(guid.id);}
@@ -57,16 +57,38 @@ struct Position{void Relocate(float,float,float){}};
struct Location{std::string Name="Stormwind";struct{unsigned MapId=0;float X=0,Y=0,Z=0;}Point;}location;
struct Locations{bool levelEligible=true;Location*Find(std::string const&){return &location;}bool LevelEligible(Player*,Location const&,std::string const&,bool){return levelEligible;}}locations;auto sAdventureLocationMgr=&locations;
struct Areas{bool allowed=true;bool AllowsPosition(Player*,unsigned,Position const&){return allowed;}}areas;auto sAdventureAreaMgr=&areas;
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct Behavior{std::unordered_map<unsigned,AdventureGroupPlan> plans;AdventureGroupPlan GroupPlan(Player*p){return plans[p->guid.id];}unsigned arms=0;void ArmGroupDeparture(Player*,AdventureGroupPlan const&){++arms;}}behavior;auto sAdventureBehaviorMgr=&behavior;
struct GroupTravel {unsigned follows=0;bool Follow(Player*){++follows;return true;}}groupTravel;auto sAdventureGroupTravelMgr=&groupTravel;
int messages=0;
void AdventureGroupMgr::Speak(Player*,std::string const&){++messages;}
'''
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=fun('float Sociability(')+'\n'+fun('bool Ready(')+'\n'+fun('std::string Activity(')+'\n'+fun('unsigned Capacity(')
for name in ['std::string AdventureGroupMgr::RecruitmentText(', 'bool AdventureGroupMgr::RecruitPlayer(', 'bool AdventureGroupMgr::BotLed(', 'bool AdventureGroupMgr::FollowMember(', 'bool AdventureGroupMgr::Forming(', 'bool AdventureGroupMgr::PlannedTransition(', 'bool AdventureGroupMgr::CanInvite(', 'bool AdventureGroupMgr::Accept(', 'void AdventureGroupMgr::CheckFullGroup(', 'void AdventureGroupMgr::Update(', 'void AdventureGroupMgr::Invite(', 'void AdventureGroupMgr::RequestPlayerAdmission(', 'bool AdventureGroupMgr::PlayerLeadership(', 'bool AdventureGroupMgr::EnforceCapacity(']:code+='\n'+fun(name)
code+=r'''
int main(){
using namespace AdventureGroupPolicy;
// Joined membership triggers gathering; pending invitations alone do not.
{AdventureGroupMgr check;Player one,two,human;one.guid={801};two.guid={802};human.guid={803};human.session.clientless=false;
Group g;g.leader=one.guid;g.count=4;one.group=two.group=&g;
Member second{&two},first{&one,&second};g.members=&first;
ObjectAccessor::players={{one.guid,&one},{two.guid,&two},{human.guid,&human}};
behavior.plans[801]={true,true,"groupformation","city","instance","destination",3600};behavior.plans[802]=behavior.plans[801];
unsigned before=behavior.arms;check._records[two.guid].Inviter=one.guid;check._records[two.guid].InviteUntil=200;
check.CheckFullGroup(&one);assert(behavior.arms==before);
g.count=5;check.CheckFullGroup(&one);assert(behavior.arms==before+2);
// A real player may lead the full group; forming and one-time departure exemption still work.
g.leader=human.guid;human.group=&g;before=behavior.arms;
assert(check.Forming(&one));check.CheckFullGroup(&one);assert(behavior.arms==before+2);
behavior.plans[801].Intent="instance";behavior.plans[801].Forming=false;behavior.plans[801].EarlyDeparture=true;
assert(check.PlannedTransition(&one)&&!check.PlannedTransition(&one));
// Ten-player native raids fill at ten; the twenty-bot cap still applies to larger raids.
behavior.plans[801]={true,true,"groupformation","city","raid","destination",3600};behavior.plans[801].Capacity=10;
g.count=9;before=behavior.arms;check.CheckFullGroup(&one);assert(behavior.arms==before);
g.count=10;check.CheckFullGroup(&one);assert(behavior.arms==before+2);
g.leader=one.guid;behavior.plans[801].Mode=2;check._records[one.guid].SearchAt=10000;
check.Update(&one);assert(one.difficulty==2&&g.raid); // native heroic10, no hardcoded25-normal reset
ObjectAccessor::players.clear();behavior.plans.clear();}
{AdventureGroupMgr check;Player leader,member;leader.guid={901};member.guid={902};Group party;party.leader=leader.guid;leader.group=member.group=&party;
ObjectAccessor::players[leader.guid]=&leader;ObjectAccessor::players[member.guid]=&member;
behavior.plans[leader.guid.id]={true,true,"groupformation","capital","instance","target"};
@@ -106,7 +128,7 @@ int main(){
b.group=&party;behavior.plans[31].Intent="instance";behavior.plans[31].Forming=false;
assert(mgr.PlannedTransition(&b));assert(!mgr.PlannedTransition(&b));
// Raid native conversion and twentieth-member boundary.
b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=19;behavior.plans[29].NextIntent="raid";
b.group=nullptr;b.invite=nullptr;a.group=&party;party.count=19;behavior.plans[29].NextIntent="raid";behavior.plans[29].Mode=1;
behavior.plans[31]={true,true,"groupformation","ironforge","raid","otherraid",3600};
mgr._records[a.guid].SearchAt=0;mgr._records[a.guid].Partners.clear();mgr._records[b.guid].Partners.clear();mgr._records[b.guid].InviteUntil=0;
mgr.Update(&a);assert(a.difficulty==RAID_DIFFICULTY_25MAN_NORMAL&&party.raid&&invites==3&&mgr.Accept(&b,&a));
+6 -3
View File
@@ -25,13 +25,15 @@ struct Maps{auto LookupEntry(uint32){return &mapEntry;}}sMapStore;
struct WorldPacket{uint32 trigger=0;WorldPacket(int,int){}WorldPacket& operator<<(uint32 id){trigger=id;return *this;}};
struct Save{uint32 GetInstanceId(){return 7;}unsigned GetPlayerCount(){return 1;}unsigned GetGroupCount(){return 0;}}save;
struct Bind{Save* save=nullptr;};
using Difficulty=unsigned;constexpr int MSG_SET_RAID_DIFFICULTY=2,MSG_SET_DUNGEON_DIFFICULTY=3;
struct Group{unsigned GetLeaderGUID(){return 0;}Difficulty GetDifficulty(bool){return 0;}}group;
struct Session;
struct Player:Position{
uint32 map=0,instance=0,health=100,maximum=100;int stand=0,damages=0,entryCalls=0,exitCalls=0,teleports=0;
bool safe=true,group=false,deny=false;Bind bind;Session* session=nullptr;
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
struct Guid{uint32 GetCounter(){return 26;}operator unsigned()const{return 26;}};Guid GetGUID(){return {};}
uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}bool IsInWorld(){return true;}
bool IsBeingTeleported(){return false;}bool GetGroup(){return group;}
bool IsBeingTeleported(){return false;}Group* GetGroup(){return group?&::group:nullptr;}
Bind* GetBoundInstance(uint32,int){return bind.save?&bind:nullptr;}int GetDifficulty(bool){return 0;}
bool IsInAreaTriggerRadius(Trigger* t){return map==t->ContinentID&&std::fabs(x-t->Pos.X)<3;}
float GetAngle(float,float){return 0;}void SetFacingTo(float){}void HandleEmoteCommand(int){}
@@ -41,7 +43,8 @@ struct Player:Position{
float GetExactDist(Position const& p){return GetExactDist(p.x,p.y,p.z);}
bool TeleportTo(uint32 m,float a,float b,float c,float o){++teleports;map=m;instance=m==36?7:0;x=a;y=b;z=c;this->o=o;return true;}
};
struct Session{Player* p;void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(WorldPacket const& packet){
namespace ObjectAccessor{Player*FindPlayer(unsigned){return nullptr;}}
struct Session{bool IsClientless(){return true;}void HandleSetRaidDifficultyOpcode(WorldPacket&){}void HandleSetDungeonDifficultyOpcode(WorldPacket&){}Player* p;void CompleteClientlessTeleport(){}void HandleAreaTriggerOpcode(WorldPacket const& packet){
if(packet.trigger==78){++p->entryCalls;if(!p->deny){p->map=36;p->instance=7;p->x=0;}}
else{++p->exitCalls;if(!p->deny){p->map=0;p->instance=0;p->x=500;}}
}};
@@ -12,7 +12,7 @@ code=r'''
#define TC_LOG_INFO(...) ((void)0)
struct Player{struct Guid{int GetCounter(){return 26;}};Guid GetGUID(){return {};}};
struct ClientlessIntent{std::string Name;std::unordered_map<std::string,std::string> Parameters;};
struct Location{bool IsArea=false;}location;
struct Location{bool IsArea=false;std::string BaseKey;}location;
struct Store{bool exists=true;Location* Find(std::string const&){return exists?&location:nullptr;}}store;auto sAdventureLocationMgr=&store;
struct Manager{void ClearIntent(int){}void SetIntent(int,ClientlessIntent){}}manager;auto sClientlessPlayerMgr=&manager;
struct AdventureBehaviorMgr{
@@ -30,6 +30,7 @@ struct AdventureBehaviorMgr{
int main(){Player p;AdventureBehaviorMgr mgr;AdventureBehaviorMgr::Record r;
mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&mgr.parameter=="id"&&mgr.target=="instance_visit_at_101"&&mgr.hard);
assert(r.State==AdventureBehaviorMgr::Phase::Travel&&!r.Dirty);
location.BaseKey="instance_visit_at_101";r.WantedLocation="instance_visit_at_101_d1";mgr.Begin(&p,r);assert(mgr.target==location.BaseKey&&r.Location==r.WantedLocation);
mgr.mapped=false;mgr.Begin(&p,r);assert(mgr.fallback==1);
mgr.mapped=true;r.WantedIntent="social";mgr.Begin(&p,r);assert(mgr.action=="travel_to_location"&&!mgr.hard);
location.IsArea=true;mgr.Begin(&p,r);assert(mgr.action=="travel_to_node"&&mgr.target=="reachable"&&mgr.hard);
@@ -45,12 +45,14 @@ struct DB{
}
}CharacterDatabase;
struct Player{
unsigned GetDifficulty(bool){return 0;}
uint32 health=100,map=36,instance=7;bool teleport=false;int stand=0;
struct Guid{uint32 GetCounter(){return 26;}};Guid GetGUID(){return {};}
bool IsBeingTeleported(){return teleport;}uint32 GetMapId(){return map;}uint32 GetInstanceId(){return instance;}
void SaveToDB(Tx& tx){tx->PAppend("CHARACTER_SAVE",health,map,instance);}
void SetStandState(int v){stand=v;}
};
struct Location{unsigned InstanceDifficulty=1;};struct Locations{Location l;Location*Find(std::string const&){return &l;}}locations;auto sAdventureLocationMgr=&locations;
struct AdventureBehaviorMgr{
enum class Phase{Idle,OnSite};
struct Record{AdventureInstancePolicy::Visit Instance;bool Schedule=false,Dirty=false;Phase State=Phase::Idle;
@@ -0,0 +1,67 @@
"""Exercise production catalog expansion with the testserver's Cataclysm DBC records."""
from pathlib import Path
import struct,subprocess,tempfile
root=Path(__file__).resolve().parents[2]
source=(root/'src/server/game/World/AdventureLocationMgr.cpp').read_text()
def function(sig):
a=source.index(sig);b=source.index('{',a);e=b+1;n=1
while n:n+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
def dbc(name):
b=(Path('/home/wow_admin/wow_cata_server/data/dbc/enUS')/name).read_bytes()
magic,count,fields,size,strings=struct.unpack_from('<4s4I',b);assert magic==b'WDBC'
return [struct.unpack_from('<'+'I'*fields,b,20+n*size)for n in range(count)]
maps={33,34,36,469,533,671}
lfg=[r for r in dbc('LFGDungeons.dbc')if r[7]in maps]
modes=[r for r in dbc('MapDifficulty.dbc')if r[1]in maps]
header=(root/'src/server/game/World/AdventureLocationMgr.h').read_text();a=header.index('struct AdventureLocation');b=header.index('struct AdventureMicroAction',a)
code=r'''
#include <cassert>
#include <algorithm>
#include <string>
#include <vector>
#include <unordered_map>
using uint8=unsigned char;using uint32=unsigned;using uint64=unsigned long long;using Difficulty=unsigned;
constexpr unsigned RAID_DIFFICULTY_25MAN_NORMAL=1;
struct ClientlessWorldPoint{std::string Id;};
'''+header[a:b]+r'''
struct MapEntry{unsigned ID;bool raid;bool IsRaid(){return raid;}bool IsDungeon(){return true;}};
struct Maps{std::unordered_map<unsigned,MapEntry>entries;MapEntry*LookupEntry(unsigned id){return entries.count(id)?&entries.at(id):nullptr;}}sMapStore;
struct MapDifficulty{unsigned maxPlayers;};struct DBC{std::unordered_map<uint64,MapDifficulty>entries;MapDifficulty*GetMapDifficultyData(unsigned id,Difficulty mode){auto key=(uint64(id)<<8)|mode;return entries.count(key)?&entries.at(key):nullptr;}}sDBCManager;
struct LFGEntry{unsigned MapID,DifficultyID,MinLevel,Maxlevel;};std::vector<LFGEntry*>sLFGDungeonStore;
struct Access{unsigned levelMin,levelMax;};struct Objects{Access*GetAccessRequirement(unsigned,Difficulty){return nullptr;}}objects;auto sObjectMgr=&objects;
struct AdventureLocationMgr{struct Band{unsigned Min=0,Max=0;};std::unordered_map<uint64,Band>_instanceBands;std::vector<AdventureLocation>_locations;void BuildInstanceVariants();
AdventureLocation const*Find(std::string const&key){for(auto const&l:_locations)if(l.Point.Id==key)return &l;return nullptr;}
void LoadBands(){
'''
a=source.index(' _instanceBands.clear();');b=source.index(' if (! WorldDatabase.Query',a)
code+=source[a:b]+'}\n};\n'+function('void AdventureLocationMgr::BuildInstanceVariants(')+'\nint main(){\n'
for id in maps:code+=f'sMapStore.entries[{id}]={{{id},{str(id in {469,533,671}).lower()}}};\n'
for r in modes:code+=f'sDBCManager.entries[({r[1]}ull<<8)|{r[2]}]={{{r[5]}}};\n'
for r in lfg:code+=f'sLFGDungeonStore.push_back(new LFGEntry{{{r[7]},{r[8]},{r[2]},{r[3]}}});\n'
code+='AdventureLocationMgr mgr;mgr.LoadBands();\n'
for id in maps:code+=f'{{AdventureLocation l;l.Point.Id="map{id}";l.Name="Map{id}";l.InstanceMap={id};mgr._locations.push_back(l);}}\n'
code+=r'''
mgr.BuildInstanceVariants();
for(auto id:{33,36}){
auto normal=mgr.Find("map"+std::to_string(id)),heroic=mgr.Find("map"+std::to_string(id)+"_d1");
assert(normal&&heroic&&normal->InstanceDifficulty==0&&heroic->InstanceDifficulty==1);
assert(normal->MinLevel<30&&normal->MaxLevel<30); // excludes obsolete84..85 normal SFK row
assert(heroic->MinLevel==85&&heroic->MaxLevel==85&&heroic->InstanceCapacity==5);
assert(heroic->BaseKey==normal->Point.Id&&heroic->Name.find("heroisch")!=std::string::npos);
}
assert(mgr.Find("map34")->MaxLevel==30&&!mgr.Find("map34_d1"));
auto classic=mgr.Find("map469");assert(classic->InstanceCapacity==20&&classic->InstanceDifficulty==0&&classic->MinLevel==60);
assert(!mgr.Find("map469_d1")&&!mgr.Find("map469_d2")&&!mgr.Find("map469_d3")); // native zero-capacity placeholders
assert(mgr.Find("map533")->InstanceCapacity==20&&mgr.Find("map533_d0")->InstanceCapacity==10);
for(unsigned mode=0;mode<4;++mode){auto key=mode==1?"map671":"map671_d"+std::to_string(mode);auto l=mgr.Find(key);
assert(l&&l->InstanceDifficulty==mode&&l->MinLevel==85&&l->InstanceCapacity==(mode%2?20:10));
assert(l->BaseKey=="map671");}
for(auto entry:sLFGDungeonStore)delete entry;
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(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('Real Cataclysm DBC: dungeon normal/heroic, obsolete SFK row, classic raid placeholders, native10/25 normal/heroic and physical entrance reuse passed.')
@@ -6,6 +6,7 @@ s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
a=s.index('void AdventureBehaviorMgr::Update(');b=s.index('bool AdventureBehaviorMgr::Command(',a)
code=r'''
#include "AdventureBehaviorPolicy.h"
#include "AdventureGroupPolicy.h"
#include <cassert>
#include <cstdint>
#include <ctime>
@@ -55,7 +56,7 @@ class AdventureBehaviorMgr {
public:
enum class Phase{Idle,Travel,OnSite,Switching,Handover};
struct Record{
bool Enabled=true,Schedule=false,Queued=false,Dirty=false;
bool Enabled=true,Schedule=false,Queued=false,Dirty=false;AdventureGroupPolicy::Departure Departure;
Phase State=Phase::Travel;
std::string Intent="social",Location="tavern",WantedIntent="social",WantedLocation="tavern",Detail,Micro,ManualNextIntent,ManualNextLocation;
time_t Deadline=1000;uint32 AreaRevision=0,Date=20261026,Timer=0,MicroTimer=0,GestureTimer=0,RetryTimer=0;int Hour=5;
@@ -71,6 +72,7 @@ public:
bool AdoptGroup(Player*,Player*){return false;}
bool UpdateNavigationRescue(Player*,Record&){return false;}
bool UpdateGathering(Player*,Record&,time_t){return false;}
bool UpdateDeparture(Player*,Record&,time_t){return false;}
void Update(Player*,uint32);
void EnterPlayerInstance(Player*);
void Generate(Player*,Record&,uint32){}
@@ -27,7 +27,7 @@ struct Field{
'''
code+=function(field,'uint8 Field::GetUInt8() const')+'\n'+function(field,'uint16 Field::GetUInt16() const')+'\n'+function(field,'uint32 Field::GetUInt32() const')
code+=r'''
struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;uint32 InstanceMap=0,ZoneId=0,AreaId=0;};
struct AdventureLocation{struct{uint32 MapId=0;std::string Category;}Point;bool IsArea=false;uint32 InstanceMap=0,ZoneId=0,AreaId=0,InstanceCapacity=5;};
struct MapEntry{bool raid=false;bool IsDungeon(){return true;}bool IsRaid(){return raid;}}dungeon,raid{true};struct Maps{MapEntry*LookupEntry(unsigned id){return id==36?&dungeon:(id==409||id==999)?&raid:nullptr;}}sMapStore;
using Difficulty=unsigned;constexpr unsigned RAID_DIFFICULTY_25MAN_NORMAL=1;struct MapDifficulty{unsigned maxPlayers=40;}largeRaid,smallRaid{10};struct DBC{MapDifficulty*GetDownscaledMapDifficultyData(unsigned id,Difficulty&){return id==999?&smallRaid:&largeRaid;}}sDBCManager;
struct AdventureLocationMgr{std::unordered_map<std::string,std::unordered_map<std::string,float>> _types;float Weight(AdventureLocation const&,std::string const&)const;};
@@ -62,7 +62,7 @@ int main(){
place.Point.Category="instance_entrance";mgr._types[place.Point.Category]["instance"]=5;mgr._types[place.Point.Category]["raid"]=5;
place.InstanceMap=36;assert(mgr.Weight(place,"instance")==5&&mgr.Weight(place,"raid")==0);
place.InstanceMap=409;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==5);
place.InstanceMap=999;assert(mgr.Weight(place,"raid")==0);
place.InstanceMap=999;assert(mgr.Weight(place,"raid")==5);place.InstanceCapacity=0;assert(mgr.Weight(place,"raid")==0);place.InstanceCapacity=5;
place.InstanceMap=0;assert(mgr.Weight(place,"instance")==0&&mgr.Weight(place,"raid")==0);
place.Point.Category="landmark";mgr._types["landmark"]["groupformation"]=1;
place.ZoneId=12;assert(mgr.Weight(place,"groupformation")==0);
@@ -3,7 +3,7 @@ from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
s=(root/'src/server/game/World/AdventureBehaviorMgr.cpp').read_text()
a=s.index('bool AdventureBehaviorMgr::UpdateNavigationRescue(');b=s.index('\nvoid AdventureBehaviorMgr::Update(',a)
a=s.index('bool AdventureBehaviorMgr::UpdateNavigationRescue(');b=s.index('\nbool AdventureBehaviorMgr::UpdateGathering(',a)
code=r'''
#include "AdventureRecoveryPolicy.h"
#include <algorithm>
@@ -0,0 +1,72 @@
"""Run production provisioning, legacy profile defaults and Northrend entry gate."""
from pathlib import Path
import subprocess,tempfile
root=Path(__file__).resolve().parents[2]
def function(source,sig):
a=source.index(sig);b=source.index('{',a);e=b+1;n=1
while n:n+=(source[e]=='{')-(source[e]=='}');e+=1
return source[a:e]
generator=(root/'src/server/game/World/AdventureGeneratorMgr.cpp').read_text()
area=(root/'src/server/game/World/AdventureAreaMgr.cpp').read_text()
flight=(root/'src/server/game/World/ClientlessFlightMgr.cpp').read_text()
lua=(root/'src/server/game/World/ClientlessLuaEngine.cpp').read_text()
a=lua.index(' // All autonomous level60+');b=lua.index(' uint32 const guid =',a)
code=r'''
#include <cassert>
#include <cmath>
#include <string>
#include <vector>
#include <unordered_set>
using uint32=unsigned;
constexpr unsigned TEAM_ALLIANCE=0,SKILL_RIDING=1,SPELL_AURA_MOD_INCREASE_MOUNTED_FLIGHT_SPEED=206,SPELL_AURA_MOUNTED=78;
struct Mount{unsigned DisplayId,Weight;};struct ClientlessAdventureProfile{bool AutonomousBehavior=true,CanFreeFly=false;unsigned Level=0;std::vector<Mount>FlightMounts;}profile;
struct Guid{unsigned GetCounter()const{return 1;}};
struct Player{unsigned level=60,team=0,riding=0;bool managed=true;std::unordered_set<unsigned>spells,blocked;
unsigned getLevel()const{return level;}unsigned GetTeamId(){return team;}Guid GetGUID()const{return {};}
bool HasSpell(unsigned spell)const{return spells.count(spell);}void LearnSpell(unsigned spell,bool){spells.insert(spell);}
unsigned GetSkillValue(unsigned)const{return riding;}void SetSkill(unsigned,unsigned,unsigned value,unsigned){riding=value;}
};
bool Learnable(Player*p,unsigned spell){return !p->blocked.count(spell);}
struct Effect{unsigned ApplyAuraName;int MiscValueB;};struct Spell{std::vector<Effect>Effects{{78,247}};}spell;
struct Spells{Spell*GetSpellInfo(unsigned id){return id==32235||id==32243?&spell:nullptr;}}spells;auto sSpellMgr=&spells;
struct Lua{ClientlessAdventureProfile*GetAdventureProfile(unsigned){return &profile;}}engine;auto sClientlessLuaEngine=&engine;
struct Config{bool enabled=true;bool GetBoolDefault(char const*,bool){return enabled;}}config;auto sConfigMgr=&config;
using SpellInfo=Spell;
'''+function(flight,'uint32 FlightType(')+r'''
struct ClientlessFlightMgr{bool Enabled(Player const*)const;}flightMgr;auto sClientlessFlightMgr=&flightMgr;
struct Position{float GetPositionX()const{return 0;}float GetPositionY()const{return 0;}float GetPositionZ()const{return 0;}};
struct AdventureAreaMgr{bool preferred=false;struct Policy{bool PreferFlight;};Policy Area(unsigned){return {preferred};}bool Applies(Player const*p){return p->managed;}bool EnsureLoaded(){return true;}unsigned AreaAt(Player const*,unsigned,Position const&){return 1;}bool AllowsArea(Player const*,unsigned){return true;}bool AllowsPosition(Player const*,uint32,Position const&);};
void Normalize(Player*player,ClientlessAdventureProfile&profile){
'''+lua[a:b]+'}\n'+function(generator,'bool ProvisionFlight(')+'\n'+function(flight,'bool ClientlessFlightMgr::Enabled(')+'\n'+function(area,'bool AdventureAreaMgr::AllowsPosition(')+r'''
int main(){Player p;Position pos;AdventureAreaMgr areas;std::string error;
p.level=59;Normalize(&p,profile);assert(!profile.CanFreeFly);assert(ProvisionFlight(&p,profile,error));assert(!areas.AllowsPosition(&p,571,pos));
p.level=60;Normalize(&p,profile);assert(profile.CanFreeFly&&profile.FlightMounts.size()==1&&profile.FlightMounts[0].DisplayId==32235);
assert(ProvisionFlight(&p,profile,error)&&p.riding==225&&p.HasSpell(90267)&&p.HasSpell(34090));
assert(!p.HasSpell(54197)&&flightMgr.Enabled(&p)&&!areas.AllowsPosition(&p,571,pos));assert(areas.AllowsPosition(&p,530,pos));
areas.preferred=true;assert(areas.AllowsPosition(&p,530,pos)); // level60 Outland, no cold weather needed
p.spells.erase(90267);assert(!areas.AllowsPosition(&p,1,pos)&&!areas.AllowsPosition(&p,646,pos));
assert(areas.AllowsPosition(&p,530,pos));p.spells.insert(90267);
p.level=59;assert(!areas.AllowsPosition(&p,530,pos));p.level=60;
p.spells.erase(32235);assert(!areas.AllowsPosition(&p,530,pos)&&!areas.AllowsPosition(&p,0,pos));p.spells.insert(32235);
areas.preferred=false;p.level=1;assert(areas.AllowsPosition(&p,530,pos)); // racial starting islands are not flight regions
p.level=60;
p.level=67;assert(ProvisionFlight(&p,profile,error)&&!areas.AllowsPosition(&p,571,pos));
p.level=68;assert(ProvisionFlight(&p,profile,error)&&p.HasSpell(54197)&&areas.AllowsPosition(&p,571,pos));
// Missing native permission, skill, learned mount or enabled flight denies ALL arrivals.
p.spells.erase(54197);assert(!areas.AllowsPosition(&p,571,pos));p.spells.insert(54197);
p.riding=150;assert(!areas.AllowsPosition(&p,571,pos));assert(ProvisionFlight(&p,profile,error));
p.spells.erase(32235);assert(!areas.AllowsPosition(&p,571,pos));assert(ProvisionFlight(&p,profile,error));
config.enabled=false;assert(!areas.AllowsPosition(&p,571,pos));config.enabled=true;
p.blocked.insert(54197);p.spells.erase(54197);assert(!ProvisionFlight(&p,profile,error)&&!error.empty()&&!areas.AllowsPosition(&p,571,pos));
// Horde legacy profiles receive a wyvern; existing mount preferences survive normalization.
profile={};p={};p.level=85;p.team=1;p.riding=300;Normalize(&p,profile);
assert(profile.FlightMounts[0].DisplayId==32243&&ProvisionFlight(&p,profile,error)&&p.riding==300&&areas.AllowsPosition(&p,571,pos));
profile.FlightMounts={{32243,17}};Normalize(&p,profile);assert(profile.FlightMounts.size()==1&&profile.FlightMounts[0].Weight==17);
p.managed=false;p.level=1;p.spells.clear();assert(areas.AllowsPosition(&p,571,pos)); // ordinary players unaffected
}
'''
with tempfile.TemporaryDirectory()as d:
p=Path(d);(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 flight: legacy defaults, level60 riding, level68 cold weather, all-entry denial, repair, faction mount, preserved weights and ordinary-player isolation passed.')
@@ -26,7 +26,7 @@ struct Core{bool succeed=true;std::vector<unsigned>started;bool StartPlayer(unsi
int CreateClientlessAdventureLuaController(){return 0;}
float sample=.5;float frand(int,int){return sample;}
#define TC_LOG_INFO(...) ((void)0)
struct Profile{unsigned ProfileVersion=0,Class=0,Level=0;std::string SourcePath;};
struct Profile{bool AutonomousBehavior=false;unsigned ProfileVersion=0,Class=0,Level=0;std::string SourcePath;};
struct Player{ObjectGuid GetGUID(){return ObjectGuid{ObjectGuid::Player,26};}};
struct Lua{Profile* profile=nullptr;Profile* GetAdventureProfile(unsigned){return profile;}}lua;auto sClientlessLuaEngine=&lua;
struct AdventurePopulationMgr{
@@ -5,7 +5,7 @@ import tempfile
root = Path(__file__).resolve().parents[2]
source = (root/'src/server/game/World/ClientlessLuaEngine.cpp').read_text()
a = source.index(' lua_getfield(_state,rootTable,"professions");')
b = source.index(' uint32 const guid', a)
b = source.index(' // All autonomous level60+', a)
code = r'''
#include <lua.hpp>
#include "AdventureProfessionPolicy.h"