Beispiel #1
0
 def precreate_checks(name, code):
     if len(code) > SNIPPET_SIZE_LIMIT:
         raise InvalidArgument(f"Snippets must be shorter than {SNIPPET_SIZE_LIMIT} characters.")
     if len(name) < 2:
         raise InvalidArgument("Snippet names must be at least 2 characters long.")
     if ' ' in name:
         raise InvalidArgument("Snippet names cannot contain spaces.")
Beispiel #2
0
    def run(self, autoctx):
        super(IEffect, self).run(autoctx)
        if isinstance(self.duration, str):
            try:
                duration = int(
                    autoctx.parse_annostr(self.duration,
                                          is_full_expression=True))
            except ValueError:
                raise InvalidArgument(
                    f"{self.duration} is not an integer (in effect duration)")
        else:
            duration = self.duration

        duration = autoctx.args.last('dur', duration, int)
        if isinstance(autoctx.target.target, init.Combatant):
            effect = init.Effect.new(autoctx.target.target.combat,
                                     autoctx.target.target,
                                     self.name,
                                     duration,
                                     autoctx.parse_annostr(self.effects),
                                     tick_on_end=self.tick_on_end,
                                     concentration=self.concentration)
            if autoctx.conc_effect:
                if autoctx.conc_effect.combatant is autoctx.target.target and self.concentration:
                    raise InvalidArgument(
                        "Concentration spells cannot add concentration effects to the caster."
                    )
                effect.set_parent(autoctx.conc_effect)
            effect_result = autoctx.target.target.add_effect(effect)
            autoctx.queue(f"**Effect**: {str(effect)}")
            if conc_conflict := effect_result['conc_conflict']:
                autoctx.queue(
                    f"**Concentration**: dropped {', '.join([e.name for e in conc_conflict])}"
                )
Beispiel #3
0
    async def replace(self, expr):
        """Replaces expressions that start with c: or s: with dice expressions for the character's check/save."""
        skill_match = INLINE_SKILL_RE.match(expr)
        if skill_match is None:
            return expr, None

        character = await self._get_character()
        check_search = skill_match.group(2).lower()
        if skill_match.group(1) == 'c':
            skill_key = next((c for c in constants.SKILL_NAMES
                              if c.lower().startswith(check_search)), None)
            if skill_key is None:
                raise InvalidArgument(
                    f"`{check_search}` is not a valid skill.")
            skill = character.skills[skill_key]
            skill_name = f"{camel_to_title(skill_key)} Check"
            check_dice = skill.d20(
                reroll=character.options.reroll,
                min_val=10 *
                bool(character.options.talent and skill.prof >= 1))
        else:
            try:
                skill = character.saves.get(check_search)
                skill_name = f"{verbose_stat(check_search[:3]).title()} Save"
            except ValueError:
                raise InvalidArgument(f"`{check_search}` is not a valid save.")
            check_dice = skill.d20(reroll=character.options.reroll)

        rest_of_expr = expr[skill_match.end():]
        return f"{check_dice}{rest_of_expr}", skill_name
Beispiel #4
0
async def update_gvar(ctx, gid, value):
    value = str(value)
    gvar = await ctx.bot.mdb.gvars.find_one({"key": gid})
    if gvar is None:
        raise InvalidArgument("Global variable not found.")
    elif gvar['owner'] != str(ctx.author.id) and not str(ctx.author.id) in gvar.get('editors', []):
        raise NotAllowed("You are not allowed to edit this variable.")
    elif len(value) > GVAR_SIZE_LIMIT:
        raise InvalidArgument(f"Gvars must be shorter than {GVAR_SIZE_LIMIT} characters.")
    await ctx.bot.mdb.gvars.update_one({"key": gid}, {"$set": {"value": value}})
Beispiel #5
0
def set_cvar(character, name, value):
    value = str(value)
    if not name.isidentifier():
        raise InvalidArgument("Cvar names must be identifiers "
                              "(only contain a-z, A-Z, 0-9, _, and not start with a number).")
    elif name in character.get_scope_locals(True):
        raise InvalidArgument(f"The variable `{name}` is already built in.")
    elif len(value) > CVAR_SIZE_LIMIT:
        raise InvalidArgument(f"Cvars must be shorter than {CVAR_SIZE_LIMIT} characters.")

    character.set_cvar(name, value)
