Add AdventureBot self-care, supply restocking and secondary professions
This commit is contained in:
1 parent
c0c799c4b6
commit
39a515dbd1
18 files changed
+889
-3
No files matched your search
@@ -1,3 +1,62 @@
|
||||
# Aktuell: Selbstversorgung und gemeinsame Nebenberufe abgeschlossen
|
||||
|
||||
Stand 27.10.2026. Branch feature/clientless-player-core, vorheriger Commit
|
||||
c0c799c4b6. Nutzer hat den lokalen Commit der Selbstversorgung beauftragt; kein Push.
|
||||
Debug-Build -j10 erfolgreich installiert, alle 55 Testskripte bestanden.
|
||||
Testserver PID 49765 läuft; frisches Ready und Port 8085 bestätigt.
|
||||
|
||||
AdventureSupplyMgr/Policy versorgt AdventureBots zu Tagesbeginn und nach
|
||||
Kampfende bis zu jeweils einem nativen Stack: Verbände, Heil-/Manatränke,
|
||||
Essen und Trinken. Vier Glutseidenstofftaschen54443 mit je22 Plätzen ergänzen
|
||||
Arthoran. Alte Gegenstände blieben erhalten, Geld weiterhin0.
|
||||
Migration world10 und characters01 angewendet und trotz SQL-Ignore gezielt
|
||||
vorgemerkt. Persistierter Tagesstempel verhindert Nachschub bei jedem Login.
|
||||
|
||||
Nutzerregeln umgesetzt: Selbstheilung beginnt erst bei mindestens10% fehlenden
|
||||
HP. Außerhalb des Kampfes zuerst ein nutzbarer Verband; bei Sperre direkt Essen,
|
||||
nach Verband Essen nur bei weiter mindestens10% Mangel. Mananutzer trinken bei
|
||||
passendem Mangel. Im Kampf nur native Tränke. Echte Item-Anfragen, Verbrauch,
|
||||
Abklingzeiten und Verbandssperre; keine direkte HP/Mana-Zuweisung außer dem
|
||||
expliziten Diagnosebefehl testrecovery. Intents behalten ihre ursprüngliche Frist.
|
||||
|
||||
Auf ausdrücklichen Nutzerwunsch erhalten alle AdventureBots die stufengerecht
|
||||
verfügbaren Nebenberufe Erste Hilfe, Kochen, Angeln und Archäologie, innerhalb
|
||||
nativer Traineranforderungen und Fertigkeitsgrenzen. Keine Hauptberufe geändert.
|
||||
Arthoran gespeichert: ErsteHilfe375/375, Kochen525/525, Angeln525/525,
|
||||
Archäologie375/375. Er verwendet schwere Netherstoffverbände21991.
|
||||
Native Trainer erlauben Kochen/Angeln525 bei diesem Charakter.
|
||||
|
||||
Notwendige native Fehlerkorrektur: Player::GetPureMaxSkillValue hatte eine
|
||||
invertierte Prüfung und lieferte für gelernte Fertigkeiten null. Fehlendes !
|
||||
korrigiert, eigener nativer Regressionstest vorhanden. Nebenberufsausbildung
|
||||
berücksichtigt sowohl Skill-Effekt118 als auch den nativen Lerneffekt44.
|
||||
|
||||
Live bestätigt:
|
||||
- Vier22er-Taschen und alle fünf Stacks20, vorhandene Item-GUIDs erhalten.
|
||||
- Markierte8%-Testverletzung: kein Verbrauch.
|
||||
- Markierte80%-Testverletzung: echter Verbandverbrauch, danach4961/5249HP;
|
||||
weniger als10% fehlend, daher kein Essen.
|
||||
- Erneute Testverletzung bei nativer Verbandssperre: echtes Essen, volleHP.
|
||||
- Gleicher Tagesstempel beim erneuten Login: Verband19/Essen19 bleiben bestehen,
|
||||
kein weiterer Tagesnachschub.
|
||||
- Echter Bärenkampf gegen Spawn300425 mit markiertem anfänglichem30%-Defizit:
|
||||
Heiltrank22829 im Kampf verbraucht. Danach exakt je ein Verband, Heiltrank
|
||||
und Essen ergänzt; alle fünf Bestände20. GespeichertHP5249, Geld0.
|
||||
- Manaspezifisches Verhalten produktiv mit Adaptern geprüft, nicht mit echtem
|
||||
Manacharakter. Administrative Testpositionierung ist kein Fernreisebeleg.
|
||||
|
||||
Arthoran bleibt angemeldet in den Verwüsteten Landen am Bären-Testplatz,
|
||||
reagiert auf Gegner und versorgt sich selbst. Normales Lua-Profil bytegleich
|
||||
restauriert; kein temporärer Override. Dokumentation: doc/clientless/supplies.md.
|
||||
Belege: doc/clientless/checkpoints/2026-10-27-supplies/.
|
||||
Logs: /tmp/vcore-supplies-build-final.log und /tmp/vcore-supplies-tests-final.log.
|
||||
DB-Helfer /tmp/vcore-instance-db.py supplies_audit. Diagnose:
|
||||
clientless combat supplies26, testrecovery26<fehlende-Prozent1..80>.
|
||||
Die bisherigen Server-/Branchregeln und vertagte Pfadfindungsarbeit gelten weiter.
|
||||
|
||||
|
||||
---
|
||||
|
||||
# Aktuell: Training, echte Gegner und Todesverhalten abgeschlossen
|
||||
|
||||
Stand 27.10.2026. Auf feature/clientless-player-core bleiben alle bisherigen
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
TrinityCore rev. c0c799c4b686+ 2026-10-27 23:17:05 +0100 (feature/clientless-player-core branch) (Unix, Debug, Static) (worldserver-daemon) ready...
|
||||
SUPPLY_SECONDARY guid=26 skill=356 value=525 max=525
|
||||
SUPPLY_SECONDARY guid=26 skill=794 value=375 max=375
|
||||
SUPPLY_SECONDARY guid=26 skill=129 value=375 max=375
|
||||
SUPPLY_SECONDARY guid=26 skill=185 value=525 max=525
|
||||
SUPPLY_TEST_INJURY guid=26 missingPercent=8
|
||||
SUPPLY_TEST_INJURY guid=26 missingPercent=80
|
||||
SUPPLY_USE_REQUEST guid=26 role='bandage' item=21991 spell=27031 combat=0
|
||||
SUPPLY_USED guid=26 role='bandage' item=21991
|
||||
SUPPLY_RECOVERY_END guid=26 health=4961
|
||||
SUPPLY_TEST_INJURY guid=26 missingPercent=30
|
||||
SUPPLY_USE_REQUEST guid=26 role='bandage' item=21991 spell=27031 combat=0
|
||||
SUPPLY_USE_REQUEST guid=26 role='food' item=27854 spell=27094 combat=0
|
||||
SUPPLY_USED guid=26 role='food' item=27854
|
||||
SUPPLY_RECOVERY_END guid=26 health=5249
|
||||
SUPPLY_TEST_INJURY guid=26 missingPercent=30
|
||||
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0 defending=0 until=0
|
||||
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0
|
||||
COMBAT_START guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0 defending=1 until=0
|
||||
SUPPLY_USE_REQUEST guid=26 role='healing_potion' item=22829 spell=28495 combat=1
|
||||
SUPPLY_USED guid=26 role='healing_potion' item=22829
|
||||
COMBAT_END guid=26 target='GUID Full: 0xf130a56000000b1c Type: Creature Entry: 42336 Low: 2844' training=0
|
||||
SUPPLY_TOPUP guid=26 role='bandage' item=21991 added=1 stack=20
|
||||
SUPPLY_TOPUP guid=26 role='healing_potion' item=22829 added=1 stack=20
|
||||
SUPPLY_TOPUP guid=26 role='food' item=27854 added=1 stack=20
|
||||
SUPPLY_RESTOCK guid=26 day=20261027 daily=0 afterCombat=1
|
||||
@@ -0,0 +1,11 @@
|
||||
Debug build -j10 installed;55 tests/clientless scripts passed.
|
||||
Native skill getter corrected and regression-tested.
|
||||
Arthoran native skill values: FirstAid375/375, Cooking525/525, Fishing525/525, Archaeology375/375. Trainer requirements permit these ranks at level60.
|
||||
Four new EmbersilkBags54443,22 slots each. Existing equipment/items preserved.
|
||||
Each supply role initially20; bandage21991, healing22829, mana22832, food27854, drink28399. Money0 unchanged.
|
||||
Marked8% injury: no supply use. Marked80% injury: real heavy netherweave bandage consumed; remainingHP4961/5249, no food because deficit<10%.
|
||||
Marked30% injury with native bandage lockout: food consumed, HP5249/5249.
|
||||
Same-day relog: bandage19/food19 remained; no daily topup.
|
||||
Real bear fight with marked initial30% deficit: healing potion consumed in combat. After native combat exit, exactly1 bandage,1 healing potion,1 food replenished to20 each; no duplicate stacks.
|
||||
Mana-specific behavior tested with production adapters, not a live mana-using character.
|
||||
Isolated test positions set administratively offline; no long-distance travel claim.
|
||||
@@ -0,0 +1,81 @@
|
||||
# Selbstversorgung und Nebenberufe
|
||||
|
||||
AdventureBots erhalten bei Tagesbeginn und nach einem tatsächlich beendeten
|
||||
Kampf passende Vorräte. Es werden vorhandene Bestände bis zu einem nativen
|
||||
vollen Stack ergänzt, keine zusätzlichen Stacks angesammelt. Fünf Rollen:
|
||||
Verbände, Heiltränke, Manatränke, Essen und Trinken. Die erlaubte kostenfreie
|
||||
Beschaffung verwendet die native Gegenstandserzeugung und Inventarverwaltung.
|
||||
Ein persistenter Tagesstempel verhindert erneute Tagesausgaben bei jedem Login.
|
||||
Bei sehr niedrigen Stufen gibt es noch keinen nutzbaren Manatrank; andere
|
||||
Vorräte werden trotzdem aufgefüllt. Bei Platzmangel wird später erneut versucht,
|
||||
ohne vorhandene Gegenstände zu verwerfen.
|
||||
|
||||
Vier Glutseidenstofftaschen (Item 54443, je 22 Plätze) ergänzen leere Taschenslots.
|
||||
Kleinere Taschen werden über den nativen Taschentausch mit Inhaltserhaltung
|
||||
ersetzt. Bereits mindestens gleich große Taschen bleiben erhalten. Alte Taschen
|
||||
und sonstige Gegenstände werden nicht gelöscht.
|
||||
|
||||
Alle AdventureBots erhalten die stufengerecht verfügbaren Ränge von Erste Hilfe,
|
||||
Kochen, Angeln und Archäologie. Lehrzauber und Mindeststufen stammen aus den
|
||||
nativen Trainerdaten, Skillstufen und Maxima aus der nativen Lernlogik. Fertigkeiten
|
||||
werden innerhalb dieser Grenzen aufgefüllt. Das vergibt keine Hauptberufe und
|
||||
ändert deren bestehende Profilfreigaben nicht. Zusätzliche Rezepte werden nur
|
||||
insoweit gelernt, wie es die native automatische Fertigkeitsbelohnung vorsieht.
|
||||
|
||||
Die Benutzung bleibt echt: native Item-Zauberanfragen mit tatsächlicher Tasche,
|
||||
Slot und Item-GUID, Selbstziel, Benutzungsvoraussetzungen, Abklingzeiten,
|
||||
Verbandssperre und Gegenstandsverbrauch. Keine direkten HP-/Mana-Zuweisungen
|
||||
im Versorgungsablauf, kein Zurücksetzen von Cooldowns. Ein gesondert markierter
|
||||
Diagnosebefehl kann für Tests einen HP-Mangel herstellen.
|
||||
|
||||
## Heilungsreihenfolge
|
||||
|
||||
- Unter 10 Prozent fehlenden HP beginnt keine Selbstheilung.
|
||||
- Außerhalb des Kampfes zuerst einen tatsächlich nutzbaren Verband versuchen.
|
||||
Eine laufende andere Zauberaktion oder globale Abklingzeit zählt nicht bereits
|
||||
als fehlgeschlagener Verbandversuch.
|
||||
- Nach dem Verband nur bei weiterhin mindestens 10 Prozent fehlenden HP essen.
|
||||
Ist kein Verband erlaubt, direkt Essen versuchen.
|
||||
- Eine bereits begonnene Mahlzeit darf bis zur vollständigen Auffrischung laufen.
|
||||
- Bei Mananutzern und mindestens 10 Prozent fehlendem Mana trinken; Krieger
|
||||
benutzen weder Wasser noch Manatränke.
|
||||
- Im Kampf nur native Heil-/Manatränke verwenden, bei mindestens 10 Prozent
|
||||
fehlender passender Ressource. HP hat bei gleichzeitigem Mangel Vorrang.
|
||||
Gemeinsame native Trank-Cooldowns und Kampfbegrenzungen bleiben wirksam.
|
||||
- Eingehender Kampf unterbricht eine eigene Erholung. Selbstverteidigung, Tod
|
||||
und Transport erhalten Vorrang. Der ursprüngliche Intent wird danach mit
|
||||
unveränderter Endzeit fortgesetzt.
|
||||
|
||||
## Daten und Diagnose
|
||||
|
||||
Migrationen:
|
||||
`world/2026_10_27_10_adventure_supplies.sql` und
|
||||
`characters/2026_10_27_01_adventure_supplies.sql` unter `sql/custom/`.
|
||||
Der editierbare Katalog enthält ausdrückliche Rollen und Stufenschwellen;
|
||||
native Item-Voraussetzungen bleiben maßgeblich. Bei Verbänden wird der beste
|
||||
bereits nutzbare Rang bevorzugt. Wiederholte Migrationen erhalten Änderungen.
|
||||
|
||||
`clientless combat supplies <guid>` zeigt Fertigkeiten, ausgewählte Vorräte,
|
||||
Bestände und Taschen. `clientless combat testrecovery <guid> <fehlende-Prozent>`
|
||||
erzeugt ausschließlich bei einem lebenden, kampffreien verwalteten Bot eine
|
||||
markierte Testverletzung (1–80 Prozent). Das ersetzt keinen echten Schadensfall.
|
||||
|
||||
Automatisierte Prüfung: 55 Testskripte erfolgreich, einschließlich produktiver
|
||||
Erholungssteuerung, exakter Zehn-Prozent-Grenze, nutzbarer Verbandränge, nativer
|
||||
Item-Anfragen, Tagesstempel und stufengerechter Nebenberufsgrenzen.
|
||||
Live erfolgreich geprüft: vier Glutseidenstofftaschen, Erste Hilfe375 und
|
||||
schwere Netherstoffverbände, alle vier Nebenberufe, erhaltene Altgegenstände und
|
||||
Geld0. Bei8%Testdefizit kein Verbrauch; bei80% zuerst ein tatsächlich verbrauchter
|
||||
Verband. Anschließend weniger als10%HP fehlend, daher kein Essen. Bei erneuter
|
||||
Testverletzung mit nativer Verbandssperre echtes Essen bis5249/5249HP.
|
||||
Gleicher Tagesstempel beim erneuten Login: keine neue Ausgabe. Echter Bärenkampf
|
||||
mit markiertem Anfangsdefizit: Heiltrank im Kampf verbraucht, danach fehlende
|
||||
Vorräte exakt bis20 ergänzt. Die Anfangsverletzungen waren Diagnosefixtures;
|
||||
Fernanreise und Manaverbrauch wurden damit nicht live geprüft. Belege unter
|
||||
`checkpoints/2026-10-27-supplies/`.
|
||||
|
||||
Die native Abfrage `Player::GetPureMaxSkillValue` hatte eine invertierte Prüfung
|
||||
auf vorhandene Fertigkeitsränge und lieferte bei gelernten Fertigkeiten null.
|
||||
Die Prüfung ist korrigiert; ein eigener nativer Regressionstest sichert reguläre
|
||||
Werte sowie fehlende, gelöschte und leere Fertigkeiten ab. Nebenberufsränge mit
|
||||
EffectLearnSkill44 und normalen SpellMgr-Skillknoten werden beide berücksichtigt.
|
||||
@@ -0,0 +1,6 @@
|
||||
-- Last completed daily top-up survives logout and server restart.
|
||||
CREATE TABLE IF NOT EXISTS vcore_adventure_supply_state (
|
||||
character_guid INT UNSIGNED NOT NULL,
|
||||
last_day INT UNSIGNED NOT NULL DEFAULT 0,
|
||||
PRIMARY KEY(character_guid)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
@@ -0,0 +1,26 @@
|
||||
-- Explicit roles and level tiers; actual native requirements, stack limits and
|
||||
-- item-use checks remain authoritative. Secondary ranks follow native trainers.
|
||||
CREATE TABLE IF NOT EXISTS vcore_adventure_supply (
|
||||
role VARCHAR(24) NOT NULL,
|
||||
item_id INT UNSIGNED NOT NULL,
|
||||
min_level TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
enabled TINYINT UNSIGNED NOT NULL DEFAULT 1,
|
||||
PRIMARY KEY(role,item_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
|
||||
INSERT IGNORE INTO vcore_adventure_supply(role,item_id,min_level) VALUES
|
||||
('bandage',1251,1),('bandage',2581,5),('bandage',3530,10),('bandage',3531,15),
|
||||
('bandage',6450,20),('bandage',6451,25),('bandage',8544,30),('bandage',8545,35),
|
||||
('bandage',14529,40),('bandage',14530,45),('bandage',21990,55),('bandage',21991,60),
|
||||
('bandage',34721,70),('bandage',34722,75),('bandage',53049,80),('bandage',53051,85),
|
||||
('healing_potion',118,1),('healing_potion',858,3),('healing_potion',929,12),
|
||||
('healing_potion',1710,21),('healing_potion',3928,35),('healing_potion',13446,45),
|
||||
('healing_potion',22829,55),('healing_potion',33447,70),('healing_potion',57191,80),
|
||||
('mana_potion',2455,5),('mana_potion',3385,14),('mana_potion',3827,22),
|
||||
('mana_potion',6149,31),('mana_potion',13443,41),('mana_potion',13444,49),
|
||||
('mana_potion',22832,55),('mana_potion',33448,70),('mana_potion',57192,80),
|
||||
('food',117,1),('food',2287,5),('food',3770,15),('food',3771,25),
|
||||
('food',4599,35),('food',8952,45),('food',27854,55),('food',33454,65),
|
||||
('food',35953,75),('food',58260,80),
|
||||
('drink',159,1),('drink',1179,5),('drink',1205,15),('drink',1708,25),
|
||||
('drink',1645,35),('drink',8766,45),('drink',28399,60),('drink',33444,65),
|
||||
('drink',33445,75),('drink',58256,80);
|
||||
@@ -5700,7 +5700,7 @@ uint16 Player::GetPureMaxSkillValue(uint32 skill) const
|
||||
return 0;
|
||||
|
||||
SkillStatusMap::const_iterator itr = mSkillStatus.find(skill);
|
||||
if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || GetSkillRank(itr->second.pos))
|
||||
if (itr == mSkillStatus.end() || itr->second.uState == SKILL_DELETED || !GetSkillRank(itr->second.pos))
|
||||
return 0;
|
||||
|
||||
uint16 field = itr->second.pos / 2;
|
||||
|
||||
@@ -285,3 +285,9 @@ bool AdventureCombatMgr::Update(Player* p,uint32 diff)
|
||||
p->StopMoving();r.AI->Tick(c,500);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool AdventureCombatMgr::Busy(Player* player) const
|
||||
{
|
||||
auto it=_records.find(player->GetGUID().GetCounter());
|
||||
return it!=_records.end() && (it->second.AI || it->second.Dead || !it->second.Damagers.empty());
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public:
|
||||
bool Train(Player* player,uint32 durationMs,time_t deadline=0);
|
||||
bool Attack(Player* player,uint32 creatureSpawnId);
|
||||
void Stop(Player* player);
|
||||
bool Busy(Player* player) const;
|
||||
private:
|
||||
class Fighter;
|
||||
struct Record
|
||||
|
||||
@@ -0,0 +1,308 @@
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
#include "DatabaseEnv.h"
|
||||
#include "DBCStores.h"
|
||||
#include "Bag.h"
|
||||
#include "Item.h"
|
||||
#include "ItemTemplate.h"
|
||||
#include "ObjectMgr.h"
|
||||
#include "Player.h"
|
||||
#include "Spell.h"
|
||||
#include "SpellCastRequest.h"
|
||||
#include "SpellHistory.h"
|
||||
#include "SpellMgr.h"
|
||||
#include "GameTime.h"
|
||||
#include "Chat.h"
|
||||
#include "Log.h"
|
||||
#include <algorithm>
|
||||
#include <memory>
|
||||
|
||||
namespace
|
||||
{
|
||||
constexpr uint32 EmbersilkBag=54443;
|
||||
uint32 Day(time_t now)
|
||||
{
|
||||
std::tm local{};localtime_r(&now,&local);
|
||||
return (local.tm_year+1900)*10000+(local.tm_mon+1)*100+local.tm_mday;
|
||||
}
|
||||
bool RoleMatches(ItemTemplate const* item,std::string const& role)
|
||||
{
|
||||
if(!item || item->GetClass()!=ITEM_CLASS_CONSUMABLE)return false;
|
||||
if(role=="bandage")return item->GetSubClass()==ITEM_SUBCLASS_BANDAGE;
|
||||
if(role=="food" || role=="drink")return item->GetSubClass()==ITEM_SUBCLASS_FOOD;
|
||||
return (role=="healing_potion" || role=="mana_potion") && item->IsPotion();
|
||||
}
|
||||
}
|
||||
AdventureSupplyMgr* AdventureSupplyMgr::instance(){static AdventureSupplyMgr mgr;return &mgr;}
|
||||
bool AdventureSupplyMgr::LoadCatalog()
|
||||
{
|
||||
if(!_catalog.empty())return true;
|
||||
auto rows=WorldDatabase.Query("SELECT role,item_id,min_level FROM vcore_adventure_supply WHERE enabled=1 ORDER BY min_level DESC,item_id DESC");
|
||||
if(!rows)return false;
|
||||
do {auto f=rows->Fetch();_catalog.push_back({f[0].GetString(),f[1].GetUInt32(),f[2].GetUInt8()});}while(rows->NextRow());
|
||||
return true;
|
||||
}
|
||||
void AdventureSupplyMgr::Online(Player* p)
|
||||
{
|
||||
auto& r=_records[p->GetGUID().GetCounter()];r={};
|
||||
LoadCatalog();LearnSecondaries(p);
|
||||
if(auto row=CharacterDatabase.PQuery("SELECT last_day FROM vcore_adventure_supply_state WHERE character_guid=%u",p->GetGUID().GetCounter()))r.LastDay=row->Fetch()[0].GetUInt32();
|
||||
r.Loaded=true;r.WasInCombat=p->IsInCombat();
|
||||
}
|
||||
void AdventureSupplyMgr::LearnSecondaries(Player* p)
|
||||
{
|
||||
if(_secondaryRanks.empty())
|
||||
if(auto rows=WorldDatabase.Query("SELECT DISTINCT SpellId,ReqLevel,ReqSkillRank FROM trainer_spell"))do
|
||||
{
|
||||
auto f=rows->Fetch();auto trainerSpell=sSpellMgr->GetSpellInfo(f[0].GetUInt32());
|
||||
if(!trainerSpell)continue;
|
||||
std::vector<uint32> taught{trainerSpell->Id};
|
||||
for(auto const& effect:trainerSpell->Effects)
|
||||
if(effect.Effect==SPELL_EFFECT_LEARN_SPELL && effect.TriggerSpell)taught.push_back(effect.TriggerSpell);
|
||||
for(auto id:taught)
|
||||
{
|
||||
auto rank=sSpellMgr->GetSpellLearnSkill(id);auto spell=sSpellMgr->GetSpellInfo(id);
|
||||
if(!spell)continue;
|
||||
uint32 skill=rank?rank->skill:0,step=rank?rank->step:0;
|
||||
// Profession ranks also use EffectLearnSkill (44), handled by
|
||||
// Player::AddSpell, rather than SpellMgr's effect118 map.
|
||||
if(!step)for(auto const& effect:spell->Effects)
|
||||
if(effect.Effect==SPELL_EFFECT_SKILL_STEP && effect.CalcValue()>0)
|
||||
{skill=effect.MiscValue;step=effect.CalcValue();break;}
|
||||
if(!step || (skill!=SKILL_FIRST_AID && skill!=SKILL_COOKING && skill!=SKILL_FISHING && skill!=SKILL_ARCHAEOLOGY))continue;
|
||||
uint32 minimum=std::max<uint32>({f[1].GetUInt8(),spell->SpellLevel,spell->BaseLevel});
|
||||
auto existing=std::find_if(_secondaryRanks.begin(),_secondaryRanks.end(),[&](SecondaryRank const& value){return value.Spell==id;});
|
||||
if(existing==_secondaryRanks.end())_secondaryRanks.push_back({skill,id,step,minimum,f[2].GetUInt32()});
|
||||
else {existing->MinLevel=std::max(existing->MinLevel,minimum);existing->Required=std::max<uint32>(existing->Required,f[2].GetUInt32());}
|
||||
}
|
||||
}while(rows->NextRow());
|
||||
std::sort(_secondaryRanks.begin(),_secondaryRanks.end(),[](SecondaryRank const& a,SecondaryRank const& b){return a.Step<b.Step;});
|
||||
for(auto const& rank:_secondaryRanks)
|
||||
{
|
||||
if(rank.MinLevel>p->getLevel() || rank.Required>p->GetPureSkillValue(rank.Skill) ||
|
||||
!p->IsSpellFitByClassAndRace(rank.Spell))continue;
|
||||
auto rc=sDBCManager.GetSkillRaceClassInfo(rank.Skill,p->getRace(),p->getClass());
|
||||
auto info=sSpellMgr->GetSpellInfo(rank.Spell);
|
||||
if(!rc || uint32(rc->MinLevel)>p->getLevel() || !info || !SpellMgr::IsSpellValid(info,p,false))continue;
|
||||
if(!p->HasSpell(rank.Spell))p->LearnSpell(rank.Spell,false);
|
||||
auto maximum=p->GetPureMaxSkillValue(rank.Skill);
|
||||
if(maximum && p->GetPureSkillValue(rank.Skill)<maximum)
|
||||
{
|
||||
p->SetSkill(rank.Skill,p->GetSkillStep(rank.Skill),maximum,maximum);
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_SECONDARY guid=%u skill=%u value=%u max=%u",p->GetGUID().GetCounter(),rank.Skill,maximum,maximum);
|
||||
}
|
||||
}
|
||||
}
|
||||
ItemTemplate const* AdventureSupplyMgr::Select(Player* p,char const* role) const
|
||||
{
|
||||
ItemTemplate const* fallback=nullptr;
|
||||
for(auto const& row:_catalog)
|
||||
{
|
||||
if(row.Role!=role || row.MinLevel>p->getLevel())continue;
|
||||
auto item=sObjectMgr->GetItemTemplate(row.Item);
|
||||
if(!RoleMatches(item,row.Role) || item->GetRequiredLevel()>p->getLevel())continue;
|
||||
if(p->CanUseItem(item)==EQUIP_ERR_OK)return item;
|
||||
// Owning a level-appropriate bandage does not grant First Aid. Prefer
|
||||
// the best usable tier, otherwise retain one appropriate future stack.
|
||||
if(row.Role=="bandage" && !fallback)fallback=item;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
bool AdventureSupplyMgr::Bags(Player* p)
|
||||
{
|
||||
auto bagTemplate=sObjectMgr->GetItemTemplate(EmbersilkBag);
|
||||
if(!bagTemplate || bagTemplate->GetContainerSlots()!=22 || p->CanUseItem(bagTemplate)!=EQUIP_ERR_OK)return false;
|
||||
for(uint8 slot=INVENTORY_SLOT_BAG_START;slot<INVENTORY_SLOT_BAG_END;++slot)
|
||||
{
|
||||
auto current=p->GetBagByPos(slot);
|
||||
if(current && current->GetBagSize()>=22)continue;
|
||||
if(current)
|
||||
{
|
||||
// Native bag swapping transfers all contents and retains the old
|
||||
// bag. Reuse a previous staged bag if a swap could not complete.
|
||||
Item* replacement=nullptr;
|
||||
for(uint8 pos=INVENTORY_SLOT_ITEM_START;pos<INVENTORY_SLOT_ITEM_END;++pos)
|
||||
if(auto item=p->GetItemByPos(INVENTORY_SLOT_BAG_0,pos))
|
||||
if(item->GetEntry()==EmbersilkBag){replacement=item;break;}
|
||||
if(!replacement)
|
||||
{
|
||||
ItemPosCountVec positions;
|
||||
if(p->CanStoreNewItem(INVENTORY_SLOT_BAG_0,NULL_SLOT,positions,EmbersilkBag,1)!=EQUIP_ERR_OK)return false;
|
||||
replacement=p->StoreNewItem(positions,EmbersilkBag,true);
|
||||
}
|
||||
if(!replacement)return false;
|
||||
p->SwapItem(replacement->GetPos(),uint16(INVENTORY_SLOT_BAG_0)<<8|slot);
|
||||
current=p->GetBagByPos(slot);
|
||||
if(!current || current->GetBagSize()<22)return false;
|
||||
}
|
||||
else
|
||||
{
|
||||
auto dispose=[&](Item* item){if(item){RemoveItemFromUpdateQueueOf(item,p);delete item;}};
|
||||
std::unique_ptr<Item,decltype(dispose)> item(Item::CreateItem(EmbersilkBag,1,p),dispose);
|
||||
uint16 destination=0;
|
||||
if(!item || p->CanEquipItem(slot,destination,item.get(),true)!=EQUIP_ERR_OK || !p->EquipItem(destination,item.get(),true))return false;
|
||||
item.release();
|
||||
}
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_BAG guid=%u slot=%u item=%u slots=22",p->GetGUID().GetCounter(),slot,EmbersilkBag);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
bool AdventureSupplyMgr::Stock(Player* p,Record& r,uint32 today)
|
||||
{
|
||||
if(!LoadCatalog())return false;
|
||||
LearnSecondaries(p);
|
||||
if(!Bags(p))return false;
|
||||
for(auto role:{"bandage","healing_potion","mana_potion","food","drink"})
|
||||
{
|
||||
auto item=Select(p,role);if(!item)continue; // No legal mana potion exists at the lowest levels.
|
||||
auto count=AdventureSupplyPolicy::TopUp(p->GetItemCount(item->GetId()),item->GetMaxStackSize());
|
||||
if(!count)continue;
|
||||
ItemPosCountVec positions;
|
||||
if(p->CanStoreNewItem(NULL_BAG,NULL_SLOT,positions,item->GetId(),count)!=EQUIP_ERR_OK || !p->StoreNewItem(positions,item->GetId(),true))return false;
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_TOPUP guid=%u role='%s' item=%u added=%u stack=%u",p->GetGUID().GetCounter(),role,item->GetId(),count,item->GetMaxStackSize());
|
||||
}
|
||||
auto transaction=CharacterDatabase.BeginTransaction();p->SaveToDB(transaction);
|
||||
transaction->PAppend("REPLACE INTO vcore_adventure_supply_state(character_guid,last_day) VALUES(%u,%u)",p->GetGUID().GetCounter(),today);
|
||||
auto commit=CharacterDatabase.AsyncCommitTransaction(transaction);
|
||||
if(!commit.m_future.get())return false;
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_RESTOCK guid=%u day=%u daily=%u afterCombat=%u",p->GetGUID().GetCounter(),today,r.LastDay!=today,r.AfterCombat);
|
||||
r.LastDay=today;r.AfterCombat=false;return true;
|
||||
}
|
||||
AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(Player* p,Record& r,Use use)
|
||||
{
|
||||
auto proto=Select(p,AdventureSupplyPolicy::Role(use));
|
||||
auto item=proto?p->GetItemByEntry(proto->GetId()):nullptr;
|
||||
if(!item || p->CanUseItem(item)!=EQUIP_ERR_OK)return UseResult::Unavailable;
|
||||
if(p->IsNonMeleeSpellCast(false) || p->HasUnitState(UNIT_STATE_CASTING))return UseResult::Busy;
|
||||
for(auto const& effect:proto->Effects)
|
||||
{
|
||||
if(effect.Trigger!=ITEM_SPELLTRIGGER_ON_USE || effect.SpellID<=0)continue;
|
||||
auto info=sSpellMgr->GetSpellInfo(effect.SpellID);
|
||||
if(!info || (p->IsInCombat() && !info->CanBeUsedInCombat()))continue;
|
||||
if(p->GetSpellHistory()->HasGlobalCooldown(info))return UseResult::Busy;
|
||||
if(!p->GetSpellHistory()->IsReady(info,proto->GetId()) || !p->CanRequestSpellCast(info))continue;
|
||||
auto probe=std::make_unique<Spell>(p,info,TRIGGERED_NONE);probe->m_CastItem=item;
|
||||
if(!probe->CanAutoCast(p))continue;
|
||||
probe.reset();
|
||||
uint32 entry=proto->GetId(),spell=info->Id;
|
||||
r.PendingItem=entry;r.PendingCount=p->GetItemCount(entry);r.PendingRole=use;r.Requested=GameTime::GetGameTime();
|
||||
WorldPackets::Spells::SpellCastRequest request;request.SpellID=spell;
|
||||
request.Target.Flags=TARGET_FLAG_UNIT;request.Target.Unit=p->GetGUID();
|
||||
p->RequestSpellCast(std::make_unique<PendingSpellCastRequest>(std::move(request),SpellCastRequestItemData(item->GetBagSlot(),item->GetSlot(),item->GetGUID())),info);
|
||||
if(!p->IsInCombat()){r.Spell=spell;r.Active=use;}
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_USE_REQUEST guid=%u role='%s' item=%u spell=%u combat=%u",p->GetGUID().GetCounter(),AdventureSupplyPolicy::Role(use),entry,spell,p->IsInCombat());
|
||||
return UseResult::Started;
|
||||
}
|
||||
return UseResult::Unavailable;
|
||||
}
|
||||
void AdventureSupplyMgr::Cancel(Player* p,Record& r)
|
||||
{
|
||||
if(r.Spell)
|
||||
{
|
||||
p->InterruptNonMeleeSpells(false,r.Spell);
|
||||
p->RemoveAurasDueToSpell(r.Spell,p->GetGUID());
|
||||
}
|
||||
if(r.Recovering && p->IsAlive())p->SetStandState(UNIT_STAND_STATE_STAND);
|
||||
r.Spell=0;r.Active=Use::None;r.Recovering=false;r.BandageTried=false;
|
||||
}
|
||||
void AdventureSupplyMgr::Offline(Player* p)
|
||||
{
|
||||
auto it=_records.find(p->GetGUID().GetCounter());if(it==_records.end())return;
|
||||
Cancel(p,it->second);_records.erase(it);
|
||||
}
|
||||
bool AdventureSupplyMgr::Update(Player* p,uint32 diff,bool combatReaction)
|
||||
{
|
||||
auto& r=_records[p->GetGUID().GetCounter()];if(!r.Loaded)Online(p);
|
||||
bool combat=p->IsInCombat();
|
||||
if(r.WasInCombat && !combat)r.AfterCombat=true;
|
||||
r.WasInCombat=combat;
|
||||
if(r.Timer>diff){r.Timer-=diff;return r.Recovering && !combat && !combatReaction && p->IsAlive();}
|
||||
r.Timer=500;auto now=GameTime::GetGameTime();
|
||||
if(r.PendingItem)
|
||||
{
|
||||
if(p->GetItemCount(r.PendingItem)<r.PendingCount)
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_USED guid=%u role='%s' item=%u",p->GetGUID().GetCounter(),AdventureSupplyPolicy::Role(r.PendingRole),r.PendingItem);
|
||||
r.PendingItem=0;
|
||||
}
|
||||
else if(now-r.Requested>5)r.PendingItem=0;
|
||||
}
|
||||
if(!p->IsAlive() || p->IsBeingTeleported() || p->GetTransport() || p->IsInFlight() || p->IsFlying() || p->IsCharmed())
|
||||
{if(r.Recovering)Cancel(p,r);return false;}
|
||||
if(combat)
|
||||
{
|
||||
if(r.Recovering)Cancel(p,r);
|
||||
bool health=AdventureSupplyPolicy::MissingTenPercent(p->GetHealth(),p->GetMaxHealth());
|
||||
bool mana=p->GetPowerType()==POWER_MANA && AdventureSupplyPolicy::MissingTenPercent(p->GetPower(POWER_MANA),p->GetMaxPower(POWER_MANA));
|
||||
if(!r.PendingItem)
|
||||
{
|
||||
auto use=AdventureSupplyPolicy::Choose(true,health,mana,false,false);
|
||||
if(use!=Use::None)TryUse(p,r,use);
|
||||
}
|
||||
return false; // Native combat and attacks keep running.
|
||||
}
|
||||
if(combatReaction)return false;
|
||||
if((r.LastDay!=Day(now) || r.AfterCombat) && now>=r.RetryStock)
|
||||
{
|
||||
if(!Stock(p,r,Day(now)))
|
||||
{
|
||||
TC_LOG_WARN("server.adventurebot","SUPPLY_RESTOCK_DEFERRED guid=%u",p->GetGUID().GetCounter());
|
||||
r.RetryStock=now+30;
|
||||
}
|
||||
else r.RetryStock=0;
|
||||
}
|
||||
if(p->IsFalling())return false;
|
||||
if(r.Recovering && now-r.Started>=90)
|
||||
{Cancel(p,r);r.RetryRecovery=now+5;return false;}
|
||||
if(r.Spell)
|
||||
{
|
||||
auto cast=p->GetCurrentSpell(CURRENT_CHANNELED_SPELL);
|
||||
if(!cast)cast=p->GetCurrentSpell(CURRENT_GENERIC_SPELL);
|
||||
bool running=(cast && cast->GetSpellInfo()->Id==r.Spell) || p->HasAura(r.Spell,p->GetGUID());
|
||||
bool full=(r.Active==Use::Food && p->GetHealth()>=p->GetMaxHealth()) ||
|
||||
(r.Active==Use::Drink && p->GetPower(POWER_MANA)>=p->GetMaxPower(POWER_MANA));
|
||||
if(running && !full)return true;
|
||||
if(!running && now-r.Requested<2)return true;
|
||||
p->InterruptNonMeleeSpells(false,r.Spell);p->RemoveAurasDueToSpell(r.Spell,p->GetGUID());
|
||||
r.Spell=0;r.Active=Use::None;
|
||||
}
|
||||
bool health=AdventureSupplyPolicy::MissingTenPercent(p->GetHealth(),p->GetMaxHealth());
|
||||
bool mana=p->GetPowerType()==POWER_MANA && AdventureSupplyPolicy::MissingTenPercent(p->GetPower(POWER_MANA),p->GetMaxPower(POWER_MANA));
|
||||
if(!health && !mana)
|
||||
{
|
||||
if(r.Recovering){TC_LOG_INFO("server.adventurebot","SUPPLY_RECOVERY_END guid=%u health=%u",p->GetGUID().GetCounter(),p->GetHealth());Cancel(p,r);}
|
||||
return false;
|
||||
}
|
||||
if(now<r.RetryRecovery)return false;
|
||||
if(!r.Recovering)
|
||||
{
|
||||
sAdventureBehaviorMgr->InterruptForCombat(p);sClientlessLuaEngine->DismountGroundMount(p,"supply-recovery");
|
||||
p->StopMoving();r.Recovering=true;r.BandageTried=false;r.Started=now;
|
||||
}
|
||||
auto use=AdventureSupplyPolicy::Choose(false,health,mana,r.BandageTried,true);
|
||||
p->SetStandState(use==Use::Bandage?UNIT_STAND_STATE_STAND:UNIT_STAND_STATE_SIT);
|
||||
auto result=TryUse(p,r,use);
|
||||
if(use==Use::Bandage && result!=UseResult::Busy)
|
||||
{
|
||||
r.BandageTried=true;
|
||||
if(result==UseResult::Unavailable)
|
||||
{
|
||||
p->SetStandState(UNIT_STAND_STATE_SIT);
|
||||
result=TryUse(p,r,Use::Food);
|
||||
}
|
||||
}
|
||||
if(result==UseResult::Unavailable){Cancel(p,r);r.RetryRecovery=now+5;return false;}
|
||||
return true;
|
||||
}
|
||||
void AdventureSupplyMgr::Report(ChatHandler* handler,Player* p)
|
||||
{
|
||||
auto& r=_records[p->GetGUID().GetCounter()];
|
||||
handler->PSendSysMessage("Supplies: lastDay=%u recovering=%u afterCombat=%u health=%u/%u",r.LastDay,r.Recovering,r.AfterCombat,p->GetHealth(),p->GetMaxHealth());
|
||||
for(uint32 skill:{uint32(SKILL_FIRST_AID),uint32(SKILL_COOKING),uint32(SKILL_FISHING),uint32(SKILL_ARCHAEOLOGY)})
|
||||
handler->PSendSysMessage("secondary=%u value=%u max=%u",skill,p->GetPureSkillValue(skill),p->GetPureMaxSkillValue(skill));
|
||||
for(auto role:{"bandage","healing_potion","mana_potion","food","drink"})
|
||||
if(auto item=Select(p,role))handler->PSendSysMessage("%s: item=%u count=%u stack=%u requiredLevel=%u requiredSkill=%u/%u usable=%u",role,item->GetId(),p->GetItemCount(item->GetId()),item->GetMaxStackSize(),item->GetRequiredLevel(),item->GetRequiredSkill(),item->GetRequiredSkillRank(),p->CanUseItem(item)==EQUIP_ERR_OK);
|
||||
for(uint8 slot=INVENTORY_SLOT_BAG_START;slot<INVENTORY_SLOT_BAG_END;++slot)
|
||||
if(auto bag=p->GetBagByPos(slot))handler->PSendSysMessage("bagSlot=%u item=%u capacity=%u",slot,bag->GetEntry(),bag->GetBagSize());
|
||||
}
|
||||
@@ -0,0 +1,46 @@
|
||||
#ifndef VCORE_ADVENTURE_SUPPLY_MGR_H
|
||||
#define VCORE_ADVENTURE_SUPPLY_MGR_H
|
||||
#include "Common.h"
|
||||
#include "AdventureSupplyPolicy.h"
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
class Player;
|
||||
class ChatHandler;
|
||||
struct ItemTemplate;
|
||||
class TC_GAME_API AdventureSupplyMgr
|
||||
{
|
||||
public:
|
||||
static AdventureSupplyMgr* instance();
|
||||
void Online(Player* player);
|
||||
void Offline(Player* player);
|
||||
// True only while an out-of-combat recovery owns movement/intent execution.
|
||||
bool Update(Player* player,uint32 diff,bool combatReaction);
|
||||
void Report(ChatHandler* handler,Player* player);
|
||||
private:
|
||||
using Use=AdventureSupplyPolicy::Use;
|
||||
struct Supply {std::string Role;uint32 Item=0,MinLevel=0;};
|
||||
struct Record
|
||||
{
|
||||
bool Loaded=false,WasInCombat=false,AfterCombat=false,Recovering=false,BandageTried=false;
|
||||
uint32 LastDay=0,Timer=0,Spell=0;
|
||||
Use Active=Use::None;
|
||||
time_t RetryStock=0,RetryRecovery=0,Requested=0,Started=0;
|
||||
uint32 PendingItem=0,PendingCount=0;Use PendingRole=Use::None;
|
||||
};
|
||||
enum class UseResult {Unavailable,Busy,Started};
|
||||
bool LoadCatalog();
|
||||
void LearnSecondaries(Player* player);
|
||||
ItemTemplate const* Select(Player* player,char const* role) const;
|
||||
bool Bags(Player* player);
|
||||
bool Stock(Player* player,Record& record,uint32 today);
|
||||
UseResult TryUse(Player* player,Record& record,Use use);
|
||||
void Cancel(Player* player,Record& record);
|
||||
struct SecondaryRank {uint32 Skill=0,Spell=0,Step=0,MinLevel=0,Required=0;};
|
||||
std::vector<SecondaryRank> _secondaryRanks;
|
||||
std::vector<Supply> _catalog;
|
||||
std::unordered_map<uint32,Record> _records;
|
||||
};
|
||||
#define sAdventureSupplyMgr AdventureSupplyMgr::instance()
|
||||
#endif
|
||||
@@ -0,0 +1,30 @@
|
||||
#ifndef VCORE_ADVENTURE_SUPPLY_POLICY_H
|
||||
#define VCORE_ADVENTURE_SUPPLY_POLICY_H
|
||||
#include <cstdint>
|
||||
namespace AdventureSupplyPolicy
|
||||
{
|
||||
enum class Use { None, Bandage, Food, Drink, HealingPotion, ManaPotion };
|
||||
inline bool MissingTenPercent(std::uint32_t current,std::uint32_t maximum)
|
||||
{ return maximum && std::uint64_t(current)*100 <= std::uint64_t(maximum)*90; }
|
||||
inline std::uint32_t TopUp(std::uint32_t count,std::uint32_t stack)
|
||||
{ return count<stack?stack-count:0; }
|
||||
inline Use Choose(bool combat,bool health,bool mana,bool bandageTried,bool bandageAllowed)
|
||||
{
|
||||
if(combat)return health?Use::HealingPotion:mana?Use::ManaPotion:Use::None;
|
||||
if(health)return !bandageTried && bandageAllowed?Use::Bandage:Use::Food;
|
||||
return mana?Use::Drink:Use::None;
|
||||
}
|
||||
inline char const* Role(Use use)
|
||||
{
|
||||
switch(use)
|
||||
{
|
||||
case Use::Bandage:return "bandage";
|
||||
case Use::Food:return "food";
|
||||
case Use::Drink:return "drink";
|
||||
case Use::HealingPotion:return "healing_potion";
|
||||
case Use::ManaPotion:return "mana_potion";
|
||||
default:return "";
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "ClientlessTravelMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "AdventureSocialActions.h"
|
||||
#include "Config.h"
|
||||
#include "Log.h"
|
||||
@@ -215,15 +216,19 @@ public:
|
||||
AdventureSocialActions::Prepare(player);
|
||||
sClientlessLuaEngine->ExecuteAdventureScript(player);
|
||||
sAdventureBehaviorMgr->Online(player);
|
||||
sAdventureSupplyMgr->Online(player);
|
||||
}
|
||||
void OnIntentChanged(Player* player, ClientlessIntent const* intent) override
|
||||
{ sAdventureBehaviorMgr->IntentChanged(player, intent); }
|
||||
void Update(Player* player, uint32 diff) override
|
||||
{ if (!sAdventureCombatMgr->Update(player, diff)) sAdventureBehaviorMgr->Update(player, diff); }
|
||||
{
|
||||
if(sAdventureSupplyMgr->Update(player,diff,sAdventureCombatMgr->Busy(player)))return;
|
||||
if(!sAdventureCombatMgr->Update(player,diff))sAdventureBehaviorMgr->Update(player,diff);
|
||||
}
|
||||
void OnDamageTaken(Player* player, Unit* attacker, uint32 damage) override
|
||||
{ sAdventureCombatMgr->DamageTaken(player, attacker, damage); }
|
||||
void OnPlayerLogout(Player* player) override
|
||||
{ sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
|
||||
{ sAdventureSupplyMgr->Offline(player); sAdventureCombatMgr->Offline(player); sAdventureBehaviorMgr->Offline(player); }
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@
|
||||
#include "ClientlessWorldPointMgr.h"
|
||||
#include "AdventureBehaviorMgr.h"
|
||||
#include "AdventureCombatMgr.h"
|
||||
#include "AdventureSupplyMgr.h"
|
||||
#include "ClientlessTravelGraphMgr.h"
|
||||
#include "ClientlessRouteRecorderMgr.h"
|
||||
#include "ClientlessLuaEngine.h"
|
||||
@@ -259,6 +260,13 @@ namespace ClientlessCombatPreparation
|
||||
for(auto const& aura:c->GetAppliedAuras())handler->PSendSysMessage("Target aura=%u",aura.first);
|
||||
return true;
|
||||
}
|
||||
if(op=="supplies"){sAdventureSupplyMgr->Report(handler,player);return true;}
|
||||
if(op=="testrecovery" && source>=1 && source<=80 && player->IsAlive() && !player->IsInCombat() && !sAdventureCombatMgr->Busy(player) && !player->IsBeingTeleported() && !player->GetTransport())
|
||||
{
|
||||
player->SetHealth(std::max(1u,player->GetMaxHealth()*(100-source)/100));
|
||||
TC_LOG_INFO("server.adventurebot","SUPPLY_TEST_INJURY guid=%u missingPercent=%u",guid,source);
|
||||
return true;
|
||||
}
|
||||
if(op=="testdeath" && player->IsAlive() && !player->IsBeingTeleported() && !player->GetTransport() && !player->InBattleground() && !player->InArena())
|
||||
{
|
||||
TC_LOG_INFO("server.adventurebot","DEATH_TEST_REQUEST guid=%u",guid);
|
||||
|
||||
@@ -0,0 +1,97 @@
|
||||
"""Execute the production recovery state machine with native-item adapters."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
source=(root/'src/server/game/World/AdventureSupplyMgr.cpp').read_text()
|
||||
def function(signature):
|
||||
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
|
||||
return source[a:e]
|
||||
header=(root/'src/server/game/World/AdventureSupplyMgr.h').read_text()
|
||||
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
|
||||
code=r'''
|
||||
#include "AdventureSupplyPolicy.h"
|
||||
#include <algorithm>
|
||||
#include <cassert>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
using uint32=unsigned;
|
||||
#define TC_GAME_API
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
#define TC_LOG_WARN(...) ((void)0)
|
||||
constexpr int POWER_MANA=0,POWER_RAGE=1,CURRENT_CHANNELED_SPELL=0,CURRENT_GENERIC_SPELL=1,UNIT_STAND_STATE_STAND=0,UNIT_STAND_STATE_SIT=1;
|
||||
struct Guid{unsigned GetCounter(){return 26;}};
|
||||
struct Spell{struct Info{unsigned Id=1;}info;Info* GetSpellInfo(){return &info;}};
|
||||
struct Player{
|
||||
unsigned hp=100,maxhp=100,mana=100,maxmana=100,type=POWER_RAGE;bool alive=true,combat=false,casting=false,aura=false,bandageAllowed=true,busy=false,foodAllowed=true;
|
||||
int posture=0,stops=0;Spell spell;std::unordered_map<unsigned,unsigned> items;
|
||||
Guid GetGUID(){return {};}bool IsInCombat(){return combat;}bool IsAlive(){return alive;}bool IsBeingTeleported(){return false;}bool GetTransport(){return false;}bool IsInFlight(){return false;}bool IsFlying(){return false;}bool IsCharmed(){return false;}bool IsFalling(){return false;}
|
||||
unsigned GetHealth(){return hp;}unsigned GetMaxHealth(){return maxhp;}unsigned GetPower(int){return mana;}unsigned GetMaxPower(int){return maxmana;}unsigned GetPowerType(){return type;}
|
||||
unsigned GetItemCount(unsigned entry){return items[entry];}void StopMoving(){++stops;}void SetStandState(int s){posture=s;}
|
||||
Spell*GetCurrentSpell(int slot){return casting && slot==0?&spell:nullptr;}
|
||||
bool HasAura(unsigned id,Guid){return aura && spell.info.Id==id;}
|
||||
void InterruptNonMeleeSpells(bool,unsigned id){if(spell.info.Id==id)casting=false;}
|
||||
void RemoveAurasDueToSpell(unsigned id,Guid){if(spell.info.Id==id)aura=false;}
|
||||
};
|
||||
namespace GameTime{time_t now=100;time_t GetGameTime(){return now;}}
|
||||
unsigned today=20261027;unsigned Day(time_t){return today;}
|
||||
struct Behavior{int interrupts=0;void InterruptForCombat(Player*){++interrupts;}}behavior;auto sAdventureBehaviorMgr=&behavior;
|
||||
struct Lua{void DismountGroundMount(Player*,char const*){}}lua;auto sClientlessLuaEngine=&lua;
|
||||
'''+header+r'''
|
||||
std::vector<AdventureSupplyPolicy::Use> uses;int stocks=0;unsigned savedDay=0;
|
||||
void AdventureSupplyMgr::Online(Player*p){auto&r=_records[26];r.Loaded=true;r.LastDay=savedDay;}
|
||||
bool AdventureSupplyMgr::Stock(Player*,Record&r,unsigned date){++stocks;r.LastDay=date;r.AfterCombat=false;savedDay=date;return true;}
|
||||
AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(Player*p,Record&r,Use use){
|
||||
uses.push_back(use);if(p->busy)return UseResult::Busy;
|
||||
if((use==Use::Bandage && !p->bandageAllowed)||(use==Use::Food && !p->foodAllowed))return UseResult::Unavailable;
|
||||
if(!p->combat){r.Active=use;r.Spell=unsigned(use);p->spell.info.Id=r.Spell;p->aura=true;r.Requested=GameTime::now;}
|
||||
return UseResult::Started;
|
||||
}
|
||||
'''+function('void AdventureSupplyMgr::Cancel(')+'\n'+function('bool AdventureSupplyMgr::Update(')+r'''
|
||||
int main(){
|
||||
using U=AdventureSupplyPolicy::Use;using namespace AdventureSupplyPolicy;
|
||||
assert(!MissingTenPercent(91,100));assert(MissingTenPercent(90,100));assert(!MissingTenPercent(1,0));
|
||||
assert(!MissingTenPercent(4725,5249));assert(MissingTenPercent(4724,5249));
|
||||
assert(TopUp(19,20)==1 && TopUp(20,20)==0 && TopUp(21,20)==0);
|
||||
AdventureSupplyMgr mgr;Player p;auto tick=[&]{GameTime::now+=3;return mgr.Update(&p,500,false);};
|
||||
assert(!tick() && stocks==1);assert(!tick() && stocks==1);
|
||||
p.hp=91;assert(!tick() && uses.empty());
|
||||
// At the exact threshold, bandage has priority over food.
|
||||
p.hp=90;assert(tick() && uses.back()==U::Bandage);
|
||||
p.aura=false;p.hp=95;assert(!tick() && uses.size()==1); // no food below10% missing
|
||||
// New recovery: bandage finishes but health deficit still10% or more.
|
||||
p.hp=40;assert(tick() && uses.back()==U::Bandage);
|
||||
p.aura=false;p.hp=75;assert(tick() && uses.back()==U::Food);
|
||||
auto count=uses.size();p.hp=95;assert(tick() && uses.size()==count); // keep current meal to full
|
||||
p.hp=100;assert(!tick() && !p.aura && p.posture==UNIT_STAND_STATE_STAND);
|
||||
// Missing First Aid / native bandage lockout: immediate food fallback.
|
||||
p.hp=80;p.bandageAllowed=false;uses.clear();assert(tick());assert((uses==std::vector<U>{U::Bandage,U::Food}));
|
||||
p.hp=100;assert(!tick());
|
||||
// A busy global cooldown must not count as a failed bandage attempt.
|
||||
p.bandageAllowed=true;p.hp=80;p.busy=true;uses.clear();assert(tick() && uses.back()==U::Bandage);
|
||||
assert(!mgr._records[26].BandageTried);p.busy=false;assert(tick() && uses.back()==U::Bandage);
|
||||
// Incoming combat interrupts only owned recovery; combat never eats/bandages.
|
||||
p.combat=true;uses.clear();assert(!tick() && uses.back()==U::HealingPotion && !p.aura);
|
||||
p.hp=100;p.type=POWER_MANA;p.mana=89;uses.clear();assert(!tick() && uses.back()==U::ManaPotion);
|
||||
// Exactly one top-up after the actual combat interval, not per enemy/tick.
|
||||
auto previous=stocks;p.combat=false;assert(tick() && stocks==previous+1 && uses.back()==U::Drink);
|
||||
assert(tick() && stocks==previous+1);p.mana=100;assert(!tick());
|
||||
// Warriors never drink; mana-only recovery starts at10% missing as well.
|
||||
p.type=POWER_RAGE;p.mana=0;uses.clear();assert(!tick() && uses.empty());
|
||||
p.type=POWER_MANA;p.mana=91;assert(!tick());p.mana=90;assert(tick() && uses.back()==U::Drink);
|
||||
p.mana=100;assert(!tick());
|
||||
// Persisted day blocks logout/login farming; next day allows a single top-up.
|
||||
mgr._records.clear();uses.clear();previous=stocks;assert(!tick() && stocks==previous);
|
||||
++today;assert(!tick() && stocks==previous+1);assert(!tick() && stocks==previous+1);
|
||||
// Dead or combat-reaction-controlled bots never start a meal.
|
||||
p.hp=40;p.alive=false;uses.clear();assert(!tick() && uses.empty());p.alive=true;
|
||||
GameTime::now+=3;assert(!mgr.Update(&p,500,true) && uses.empty());
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-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 supplies: exact10% boundary, bandage-before-food, native lockout fallback, recovery completion, combat potions, mana-only drinking and daily/post-combat top-ups passed.')
|
||||
@@ -0,0 +1,20 @@
|
||||
"""Validate additive supply catalogs and persistent daily stamps on SQL fixtures."""
|
||||
from pathlib import Path
|
||||
import sqlite3,re
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
def sqlite(sql):
|
||||
sql=re.sub(r' ENGINE=InnoDB[^;]*','',sql)
|
||||
return sql.replace('INSERT IGNORE','INSERT OR IGNORE').replace('REPLACE INTO','INSERT OR REPLACE INTO')
|
||||
db=sqlite3.connect(':memory:')
|
||||
world=(root/'sql/custom/world/2026_10_27_10_adventure_supplies.sql').read_text()
|
||||
characters=(root/'sql/custom/characters/2026_10_27_01_adventure_supplies.sql').read_text()
|
||||
db.executescript(sqlite(world));db.executescript(sqlite(characters))
|
||||
roles={r[0] for r in db.execute('SELECT DISTINCT role FROM vcore_adventure_supply')}
|
||||
assert roles=={'bandage','healing_potion','mana_potion','food','drink'}
|
||||
assert db.execute("SELECT min_level FROM vcore_adventure_supply WHERE role='bandage' AND item_id=21991").fetchone()==(60,)
|
||||
for role in roles:assert db.execute('SELECT COUNT(*) FROM vcore_adventure_supply WHERE role=? AND min_level<=60',(role,)).fetchone()[0]>0
|
||||
db.execute('UPDATE vcore_adventure_supply SET enabled=0 WHERE item_id=22829')
|
||||
db.execute('UPDATE vcore_adventure_supply SET min_level=59 WHERE item_id=21991')
|
||||
db.execute('INSERT INTO vcore_adventure_supply_state VALUES(26,20261027)')
|
||||
before=list(db.iterdump());db.executescript(sqlite(world));db.executescript(sqlite(characters));assert list(db.iterdump())==before
|
||||
print('Supply migrations: all five roles, level60 netherweave tier, editable/disabled catalog preservation and persistent daily stamp passed.')
|
||||
@@ -0,0 +1,122 @@
|
||||
"""Execute production item selection/use and secondary-rank gates."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
source=(root/'src/server/game/World/AdventureSupplyMgr.cpp').read_text()
|
||||
def function(signature):
|
||||
a=source.index(signature);b=source.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(source[e]=='{')-(source[e]=='}');e+=1
|
||||
return source[a:e]
|
||||
header=(root/'src/server/game/World/AdventureSupplyMgr.h').read_text()
|
||||
header='\n'.join(x for x in header.splitlines() if not x.startswith('#include')).replace('private:','public:')
|
||||
code=r'''
|
||||
#include "AdventureSupplyPolicy.h"
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cassert>
|
||||
#include <memory>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
using uint32=unsigned;
|
||||
#define TC_GAME_API
|
||||
#define TC_LOG_INFO(...) ((void)0)
|
||||
constexpr int ITEM_CLASS_CONSUMABLE=0,ITEM_SUBCLASS_BANDAGE=7,ITEM_SUBCLASS_FOOD=5,ITEM_SUBCLASS_POTION=1,EQUIP_ERR_OK=0,UNIT_STATE_CASTING=1,ITEM_SPELLTRIGGER_ON_USE=0,TRIGGERED_NONE=0,TARGET_FLAG_UNIT=2,SPELL_EFFECT_LEARN_SPELL=36,SPELL_EFFECT_SKILL_STEP=44;
|
||||
constexpr unsigned SKILL_FIRST_AID=129,SKILL_COOKING=185,SKILL_FISHING=356,SKILL_ARCHAEOLOGY=794;
|
||||
struct Guid{unsigned id=26;unsigned GetCounter(){return id;}};
|
||||
struct Effect{int Trigger=0,SpellID=0,Effect=0;unsigned TriggerSpell=0;int MiscValue=0,value=0;int CalcValue()const{return value;}};
|
||||
struct ItemTemplate{
|
||||
unsigned id,level,subclass,skill=0,rank=0;std::vector<Effect> Effects;
|
||||
unsigned GetId()const{return id;}unsigned GetRequiredLevel()const{return level;}unsigned GetClass()const{return 0;}unsigned GetSubClass()const{return subclass;}bool IsPotion()const{return subclass==1;}
|
||||
};
|
||||
struct Item{ItemTemplate*proto;unsigned count=20;unsigned GetBagSlot(){return 19;}unsigned GetSlot(){return 0;}Guid GetGUID(){return {proto->id};}};
|
||||
struct SpellInfo{unsigned Id=0,SpellLevel=0,BaseLevel=0;bool combat=true,autoCast=true;std::vector<Effect>Effects;bool CanBeUsedInCombat(){return combat;}};
|
||||
struct Rank{unsigned skill,step,maximum;};
|
||||
struct Player;
|
||||
struct SpellMgr{
|
||||
std::unordered_map<unsigned,SpellInfo>spells;std::unordered_map<unsigned,Rank>ranks;
|
||||
SpellInfo*GetSpellInfo(unsigned id){auto i=spells.find(id);return i==spells.end()?nullptr:&i->second;}
|
||||
Rank*GetSpellLearnSkill(unsigned id){auto i=ranks.find(id);return i==ranks.end()?nullptr:&i->second;}
|
||||
static bool IsSpellValid(SpellInfo*,Player*,bool){return true;}
|
||||
}spellMgr;auto sSpellMgr=&spellMgr;
|
||||
struct ObjectMgr{std::unordered_map<unsigned,ItemTemplate>items;ItemTemplate*GetItemTemplate(unsigned id){auto i=items.find(id);return i==items.end()?nullptr:&i->second;}}objects;auto sObjectMgr=&objects;
|
||||
struct RC{int MinLevel=1;}rc;
|
||||
struct DBC{RC*GetSkillRaceClassInfo(unsigned,unsigned,unsigned){return &rc;}}sDBCManager;
|
||||
struct Field{unsigned value=0;std::string GetString(){return {};};unsigned GetUInt32(){return value;}unsigned GetUInt8(){return value;}unsigned GetUInt16(){return value;}};
|
||||
struct Row{std::vector<std::array<Field,3>> rows;size_t index=0;Field*Fetch(){return rows[index].data();}bool NextRow(){return ++index<rows.size();}};
|
||||
struct Database{bool active=false;Row fixture;Row*Query(char const*){fixture.index=0;return active?&fixture:nullptr;}}WorldDatabase;
|
||||
namespace GameTime{time_t GetGameTime(){return 100;}}
|
||||
struct History{bool cooldown=false,gcd=false;bool HasGlobalCooldown(SpellInfo*){return gcd;}bool IsReady(SpellInfo*,unsigned){return !cooldown;}};
|
||||
namespace WorldPackets{namespace Spells{struct SpellCastRequest{unsigned SpellID=0;struct{unsigned Flags=0;Guid Unit;}Target;};}}
|
||||
struct SpellCastRequestItemData{unsigned bag,slot;Guid guid;SpellCastRequestItemData(unsigned b,unsigned s,Guid g):bag(b),slot(s),guid(g){}};
|
||||
struct PendingSpellCastRequest{WorldPackets::Spells::SpellCastRequest request;SpellCastRequestItemData item;PendingSpellCastRequest(WorldPackets::Spells::SpellCastRequest&&r,SpellCastRequestItemData i):request(r),item(i){}};
|
||||
struct Player{
|
||||
unsigned level=60;bool combat=false,casting=false;History history;int requests=0;std::unordered_map<unsigned,unsigned>skills,caps,steps;std::unordered_set<unsigned>known;
|
||||
std::unordered_map<unsigned,std::unique_ptr<Item>>inventory;
|
||||
Guid GetGUID(){return {};}unsigned getLevel(){return level;}unsigned getRace(){return 1;}unsigned getClass(){return 1;}
|
||||
bool IsSpellFitByClassAndRace(unsigned){return true;}bool HasSpell(unsigned id){return known.count(id);}
|
||||
unsigned GetPureSkillValue(unsigned skill){return skills[skill];}unsigned GetPureMaxSkillValue(unsigned skill){return caps[skill];}unsigned GetSkillStep(unsigned skill){return steps[skill];}
|
||||
void LearnSpell(unsigned id,bool){known.insert(id);auto rank=spellMgr.GetSpellLearnSkill(id);if(rank){caps[rank->skill]=rank->maximum;steps[rank->skill]=rank->step;}
|
||||
else for(auto e:spellMgr.GetSpellInfo(id)->Effects)if(e.Effect==SPELL_EFFECT_SKILL_STEP){caps[e.MiscValue]=e.value*75;steps[e.MiscValue]=e.value;}}
|
||||
void SetSkill(unsigned id,unsigned step,unsigned val,unsigned max){skills[id]=val;caps[id]=max;steps[id]=step;}
|
||||
int CanUseItem(ItemTemplate const*p){return p->level<=level && (!p->skill || skills[p->skill]>=p->rank)?0:1;}
|
||||
int CanUseItem(Item*i){return CanUseItem(i->proto);}
|
||||
Item*GetItemByEntry(unsigned id){auto i=inventory.find(id);return i==inventory.end()?nullptr:i->second.get();}
|
||||
unsigned GetItemCount(unsigned id){auto i=GetItemByEntry(id);return i?i->count:0;}
|
||||
bool IsInCombat(){return combat;}bool IsNonMeleeSpellCast(bool){return casting;}bool HasUnitState(int){return casting;}
|
||||
History*GetSpellHistory(){return &history;}bool CanRequestSpellCast(SpellInfo*){return true;}
|
||||
void RequestSpellCast(std::unique_ptr<PendingSpellCastRequest>r,SpellInfo*){
|
||||
assert(r->request.Target.Flags==TARGET_FLAG_UNIT && r->request.Target.Unit.id==26 && r->item.bag==19 && r->item.slot==0);
|
||||
auto i=GetItemByEntry(r->item.guid.id);assert(i && i->count);++requests;
|
||||
if(--i->count==0)inventory.erase(r->item.guid.id); // last item may disappear immediately
|
||||
}
|
||||
void Add(ItemTemplate*p,unsigned count=20){inventory[p->id]=std::make_unique<Item>(Item{p,count});}
|
||||
};
|
||||
struct Spell{Player*p;SpellInfo*info;Item*m_CastItem=nullptr;Spell(Player*q,SpellInfo*i,int):p(q),info(i){}bool CanAutoCast(Player*q){assert(q==p && m_CastItem);return info->autoCast;}};
|
||||
'''+header+'\n'+function(' bool RoleMatches(')+'\n'+function('ItemTemplate const* AdventureSupplyMgr::Select(')+'\n'+function('AdventureSupplyMgr::UseResult AdventureSupplyMgr::TryUse(')+'\n'+function('void AdventureSupplyMgr::LearnSecondaries(')+r'''
|
||||
int main(){
|
||||
using U=AdventureSupplyPolicy::Use;AdventureSupplyMgr mgr;Player p;auto&r=mgr._records[26];
|
||||
objects.items.emplace(21991,ItemTemplate{21991,0,7,129,325,{{0,100}}});
|
||||
objects.items.emplace(14530,ItemTemplate{14530,0,7,129,225,{{0,101}}});
|
||||
objects.items.emplace(22829,ItemTemplate{22829,55,1,0,0,{{0,102}}});
|
||||
objects.items.emplace(57191,ItemTemplate{57191,80,1,0,0,{{0,103}}});
|
||||
objects.items.emplace(27854,ItemTemplate{27854,55,5,0,0,{{0,104}}});
|
||||
mgr._catalog={{"healing_potion",57191,80},{"bandage",21991,60},{"bandage",14530,45},{"healing_potion",22829,55},{"food",27854,55}};
|
||||
for(unsigned id=100;id<=104;++id)spellMgr.spells[id].Id=id;
|
||||
spellMgr.spells[104].combat=false;
|
||||
assert(mgr.Select(&p,"bandage")->id==21991); // owning future tier never grants skill
|
||||
p.Add(&objects.items[21991]);assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);
|
||||
p.skills[129]=225;assert(mgr.Select(&p,"bandage")->id==14530);
|
||||
p.skills[129]=375;assert(mgr.Select(&p,"bandage")->id==21991);
|
||||
p.history.gcd=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Busy);p.history.gcd=false;
|
||||
p.history.cooldown=true;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable);p.history.cooldown=false;
|
||||
spellMgr.spells[100].autoCast=false;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Unavailable && p.requests==0);spellMgr.spells[100].autoCast=true;
|
||||
p.inventory[21991]->count=1;assert(mgr.TryUse(&p,r,U::Bandage)==AdventureSupplyMgr::UseResult::Started && p.requests==1 && p.GetItemCount(21991)==0);
|
||||
p.Add(&objects.items[27854]);p.combat=true;assert(mgr.TryUse(&p,r,U::Food)==AdventureSupplyMgr::UseResult::Unavailable);
|
||||
assert(mgr.Select(&p,"healing_potion")->id==22829);p.Add(&objects.items[22829]);assert(mgr.TryUse(&p,r,U::HealingPotion)==AdventureSupplyMgr::UseResult::Started && p.GetItemCount(22829)==19);
|
||||
// Native trainer gates, native skill caps and explicit secondary-only ranks.
|
||||
mgr._secondaryRanks.clear();p.skills.clear();p.caps.clear();p.steps.clear();p.known.clear();
|
||||
for(unsigned skill:{129u,185u,356u,794u})for(unsigned step=1;step<=6;++step){unsigned id=skill*10+step;spellMgr.ranks[id]={skill,step,step<=4?step*75:step==5?375u:450u};spellMgr.spells[id].Id=id;mgr._secondaryRanks.push_back({skill,id,step,step==6?65u:step==5?50u:1u,step==1?0u:(step-1)*75-25});}
|
||||
mgr.LearnSecondaries(&p);
|
||||
for(unsigned skill:{129u,185u,356u,794u})assert(p.GetPureSkillValue(skill)==375 && p.GetPureMaxSkillValue(skill)==375 && !p.HasSpell(skill*10+6));
|
||||
auto known=p.known;mgr.LearnSecondaries(&p);assert(p.known==known);
|
||||
|
||||
// Real profession rank representation: trainer wrapper -> learned effect44.
|
||||
Player fresh;AdventureSupplyMgr discovered;
|
||||
SpellInfo wrapper;wrapper.Id=3279;wrapper.Effects={{0,0,36,3273}};spellMgr.spells[3279]=wrapper;
|
||||
SpellInfo apprentice;apprentice.Id=3273;apprentice.Effects={{0,0,44,0,129,1}};spellMgr.spells[3273]=apprentice;
|
||||
SpellInfo primary;primary.Id=2575;primary.Effects={{0,0,44,0,186,1}};spellMgr.spells[2575]=primary;
|
||||
WorldDatabase.active=true;WorldDatabase.fixture.rows={{{{3279},{0},{0}}},{{{2575},{0},{0}}}};
|
||||
discovered.LearnSecondaries(&fresh);
|
||||
assert(fresh.GetPureSkillValue(129)==75 && fresh.GetPureMaxSkillValue(129)==75);
|
||||
assert(discovered._secondaryRanks.size()==1 && !fresh.skills.count(186));
|
||||
assert(!p.skills.count(186) && !p.skills.count(164)); // never invent primary professions
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-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 native-item delegation: usable bandage tiers, native cooldown/lockout gates, real inventory consumption, self targeting and level60 secondary caps passed.')
|
||||
@@ -0,0 +1,34 @@
|
||||
"""Execute the native pure-skill-cap getter; populated skills must not return0."""
|
||||
from pathlib import Path
|
||||
import subprocess,tempfile
|
||||
root=Path(__file__).resolve().parents[2]
|
||||
s=(root/'src/server/game/Entities/Player/Player.cpp').read_text()
|
||||
a=s.index('uint16 Player::GetPureMaxSkillValue(');b=s.index('{',a);e=b+1;depth=1
|
||||
while depth:depth+=(s[e]=='{')-(s[e]=='}');e+=1
|
||||
code=r'''
|
||||
#include <cassert>
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
using uint16=uint16_t;using uint8=uint8_t;using uint32=uint32_t;
|
||||
constexpr int SKILL_DELETED=1,PLAYER_SKILL_MAX_RANK_0=100;
|
||||
struct Status{unsigned pos=0;int uState=0;};using SkillStatusMap=std::unordered_map<unsigned,Status>;
|
||||
struct Player{
|
||||
SkillStatusMap mSkillStatus;unsigned rank=1;uint16 cap=375;
|
||||
unsigned GetSkillRank(unsigned)const{return rank;}
|
||||
uint16 GetUInt16Value(unsigned field,unsigned offset)const{assert(field==101 && offset==1);return cap;}
|
||||
uint16 GetPureMaxSkillValue(uint32)const;
|
||||
};
|
||||
'''+s[a:e]+r'''
|
||||
int main(){
|
||||
Player p;assert(p.GetPureMaxSkillValue(0)==0);assert(p.GetPureMaxSkillValue(129)==0);
|
||||
p.mSkillStatus[129]={3,0};assert(p.GetPureMaxSkillValue(129)==375);
|
||||
p.cap=75;assert(p.GetPureMaxSkillValue(129)==75);
|
||||
p.rank=0;assert(p.GetPureMaxSkillValue(129)==0);p.rank=1;
|
||||
p.mSkillStatus[129].uState=SKILL_DELETED;assert(p.GetPureMaxSkillValue(129)==0);
|
||||
}
|
||||
'''
|
||||
with tempfile.TemporaryDirectory() as d:
|
||||
p=Path(d);(p/'test.cpp').write_text(code)
|
||||
subprocess.run(['g++','-std=c++17',str(p/'test.cpp'),'-o',str(p/'test')],check=True)
|
||||
subprocess.run([str(p/'test')],check=True)
|
||||
print('Native pure skill cap: learned skills return stored cap; missing/deleted/zero-rank skills return0.')
|
||||
Reference in new issue
Block a user