b96242db3b
- quest_validator.sql : 11 regles statiques (lecture seule), perimetre INST/WORLD, filtre LIVE "donneur jouable", calibre (R9 questitem-aware, R7 annotee). - FIX_quest_validator_20260623.sql : Thal'ena 102431 AIName=SmartAI ; Rocknot 9503 + Alurmi 57864 NOT_SELECTABLE retire ; goober 232625 Data1=0. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
248 lines
13 KiB
SQL
248 lines
13 KiB
SQL
-- =====================================================================
|
|
-- quest_validator.sql — Validateur statique de quetes (Couche 1)
|
|
-- Serveur : DestinyCore / SylvaniaCore (Legion 7.3.5, base dc_world)
|
|
-- LECTURE SEULE : ne cree que des TEMPORARY TABLE, aucun write persistant.
|
|
-- Usage : MYSQL_PWD=xxx mysql -u blamacfly -t dc_world < quest_validator.sql
|
|
--
|
|
-- Regle flags effectifs (confirmee Creature.cpp, chemin save l.1189) :
|
|
-- effectif = spawn.<flag> si <>0, sinon creature_template.<flag>
|
|
-- Constantes :
|
|
-- NPC_FLAG_QUESTGIVER = 0x2 = 2
|
|
-- UNIT_FLAG_NOT_SELECTABLE = 0x02000000 = 33554432
|
|
-- gameobject_template.type : 3=CHEST(Data1=lootId) 10=GOOBER(Data1=questID)
|
|
-- quest_objectives.Type : 0=Kill 2=GameObject 3=TalkTo
|
|
-- Limite MySQL geree : une TEMP table ne peut etre lue 2x dans 1 requete
|
|
-- -> copie _cre_b pour les regles qui lisent _cre deux fois.
|
|
-- =====================================================================
|
|
|
|
SET SESSION sql_mode = ''; -- desactive ONLY_FULL_GROUP_BY + strict (truncation -> warning)
|
|
SET SESSION group_concat_max_len = 8192;
|
|
|
|
-- ---------- table de resultats ----------
|
|
DROP TEMPORARY TABLE IF EXISTS _qfind;
|
|
CREATE TEMPORARY TABLE _qfind (
|
|
severity TINYINT, -- 1=CRITIQUE 2=ELEVE 3=MOYEN
|
|
scope VARCHAR(6), -- INST = touche un donjon/raid, sinon WORLD
|
|
rule VARCHAR(32),
|
|
quest_id INT NULL,
|
|
object_id INT NULL,
|
|
detail VARCHAR(255)
|
|
) ENGINE=MEMORY;
|
|
|
|
-- ---------- jeux d'aide ----------
|
|
DROP TEMPORARY TABLE IF EXISTS _cre;
|
|
CREATE TEMPORARY TABLE _cre ENGINE=MEMORY AS
|
|
SELECT c.id AS entry,
|
|
MAX(it.map IS NOT NULL) AS on_inst,
|
|
MAX(((IF(c.npcflag<>0, c.npcflag, t.npcflag)) & 2) <> 0) AS qg,
|
|
MAX(((IF(c.unit_flags<>0,c.unit_flags,t.unit_flags)) & 33554432) = 0) AS selectable
|
|
FROM creature c
|
|
JOIN creature_template t ON t.entry = c.id
|
|
LEFT JOIN instance_template it ON it.map = c.map
|
|
GROUP BY c.id;
|
|
ALTER TABLE _cre ADD PRIMARY KEY(entry);
|
|
|
|
DROP TEMPORARY TABLE IF EXISTS _cre_b; -- copie (limite reopen TEMP)
|
|
CREATE TEMPORARY TABLE _cre_b ENGINE=MEMORY AS SELECT * FROM _cre;
|
|
ALTER TABLE _cre_b ADD PRIMARY KEY(entry);
|
|
|
|
DROP TEMPORARY TABLE IF EXISTS _killcredit; -- entrees atteignables via KillCredit1/2 d'un spawn
|
|
CREATE TEMPORARY TABLE _killcredit ENGINE=MEMORY AS
|
|
SELECT DISTINCT k AS entry FROM (
|
|
SELECT DISTINCT t.KillCredit1 k FROM creature c JOIN creature_template t ON t.entry=c.id WHERE t.KillCredit1<>0
|
|
UNION
|
|
SELECT DISTINCT t.KillCredit2 FROM creature c JOIN creature_template t ON t.entry=c.id WHERE t.KillCredit2<>0
|
|
) z;
|
|
ALTER TABLE _killcredit ADD PRIMARY KEY(entry);
|
|
|
|
DROP TEMPORARY TABLE IF EXISTS _go;
|
|
CREATE TEMPORARY TABLE _go ENGINE=MEMORY AS
|
|
SELECT g.id AS entry, MAX(it.map IS NOT NULL) AS on_inst
|
|
FROM gameobject g LEFT JOIN instance_template it ON it.map = g.map
|
|
GROUP BY g.id;
|
|
ALTER TABLE _go ADD PRIMARY KEY(entry);
|
|
|
|
DROP TEMPORARY TABLE IF EXISTS _qinst; -- quetes touchant une map d'instance
|
|
CREATE TEMPORARY TABLE _qinst ENGINE=MEMORY AS
|
|
SELECT DISTINCT q AS quest FROM (
|
|
SELECT cs.quest q FROM creature_queststarter cs JOIN creature c ON c.id=cs.id JOIN instance_template it ON it.map=c.map
|
|
UNION SELECT ce.quest FROM creature_questender ce JOIN creature c ON c.id=ce.id JOIN instance_template it ON it.map=c.map
|
|
UNION SELECT o.QuestID FROM quest_objectives o JOIN creature c ON c.id=o.ObjectID JOIN instance_template it ON it.map=c.map WHERE o.Type IN (0,3)
|
|
UNION SELECT o.QuestID FROM quest_objectives o JOIN gameobject g ON g.id=o.ObjectID JOIN instance_template it ON it.map=g.map WHERE o.Type=2
|
|
) z;
|
|
ALTER TABLE _qinst ADD PRIMARY KEY(quest);
|
|
|
|
DROP TEMPORARY TABLE IF EXISTS _qdisabled; -- quetes desactivees volontairement (sourceType 1 = QUEST)
|
|
CREATE TEMPORARY TABLE _qdisabled ENGINE=MEMORY AS SELECT DISTINCT entry AS quest FROM disables WHERE sourceType=1;
|
|
ALTER TABLE _qdisabled ADD PRIMARY KEY(quest);
|
|
|
|
-- _qlive : quetes qu'un joueur peut REELLEMENT obtenir (donneur spawne + flag QUESTGIVER + selectionnable).
|
|
-- Filtre cle : une quete a objectif/rendu casse n'est un VRAI bug joueur que si elle est obtenable.
|
|
DROP TEMPORARY TABLE IF EXISTS _qlive;
|
|
CREATE TEMPORARY TABLE _qlive ENGINE=MEMORY AS
|
|
SELECT DISTINCT s.quest AS quest
|
|
FROM creature_queststarter s
|
|
JOIN creature c ON c.id=s.id
|
|
JOIN creature_template t ON t.entry=c.id
|
|
WHERE ((IF(c.npcflag<>0, c.npcflag, t.npcflag)) & 2) <> 0
|
|
AND ((IF(c.unit_flags<>0,c.unit_flags,t.unit_flags)) & 33554432) = 0;
|
|
ALTER TABLE _qlive ADD PRIMARY KEY(quest);
|
|
|
|
-- =====================================================================
|
|
-- REGLES
|
|
-- =====================================================================
|
|
|
|
-- R1 STARTER_NO_SPAWN (sev2) : donneurs presents mais aucun spawne -> indonnable
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(qi.quest IS NULL,'WORLD','INST'), 'STARTER_NO_SPAWN', s.quest, NULL,
|
|
LEFT(CONCAT('donneur(s) jamais spawne(s): ', GROUP_CONCAT(DISTINCT s.id)),255)
|
|
FROM creature_queststarter s
|
|
LEFT JOIN _qinst qi ON qi.quest=s.quest
|
|
WHERE s.quest NOT IN (SELECT quest FROM _qdisabled)
|
|
AND NOT EXISTS (SELECT 1 FROM creature_queststarter cs JOIN creature c ON c.id=cs.id WHERE cs.quest=s.quest)
|
|
GROUP BY s.quest, qi.quest;
|
|
|
|
-- R2 ENDER_NO_SPAWN (sev1) : rendeurs presents mais aucun spawne -> inrendable
|
|
INSERT INTO _qfind
|
|
SELECT 1, IF(qi.quest IS NULL,'WORLD','INST'), 'ENDER_NO_SPAWN', e.quest, NULL,
|
|
LEFT(CONCAT('rendeur(s) jamais spawne(s): ', GROUP_CONCAT(DISTINCT e.id)),255)
|
|
FROM creature_questender e
|
|
LEFT JOIN _qinst qi ON qi.quest=e.quest
|
|
WHERE e.quest NOT IN (SELECT quest FROM _qdisabled)
|
|
AND NOT EXISTS (SELECT 1 FROM creature_questender ce JOIN creature c ON c.id=ce.id WHERE ce.quest=e.quest)
|
|
GROUP BY e.quest, qi.quest;
|
|
|
|
-- R3 STARTER_NO_QGFLAG (sev2) : donneur spawne mais aucun spawn flag QUESTGIVER
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(qi.quest IS NULL,'WORLD','INST'), 'STARTER_NO_QGFLAG', s.quest, NULL,
|
|
LEFT(CONCAT('donneur spawne sans flag QUESTGIVER: ', GROUP_CONCAT(DISTINCT s.id)),255)
|
|
FROM creature_queststarter s
|
|
LEFT JOIN _qinst qi ON qi.quest=s.quest
|
|
WHERE s.quest NOT IN (SELECT quest FROM _qdisabled)
|
|
AND EXISTS (SELECT 1 FROM creature_queststarter c1 JOIN _cre x ON x.entry=c1.id WHERE c1.quest=s.quest)
|
|
AND NOT EXISTS (SELECT 1 FROM creature_queststarter c2 JOIN _cre_b x2 ON x2.entry=c2.id WHERE c2.quest=s.quest AND x2.qg=1)
|
|
GROUP BY s.quest, qi.quest;
|
|
|
|
-- R4 ENDER_NO_QGFLAG (sev1) : rendeur spawne mais aucun spawn flag QUESTGIVER
|
|
INSERT INTO _qfind
|
|
SELECT 1, IF(qi.quest IS NULL,'WORLD','INST'), 'ENDER_NO_QGFLAG', e.quest, NULL,
|
|
LEFT(CONCAT('rendeur spawne sans flag QUESTGIVER: ', GROUP_CONCAT(DISTINCT e.id)),255)
|
|
FROM creature_questender e
|
|
LEFT JOIN _qinst qi ON qi.quest=e.quest
|
|
WHERE e.quest NOT IN (SELECT quest FROM _qdisabled)
|
|
AND EXISTS (SELECT 1 FROM creature_questender c1 JOIN _cre x ON x.entry=c1.id WHERE c1.quest=e.quest)
|
|
AND NOT EXISTS (SELECT 1 FROM creature_questender c2 JOIN _cre_b x2 ON x2.entry=c2.id WHERE c2.quest=e.quest AND x2.qg=1)
|
|
GROUP BY e.quest, qi.quest;
|
|
|
|
-- R5 GIVER_NOT_SELECTABLE (sev1) : donneur/rendeur dont aucun spawn n'est selectionnable (cas A'zak)
|
|
INSERT INTO _qfind
|
|
SELECT 1, IF(qi.quest IS NULL,'WORLD','INST'), 'GIVER_NOT_SELECTABLE', g.quest, NULL,
|
|
LEFT(CONCAT('PNJ donneur/rendeur non selectionnable: ', GROUP_CONCAT(DISTINCT g.id)),255)
|
|
FROM (SELECT quest,id FROM creature_queststarter UNION SELECT quest,id FROM creature_questender) g
|
|
JOIN _cre x ON x.entry=g.id AND x.selectable=0
|
|
LEFT JOIN _qinst qi ON qi.quest=g.quest
|
|
WHERE g.quest NOT IN (SELECT quest FROM _qdisabled)
|
|
AND NOT EXISTS (SELECT 1
|
|
FROM (SELECT quest,id FROM creature_queststarter UNION SELECT quest,id FROM creature_questender) g2
|
|
JOIN _cre_b x2 ON x2.entry=g2.id
|
|
WHERE g2.quest=g.quest AND x2.selectable=1)
|
|
GROUP BY g.quest, qi.quest;
|
|
|
|
-- R6 OBJ_KILL_UNSPAWNED (sev2) : objectif kill/talk cible ni spawnee ni credit de mise a mort
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(qi.quest IS NULL,'WORLD','INST'), 'OBJ_KILL_UNSPAWNED', o.QuestID, o.ObjectID,
|
|
LEFT(CONCAT('objectif Type', o.Type, ' cible creature ', o.ObjectID, ' jamais spawnee (verifier summon scripte)'),255)
|
|
FROM quest_objectives o
|
|
LEFT JOIN _qinst qi ON qi.quest=o.QuestID
|
|
WHERE o.Type IN (0,3) AND o.ObjectID>0
|
|
AND o.QuestID NOT IN (SELECT quest FROM _qdisabled)
|
|
AND NOT EXISTS (SELECT 1 FROM _cre x WHERE x.entry=o.ObjectID)
|
|
AND NOT EXISTS (SELECT 1 FROM _killcredit k WHERE k.entry=o.ObjectID);
|
|
|
|
-- R7 OBJ_GO_UNSPAWNED (sev2) : objectif sur objet de jeu jamais spawne
|
|
-- ⚠️ CALIBRE 23/06 : beaucoup d'objectifs GO Legion sont spawnes par scenario/phasing/script, PAS en statique
|
|
-- (meme la TDB 7.3.5 propre ne les a pas dans `gameobject`). Donc un hit ici = candidat ; le FIX est souvent
|
|
-- de la reconstruction de contenu scenarise (coords introuvables en reference), pas une simple copie de spawn.
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(qi.quest IS NULL,'WORLD','INST'), 'OBJ_GO_UNSPAWNED', o.QuestID, o.ObjectID,
|
|
LEFT(CONCAT('objectif GameObject ', o.ObjectID, ' jamais spawne'),255)
|
|
FROM quest_objectives o
|
|
LEFT JOIN _qinst qi ON qi.quest=o.QuestID
|
|
WHERE o.Type=2 AND o.ObjectID>0
|
|
AND o.QuestID NOT IN (SELECT quest FROM _qdisabled)
|
|
AND NOT EXISTS (SELECT 1 FROM _go x WHERE x.entry=o.ObjectID);
|
|
|
|
-- R8 GOOBER_QUESTID_MISMATCH (sev2) : goober objectif dont Data1(questID) != la quete (cas oeufs Azjol)
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(qi.quest IS NULL,'WORLD','INST'), 'GOOBER_QUESTID_MISMATCH', o.QuestID, o.ObjectID,
|
|
LEFT(CONCAT('goober ', o.ObjectID, ' Data1=', gt.Data1, ' != quete ', o.QuestID, ' -> Use() break avant credit'),255)
|
|
FROM quest_objectives o
|
|
JOIN gameobject_template gt ON gt.entry=o.ObjectID AND gt.type=10
|
|
LEFT JOIN _qinst qi ON qi.quest=o.QuestID
|
|
WHERE o.Type=2 AND o.QuestID NOT IN (SELECT quest FROM _qdisabled)
|
|
AND gt.Data1<>0 AND gt.Data1<>o.QuestID;
|
|
|
|
-- R9 CHEST_EMPTY_LOOT (sev2) : coffre OBJECTIF de quete, SANS gameobject_questitem, au loot vide (cas lames SM, WotLK)
|
|
-- ⚠️ CALIBRE 23/06 : en Legion l'objet de quete est souvent credite a l'ouverture via gameobject_questitem
|
|
-- (butin personnel) -> loot_template vide = NORMAL, pas un bug. On EXCLUT donc les coffres a questitem
|
|
-- (sinon 100% faux-positifs, ex: 25 coffres 237xxx INST). Reste seulement le vrai cas "doit looter mais loot vide".
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(g.on_inst=1,'INST','WORLD'), 'CHEST_EMPTY_LOOT', NULL, gt.entry,
|
|
LEFT(CONCAT('coffre ', gt.entry, ' Data1(lootId)=', gt.Data1,
|
|
IF(gt.Data1=0,' (aucun loot)',' (gameobject_loot_template vide)')),255)
|
|
FROM gameobject_template gt
|
|
JOIN _go g ON g.entry=gt.entry
|
|
WHERE gt.type=3
|
|
AND EXISTS (SELECT 1 FROM quest_objectives o WHERE o.Type=2 AND o.ObjectID=gt.entry)
|
|
AND NOT EXISTS (SELECT 1 FROM gameobject_questitem q WHERE q.GameObjectEntry=gt.entry)
|
|
AND ( gt.Data1=0 OR NOT EXISTS (SELECT 1 FROM gameobject_loot_template lt WHERE lt.Entry=gt.Data1) );
|
|
|
|
-- R10 SMARTAI_NOT_WIRED_CRE (sev2) : smart_scripts presents mais AIName != 'SmartAI' -> script mort
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(EXISTS(SELECT 1 FROM _cre x WHERE x.entry=ss.entryorguid AND x.on_inst=1),'INST','WORLD'),
|
|
'SMARTAI_NOT_WIRED_CRE', NULL, ss.entryorguid,
|
|
LEFT(CONCAT('creature ', ss.entryorguid, ' a ', COUNT(*), ' smart_scripts mais AIName="', COALESCE(t.AIName,''), '"'),255)
|
|
FROM smart_scripts ss
|
|
JOIN creature_template t ON t.entry=ss.entryorguid
|
|
WHERE ss.source_type=0 AND ss.entryorguid>0 AND COALESCE(t.AIName,'')<>'SmartAI'
|
|
GROUP BY ss.entryorguid, t.AIName;
|
|
|
|
-- R11 SMARTAI_NOT_WIRED_GO (sev2) : idem objets de jeu (AIName != 'SmartGameObjectAI')
|
|
INSERT INTO _qfind
|
|
SELECT 2, IF(EXISTS(SELECT 1 FROM _go x WHERE x.entry=ss.entryorguid AND x.on_inst=1),'INST','WORLD'),
|
|
'SMARTAI_NOT_WIRED_GO', NULL, ss.entryorguid,
|
|
LEFT(CONCAT('gameobject ', ss.entryorguid, ' a ', COUNT(*), ' smart_scripts mais AIName="', COALESCE(gt.AIName,''), '"'),255)
|
|
FROM smart_scripts ss
|
|
JOIN gameobject_template gt ON gt.entry=ss.entryorguid
|
|
WHERE ss.source_type=1 AND ss.entryorguid>0 AND COALESCE(gt.AIName,'')<>'SmartGameObjectAI'
|
|
GROUP BY ss.entryorguid, gt.AIName;
|
|
|
|
-- =====================================================================
|
|
-- RAPPORTS
|
|
-- =====================================================================
|
|
SELECT '===== RESUME PAR REGLE =====' AS '';
|
|
SELECT scope, severity AS sev, rule, COUNT(*) AS n
|
|
FROM _qfind GROUP BY scope, severity, rule
|
|
ORDER BY scope DESC, severity, n DESC;
|
|
|
|
SELECT '===== TOTAL PAR PERIMETRE =====' AS '';
|
|
SELECT scope, COUNT(*) AS anomalies, COUNT(DISTINCT quest_id) AS quetes_distinctes
|
|
FROM _qfind GROUP BY scope;
|
|
|
|
SELECT '===== DETAIL DONJON/RAID (INST) — backlog prioritaire =====' AS '';
|
|
SELECT severity AS sev, rule, quest_id, object_id, detail
|
|
FROM _qfind WHERE scope='INST'
|
|
ORDER BY severity, rule, quest_id;
|
|
|
|
-- LIVE & CASSE : findings sur des quetes qu'un joueur peut REELLEMENT obtenir = vrais pieges a corriger en priorite.
|
|
SELECT '===== LIVE & CASSE (donneur jouable) — backlog VRAIMENT actionnable =====' AS '';
|
|
SELECT f.severity AS sev, f.scope, f.rule, f.quest_id, f.object_id, f.detail
|
|
FROM _qfind f JOIN _qlive l ON l.quest=f.quest_id
|
|
ORDER BY f.scope DESC, f.severity, f.rule, f.quest_id;
|
|
|
|
SELECT '===== LIVE & CASSE : compte par regle/perimetre =====' AS '';
|
|
SELECT f.scope, f.severity AS sev, f.rule, COUNT(*) AS n
|
|
FROM _qfind f JOIN _qlive l ON l.quest=f.quest_id
|
|
GROUP BY f.scope, f.severity, f.rule
|
|
ORDER BY f.scope DESC, f.severity, n DESC;
|