from pathlib import Path import re,json,csv,hashlib,collections B=Path(__file__).parent;R=B/'research';O=B/'resources/atlas';O.mkdir(parents=True,exist_ok=True) def dump(name,data): (O/name).write_text(json.dumps(data,ensure_ascii=False,separators=(',',':')),encoding='utf8') def rows(name):return list(csv.DictReader((R/name).open(encoding='utf-8-sig',newline=''))) def unquote(v): if v=='NULL':return None if not v.startswith("'"):return float(v) if '.' in v else int(v) return re.sub(r"\\(.)",lambda m:{'n':'\n','r':'\r','t':'\t','0':'','Z':''}.get(m[1],m[1]),v[1:-1]) SQL=(R/'ClassicDB.sql').read_text(encoding='utf8') VALUE=r"'(?:\\.|[^'\\])*'|NULL|-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?" TUPLE=r"\((?:'(?:\\.|[^'\\])*'|[^'()])*\)" def table(name): start=SQL.index('CREATE TABLE `'+name+'`');end=SQL.index('ENGINE=',start) cols=re.findall(r'^ `([^`]+)`',SQL[start:end],re.M) out=[] for m in re.finditer(r'INSERT INTO `'+name+r'` VALUES ',SQL): # The next line is the end of this dump statement. Semicolons inside text are legal. end=SQL.find('\n',m.end());block=SQL[m.end():end if end>=0 else None] for t in re.finditer(TUPLE,block): values=[unquote(x[0]) for x in re.finditer(VALUE,t[0])] assert len(values)==len(cols),(name,len(values),len(cols)) out.append(dict(zip(cols,values))) return out areas={int(x['ID']):x['AreaName_lang'] for x in rows('AreaTable-frFR.csv')} fr={} for line in (R/'French_Quest.sql').read_text(encoding='utf8').splitlines(): m=re.search(r'WHERE `entry`\s*=\s*(\d+)',line,re.I) if not m:continue fr[int(m[1])]={k:unquote(v) for k,v in re.findall(r'`(\w+)`\s*=\s*(\'(?:\\.|[^\'\\])*\'|NULL)',line)} print('French quests',len(fr),flush=True) clientids={int(x['ID']) for x in rows('QuestV2-frFR.csv')} quests=table('quest_template');print('Classic quests',len(quests),flush=True) creatures={x['Entry']:x for x in table('creature_template')}; print('Creature fields',list(next(iter(creatures.values())).keys())[:10],flush=True) starts=collections.defaultdict(list);ends=collections.defaultdict(list) for name,target in [('creature_questrelation',starts),('creature_involvedrelation',ends)]: for x in table(name): c=creatures.get(x['id'],{});target[x['quest']].append({'id':x['id'],'name':c.get('Name',str(x['id']))}) result=[];translations={} for q in quests: id=q['entry'] if id not in clientids or q['QuestLevel']>60:continue loc=fr.get(id,{}) if not loc.get('title_loc2'):continue zoneid=q['ZoneOrSort'];races=q['RequiredRaces'];faction='Alliance' if races and not(races&178) else 'Horde' if races and not(races&77) else 'Toutes' result.append({'id':id,'name':q['Title'],'level':q['QuestLevel'],'min_level':q['MinLevel'],'zone_id':zoneid,'zone':areas.get(zoneid,'Autres quêtes' if zoneid<=0 else 'Zone '+str(zoneid)),'faction':faction,'details':q['Details'] or '', 'objectives':q['Objectives'] or '', 'previous':q['PrevQuestId'],'next':q['NextQuestInChain'] or q['NextQuestId'],'start':starts[id],'end':ends[id],'money':q['RewOrReqMoney'],'choice_items':[{'id':q['RewChoiceItemId'+str(i)],'count':q['RewChoiceItemCount'+str(i)]} for i in range(1,7) if q['RewChoiceItemId'+str(i)]],'reward_items':[{'id':q['RewItemId'+str(i)],'count':q['RewItemCount'+str(i)]} for i in range(1,5) if q['RewItemId'+str(i)]],'players':q['SuggestedPlayers']}) translations[str(id)]={'name':loc['title_loc2'],'details':loc.get('details_loc2') or '', 'objectives':loc.get('objectives_loc2') or ''} for kind,file in [('creatures','French_Creature.sql'),('items','French_Items.sql')]: names={} for line in (R/file).read_text(encoding='utf8').splitlines(): m=re.search(r'WHERE `entry`\s*=\s*(\d+)',line,re.I);v=re.search(r"`name_loc2`='((?:\\.|[^'\\])*)'",line) if m and v:names[m[1]]=unquote("'"+v[1]+"'") dump(kind+'-fr.json',{'license':'CC-BY-NC-SA-3.0','source':'MaNGOS / MOTE / UDBFR et contributeurs','names':names}) meta={'version':'Classic 1.12.1','status':'classic_reference','forever_build':'1.60.1.70630','forever_verified':False,'source':'https://github.com/cmangos/classic-db','commit':json.loads((R/'cmangos-commit.json').read_text())['sha'],'license':'GPL-3.0','coverage':'Quêtes Classic dont l’identifiant existe dans le client Forever ; règles et récompenses non vérifiées dans Forever.'} dump('quests-classic.json',{'source':meta,'quests':result}) dump('quests-fr.json',{'source':{'url':'https://github.com/mangoszero/database','commit':json.loads((R/'mangos-commit.json').read_text())['sha'],'license':'CC-BY-NC-SA-3.0','authors':'MaNGOS / MOTE / UDBFR et contributeurs','modifications':'Extraction des titres, descriptions et objectifs français, sélection par identifiant.'},'translations':translations}) # Official French labels, separate from immutable gameplay snapshots. namesEN={int(x['ID']):x['Name_lang'] for x in rows('SpellName-enUS.csv')};namesFR={int(x['ID']):x['Name_lang'] for x in rows('SpellName-frFR.csv')} byname=collections.defaultdict(list) for id,en in namesEN.items(): if namesFR.get(id):byname[en].append((id,namesFR[id])) mapnames={'zones':{x['ID']:x['Name_lang'] for x in rows('UiMap-frFR.csv')},'entrances':{x['ID']:x['MapName_lang'] for x in rows('Map-frFR.csv')},'flightNodes':{x['ID']:x['Name_lang'] for x in rows('TaxiNodes-frFR.csv')}} dump('labels-fr.json',{'build':'1.60.1.70630','spells':{str(k):v for k,v in namesFR.items()},'by_name':{k:v[0][1] for k,v in byname.items()},'map':mapnames,'races':{'Human':'Humain','Dwarf':'Nain','Night Elf':'Elfe de la nuit','Gnome':'Gnome','Orc':'Orc','Troll':'Troll','Tauren':'Tauren','Undead':'Mort-vivant','High Elf':'Haut-elfe','Blood Elf':'Elfe de sang','Skyborne':'Céleste'}}) print('Exported',len(result),'quests',len(translations),'translations',flush=True)