Skip to content

Commit

Permalink
Remove powerfulMoveEffects array (rh-hideout#3515)
Browse files Browse the repository at this point in the history
* Remove powerfulMoveEffects array

* Solar Beam test
  • Loading branch information
AlexOn1ine authored and Kasenn committed Dec 29, 2023
1 parent 8ed5460 commit 85fe877
Show file tree
Hide file tree
Showing 4 changed files with 88 additions and 92 deletions.
2 changes: 1 addition & 1 deletion include/battle_ai_util.h
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ bool32 AI_IsDamagedByRecoil(u32 battler);
u32 GetNoOfHitsToKO(u32 dmg, s32 hp);
u32 GetNoOfHitsToKOBattlerDmg(u32 dmg, u32 battlerDef);
u32 GetNoOfHitsToKOBattler(u32 battlerAtk, u32 battlerDef, u32 moveIndex);
bool32 IsInIgnoredPowerfulMoveEffects(u32 effect);
void SetMovesDamageResults(u32 battlerAtk, u16 *moves);
u32 GetMoveDamageResult(u32 battlerAtk, u32 battlerDef, u32 moveIndex);
u32 GetCurrDamageHpPercent(u32 battlerAtk, u32 battlerDef);
Expand All @@ -113,6 +112,7 @@ bool32 IsMoveRedirectionPrevented(u32 move, u32 atkAbility);
bool32 IsMoveEncouragedToHit(u32 battlerAtk, u32 battlerDef, u32 move);
bool32 IsHazardMoveEffect(u32 moveEffect);
bool32 IsEncoreEncouragedEffect(u32 moveEffect);
bool32 IsChargingMove(u32 battlerAtk, u32 effect);
void ProtectChecks(u32 battlerAtk, u32 battlerDef, u32 move, u32 predictedMove, s32 *score);
bool32 ShouldSetSandstorm(u32 battler, u32 ability, u32 holdEffect);
bool32 ShouldSetHail(u32 battler, u32 ability, u32 holdEffect);
Expand Down
58 changes: 13 additions & 45 deletions src/battle_ai_main.c
Original file line number Diff line number Diff line change
Expand Up @@ -761,6 +761,9 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
if (IsSemiInvulnerable(battlerDef, move) && moveEffect != EFFECT_SEMI_INVULNERABLE && AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER)
RETURN_SCORE_MINUS(20); // if target off screen and we go first, don't use move

if (IsChargingMove(battlerAtk, moveEffect) && CanTargetFaintAi(battlerDef, battlerAtk))
RETURN_SCORE_MINUS(10);

// check if negates type
switch (effectiveness)
{
Expand Down Expand Up @@ -1355,22 +1358,13 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
}
break;
//case EFFECT_BIDE:
//case EFFECT_SUPER_FANG:
//case EFFECT_RECHARGE:
case EFFECT_LEVEL_DAMAGE:
case EFFECT_PSYWAVE:
//case EFFECT_COUNTER:
//case EFFECT_FLAIL:
case EFFECT_RETURN:
case EFFECT_PRESENT:
case EFFECT_FRUSTRATION:
case EFFECT_SONICBOOM:
//case EFFECT_MIRROR_COAT:
case EFFECT_SKULL_BASH:
case EFFECT_FOCUS_PUNCH:
case EFFECT_SUPERPOWER:
//case EFFECT_ENDEAVOR:
case EFFECT_LOW_KICK:
// AI_CBM_HighRiskForDamage
if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2)
ADJUST_SCORE(-10);
Expand Down Expand Up @@ -1915,17 +1909,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
|| (gBattleMons[battlerDef].status2 & (STATUS2_TRANSFORMED | STATUS2_SUBSTITUTE))) //Leave out Illusion b/c AI is supposed to be fooled
ADJUST_SCORE(-10);
break;
case EFFECT_TWO_TURNS_ATTACK:
if (aiData->holdEffects[battlerAtk] != HOLD_EFFECT_POWER_HERB && CanTargetFaintAi(battlerDef, battlerAtk))
ADJUST_SCORE(-6);
break;
case EFFECT_RECHARGE:
if (aiData->abilities[battlerDef] == ABILITY_WONDER_GUARD && effectiveness < AI_EFFECTIVENESS_x2)
ADJUST_SCORE(-10);
else if (aiData->abilities[battlerAtk] != ABILITY_TRUANT
&& !CanIndexMoveFaintTarget(battlerAtk, battlerDef, AI_THINKING_STRUCT->movesetIndex, 0))
ADJUST_SCORE(-2);
break;
case EFFECT_SPITE:
case EFFECT_MIMIC:
if (AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_FASTER) // Attacker should go first
Expand Down Expand Up @@ -2118,13 +2101,6 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
break;
case EFFECT_SPECTRAL_THIEF:
break;
case EFFECT_SOLAR_BEAM:
if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_POWER_HERB
|| ((AI_GetWeather(aiData) & B_WEATHER_SUN) && aiData->holdEffects[battlerAtk] != HOLD_EFFECT_UTILITY_UMBRELLA))
break;
if (CanTargetFaintAi(battlerDef, battlerAtk)) //Attacker can be knocked out
ADJUST_SCORE(-4);
break;
case EFFECT_SEMI_INVULNERABLE:
if (predictedMove != MOVE_NONE
&& AI_WhoStrikesFirst(battlerAtk, battlerDef, move) == AI_IS_SLOWER
Expand Down Expand Up @@ -2682,6 +2658,10 @@ static s32 AI_CheckBadMove(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
&& !BattlerStatCanRise(battlerAtk, aiData->abilities[battlerAtk], STAT_SPDEF))
ADJUST_SCORE(-10);
break;
case EFFECT_LOW_KICK:
if (IsDynamaxed(battlerDef))
ADJUST_SCORE(-10);
break;
case EFFECT_PLACEHOLDER:
return 0; // cannot even select
} // move effect checks
Expand Down Expand Up @@ -3172,7 +3152,7 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)
s32 score = 0;
s32 leastHits = 1000, leastHitsId = 0;
u16 *moves = GetMovesArray(battlerAtk);
bool8 isPowerfulIgnoredEffect[MAX_MON_MOVES];
bool8 isChargingMoveEffect[MAX_MON_MOVES];

