Rivage brise : placements des cartes 1460 et 1666, generateur unifie

NON APPLIQUES au serveur. Verses pour relecture.

DECOUVERTE QUI RECADRE LE CHANTIER
Il y a DEUX Rivages brises, et je preparais le second en premier :
  carte 1460 -- le lancement de Legion (Voljin, Varian, la defaite),
               c est le contenu de la video fournie ;
  carte 1666 -- l assaut de la 7.2 (Kalgorath, Arganoth, Mephistroth).

Pour la 1460, NOTRE SCRIPT EXISTE DEJA : scenario_broken_shore_intro.cpp,
464 lignes, correctement rattache -- instance_template reclame
scenario_broken_shore_intro et c est exactement le nom sous lequel il
s enregistre (RegisterInstanceScript, map 1460). Son scenario est aussi
deja declare (786). Il ne lui manquait QUE les placements. Aucun portage
C++ n est necessaire pour cette carte.

VERIFICATION CROISEE faite avant de generer : sur les 17 entrees que le
script attend, 10 sont placees dans la reference (Voljin, Sylvanas,
Baine, Thrall, Varian, Jaina, Mekkatorque, Genn, Krosus, Arganoth) et
les 7 autres sont INVOQUEES par le script lui-meme (Khadgar l.335,
ancres l.370-371, Tirion l.380, Guldan l.398, troupes via TroopEntry,
credit de fin l.431). La repartition est donc coherente.

CONTENU
  carte 1460 : 757 creatures, 101 objets, 84 chemins (526 points)
  carte 1666 : 593 creatures, 59 objets, 180 chemins (1495 points)
  Toutes les creatures mobiles ont leur chemin, aucune orpheline.

BUG RATTRAPE A LA VALIDATION
Une premiere version de l outil, obtenue en substituant mecaniquement le
numero de carte, produisait pour la 1460 un fichier dont les DELETE
visaient encore la 1666 : il aurait vide la mauvaise carte et laisse
l autre intacte. Le generateur est reecrit, le numero de carte ne vit
plus qu a un seul endroit, et la difficulte est desormais DEDUITE du
spawnMask (verifiee puissance de 2, uniforme) au lieu d etre codee.

