Skip to content

Commit

Permalink
feature: additional SecLoop, ExprElement and EffWait syntax (#80)
Browse files Browse the repository at this point in the history
* feature: 'loop N times' in SecLoop

* feature: 'N random elements' in ExprElement

* feature: 'wait while' in EffWait

* improve: allow expressions in SecLoop

* req/impr: add request and only allow 1 or more in 'loop N times'

* request: add request

* improve: small code improvements

Co-authored-by: CHarcoalToast <[email protected]>
  • Loading branch information
Mwexim and CHarcoalToast authored Oct 25, 2020
1 parent 7a30391 commit f938843
Show file tree
Hide file tree
Showing 6 changed files with 184 additions and 43 deletions.
81 changes: 69 additions & 12 deletions src/main/java/io/github/syst3ms/skriptparser/effects/EffWait.java
Original file line number Diff line number Diff line change
@@ -1,41 +1,57 @@
package io.github.syst3ms.skriptparser.effects;

import io.github.syst3ms.skriptparser.Parser;
import io.github.syst3ms.skriptparser.lang.Effect;
import io.github.syst3ms.skriptparser.lang.Expression;
import io.github.syst3ms.skriptparser.lang.Statement;
import io.github.syst3ms.skriptparser.lang.TriggerContext;
import io.github.syst3ms.skriptparser.lang.*;
import io.github.syst3ms.skriptparser.parsing.ParseContext;
import io.github.syst3ms.skriptparser.util.ThreadUtils;
import io.github.syst3ms.skriptparser.util.TimeUtils;
import org.jetbrains.annotations.Nullable;

import java.time.Duration;
import java.util.Optional;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;

/**
* Waits a certain duration and then executes all the code after this effect.
* Note that new events may be triggered during the wait time.
* When using the {@code wait while %=boolean%} effect, if the condition is never met,
* the program could go to a recursive state, never escaping from an infinite loop.
* This is why we advice you to give a limit.
*
* @name Wait
* @pattern (wait|halt) [for] %duration%
* @pattern (wait|halt) (0:until|1:while) %=boolean% [for %*duration%]
* @since ALPHA
* @author Mwexim
*/
public class EffWait extends Effect {

static {
Parser.getMainRegistration().addEffect(
EffWait.class,
"(wait|halt) [for] %duration%"
EffWait.class,
"(wait|halt) [for] %duration%",
"(wait|halt) (0:until|1:while) %=boolean% [for %*duration%]"
);
}

private Expression<Duration> duration;
private Expression<Boolean> condition;
private boolean isConditional;
private boolean negated;

@SuppressWarnings("unchecked")
@Override
public boolean init(Expression<?>[] expressions, int matchedPattern, ParseContext parseContext) {
duration = (Expression<Duration>) expressions[0];
isConditional = matchedPattern == 1;
if (isConditional) {
condition = (Expression<Boolean>) expressions[0];
if (expressions.length == 2)
duration = (Literal<Duration>) expressions[1];
negated = parseContext.getParseMark() == 0;
} else {
duration = (Expression<Duration>) expressions[0];
}
return true;
}

Expand All @@ -44,14 +60,55 @@ protected void execute(TriggerContext ctx) {
throw new UnsupportedOperationException();
}

@SuppressWarnings("unchecked")
@Override
public Optional<? extends Statement> walk(TriggerContext ctx) {
Optional<? extends Duration> dur = duration.getSingle(ctx);
if (dur.isEmpty())
return getNext();
if (getNext().isEmpty())
return Optional.empty();
ThreadUtils.runAfter(() -> Statement.runAll(getNext().get(), ctx), dur.get());

if (isConditional) {
// The code we want to run each check.
Consumer<ExecutorService> code = exec -> {
if (condition.getSingle(ctx).filter(b -> negated == b).isPresent()) {
Statement.runAll(getNext().get(), ctx);
exec.shutdownNow();
}
};

if (duration == null) {
var thread = ThreadUtils.buildPeriodic();
thread.scheduleAtFixedRate(
() -> code.accept(thread),
0,
TimeUtils.TICK.toMillis(),
TimeUnit.MILLISECONDS
);
} else {
var dur = ((Optional<Duration>) ((Literal<Duration>) duration).getSingle()).orElse(Duration.ZERO);
long millis = dur.toMillis();
var thread = ThreadUtils.buildPeriodic();
thread.scheduleAtFixedRate(
() -> code.accept(thread),
0,
TimeUtils.TICK.toMillis(),
TimeUnit.MILLISECONDS
);
thread.schedule(
() -> {
Statement.runAll(getNext().get(), ctx);
thread.shutdownNow();
},
millis,
TimeUnit.MILLISECONDS
);
}
} else {
Optional<? extends Duration> dur = duration.getSingle(ctx);
if (dur.isEmpty())
return getNext();

ThreadUtils.runAfter(() -> Statement.runAll(getNext().get(), ctx), dur.get());
}
return Optional.empty();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@

import java.math.BigInteger;
import java.util.Arrays;
import java.util.Collections;
import java.util.concurrent.ThreadLocalRandom;

/**
Expand All @@ -19,6 +20,7 @@
* @type EXPRESSION
* @pattern ([the] first|[the] last|[a] random|[the] %integer%(st|nd|rd|th)) element out [of] %objects%
* @pattern [the] (first|last) %integer% elements out [of] %objects%
* @pattern %integer% random elements out [of] %objects%
* @pattern %objects%\[%integer%\]
* @since ALPHA
* @author Mwexim
Expand All @@ -32,6 +34,7 @@ public class ExprElement implements Expression<Object> {
true,
"(0:[the] first|1:[the] last|2:[a] random|3:[the] %integer%(st|nd|rd|th)) element out [of] %objects%",
"[the] (0:first|1:last) %integer% elements out [of] %objects%",
"%integer% random elements out [of] %objects%",
"%objects%\\[%integer%\\]");
}

Expand All @@ -58,13 +61,16 @@ public boolean init(Expression<?>[] expressions, int matchedPattern, ParseContex
}
break;
case 1:
case 2:
range = (Expression<BigInteger>) expressions[0];
expr = (Expression<Object>) expressions[1];
break;
default:
case 3:
expr = (Expression<Object>) expressions[0];
range = (Expression<BigInteger>) expressions[1];
break;
default:
throw new IllegalStateException();
}
return true;
}
Expand Down Expand Up @@ -113,6 +119,10 @@ public Object[] getValues(TriggerContext ctx) {
return Arrays.copyOfRange(values, values.length - r, values.length);
}
case 2:
var shuffled = Arrays.asList(values);
Collections.shuffle(shuffled, random);
return shuffled.subList(0, r).toArray();
case 3:
return new Object[] {values[r - 1]};
default:
return new Object[0];
Expand All @@ -123,12 +133,13 @@ public Object[] getValues(TriggerContext ctx) {
public String toString(@Nullable TriggerContext ctx, boolean debug) {
switch (pattern) {
case 0:
case 2:
case 3:
return "element out of " + expr.toString(ctx, debug);
case 1:
case 2:
return "elements out of " + expr.toString(ctx, debug);
default:
return "";
throw new IllegalStateException();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ public boolean init(Expression<?>[] expressions, int matchedPattern, ParseContex
comparator = Comparators.getComparator(from.getReturnType(), to.getReturnType()).orElse(null);
if (range == null) {
SkriptLogger logger = parseContext.getLogger();
logger.error("Cannot get a range between " + from.toString(null, logger.isDebug()) + " and " + from.toString(null, logger.isDebug()), ErrorType.SEMANTIC_ERROR);
logger.error("Cannot get a range between " + from.toString(null, logger.isDebug()) + " and " + to.toString(null, logger.isDebug()), ErrorType.SEMANTIC_ERROR);
return false;
}
return true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ public boolean isLoopOf(String loop) {

@Override
public Iterator<T> iterator(TriggerContext context) {
if (!isSingle())
if (isSingle())
throw new SkriptRuntimeException("Can't loop a single literal !");
return CollectionUtils.iterator(values);
}
Expand Down
79 changes: 57 additions & 22 deletions src/main/java/io/github/syst3ms/skriptparser/sections/SecLoop.java
Original file line number Diff line number Diff line change
@@ -1,16 +1,15 @@
package io.github.syst3ms.skriptparser.sections;

import io.github.syst3ms.skriptparser.Parser;
import io.github.syst3ms.skriptparser.lang.Expression;
import io.github.syst3ms.skriptparser.lang.Statement;
import io.github.syst3ms.skriptparser.lang.TriggerContext;
import io.github.syst3ms.skriptparser.lang.Variable;
import io.github.syst3ms.skriptparser.lang.*;
import io.github.syst3ms.skriptparser.lang.lambda.ArgumentSection;
import io.github.syst3ms.skriptparser.lang.lambda.SkriptConsumer;
import io.github.syst3ms.skriptparser.log.ErrorType;
import io.github.syst3ms.skriptparser.parsing.ParseContext;
import io.github.syst3ms.skriptparser.types.ranges.Ranges;
import org.jetbrains.annotations.Nullable;

import java.math.BigInteger;
import java.util.Iterator;
import java.util.Map;
import java.util.Optional;
Expand All @@ -20,35 +19,71 @@
* A section that iterates over a collection of elements
*/
public class SecLoop extends ArgumentSection {
private Expression<?> expr;
private final transient Map<TriggerContext, Object> current = new WeakHashMap<>();
private final transient Map<TriggerContext, Iterator<?>> currentIter = new WeakHashMap<>();
private SkriptConsumer<SecLoop> lambda;

static {
Parser.getMainRegistration().addSection(
SecLoop.class,
"loop %objects%"
SecLoop.class,
"loop %integer% times",
"loop %objects%"
);
}

private Expression<?> expr;
private Expression<BigInteger> times;
private SkriptConsumer<SecLoop> lambda;
private boolean isNumericLoop;
private final transient Map<TriggerContext, Object> current = new WeakHashMap<>();
private final transient Map<TriggerContext, Iterator<?>> currentIter = new WeakHashMap<>();

@SuppressWarnings("unchecked")
@Override
public boolean init(Expression<?>[] expressions, int matchedPattern, ParseContext parseContext) {
expr = expressions[0];
if (expr.isSingle()) {
parseContext.getLogger().error(
"Cannot loop a single value",
ErrorType.SEMANTIC_ERROR,
"Remove this loop, because you clearly don't need to loop a single value"
);
return false;
isNumericLoop = matchedPattern == 0;
if (isNumericLoop) {
times = (Expression<BigInteger>) expressions[0];
// We can do some certainty checks with Literals.
if (times instanceof Literal<?>) {
var t = ((Optional<BigInteger>) ((Literal<BigInteger>) times).getSingle()).orElse(BigInteger.ONE);
if (t.intValue() <= 0) {
parseContext.getLogger().error("Cannot loop a negative or zero amount of times", ErrorType.SEMANTIC_ERROR);
return false;
} else if (t.intValue() == 1) {
parseContext.getLogger().error(
"Cannot loop a single time",
ErrorType.SEMANTIC_ERROR,
"Remove this loop, because looping something once can be achieved without a loop-statement"
);
return false;
}
}
} else {
expr = expressions[0];
if (expr.isSingle()) {
parseContext.getLogger().error(
"Cannot loop a single value",
ErrorType.SEMANTIC_ERROR,
"Remove this loop, because you clearly don't need to loop a single value"
);
return false;
}
}
lambda = SkriptConsumer.create(this);
return true;
}

@Override
public Optional<? extends Statement> walk(TriggerContext ctx) {
if (isNumericLoop) {
BigInteger[] range = (BigInteger[]) times.getSingle(ctx)
.filter(t -> t.compareTo(BigInteger.ZERO) > 0)
.map(t -> Ranges.getRange(BigInteger.class).orElseThrow()
.getFunction()
.apply(BigInteger.ONE, t)) // Upper bound is inclusive
.orElse(new BigInteger[0]);
// We just set the looped expression to a range from 1 to the amount of times.
// This allows the usage of 'loop-number' to get the current iteration
expr = new SimpleLiteral<>(BigInteger.class, range);
}

Iterator<?> iter = currentIter.get(ctx);
if (iter == null) {
iter = expr instanceof Variable ? ((Variable<?>) expr).variablesIterator(ctx) : expr.iterator(ctx);
Expand All @@ -75,12 +110,12 @@ public Optional<? extends Statement> walk(TriggerContext ctx) {

@Override
public String toString(@Nullable TriggerContext ctx, boolean debug) {
return "loop " + expr.toString(ctx, debug);
return "loop " + (isNumericLoop ? times.toString(ctx, debug) + " times" : expr.toString(ctx, debug));
}

@Nullable
public Object getCurrent(TriggerContext e) {
return current.get(e);
public Object getCurrent(TriggerContext ctx) {
return current.get(ctx);
}

/**
Expand Down
Loading

0 comments on commit f938843

Please sign in to comment.