Implement instance modes, full-group departures and mandatory flight regions
This commit is contained in:
1 parent
bc8f171055
commit
373818628d
40 files changed
+1838
-82
No files matched your search
@@ -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
|
||||
}
|
||||
+4
@@ -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
|
||||
+16
@@ -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
|
||||
+278
@@ -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;
|
||||
@@ -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)
|
||||
{
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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())
|
||||
{
|
||||
|
||||
@@ -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;}
|
||||
|
||||
@@ -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());
|
||||
}
|
||||
@@ -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,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 ↦}
|
||||
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));
|
||||
|
||||
@@ -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"
|
||||
|
||||
Reference in new issue
Block a user