Beispiel #6
0
async def set_uvar(ctx, name, value):
    value = str(value)
    if not name.isidentifier():
        raise InvalidArgument("Uvar names must be valid identifiers "
                              "(only contain a-z, A-Z, 0-9, _, and not start with a number).")
    elif len(value) > UVAR_SIZE_LIMIT:
        raise InvalidArgument(f"Uvars must be shorter than {UVAR_SIZE_LIMIT} characters.")
    await ctx.bot.mdb.uvars.update_one(
        {"owner": str(ctx.author.id), "name": name},
        {"$set": {"value": value}},
        True)
Beispiel #7
0
async def create_servsnippet(ctx, snipname, snippet):
    snippet = str(snippet)
    if len(snippet) > SNIPPET_SIZE_LIMIT:
        raise InvalidArgument(f"Snippets must be shorter than {SNIPPET_SIZE_LIMIT} characters.")
    elif len(snipname) < 2:
        raise InvalidArgument("Snippet names must be at least 2 characters long.")
    elif ' ' in snipname:
        raise InvalidArgument("Snippet names cannot contain spaces.")

    await ctx.bot.mdb.servsnippets.update_one({"server": str(ctx.guild.id), "name": snipname},
                                              {"$set": {"snippet": snippet}}, True)
Beispiel #8
0
    async def select_details(self, ctx):
        author = ctx.author
        channel = ctx.channel

        def chk(m):
            return m.author == author and m.channel == channel

        await ctx.send(author.mention + " What race?")
        try:
            race_response = await self.bot.wait_for('message',
                                                    timeout=90,
                                                    check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for race.")
        race = await search_and_select(ctx, c.fancyraces,
                                       race_response.content, lambda e: e.name)

        await ctx.send(author.mention + " What class?")
        try:
            class_response = await self.bot.wait_for('message',
                                                     timeout=90,
                                                     check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for class.")
        _class = await search_and_select(ctx, c.classes,
                                         class_response.content,
                                         lambda e: e['name'])

        if 'subclasses' in _class:
            await ctx.send(author.mention + " What subclass?")
            try:
                subclass_response = await self.bot.wait_for('message',
                                                            timeout=90,
                                                            check=chk)
            except asyncio.TimeoutError:
                raise InvalidArgument("Timed out waiting for subclass.")
            subclass = await search_and_select(ctx, _class['subclasses'],
                                               subclass_response.content,
                                               lambda e: e['name'])
        else:
            subclass = None

        await ctx.send(author.mention + " What background?")
        try:
            bg_response = await self.bot.wait_for('message',
                                                  timeout=90,
                                                  check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for background.")
        background = await search_and_select(ctx, c.backgrounds,
                                             bg_response.content,
                                             lambda e: e.name)
        return race, _class, subclass, background
Beispiel #9
0
async def set_svar(ctx, name, value):
    if ctx.guild is None:
        raise NotAllowed("You cannot set a svar in a private message.")
    value = str(value)
    if not name.isidentifier():
        raise InvalidArgument("Svar names must be valid identifiers "
                              "(only contain a-z, A-Z, 0-9, _, and not start with a number).")
    elif len(value) > SVAR_SIZE_LIMIT:
        raise InvalidArgument(f"Svars must be shorter than {SVAR_SIZE_LIMIT} characters.")
    await ctx.bot.mdb.svars.update_one(
        {"owner": ctx.guild.id, "name": name},
        {"$set": {"value": value}},
        True)
Beispiel #10
0
 def add_known_spell(self, spell, dc: int = None, sab: int = None, mod: int = None):
     """Adds a spell to the character's known spell list."""
     if spell.name in self.spellbook:
         raise InvalidArgument("You already know this spell.")
     sbs = SpellbookSpell.from_spell(spell, dc, sab, mod)
     self.spellbook.spells.append(sbs)
     self.overrides.spells.append(sbs)
Beispiel #11
0
    def run(self, autoctx):
        super(IEffect, self).run(autoctx)
        if isinstance(self.duration, str):
            try:
                duration = autoctx.parse_intexpression(self.duration)
            except Exception:
                raise AutomationException(f"{self.duration} is not an integer (in effect duration)")
        else:
            duration = self.duration

        if self.desc:
            desc = autoctx.parse_annostr(self.desc)
            if len(desc) > 500:
                desc = f"{desc[:500]}..."
        else:
            desc = None

        duration = autoctx.args.last('dur', duration, int)
        conc_conflict = []
        if isinstance(autoctx.target.target, init.Combatant):
            effect = init.Effect.new(autoctx.target.target.combat, autoctx.target.target, self.name,
                                     duration, autoctx.parse_annostr(self.effects), tick_on_end=self.tick_on_end,
                                     concentration=self.concentration, desc=desc)
            if autoctx.conc_effect:
                if autoctx.conc_effect.combatant is autoctx.target.target and self.concentration:
                    raise InvalidArgument("Concentration spells cannot add concentration effects to the caster.")
                effect.set_parent(autoctx.conc_effect)
            effect_result = autoctx.target.target.add_effect(effect)
            autoctx.queue(f"**Effect**: {effect.get_str(description=False)}")
            if conc_conflict := effect_result['conc_conflict']:
                autoctx.queue(f"**Concentration**: dropped {', '.join([e.name for e in conc_conflict])}")
Beispiel #12
0
    def new(cls, combat, combatant, name, duration, effect_args, concentration: bool = False, character=None,
            tick_on_end=False, desc: str = None):
        if isinstance(effect_args, str):
            if (combatant and combatant.type == CombatantType.PLAYER) or character:
                effect_args = argparse(effect_args, combatant.character or character)
            else:
                effect_args = argparse(effect_args)

        effect_dict = {}
        for arg in effect_args:
            arg_arg = None
            if arg in LIST_ARGS:
                arg_arg = effect_args.get(arg, [])
            elif arg in VALID_ARGS:
                arg_arg = effect_args.last(arg)

            if arg in SPECIAL_ARGS:
                effect_dict[arg] = SPECIAL_ARGS[arg][0](arg_arg, name)
            elif arg_arg is not None:
                effect_dict[arg] = arg_arg

        try:
            duration = int(duration)
        except (ValueError, TypeError):
            raise InvalidArgument("Effect duration must be an integer.")

        id = create_effect_id()

        return cls(combat, combatant, id, name, duration, duration, effect_dict, concentration=concentration,
                   tonend=tick_on_end, desc=desc)
Beispiel #13
0
async def definitely_combat(combat, args, allow_groups=True):
    target_args = args.get('t')
    targets = []

    for i, t in enumerate(target_args):
        contextargs = None
        if '|' in t:
            t, contextargs = t.split('|', 1)
            contextargs = argparse(contextargs)

        try:
            target = await combat.select_combatant(t,
                                                   f"Select target #{i + 1}.",
                                                   select_group=allow_groups)
        except SelectionException:
            raise InvalidArgument(f"Target {t} not found.")

        if isinstance(target, CombatantGroup):
            for combatant in target.get_combatants():
                if contextargs:
                    args.add_context(combatant, contextargs)
                targets.append(combatant)
        else:
            if contextargs:
                args.add_context(target, contextargs)
            targets.append(target)

    return targets
Beispiel #14
0
def run_save(save_key, caster, args, embed):
    """
    Runs a caster's saving throw, building on an existing embed and handling most arguments.

    Also handles save bonuses from ieffects if caster is a combatant.

    :type save_key: str
    :type caster: cogs5e.models.sheet.statblock.StatBlock
    :type args: utils.argparser.ParsedArguments
    :type embed: discord.Embed
    :return: The total of each save.
    :rtype: list of int
    """
    try:
        save = caster.saves.get(save_key)
        save_name = f"{verbose_stat(save_key[:3]).title()} Save"
    except ValueError:
        raise InvalidArgument('That\'s not a valid save.')

    # -title
    if args.last('title'):
        embed.title = args.last('title', '') \
            .replace('[name]', caster.get_title_name()) \
            .replace('[sname]', save_name)
    elif args.last('h'):
        embed.title = f"An unknown creature makes {a_or_an(save_name)}!"
    else:
        embed.title = f'{caster.get_title_name()} makes {a_or_an(save_name)}!'

    # ieffect -sb
    if isinstance(caster, init.Combatant):
        args['b'] = args.get('b') + caster.active_effects('sb')

    return _run_common(save, args, embed, rr_format="Save {}")
Beispiel #15
0
    def run(self, autoctx):
        super(IEffect, self).run(autoctx)
        if isinstance(self.duration, str):
            try:
                self.duration = int(autoctx.parse_annostr(self.duration))
            except ValueError:
                raise InvalidArgument(
                    f"{self.duration} is not an integer (in effect duration)")

        duration = autoctx.args.last('dur', self.duration, int)
        if isinstance(autoctx.target.target, Combatant):
            effect = initiative.Effect.new(autoctx.target.target.combat,
                                           autoctx.target.target,
                                           self.name,
                                           duration,
                                           autoctx.parse_annostr(self.effects),
                                           tick_on_end=self.tick_on_end)
            if autoctx.conc_effect:
                effect.set_parent(autoctx.conc_effect)
            autoctx.target.target.add_effect(effect)
        else:
            effect = initiative.Effect.new(None,
                                           None,
                                           self.name,
                                           duration,
                                           autoctx.parse_annostr(self.effects),
                                           tick_on_end=self.tick_on_end)
        autoctx.queue(f"**Effect**: {str(effect)}")
Beispiel #16
0
def parse_stat_choice(args, _):
    for i, arg in enumerate(args):
        if arg == 'True':  # hack: sadv/sdis on their own should be equivalent to -sadv/sdis all
            args[i] = arg = 'all'
        if arg not in STAT_ABBREVIATIONS and arg != 'all':
            raise InvalidArgument(f"{arg} is not a valid stat")
    return args
 def set_cvar(self, name, val: str):
     """Sets a cvar to a string value."""
     if any(c in name for c in '/()[]\\.^$*+?|{}'):
         raise InvalidArgument("Cvar contains invalid character.")
     self.character['cvars'] = self.character.get('cvars', {})  # set value
     self.character['cvars'][name] = str(val)
     return self
Beispiel #18
0
 def set_cvar(self, name: str, val: str):
     """Sets a cvar to a string value."""
     if not name.isidentifier():
         raise InvalidArgument(
             "Cvar name must be a valid identifier "
             "(contains only a-z, A-Z, 0-9, and _, and not start with a number)."
         )
     self.cvars[name] = str(val)
Beispiel #19
0
async def set_uvar(ctx, name, value):
    if not name.isidentifier():
        raise InvalidArgument("Uvar names must be valid identifiers "
                              "(only contain a-z, A-Z, 0-9, _, and not start with a number).")
    await ctx.bot.mdb.uvars.update_one(
        {"owner": str(ctx.author.id), "name": name},
        {"$set": {"value": value}},
        True)
Beispiel #20
0
 def add_known_spell(self, spell):
     """Adds a spell to the character's known spell list.
     :param spell (Spell) - the Spell.
     :returns self"""
     if spell.name in self.spellbook:
         raise InvalidArgument("You already know this spell.")
     sbs = SpellbookSpell.from_spell(spell)
     self.spellbook.spells.append(sbs)
     self.overrides.spells.append(sbs)
Beispiel #21
0
    def run(self, autoctx):
        super(Roll, self).run(autoctx)
        d = autoctx.args.join('d', '+')
        maxdmg = autoctx.args.last('max', None, bool)

        # add on combatant damage effects (#224)
        if autoctx.combatant:
            effect_d = '+'.join(autoctx.combatant.active_effects('d'))
            if effect_d:
                if d:
                    d = f"{d}+{effect_d}"
                else:
                    d = effect_d

        dice = self.dice
        if self.cantripScale:

            def cantrip_scale(matchobj):
                level = autoctx.caster.spellcasting.casterLevel
                if level < 5:
                    levelDice = "1"
                elif level < 11:
                    levelDice = "2"
                elif level < 17:
                    levelDice = "3"
                else:
                    levelDice = "4"
                return levelDice + 'd' + matchobj.group(2)

            dice = re.sub(r'(\d+)d(\d+)', cantrip_scale, dice)

        if self.higher and not autoctx.get_cast_level() == autoctx.spell.level:
            higher = self.higher.get(str(autoctx.get_cast_level()))
            if higher:
                dice = f"{dice}+{higher}"
        if d and not self.hidden:
            dice = f"{dice}+{d}"

        if maxdmg:

            def maxSub(matchobj):
                return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}"

            dice = re.sub(r'(\d+)d(\d+)', maxSub, dice)

        rolled = roll(dice,
                      rollFor=self.name.title(),
                      inline=True,
                      show_blurbs=False)
        if not self.hidden:
            autoctx.meta_queue(rolled.result)

        if not rolled.raw_dice:
            raise InvalidArgument(
                f"Invalid roll in meta roll: {rolled.result}")

        autoctx.metavars[self.name] = rolled.consolidated()
Beispiel #22
0
async def create_gvar(ctx, value):
    value = str(value)
    if len(value) > GVAR_SIZE_LIMIT:
        raise InvalidArgument(f"Gvars must be shorter than {GVAR_SIZE_LIMIT} characters.")
    name = str(uuid.uuid4())
    data = {'key': name, 'owner': str(ctx.author.id), 'owner_name': str(ctx.author), 'value': value,
            'editors': []}
    await ctx.bot.mdb.gvars.insert_one(data)
    return name
Beispiel #23
0
def parse_stat_choice(args, _):
    for i, arg in enumerate(args):
        if arg == 'True':  # hack: sadv/sdis on their own should be equivalent to -sadv/sdis all
            args[i] = arg = 'all'
        else:
            args[i] = arg = arg[:3].lower(
            )  # only check first three arg characters against STAT_ABBREVIATIONS
        if arg not in STAT_ABBREVIATIONS and arg != 'all':
            raise InvalidArgument(f"{arg} is not a valid stat")
    return args
Beispiel #24
0
    async def select_details(self, ctx):
        author = ctx.author
        channel = ctx.channel

        def chk(m):
            return m.author == author and m.channel == channel

        await ctx.send(author.mention + " What race?")
        try:
            race_response = await self.bot.wait_for('message', timeout=90, check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for race.")
        race_choices = await get_race_choices(ctx)
        race = await search_and_select(ctx, race_choices, race_response.content, lambda e: e.name)

        await ctx.send(author.mention + " What class?")
        try:
            class_response = await self.bot.wait_for('message', timeout=90, check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for class.")
        class_choices = await available(ctx, compendium.classes, 'class')
        _class = await search_and_select(ctx, class_choices, class_response.content, lambda e: e.name)

        subclass_choices = await available(ctx, _class.subclasses, 'class')
        if subclass_choices:
            await ctx.send(author.mention + " What subclass?")
            try:
                subclass_response = await self.bot.wait_for('message', timeout=90, check=chk)
            except asyncio.TimeoutError:
                raise InvalidArgument("Timed out waiting for subclass.")
            subclass = await search_and_select(ctx, subclass_choices, subclass_response.content,
                                               lambda e: e.name)
        else:
            subclass = None

        await ctx.send(author.mention + " What background?")
        try:
            bg_response = await self.bot.wait_for('message', timeout=90, check=chk)
        except asyncio.TimeoutError:
            raise InvalidArgument("Timed out waiting for background.")
        background_choices = await available(ctx, compendium.backgrounds, 'background')
        background = await search_and_select(ctx, background_choices, bg_response.content, lambda e: e.name)
        return race, _class, subclass, background
Beispiel #25
0
 def remove_known_spell(self, sb_spell):
     """
     Removes a spell from the character's spellbook override.
     :param sb_spell: The spell to remove.
     :type sb_spell SpellbookSpell
     """
     if sb_spell not in self.overrides.spells:
         raise InvalidArgument("This spell is not in the overrides.")
     self.overrides.spells.remove(sb_spell)
     spell_in_book = next(s for s in self.spellbook.spells if s.name == sb_spell.name)
     self.spellbook.spells.remove(spell_in_book)
Beispiel #26
0
def parse_attack_arg(arg, name):
    data = arg.split('|')
    if not len(data) == 3:
        raise InvalidArgument(
            "`attack` arg must be formatted `HIT|DAMAGE|TEXT`")
    return {
        'name': name,
        'attackBonus': data[0] or None,
        'damage': data[1] or None,
        'details': data[2] or None
    }
Beispiel #27
0
async def create_servalias(ctx, alias_name, commands):
    if len(commands) > ALIAS_SIZE_LIMIT:
        raise InvalidArgument(
            f"Aliases must be shorter than {ALIAS_SIZE_LIMIT} characters.")
    await ctx.bot.mdb.servaliases.update_one(
        {
            "server": str(ctx.guild.id),
            "name": alias_name
        }, {"$set": {
            "commands": commands.lstrip('!')
        }}, True)
Beispiel #28
0
    def run(self, autoctx):
        super(Roll, self).run(autoctx)
        d = autoctx.args.join('d', '+', ephem=True)
        maxdmg = autoctx.args.last('max', None, bool, ephem=True)
        mi = autoctx.args.last('mi', None, int)

        # add on combatant damage effects (#224)
        if autoctx.combatant:
            effect_d = '+'.join(autoctx.combatant.active_effects('d'))
            if effect_d:
                if d:
                    d = f"{d}+{effect_d}"
                else:
                    d = effect_d

        dice = self.dice

        if autoctx.is_spell:
            if self.cantripScale:
                dice = autoctx.cantrip_scale(dice)

            if self.higher and not autoctx.get_cast_level(
            ) == autoctx.spell.level:
                higher = self.higher.get(str(autoctx.get_cast_level()))
                if higher:
                    dice = f"{dice}+{higher}"

        if not self.hidden:
            # -mi # (#527)
            if mi:
                dice = re.sub(r'(\d+d\d+)', rf'\1mi{mi}', dice)

            if d:
                dice = f"{dice}+{d}"

        if maxdmg:

            def maxSub(matchobj):
                return f"{matchobj.group(1)}d{matchobj.group(2)}mi{matchobj.group(2)}"

            dice = re.sub(r'(\d+)d(\d+)', maxSub, dice)

        rolled = roll(dice,
                      rollFor=self.name.title(),
                      inline=True,
                      show_blurbs=False)
        if not self.hidden:
            autoctx.meta_queue(rolled.result)

        if not rolled.raw_dice:
            raise InvalidArgument(
                f"Invalid roll in meta roll: {rolled.result}")

        autoctx.metavars[self.name] = rolled.consolidated()
Beispiel #29
0
    def evaluate_math(self, varstr):
        """Evaluates a cvar expression in a MathEvaluator.
        :param varstr - the expression to evaluate.
        :returns int - the value of the expression."""
        varstr = str(varstr).strip('<>{}')
        evaluator = aliasing.evaluators.MathEvaluator.with_character(self)

        try:
            return int(evaluator.eval(varstr))
        except Exception as e:
            raise InvalidArgument(f"Cannot evaluate {varstr}: {e}")
Beispiel #30
0
def run_save(save_key, caster, args, embed):
    """
    Runs a caster's saving throw, building on an existing embed and handling most arguments.

    Also handles save bonuses from ieffects if caster is a combatant.

    :type save_key: str
    :type caster: cogs5e.models.sheet.statblock.StatBlock
    :type args: utils.argparser.ParsedArguments
    :type embed: discord.Embed
    :return: The total of each save.
    :rtype: SaveResult
    """
    if save_key.startswith('death'):
        save = Skill(0)
        stat_name = stat = 'Death'
        save_name = 'Death Save'
    else:
        try:
            save = caster.saves.get(save_key)
            stat = save_key[:3]
            stat_name = verbose_stat(stat).title()
            save_name = f"{stat_name} Save"
        except ValueError:
            raise InvalidArgument('That\'s not a valid save.')

    # -title
    if args.last('title'):
        embed.title = args.last('title', '') \
            .replace('[name]', caster.get_title_name()) \
            .replace('[sname]', save_name)
    elif args.last('h'):
        embed.title = f"An unknown creature makes {a_or_an(save_name)}!"
    else:
        embed.title = f'{caster.get_title_name()} makes {a_or_an(save_name)}!'

    # ieffect handling
    if isinstance(caster, init.Combatant):
        # -sb
        args['b'] = args.get('b') + caster.active_effects('sb')
        # -sadv/sdis
        sadv_effects = caster.active_effects('sadv')
        sdis_effects = caster.active_effects('sdis')
        if 'all' in sadv_effects or stat in sadv_effects:
            args[
                'adv'] = True  # Because adv() only checks last() just forcibly add them
        if 'all' in sdis_effects or stat in sdis_effects:
            args['dis'] = True

    result = _run_common(save, args, embed, rr_format="Save {}")
    return SaveResult(rolls=result.rolls,
                      skill=save,
                      skill_name=stat_name,
                      skill_roll_result=result)