VALIDATION en base jetable pour les deux fichiers : import reussi,
comptages exacts, zero creature hors carte, zero mauvaise difficulte,
zero mobile sans chemin, zero collision de GUID avec la base reelle.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
SylvaniaCore deploy
2026-08-27 23:50:14 +02:00
parent 1b1eb863e2
commit c8f1d570e1
4 changed files with 1635 additions and 229 deletions
@@ -1,215 +0,0 @@
# -*- coding: utf-8 -*-
"""Genere le SQL de placement de la carte 1666 dans NOTRE schema.
N'applique rien : ecrit un fichier.
La correspondance est pilotee par les NOMS de colonnes, pas par leur
position : si les schemas bougent, le generateur ne produira pas de
decalage silencieux -- il s'arretera.
"""
import io, re, sys
from collections import Counter
DUMP = '/home/ubuntu/tmp/abs/LegionCore_world_2020_04_25.sql'
SORTIE = '/home/ubuntu/DestinyCore/sql/sylvania/rivage_brise_placements.sql'
GUID_CREA = 290100000
GUID_GO = 210200000
NOS_CREA = ['guid','id','map','zoneId','areaId','spawnDifficulties','phaseUseFlags','PhaseId',
'PhaseGroup','terrainSwapMap','modelid','equipment_id','position_x','position_y',
'position_z','orientation','spawntimesecs','spawndist','currentwaypoint','curhealth',
'curmana','MovementType','npcflag','unit_flags','unit_flags2','unit_flags3',
'dynamicflags','ScriptName','movementmode','VerifiedBuild']
NOS_GO = ['guid','id','map','zoneId','areaId','spawnDifficulties','phaseUseFlags','PhaseId',
'PhaseGroup','terrainSwapMap','position_x','position_y','position_z','orientation',
'rotation0','rotation1','rotation2','rotation3','spawntimesecs','animprogress','state',
'isActive','ScriptName','VerifiedBuild']
NOS_WP = ['id','point','position_x','position_y','position_z','orientation','delay',
'move_type','action','action_chance','wpguid']
# valeurs imposees, quelle que soit la reference
FIXES = {
'spawnDifficulties': "'12'", # 4096 = 2^12, difficulte « Normal Scenario »
'phaseUseFlags': '0',
'PhaseGroup': '0',
'terrainSwapMap': '-1',
'unit_flags2': '0',
'ScriptName': "''",
'movementmode': '0',
'VerifiedBuild': '0',
'wpguid': '0',
}
# nos colonnes qui portent un autre nom dans la reference
ALIAS = {'move_type': 'move_flag'}
def decoupe_tuples(bloc):
res, cur, prof, chaine, echap = [], [], 0, False, False
for c in bloc:
if chaine:
cur.append(c)
if echap: echap = False
elif c == '\\': echap = True
elif c == "'": chaine = False
continue
if c == "'":
chaine = True; cur.append(c)
elif c == '(':
prof += 1
cur = [] if prof == 1 else cur + [c]
elif c == ')':
prof -= 1
if prof == 0: res.append(''.join(cur))
else: cur.append(c)
elif prof > 0:
cur.append(c)
return res
def champs(t):
vals, cur, chaine, echap = [], [], False, False
for c in t:
if chaine:
cur.append(c)
if echap: echap = False
elif c == '\\': echap = True
elif c == "'": chaine = False
continue
if c == "'":
chaine = True; cur.append(c)
elif c == ',':
vals.append(''.join(cur).strip()); cur = []
else:
cur.append(c)
vals.append(''.join(cur).strip())
return vals
def lire(tables):
"""Renvoie {table: (colonnes, [tuples decoupes])} pour tout le dump."""
motif = re.compile(r"insert\s+into\s+`(%s)`\(([^)]*)\)\s+values\s*" % '|'.join(tables), re.I)
out = {t: {'cols': None, 'rows': []} for t in tables}
with io.open(DUMP, encoding='utf-8', errors='replace') as f:
ligne = f.readline()
while ligne:
m = motif.match(ligne.lstrip())
if not m:
ligne = f.readline(); continue
table = m.group(1).lower()
cols = [c.strip().strip('`') for c in m.group(2).split(',')]
morceaux = [ligne[ligne.lower().find('values') + 6:]]
while not morceaux[-1].rstrip().endswith(';'):
s = f.readline()
if not s: break
morceaux.append(s)
out[table]['cols'] = cols
for t in decoupe_tuples(''.join(morceaux)):
v = champs(t)
if len(v) == len(cols):
out[table]['rows'].append(v)
ligne = f.readline()
return out
def valeur(nom, cols, v, guid_neuf):
if nom == 'guid':
return str(guid_neuf)
if nom in FIXES:
return FIXES[nom]
src = ALIAS.get(nom, nom)
if src not in cols:
raise KeyError("colonne '%s' absente de la reference" % src)
brut = v[cols.index(src)]
if nom in ('PhaseId', 'PhaseGroup'): # chaine cote reference, entier chez nous
b = brut.strip().strip("'")
return b if b else '0'
return brut
print('lecture du dump...')
d = lire(['creature', 'gameobject', 'waypoint_data'])
crea = [v for v in d['creature']['rows'] if v[d['creature']['cols'].index('map')] == '1666']
go = [v for v in d['gameobject']['rows'] if v[d['gameobject']['cols'].index('map')] == '1666']
print('creatures 1666 : %d | objets 1666 : %d' % (len(crea), len(go)))
assert crea and go, 'extraction vide -- refuser de generer'
# renumerotation, en gardant la trace ancien -> nouveau
i_guid_c = d['creature']['cols'].index('guid')
map_guid = {}
for n, v in enumerate(crea):
map_guid[v[i_guid_c]] = GUID_CREA + n
# chemins des creatures mobiles
i_mt = d['creature']['cols'].index('MovementType')
mobiles = {v[i_guid_c] for v in crea if v[i_mt] == '2'}
wp_cols = d['waypoint_data']['cols']
i_wid = wp_cols.index('id')
wp = [v for v in d['waypoint_data']['rows'] if v[i_wid] in mobiles]
chemins = len({v[i_wid] for v in wp})
print('creatures mobiles : %d | chemins trouves : %d | points : %d' % (len(mobiles), chemins, len(wp)))
lignes = []
A = lignes.append
A("-- =====================================================================")
A("-- Assaut du Rivage brise (carte 1666) -- PLACEMENTS")
A("--")
A("-- Transpose depuis le dump world de dufernst/LegionCore-7.3.5, meme")
A("-- lignee uwow que notre fork. Les deux schemas divergent : la")
A("-- correspondance ci-dessous est pilotee par les NOMS de colonnes, et le")
A("-- generateur s'arrete plutot que de produire un decalage silencieux.")
A("--")
A("-- DECISIONS, etablies sur donnees et non supposees :")
A("-- spawnMask 4096 = 2^12 -> spawnDifficulties '12', la difficulte 12")
A("-- etant « Normal Scenario » d'apres Difficulty.db2 du build")
A("-- 7.3.5.26972 (type instance 5, 5 joueurs).")
A("-- GUID renumerotes a partir de %d (creatures) et %d (objets)," % (GUID_CREA, GUID_GO))
A("-- au-dessus de nos maxima 290000114 et 210120986.")
A("-- PhaseId : chaine vide cote reference -> 0 chez nous (entier).")
A("--")
A("-- ABANDONNE faute d'equivalent : npcflag2, AiID, MovementID, MeleeID,")
A("-- isActive (creature), skipClone, personal_size, isTeemingSpawn.")
A("-- 12 creatures portaient AiID=7424, une table d'IA propre au coeur de")
A("-- reference : leur comportement est PERDU et reste a refaire.")
A("--")
A("-- Contenu : %d creatures, %d objets, %d chemins (%d points)." % (len(crea), len(go), chemins, len(wp)))
A("-- =====================================================================")
A("")
A("DELETE FROM `creature` WHERE `map`=1666;")
A("DELETE FROM `gameobject` WHERE `map`=1666;")
A("DELETE FROM `waypoint_data` WHERE `id` BETWEEN %d AND %d;" % (GUID_CREA, GUID_CREA + len(crea)))
A("")
A("INSERT INTO `creature` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_CREA))
bouts = []
for n, v in enumerate(crea):
bouts.append('(' + ','.join(valeur(c, d['creature']['cols'], v, GUID_CREA + n) for c in NOS_CREA) + ')')
A(',\n'.join(bouts) + ';')
A("")
A("INSERT INTO `gameobject` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_GO))
bouts = []
for n, v in enumerate(go):
bouts.append('(' + ','.join(valeur(c, d['gameobject']['cols'], v, GUID_GO + n) for c in NOS_GO) + ')')
A(',\n'.join(bouts) + ';')
A("")
if wp:
A("INSERT INTO `waypoint_data` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_WP))
bouts = []
for v in wp:
anc = v[i_wid]
ligne_vals = []
for c in NOS_WP:
if c == 'id':
ligne_vals.append(str(map_guid[anc]))
else:
ligne_vals.append(valeur(c, wp_cols, v, 0))
bouts.append('(' + ','.join(ligne_vals) + ')')
A(',\n'.join(bouts) + ';')
io.open(SORTIE, 'w', encoding='utf-8').write('\n'.join(lignes) + '\n')
print('ecrit : %s' % SORTIE)
@@ -0,0 +1,210 @@
# -*- coding: utf-8 -*-
"""Genere le SQL de placement d'une carte depuis le dump de reference,
transpose dans NOTRE schema.
python3 generer_placements.py <carte> <guid_creature> <guid_objet>
La correspondance est pilotee par les NOMS de colonnes : si un schema
bouge, le generateur s'arrete au lieu de produire un decalage silencieux.
ATTENTION -- une version precedente de cet outil, obtenue en substituant
mecaniquement le numero de carte, produisait un fichier dont les DELETE
visaient encore l'ancienne carte. Le fichier aurait vide la mauvaise et
laisse l'autre intacte. Ici le numero de carte ne vit qu'a UN endroit,
la variable CARTE, et sert partout.
"""
import io, re, sys
if len(sys.argv) != 4:
sys.exit(__doc__)
CARTE = sys.argv[1]
GUID_CREA = int(sys.argv[2])
GUID_GO = int(sys.argv[3])
DUMP = '/home/ubuntu/tmp/abs/LegionCore_world_2020_04_25.sql'
SORTIE = '/home/ubuntu/DestinyCore/sql/sylvania/rivage_brise_placements_%s.sql' % CARTE
NOS_CREA = ['guid','id','map','zoneId','areaId','spawnDifficulties','phaseUseFlags','PhaseId',
'PhaseGroup','terrainSwapMap','modelid','equipment_id','position_x','position_y',
'position_z','orientation','spawntimesecs','spawndist','currentwaypoint','curhealth',
'curmana','MovementType','npcflag','unit_flags','unit_flags2','unit_flags3',
'dynamicflags','ScriptName','movementmode','VerifiedBuild']
NOS_GO = ['guid','id','map','zoneId','areaId','spawnDifficulties','phaseUseFlags','PhaseId',
'PhaseGroup','terrainSwapMap','position_x','position_y','position_z','orientation',
'rotation0','rotation1','rotation2','rotation3','spawntimesecs','animprogress','state',
'isActive','ScriptName','VerifiedBuild']
NOS_WP = ['id','point','position_x','position_y','position_z','orientation','delay',
'move_type','action','action_chance','wpguid']
FIXES = {'phaseUseFlags':'0', 'PhaseGroup':'0', 'terrainSwapMap':'-1', 'unit_flags2':'0',
'ScriptName':"''", 'movementmode':'0', 'VerifiedBuild':'0', 'wpguid':'0'}
ALIAS = {'move_type': 'move_flag'}
def decoupe_tuples(bloc):
res, cur, prof, chaine, echap = [], [], 0, False, False
for c in bloc:
if chaine:
cur.append(c)
if echap: echap = False
elif c == '\\': echap = True
elif c == "'": chaine = False
continue
if c == "'":
chaine = True; cur.append(c)
elif c == '(':
prof += 1
cur = [] if prof == 1 else cur + [c]
elif c == ')':
prof -= 1
if prof == 0: res.append(''.join(cur))
else: cur.append(c)
elif prof > 0:
cur.append(c)
return res
def champs(t):
vals, cur, chaine, echap = [], [], False, False
for c in t:
if chaine:
cur.append(c)
if echap: echap = False
elif c == '\\': echap = True
elif c == "'": chaine = False
continue
if c == "'":
chaine = True; cur.append(c)
elif c == ',':
vals.append(''.join(cur).strip()); cur = []
else:
cur.append(c)
vals.append(''.join(cur).strip())
return vals
def lire(tables):
motif = re.compile(r"insert\s+into\s+`(%s)`\(([^)]*)\)\s+values\s*" % '|'.join(tables), re.I)
out = {t: {'cols': None, 'rows': []} for t in tables}
with io.open(DUMP, encoding='utf-8', errors='replace') as f:
ligne = f.readline()
while ligne:
m = motif.match(ligne.lstrip())
if not m:
ligne = f.readline(); continue
table = m.group(1).lower()
cols = [c.strip().strip('`') for c in m.group(2).split(',')]
morceaux = [ligne[ligne.lower().find('values') + 6:]]
while not morceaux[-1].rstrip().endswith(';'):
s = f.readline()
if not s: break
morceaux.append(s)
out[table]['cols'] = cols
for t in decoupe_tuples(''.join(morceaux)):
v = champs(t)
if len(v) == len(cols):
out[table]['rows'].append(v)
ligne = f.readline()
return out
def valeur(nom, cols, v, guid_neuf, difficulte):
if nom == 'guid':
return str(guid_neuf)
if nom == 'spawnDifficulties':
return "'%s'" % difficulte
if nom in FIXES:
return FIXES[nom]
src = ALIAS.get(nom, nom)
if src not in cols:
raise KeyError("colonne '%s' absente de la reference" % src)
brut = v[cols.index(src)]
if nom in ('PhaseId', 'PhaseGroup'):
b = brut.strip().strip("'")
return b if b else '0'
return brut
print('lecture du dump (carte %s)...' % CARTE)
d = lire(['creature', 'gameobject', 'waypoint_data'])
cc, cg = d['creature']['cols'], d['gameobject']['cols']
crea = [v for v in d['creature']['rows'] if v[cc.index('map')] == CARTE]
go = [v for v in d['gameobject']['rows'] if v[cg.index('map')] == CARTE]
if not crea:
sys.exit('extraction vide pour la carte %s -- on ne genere rien' % CARTE)
# la difficulte se DEDUIT du spawnMask, elle n'est pas supposee
masques = {v[cc.index('spawnMask')] for v in crea}
if len(masques) != 1:
sys.exit('spawnMask non uniforme (%s) -- transposition a decider a la main' % masques)
masque = int(masques.pop())
if masque & (masque - 1):
sys.exit('spawnMask %d n est pas une puissance de 2 -- a decider a la main' % masque)
DIFFICULTE = masque.bit_length() - 1
print('spawnMask %d -> difficulte %d' % (masque, DIFFICULTE))
i_guid = cc.index('guid')
map_guid = {v[i_guid]: GUID_CREA + n for n, v in enumerate(crea)}
mobiles = {v[i_guid] for v in crea if v[cc.index('MovementType')] == '2'}
wpc = d['waypoint_data']['cols']
wp = [v for v in d['waypoint_data']['rows'] if v[wpc.index('id')] in mobiles]
chemins = len({v[wpc.index('id')] for v in wp})
print('creatures %d | objets %d | mobiles %d | chemins %d | points %d'
% (len(crea), len(go), len(mobiles), chemins, len(wp)))
if chemins != len(mobiles):
print(' ATTENTION : %d creatures mobiles sans chemin' % (len(mobiles) - chemins))
L = []
L.append("-- =====================================================================")
L.append("-- Rivage brise -- PLACEMENTS de la carte %s" % CARTE)
L.append("--")
L.append("-- Transpose depuis le dump world de dufernst/LegionCore-7.3.5, meme")
L.append("-- lignee uwow que notre fork. Les schemas divergent : la")
L.append("-- correspondance est pilotee par les NOMS de colonnes, et le")
L.append("-- generateur s'arrete plutot que de produire un decalage silencieux.")
L.append("--")
L.append("-- DIFFICULTE : deduite, pas supposee. Les %d placements portent tous" % len(crea))
L.append("-- spawnMask %d = 2^%d, d'ou spawnDifficulties '%d'." % (masque, DIFFICULTE, DIFFICULTE))
L.append("--")
L.append("-- GUID renumerotes a partir de %d (creatures) et %d (objets)," % (GUID_CREA, GUID_GO))
L.append("-- au-dessus de nos maxima connus 290000114 et 210120986.")
L.append("-- PhaseId : chaine vide cote reference -> 0 chez nous (colonne entiere).")
L.append("--")
L.append("-- ABANDONNE faute d'equivalent chez nous : npcflag2, AiID, MovementID,")
L.append("-- MeleeID, isActive (creature), skipClone, personal_size, isTeemingSpawn.")
L.append("--")
L.append("-- Contenu : %d creatures, %d objets, %d chemins (%d points)." % (len(crea), len(go), chemins, len(wp)))
L.append("-- =====================================================================")
L.append("")
L.append("DELETE FROM `creature` WHERE `map`=%s;" % CARTE)
L.append("DELETE FROM `gameobject` WHERE `map`=%s;" % CARTE)
L.append("DELETE FROM `waypoint_data` WHERE `id` BETWEEN %d AND %d;" % (GUID_CREA, GUID_CREA + len(crea)))
L.append("")
L.append("INSERT INTO `creature` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_CREA))
L.append(',\n'.join('(' + ','.join(valeur(c, cc, v, GUID_CREA + n, DIFFICULTE) for c in NOS_CREA) + ')'
for n, v in enumerate(crea)) + ';')
L.append("")
if go:
L.append("INSERT INTO `gameobject` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_GO))
L.append(',\n'.join('(' + ','.join(valeur(c, cg, v, GUID_GO + n, DIFFICULTE) for c in NOS_GO) + ')'
for n, v in enumerate(go)) + ';')
L.append("")
if wp:
L.append("INSERT INTO `waypoint_data` (%s) VALUES" % ','.join('`%s`' % c for c in NOS_WP))
bouts = []
for v in wp:
vals = [str(map_guid[v[wpc.index('id')]]) if c == 'id'
else valeur(c, wpc, v, 0, DIFFICULTE) for c in NOS_WP]
bouts.append('(' + ','.join(vals) + ')')
L.append(',\n'.join(bouts) + ';')
io.open(SORTIE, 'w', encoding='utf-8').write('\n'.join(L) + '\n')
print('ecrit : %s' % SORTIE)
File diff suppressed because it is too large Load Diff
@@ -1,23 +1,20 @@
-- =====================================================================
-- Assaut du Rivage brise (carte 1666) -- PLACEMENTS
-- Rivage brise -- PLACEMENTS de la carte 1666
--
-- Transpose depuis le dump world de dufernst/LegionCore-7.3.5, meme
-- lignee uwow que notre fork. Les deux schemas divergent : la
-- correspondance ci-dessous est pilotee par les NOMS de colonnes, et le
-- lignee uwow que notre fork. Les schemas divergent : la
-- correspondance est pilotee par les NOMS de colonnes, et le
-- generateur s'arrete plutot que de produire un decalage silencieux.
--
-- DECISIONS, etablies sur donnees et non supposees :
-- spawnMask 4096 = 2^12 -> spawnDifficulties '12', la difficulte 12
-- etant « Normal Scenario » d'apres Difficulty.db2 du build
-- 7.3.5.26972 (type instance 5, 5 joueurs).
-- GUID renumerotes a partir de 290100000 (creatures) et 210200000 (objets),
-- au-dessus de nos maxima 290000114 et 210120986.
-- PhaseId : chaine vide cote reference -> 0 chez nous (entier).
-- DIFFICULTE : deduite, pas supposee. Les 593 placements portent tous
-- spawnMask 4096 = 2^12, d'ou spawnDifficulties '12'.
--
-- ABANDONNE faute d'equivalent : npcflag2, AiID, MovementID, MeleeID,
-- isActive (creature), skipClone, personal_size, isTeemingSpawn.
-- 12 creatures portaient AiID=7424, une table d'IA propre au coeur de
-- reference : leur comportement est PERDU et reste a refaire.
-- GUID renumerotes a partir de 290100000 (creatures) et 210200000 (objets),
-- au-dessus de nos maxima connus 290000114 et 210120986.
-- PhaseId : chaine vide cote reference -> 0 chez nous (colonne entiere).
--
-- ABANDONNE faute d'equivalent chez nous : npcflag2, AiID, MovementID,
-- MeleeID, isActive (creature), skipClone, personal_size, isTeemingSpawn.
--
-- Contenu : 593 creatures, 59 objets, 180 chemins (1495 points).
-- =====================================================================