for (i = 0; i < MAX_MON_MOVES; i++)
{
Expand All @@ -3185,13 +3165,13 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)
leastHitsId = i;
}
viableMoveScores[i] = AI_SCORE_DEFAULT;
isPowerfulIgnoredEffect[i] = IsInIgnoredPowerfulMoveEffects(gBattleMoves[moves[i]].effect);
isChargingMoveEffect[i] = IsChargingMove(battlerAtk, gBattleMoves[moves[i]].effect);
}
else
{
noOfHits[i] = -1;
viableMoveScores[i] = 0;
isPowerfulIgnoredEffect[i] = FALSE;
isChargingMoveEffect[i] = FALSE;
}
/*
MgbaPrintf_("%S: required hits: %d Dmg: %d", gMoveNames[moves[i]], noOfHits[i], AI_DATA->simulatedDmg[battlerAtk][battlerDef][i]);
Expand All @@ -3200,7 +3180,7 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)

// Priority list:
// 1. Less no of hits to ko
// 2. Not in the powerful but ignored move effects table
// 2. Not charging
// 3. More accuracy
// 4. Better effect

Expand All @@ -3215,9 +3195,9 @@ static s32 AI_CompareDamagingMoves(u32 battlerAtk, u32 battlerDef, u32 currId)
{
multipleBestMoves = TRUE;
// We need to make sure it's the current move which is objectively better.
if (isPowerfulIgnoredEffect[i] && !isPowerfulIgnoredEffect[currId])
if (isChargingMoveEffect[i] && !isChargingMoveEffect[currId])
viableMoveScores[i] -= 3;
else if (!isPowerfulIgnoredEffect[i] && isPowerfulIgnoredEffect[currId])
else if (!isChargingMoveEffect[i] && isChargingMoveEffect[currId])
viableMoveScores[currId] -= 3;

switch (CompareMoveAccuracies(battlerAtk, battlerDef, currId, i))
Expand Down Expand Up @@ -4873,15 +4853,6 @@ static s32 AI_CheckViability(u32 battlerAtk, u32 battlerDef, u32 move, s32 score
IncreasePoisonScore(battlerAtk, battlerDef, move, &score);
IncreaseStatUpScore(battlerAtk, battlerDef, STAT_SPEED, &score);
break;
case EFFECT_SOLAR_BEAM:
if (GetNoOfHitsToKOBattler(battlerAtk, battlerDef, movesetIndex) >= 2
&& HasMoveEffect(battlerAtk, EFFECT_SUNNY_DAY) && (AI_GetWeather(aiData) & B_WEATHER_SUN)) // Use Sunny Day to boost damage.
ADJUST_SCORE(-3);
case EFFECT_TWO_TURNS_ATTACK:
case EFFECT_SKULL_BASH:
if (aiData->holdEffects[battlerAtk] == HOLD_EFFECT_POWER_HERB)
ADJUST_SCORE(2);
break;
case EFFECT_COUNTER:
if (!IsBattlerIncapacitated(battlerDef, aiData->abilities[battlerDef]) && predictedMove != MOVE_NONE)
{
Expand Down Expand Up @@ -5272,9 +5243,6 @@ static s32 AI_HPAware(u32 battlerAtk, u32 battlerDef, u32 move, s32 score)
case EFFECT_BELLY_DRUM:
case EFFECT_PSYCH_UP:
case EFFECT_MIRROR_COAT:
case EFFECT_SOLAR_BEAM:
case EFFECT_TWO_TURNS_ATTACK:
case EFFECT_ERUPTION:
case EFFECT_TICKLE:
case EFFECT_SUNNY_DAY:
case EFFECT_SANDSTORM:
Expand Down
63 changes: 25 additions & 38 deletions src/battle_ai_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -363,25 +363,6 @@ static const u16 sEncouragedEncoreEffects[] =
EFFECT_CAMOUFLAGE,
};

// For the purposes of determining the most powerful move in a moveset, these
// moves are treated the same as having a power of 0 or 1
#define IGNORED_MOVES_END 0xFFFF
static const u16 sIgnoredPowerfulMoveEffects[] =
{
EFFECT_EXPLOSION,
EFFECT_DREAM_EATER,
EFFECT_RECHARGE,
EFFECT_SKULL_BASH,
EFFECT_SOLAR_BEAM,
EFFECT_FOCUS_PUNCH,
EFFECT_SUPERPOWER,
EFFECT_ERUPTION,
EFFECT_OVERHEAT,
EFFECT_MIND_BLOWN,
EFFECT_MAKE_IT_RAIN,
IGNORED_MOVES_END
};

// Functions
u32 GetAIChosenMove(u32 battlerId)
{
Expand Down Expand Up @@ -985,6 +966,11 @@ static bool32 AI_IsMoveEffectInMinus(u32 battlerAtk, u32 battlerDef, u32 move, s
switch (gBattleMoves[move].effect)
{
case EFFECT_RECHARGE:
case EFFECT_SUPERPOWER:
case EFFECT_OVERHEAT:
case EFFECT_MAKE_IT_RAIN:
case EFFECT_MIND_BLOWN:
case EFFECT_STEEL_BEAM:
return TRUE;
case EFFECT_RECOIL_25:
case EFFECT_RECOIL_IF_MISS:
Expand Down Expand Up @@ -1060,22 +1046,6 @@ u32 GetNoOfHitsToKOBattler(u32 battlerAtk, u32 battlerDef, u32 moveIndex)
return GetNoOfHitsToKOBattlerDmg(AI_DATA->simulatedDmg[battlerAtk][battlerDef][moveIndex], battlerDef);
}

bool32 IsInIgnoredPowerfulMoveEffects(u32 effect)
{
u32 i;
for (i = 0; sIgnoredPowerfulMoveEffects[i] != IGNORED_MOVES_END; i++)
{
if (effect == sIgnoredPowerfulMoveEffects[i])
{
// Don't ingore Solar Beam if doesn't have a charging turn.
if (effect == EFFECT_SOLAR_BEAM && (AI_GetWeather(AI_DATA) & B_WEATHER_SUN))
break;
return TRUE;
}
}
return FALSE;
}

void SetMovesDamageResults(u32 battlerAtk, u16 *moves)
{
s32 i, j, battlerDef, bestId, currId, hp, result;
Expand All @@ -1085,7 +1055,7 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves)
for (i = 0; i < MAX_MON_MOVES; i++)
{
u32 move = moves[i];
if (move == MOVE_NONE || move == MOVE_UNAVAILABLE || gBattleMoves[move].power == 0 || IsInIgnoredPowerfulMoveEffects(gBattleMoves[move].effect))
if (move == MOVE_NONE || move == MOVE_UNAVAILABLE || gBattleMoves[move].power == 0)
isNotConsidered[i] = TRUE;
else
isNotConsidered[i] = FALSE;
Expand All @@ -1100,11 +1070,10 @@ void SetMovesDamageResults(u32 battlerAtk, u16 *moves)

if (isNotConsidered[i])
{
result = MOVE_POWER_OTHER; // Move has a power of 0/1, or is in the group sIgnoredPowerfulMoveEffects
result = MOVE_POWER_OTHER; // Move has a power of 0/1
}
else
{
// Considered move has power and is not in sIgnoredPowerfulMoveEffects
// Check all other moves and calculate their power
for (j = 0; j < MAX_MON_MOVES; j++)
{
Expand Down Expand Up @@ -2410,6 +2379,24 @@ bool32 IsEncoreEncouragedEffect(u32 moveEffect)
return FALSE;
}

bool32 IsChargingMove(u32 battlerAtk, u32 effect)
{
switch (effect)
{
case EFFECT_SOLAR_BEAM:
if (AI_GetWeather(AI_DATA) & B_WEATHER_SUN)
return FALSE;
case EFFECT_SKULL_BASH:
case EFFECT_METEOR_BEAM:
case EFFECT_TWO_TURNS_ATTACK:
if (AI_DATA->holdEffects[battlerAtk] == HOLD_EFFECT_POWER_HERB)
return FALSE;
return TRUE;
default:
return FALSE;
}
}

static u32 GetLeechSeedDamage(u32 battlerId)
{
u32 damage = 0;
Expand Down
57 changes: 49 additions & 8 deletions test/battle/ai.c
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,9 @@ AI_SINGLE_BATTLE_TEST("AI prefers moves which deal more damage instead of moves
{
u8 turns = 0;
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
u16 expectedMove, abilityAtk, abilityDef, expectedMove2;
u16 expectedMove, abilityAtk, abilityDef;

abilityAtk = ABILITY_NONE;
expectedMove2 = MOVE_NONE;

// Scald and Poison Jab take 3 hits, Waterfall takes 2.
PARAMETRIZE { move1 = MOVE_WATERFALL; move2 = MOVE_SCALD; move3 = MOVE_POISON_JAB; move4 = MOVE_WATER_GUN; expectedMove = MOVE_WATERFALL; turns = 2; }
Expand Down Expand Up @@ -212,7 +211,7 @@ AI_SINGLE_BATTLE_TEST("AI prefers a weaker move over a one with a downside effec
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET) { HP(hp); }
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_TYPHLOSION) { Moves(move1, move2); }
OPPONENT(SPECIES_TYPHLOSION) { Moves(move1, move2, move3, move4); }
} WHEN {
switch (turns)
{
Expand Down Expand Up @@ -261,8 +260,8 @@ AI_SINGLE_BATTLE_TEST("AI can choose a status move that boosts the attack by two
AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking into account accuracy and move effect")
{
u16 move1 = MOVE_NONE, move2 = MOVE_NONE, move3 = MOVE_NONE, move4 = MOVE_NONE;
u16 expectedMove, abilityAtk = ABILITY_NONE, abilityDef;
u16 expectedMove2 = MOVE_NONE;
u16 expectedMove, expectedMove2 = MOVE_NONE;
u16 abilityAtk = ABILITY_NONE, holdItemAtk = ITEM_NONE;

// Psychic is not very effective, but always hits. Solarbeam requires a charging turn, Double Edge has recoil and Focus Blast can miss;
PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE; expectedMove = MOVE_PSYCHIC; }
Expand All @@ -273,12 +272,24 @@ AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking
// This time it's Solarbeam + Psychic, because the weather is sunny.
PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; }
// Psychic and Solar Beam are chosen because user is holding Power Herb
PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SOLAR_BEAM; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SOLAR_BEAM; }
// Psychic and Skull Bash are chosen because user is holding Power Herb
PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_PSYCHIC; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_PSYCHIC; expectedMove2 = MOVE_SKULL_BASH; }
// Skull Bash is chosen because it's the most accurate and is holding Power Herb
PARAMETRIZE { abilityAtk = ABILITY_STURDY; holdItemAtk = ITEM_POWER_HERB; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_SLAM; move4 = MOVE_DOUBLE_EDGE;
expectedMove = MOVE_SKULL_BASH; }
// Crabhammer is chosen even if Skull Bash is more accurate, the user has no Power Herb
PARAMETRIZE { abilityAtk = ABILITY_STURDY; move1 = MOVE_FOCUS_BLAST; move2 = MOVE_SKULL_BASH; move3 = MOVE_SLAM; move4 = MOVE_CRABHAMMER;
expectedMove = MOVE_CRABHAMMER; }

GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET) { HP(5); }
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_GEODUDE) { Moves(move1, move2, move3, move4); Ability(abilityAtk); }
OPPONENT(SPECIES_GEODUDE) { Moves(move1, move2, move3, move4); Ability(abilityAtk); Item(holdItemAtk); }
} WHEN {
TURN { if (expectedMove2 == MOVE_NONE) { EXPECT_MOVE(opponent, expectedMove); SEND_OUT(player, 1); }
else {EXPECT_MOVES(opponent, expectedMove, expectedMove2); SCORE_EQ(opponent, expectedMove, expectedMove2); SEND_OUT(player, 1);}
Expand All @@ -289,6 +300,36 @@ AI_SINGLE_BATTLE_TEST("AI chooses the safest option to faint the target, taking
}
}

AI_SINGLE_BATTLE_TEST("AI won't use Solar Beam if there is no Sun up or the user is not holding Power Herb")
{
u16 abilityAtk = ABILITY_NONE;
u16 holdItemAtk = ITEM_NONE;

PARAMETRIZE { abilityAtk = ABILITY_DROUGHT; }
PARAMETRIZE { holdItemAtk = ITEM_POWER_HERB; }
PARAMETRIZE { }

GIVEN {
AI_FLAGS(AI_FLAG_CHECK_BAD_MOVE | AI_FLAG_CHECK_VIABILITY | AI_FLAG_TRY_TO_FAINT);
PLAYER(SPECIES_WOBBUFFET) { HP(211); }
PLAYER(SPECIES_WOBBUFFET);
OPPONENT(SPECIES_TYPHLOSION) { Moves(MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); Ability(abilityAtk); Item(holdItemAtk); }
} WHEN {
if (abilityAtk == ABILITY_DROUGHT) {
TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); }
TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); }
} else if (holdItemAtk == ITEM_POWER_HERB) {
TURN { EXPECT_MOVES(opponent, MOVE_SOLAR_BEAM, MOVE_GRASS_PLEDGE); MOVE(player, MOVE_KNOCK_OFF); }
TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); }
} else {
TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); }
TURN { EXPECT_MOVE(opponent, MOVE_GRASS_PLEDGE); SEND_OUT(player, 1); }
}
} SCENE {
MESSAGE("Wobbuffet fainted!");
}
}

AI_SINGLE_BATTLE_TEST("AI won't use ground type attacks against flying type Pokemon unless Gravity is in effect")
{
GIVEN {
Expand Down Expand Up @@ -354,7 +395,7 @@ AI_DOUBLE_BATTLE_TEST("AI won't use a Weather changing move if partner already c
{
u32 j, k;
static const u16 weatherMoves[] = {MOVE_SUNNY_DAY, MOVE_HAIL, MOVE_RAIN_DANCE, MOVE_SANDSTORM, MOVE_SNOWSCAPE};
u16 weatherMoveLeft, weatherMoveRight;
u16 weatherMoveLeft = MOVE_NONE, weatherMoveRight = MOVE_NONE;

for (j = 0; j < ARRAY_COUNT(weatherMoves); j++)
{
Expand Down Expand Up @@ -407,7 +448,7 @@ AI_DOUBLE_BATTLE_TEST("AI will not use Helping Hand if partner does not have any
AI_DOUBLE_BATTLE_TEST("AI will not use a status move if partner already chose Helping Hand")
{
s32 j;
u32 statusMove;
u32 statusMove = MOVE_NONE;

for (j = MOVE_NONE + 1; j < MOVES_COUNT; j++)
{
Expand Down

0 comments on commit 85fe877

Please sign in to comment.