diff --git a/PyPoE/cli/exporter/wiki/parsers/alternatePassives.py b/PyPoE/cli/exporter/wiki/parsers/alternatePassives.py
new file mode 100644
index 00000000..be0c9eaa
--- /dev/null
+++ b/PyPoE/cli/exporter/wiki/parsers/alternatePassives.py
@@ -0,0 +1,372 @@
+"""
+Overview
+===============================================================================
+
++----------+------------------------------------------------------------------+
+| Path | PyPoE/cli/exporter/wiki/parsers/passives.py |
++----------+------------------------------------------------------------------+
+| Version | 1.0.0a0 |
++----------+------------------------------------------------------------------+
+| Revision | $Id$ |
++----------+------------------------------------------------------------------+
+| Author | Omega_K2 |
++----------+------------------------------------------------------------------+
+
+Description
+===============================================================================
+
+
+
+Agreement
+===============================================================================
+
+See PyPoE/LICENSE
+
+Documentation
+===============================================================================
+
+Public API
+-------------------------------------------------------------------------------
+
+Internal API
+-------------------------------------------------------------------------------
+"""
+
+# =============================================================================
+# Imports
+# =============================================================================
+
+# Python
+import re
+import os.path
+import warnings
+from functools import partialmethod
+from collections import OrderedDict
+
+# 3rd-party
+
+# self
+from PyPoE.cli.core import console, Msg
+from PyPoE.cli.exporter.wiki import parser
+from PyPoE.cli.exporter.wiki.handler import ExporterHandler, ExporterResult
+from PyPoE.poe.file.psg import PSGFile
+
+# =============================================================================
+# Globals
+# =============================================================================
+
+__all__ = []
+
+
+# =============================================================================
+# Classes
+# =============================================================================
+
+
+class WikiCondition(parser.WikiCondition):
+ COPY_KEYS = (
+ 'main_page',
+ )
+
+ NAME = 'Passive skill'
+ ADD_INCLUDE = False
+ INDENT = 36
+
+
+class AlternatePassiveSkillCommandHandler(ExporterHandler):
+ def __init__(self, sub_parser):
+ self.parser = sub_parser.add_parser(
+ 'alt-passive',
+ help='Passive skill exporter',
+ )
+ self.parser.set_defaults(func=lambda args: self.parser.print_help())
+
+ self.add_default_subparser_filters(
+ sub_parser=self.parser.add_subparsers(),
+ cls=AlternatePassiveSkillParser,
+ )
+
+ # filtering
+ '''a_filter = sub.add_parser(
+ 'filter',
+ help='Extract passives using filters.'
+ )
+ self.add_default_parsers(
+ parser=a_filter,
+ cls=PassiveSkillParser,
+ func=PassiveSkillParser.by_filter,
+ )
+
+ a_filter.add_argument(
+ '-ft-id', '--filter-id', '--filter-metadata-id',
+ help='Regular expression on the id',
+ type=str,
+ dest='re_id',
+ )'''
+
+ def add_default_parsers(self, *args, **kwargs):
+ super().add_default_parsers(*args, **kwargs)
+ self.add_format_argument(kwargs['parser'])
+ self.add_image_arguments(kwargs['parser'])
+ kwargs['parser'].add_argument(
+ '-ft-id', '--filter-id', '--filter-metadata-id',
+ help='Regular expression on the id',
+ type=str,
+ dest='re_id',
+ )
+
+
+class AlternatePassiveSkillParser(parser.BaseParser):
+ _files = [
+ 'AlternatePassiveSkills.dat',
+ ]
+
+ _passive_column_index_filter = partialmethod(
+ parser.BaseParser._column_index_filter,
+ dat_file_name='AlternatePassiveSkills.dat',
+ error_msg='Several passives have not been found:\n%s',
+ )
+
+ _MAX_STAT_ID = 4
+
+ _COPY_KEYS = OrderedDict((
+ ('Id', {
+ 'template': 'id',
+ }),
+ ('Name', {
+ 'template': 'name',
+ }),
+ ('FlavourText', {
+ 'template': 'flavour_text',
+ 'default': '',
+ }),
+ ('AlternateTreeVersionsKey', {
+ 'template': 'jewel_type',
+ 'format': lambda value: [x for x in value][0],
+ 'condition': lambda passive: passive['AlternateTreeVersionsKey']
+ }),
+ ))
+
+ def _apply_filter(self, parsed_args, passives):
+ if parsed_args.re_id:
+ parsed_args.re_id = re.compile(parsed_args.re_id, flags=re.UNICODE)
+ else:
+ return passives
+
+ new = []
+
+ for passive in passives:
+ if parsed_args.re_id and not \
+ parsed_args.re_id.match(passive['Id']):
+ continue
+
+ new.append(passive)
+
+ return new
+
+ def by_rowid(self, parsed_args):
+ return self.export(
+ parsed_args,
+ self.rr['AlternatePassiveSkills.dat'][parsed_args.start:parsed_args.end],
+ )
+
+ def by_id(self, parsed_args):
+ return self.export(parsed_args, self._passive_column_index_filter(
+ column_id='Id', arg_list=parsed_args.id
+ ))
+
+ def by_name(self, parsed_args):
+ return self.export(parsed_args, self._passive_column_index_filter(
+ column_id='Name', arg_list=parsed_args.name
+ ))
+
+ def export(self, parsed_args, passives):
+ r = ExporterResult()
+
+ passives = self._apply_filter(parsed_args, passives)
+
+ console(f'Found {len(passives)} passives. Removing Royale passives...')
+ passives = [
+ passive for passive in passives
+ if not passive['Id'].startswith('royale')
+ ]
+ console(f'{len(passives)} passives left for processing.')
+
+ if not passives:
+ console(
+ 'No passives found for the specified parameters. Quitting.',
+ msg=Msg.warning,
+ )
+ return r
+
+ console('Accessing additional data...')
+
+ psg = PSGFile()
+ psg.read(
+ file_path_or_raw=self.file_system.get_file(
+ 'Metadata/PassiveSkillGraph.psg'
+ ),
+ )
+
+ node_index = {}
+ for group in psg.groups:
+ for node in group.nodes:
+ node_index[node.passive_skill] = node
+ # Connections are one-way, make them two way
+ for psg_id, node in node_index.items():
+ for other_psg_id in node.connections:
+ node_index[other_psg_id].connections.append(psg_id)
+
+ self._image_init(parsed_args)
+
+ console('Found %s, parsing...' % len(passives))
+
+ for passive in passives:
+ data = OrderedDict()
+
+ # Copy over simple fields from the .dat
+ for row_key, copy_data in self._COPY_KEYS.items():
+ value = passive[row_key]
+
+ condition = copy_data.get('condition')
+ if condition is not None and not condition(passive):
+ continue
+
+ # Skip default values to reduce size of template
+ if value == copy_data.get('default'):
+ continue
+
+ fmt = copy_data.get('format')
+ if fmt:
+ value = fmt(value)
+ data[copy_data['template']] = value
+
+ # Flag if it's an atlas skill
+ if passive['Id'].startswith('atlas'):
+ data['is_atlas_passive'] = True
+
+ for i in range(len(passive['PassiveType'])):
+ if passive['PassiveType'][i] == 4:
+ data['is_keystone'] = True
+ elif passive['PassiveType'][i] == 3:
+ data['is_notable'] = True
+
+ data['int_id'] = 0
+
+ # Handle icon paths
+ if passive['Icon_DDSFile']:
+ icon = passive['Icon_DDSFile'].split('/')
+ if passive['Icon_DDSFile'].startswith(
+ 'Art/2DArt/SkillIcons/passives/'):
+ if icon[-2] == 'passives':
+ data['icon'] = icon[-1]
+ else:
+ data['icon'] = '%s (%s)' % (icon[-1], icon[-2])
+ else:
+ data['icon'] = icon[-1]
+ # atlas_start_node doesn't have an icon path
+ else:
+ data['icon'] = ''
+ warnings.warn(f"Icon path file not found for {passive['Id']}: {passive['Name']}")
+
+ data['icon'] = data['icon'].replace('.dds', '')
+
+ # Handle Stats
+ stat_ids = []
+ values = []
+
+ j = 0
+ for i in range(0, self._MAX_STAT_ID):
+ try:
+ stat = passive['StatsKeys'][i]
+ except IndexError:
+ break
+ j = i + 1
+ stat_ids.append(stat['Id'])
+ data['stat%s_id' % j] = stat['Id']
+ values.append(passive['Stat%sValue' % j])
+ data['stat%s_value' % j] = passive['Stat%sValue' % j]
+
+ data['stat_text'] = '
'.join(self._get_stats(
+ stat_ids, values,
+ translation_file=get_translation_file(passive['Id'])
+ ))
+
+ # For now this is being added to the stat text
+ # for ps_buff in passive['PassiveSkillBuffsKeys']:
+ # stat_ids = [stat['Id'] for stat in
+ # ps_buff['BuffDefinitionsKey']['StatsKeys']]
+ # values = ps_buff['Buff_StatValues']
+ # # if passive['Id'] == 'AscendancyChampion7':
+ # # index = stat_ids.index('damage_taken_+%_from_hits')
+ # # del stat_ids[index]
+ # # del values[index]
+ # for i, (sid, val) in enumerate(zip(stat_ids, values)):
+ # j += 1
+ # data['stat%s_id' % j] = sid
+ # data['stat%s_value' % j] = val
+ #
+ # text = '
'.join(self._get_stats(
+ # stat_ids, values,
+ # translation_file='passive_skill_aura_stat_descriptions.txt'
+ # ))
+ #
+ # if data['stat_text']:
+ # data['stat_text'] += '
' + text
+ # else:
+ # data['stat_text'] = text
+
+ # node = node_index.get(passive['PassiveSkillGraphId'])
+ # if node and node.connections:
+ # data['connections'] = ','.join([
+ # self.rr['AlternatePassiveSkills.dat'].index['PassiveSkillGraphId'][
+ # psg_id]['Id'] for psg_id in node.connections])
+
+ # extract icons if specified
+ if parsed_args.store_images and data['icon'] != '':
+ fn = data['icon'] + ' passive skill icon'
+ dds = os.path.join(self._img_path, fn + '.dds')
+ png = os.path.join(self._img_path, fn + '.png')
+ if not (os.path.exists(dds) or os.path.exists(png)):
+ self._write_dds(
+ data=self.file_system.get_file(passive['Icon_DDSFile']),
+ out_path=dds,
+ parsed_args=parsed_args,
+ )
+
+ cond = WikiCondition(
+ data=data,
+ cmdargs=parsed_args,
+ )
+
+ r.add_result(
+ text=cond,
+ out_file='passive_skill_%s.txt' % data['id'],
+ wiki_page=[
+ {
+ 'page': 'Passive Skill:' + self._format_wiki_title(data['id']),
+ 'condition': cond,
+ },
+ ],
+ wiki_message='Passive skill updater',
+ )
+
+ return r
+
+
+# =============================================================================
+# Functions
+# =============================================================================
+
+def get_translation_file(passive_id: str):
+ '''
+ Determines which translation file should be used based on the passive skill ID.
+
+ Parameters
+ ----------
+ passive_id: the Id of the passive skill
+ '''
+ if passive_id.startswith('atlas'):
+ return 'atlas_stat_descriptions.txt'
+ else:
+ return 'passive_skill_stat_descriptions.txt'
diff --git a/PyPoE/poe/file/specification/data/stable.py b/PyPoE/poe/file/specification/data/stable.py
index e0235a44..5d105540 100644
--- a/PyPoE/poe/file/specification/data/stable.py
+++ b/PyPoE/poe/file/specification/data/stable.py
@@ -856,19 +856,19 @@
key='Stats.dat',
),
Field(
- name='Stat1Min',
+ name='Stat1Value',
type='int',
),
Field(
- name='Stat1Max',
+ name='Stat2Value',
type='int',
),
Field(
- name='Stat2Min',
+ name='Stat3Value',
type='int',
),
Field(
- name='Stat2Max',
+ name='Stat4Value',
type='int',
),
Field(
@@ -924,7 +924,7 @@
type='ref|string',
),
Field(
- name='DDSIcon',
+ name='Icon_DDSFile',
type='ref|string',
file_path=True,
file_ext='.dds',