Skip to content

Commit ab707f8

Browse files
Script development auto reloading on save (#7464)
* Finish base implementation for auto reloading * Finish auto reloading * Adjust entryValidator method * Add the [Skript] prefix * Use LiteralString for parsing * Add multiple expressions support for single line parsing * Clean imports * Apply suggestions from code review * Update src/main/java/ch/njol/skript/structures/StructAutoReload.java * Fix bugs, simplify implementation, add `reloading...` message. * uuids too --------- Co-authored-by: sovdee <[email protected]>
1 parent 30edc5d commit ab707f8

File tree

4 files changed

+261
-7
lines changed

4 files changed

+261
-7
lines changed

src/main/java/ch/njol/skript/Skript.java

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@
112112
import org.skriptlang.skript.lang.structure.Structure;
113113
import org.skriptlang.skript.lang.structure.StructureInfo;
114114
import org.skriptlang.skript.log.runtime.RuntimeErrorManager;
115+
import org.skriptlang.skript.registration.DefaultSyntaxInfos;
115116
import org.skriptlang.skript.registration.SyntaxInfo;
116117
import org.skriptlang.skript.registration.SyntaxOrigin;
117118
import org.skriptlang.skript.registration.SyntaxRegistry;
@@ -1674,21 +1675,28 @@ public static <E extends Structure> void registerStructure(Class<E> structureCla
16741675
public static <E extends Structure> void registerSimpleStructure(Class<E> structureClass, String... patterns) {
16751676
checkAcceptRegistrations();
16761677
skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass)
1677-
.origin(getSyntaxOrigin(structureClass))
1678-
.addPatterns(patterns)
1679-
.nodeType(SyntaxInfo.Structure.NodeType.SIMPLE)
1680-
.build()
1678+
.origin(getSyntaxOrigin(structureClass))
1679+
.addPatterns(patterns)
1680+
.nodeType(SyntaxInfo.Structure.NodeType.SIMPLE)
1681+
.build()
16811682
);
16821683
}
16831684

16841685
public static <E extends Structure> void registerStructure(
16851686
Class<E> structureClass, EntryValidator entryValidator, String... patterns
1687+
) {
1688+
registerStructure(structureClass, entryValidator, DefaultSyntaxInfos.Structure.NodeType.SECTION, patterns);
1689+
}
1690+
1691+
public static <E extends Structure> void registerStructure(
1692+
Class<E> structureClass, EntryValidator entryValidator, DefaultSyntaxInfos.Structure.NodeType nodeType, String... patterns
16861693
) {
16871694
checkAcceptRegistrations();
16881695
skript.syntaxRegistry().register(SyntaxRegistry.STRUCTURE, SyntaxInfo.Structure.builder(structureClass)
16891696
.origin(getSyntaxOrigin(structureClass))
16901697
.addPatterns(patterns)
16911698
.entryValidator(entryValidator)
1699+
.nodeType(nodeType)
16921700
.build()
16931701
);
16941702
}
Lines changed: 233 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,233 @@
1+
package ch.njol.skript.structures;
2+
3+
import ch.njol.skript.ScriptLoader;
4+
import ch.njol.skript.Skript;
5+
import ch.njol.skript.doc.Description;
6+
import ch.njol.skript.doc.Example;
7+
import ch.njol.skript.doc.Name;
8+
import ch.njol.skript.doc.Since;
9+
import ch.njol.skript.lang.*;
10+
import ch.njol.skript.lang.SkriptParser.ParseResult;
11+
import ch.njol.skript.localization.ArgsMessage;
12+
import ch.njol.skript.localization.Language;
13+
import ch.njol.skript.localization.PluralizingArgsMessage;
14+
import ch.njol.skript.log.LogEntry;
15+
import ch.njol.skript.log.RedirectingLogHandler;
16+
import ch.njol.skript.log.TimingLogHandler;
17+
import ch.njol.skript.util.Task;
18+
import ch.njol.skript.util.Utils;
19+
import ch.njol.util.OpenCloseable;
20+
import ch.njol.util.StringUtils;
21+
import com.google.common.collect.Lists;
22+
import org.bukkit.Bukkit;
23+
import org.bukkit.command.CommandSender;
24+
import org.bukkit.event.Event;
25+
import org.jetbrains.annotations.NotNull;
26+
import org.jetbrains.annotations.Nullable;
27+
import org.jetbrains.annotations.Unmodifiable;
28+
import org.skriptlang.skript.lang.entry.EntryContainer;
29+
import org.skriptlang.skript.lang.entry.EntryValidator;
30+
import org.skriptlang.skript.lang.entry.util.ExpressionEntryData;
31+
import org.skriptlang.skript.lang.script.Script;
32+
import org.skriptlang.skript.lang.script.ScriptData;
33+
import org.skriptlang.skript.lang.structure.Structure;
34+
import org.skriptlang.skript.registration.DefaultSyntaxInfos.Structure.NodeType;
35+
36+
import java.io.File;
37+
import java.util.*;
38+
import java.util.logging.Level;
39+
40+
@Name("Auto Reload")
41+
@Description("""
42+
Place at the top of a script file to enable and configure automatic reloading of the script.
43+
When the script is saved, Skript will automatically reload the script.
44+
The config.sk node 'script loader thread size' must be set to a positive number (async or parallel loading) \
45+
for this to be enabled.
46+
47+
available optional nodes:
48+
recipients: The players to send reload messages to. Defaults to console.
49+
permission: The permission required to receive reload messages. 'recipients' will override this node.
50+
""")
51+
@Example("auto reload")
52+
@Example("""
53+
auto reload:
54+
recipients: "SkriptDev", "61699b2e-d327-4a01-9f1e-0ea8c3f06bc6" and "Njol"
55+
permission: "skript.reloadnotify"
56+
""") // UUID is Dinnerbone's.
57+
@Since("INSERT VERSION")
58+
public class StructAutoReload extends Structure {
59+
60+
public static final Priority PRIORITY = new Priority(10);
61+
private static final EntryValidator VALIDATOR = EntryValidator.builder()
62+
.addEntryData(new ExpressionEntryData<>("recipients", null, true, String.class, SkriptParser.PARSE_EXPRESSIONS)) // LiteralString doesn't work with PARSE_LITERALS
63+
.addEntry("permission", "skript.reloadnotify", true)
64+
.build();
65+
66+
static {
67+
Skript.registerStructure(StructAutoReload.class, VALIDATOR, NodeType.BOTH, "auto[matically] reload [(this|the) script]");
68+
}
69+
70+
private Script script;
71+
private Task task;
72+
73+
@Override
74+
public boolean init(Literal<?> @NotNull [] arguments, int pattern, ParseResult result, EntryContainer container) {
75+
if (!ScriptLoader.isAsync()) {
76+
Skript.error(Language.get("log.auto reload.async required"));
77+
return false;
78+
}
79+
80+
String[] recipients = null;
81+
String permission = "skript.reloadnotify";
82+
83+
// Container can be null if the structure is simple.
84+
if (container != null) {
85+
@SuppressWarnings("unchecked")
86+
Expression<String> expression = (Expression<String>) container.getOptional("recipients", false); // Must be false otherwise the API will throw an exception.
87+
List<String> strings = new ArrayList<>();
88+
if (expression instanceof LiteralString literal) {
89+
strings.add(literal.getSingle());
90+
} else if (expression instanceof ExpressionList<String> list) {
91+
list.getAllExpressions().forEach(expr -> {
92+
if (expr instanceof LiteralString literalString)
93+
strings.add(literalString.getSingle());
94+
});
95+
}
96+
if (!strings.isEmpty()) {
97+
recipients = strings.toArray(String[]::new);
98+
}
99+
permission = container.getOptional("permission", String.class, false);
100+
}
101+
102+
script = getParser().getCurrentScript();
103+
File file = script.getConfig().getFile();
104+
if (file == null || !file.exists()) {
105+
Skript.error(Language.get("log.auto reload.file not found"));
106+
return false;
107+
}
108+
script.addData(new AutoReload(file.lastModified(), permission, recipients));
109+
return true;
110+
}
111+
112+
@Override
113+
public boolean load() {
114+
return true;
115+
}
116+
117+
@Override
118+
public boolean postLoad() {
119+
task = new Task(Skript.getInstance(), 0, 20 * 2, true) {
120+
@Override
121+
public void run() {
122+
AutoReload data = script.getData(AutoReload.class);
123+
File file = script.getConfig().getFile();
124+
if (data == null || file == null || !file.exists())
125+
return;
126+
long lastModified = file.lastModified();
127+
if (lastModified <= data.getLastReloadTime())
128+
return;
129+
130+
data.setLastReloadTime(lastModified);
131+
try (
132+
RedirectingLogHandler logHandler = new RedirectingLogHandler(data.getRecipients(), "").start();
133+
TimingLogHandler timingLogHandler = new TimingLogHandler().start()
134+
) {
135+
reloading(logHandler);
136+
OpenCloseable openCloseable = OpenCloseable.combine(logHandler, timingLogHandler);
137+
ScriptLoader.reloadScript(script, openCloseable).thenRun(() -> reloaded(logHandler, timingLogHandler));
138+
} catch (Exception e) {
139+
//noinspection ThrowableNotThrown
140+
Skript.exception(e, "Exception occurred while automatically reloading a script", script.getConfig().getFileName());
141+
}
142+
}
143+
};
144+
return true;
145+
}
146+
147+
@Override
148+
public void unload() {
149+
task.cancel();
150+
}
151+
152+
@Override
153+
public Priority getPriority() {
154+
return PRIORITY;
155+
}
156+
157+
@Override
158+
public String toString(@Nullable Event event, boolean debug) {
159+
return "auto reload";
160+
}
161+
162+
private void reloading(RedirectingLogHandler logHandler) {
163+
String prefix = Language.get("skript.prefix");
164+
String what = PluralizingArgsMessage.format(Language.format("log.auto reload.script", script.getConfig().getFileName()));
165+
String message = StringUtils.fixCapitalization(PluralizingArgsMessage.format(Language.format("log.auto reload.reloading", what)));
166+
logHandler.log(new LogEntry(Level.INFO, Utils.replaceEnglishChatStyles(prefix + message)));
167+
}
168+
169+
private void reloaded(RedirectingLogHandler logHandler, TimingLogHandler timingLogHandler) {
170+
String prefix = Language.get("skript.prefix");
171+
ArgsMessage m_reload_error = new ArgsMessage("log.auto reload.error");
172+
ArgsMessage m_reloaded = new ArgsMessage("log.auto reload.reloaded");
173+
String what = PluralizingArgsMessage.format(Language.format("log.auto reload.script", script.getConfig().getFileName()));
174+
String timeTaken = String.valueOf(timingLogHandler.getTimeTaken());
175+
176+
String message;
177+
if (logHandler.numErrors() == 0) {
178+
message = StringUtils.fixCapitalization(PluralizingArgsMessage.format(m_reloaded.toString(what, timeTaken)));
179+
logHandler.log(new LogEntry(Level.INFO, Utils.replaceEnglishChatStyles(prefix + message)));
180+
} else {
181+
message = StringUtils.fixCapitalization(PluralizingArgsMessage.format(m_reload_error.toString(what, logHandler.numErrors(), timeTaken)));
182+
logHandler.log(new LogEntry(Level.SEVERE, Utils.replaceEnglishChatStyles(prefix + message)));
183+
}
184+
}
185+
186+
public static final class AutoReload implements ScriptData {
187+
188+
private final Set<String> recipients = new HashSet<>();
189+
private final String permission;
190+
private long lastReload; // Compare with File#lastModified()
191+
192+
// private constructor to prevent instantiation.
193+
private AutoReload(long lastReload, @Nullable String permission, @Nullable String... recipients) {
194+
if (recipients != null)
195+
this.recipients.addAll(Lists.newArrayList(recipients));
196+
197+
this.permission = permission;
198+
this.lastReload = lastReload;
199+
}
200+
201+
/**
202+
* Returns a new list of the recipients to receive reload errors.
203+
* Console command sender included.
204+
*
205+
* @return the recipients in a list
206+
*/
207+
public @Unmodifiable List<CommandSender> getRecipients() {
208+
List<CommandSender> senders = Lists.newArrayList(Bukkit.getConsoleSender());
209+
if (!recipients.isEmpty()) {
210+
Bukkit.getOnlinePlayers().stream()
211+
.filter(p -> recipients.contains(p.getName()) || recipients.contains(p.getUniqueId().toString()))
212+
.forEach(senders::add);
213+
return Collections.unmodifiableList(senders);
214+
}
215+
216+
// Collect players with permission. Recipients overrides the permission node.
217+
Bukkit.getOnlinePlayers().stream()
218+
.filter(p -> p.hasPermission(permission))
219+
.forEach(senders::add);
220+
return Collections.unmodifiableList(senders); // Unmodifiable to denote that changes won't affect the data.
221+
}
222+
223+
public long getLastReloadTime() {
224+
return lastReload;
225+
}
226+
227+
public void setLastReloadTime(long lastReload) {
228+
this.lastReload = lastReload;
229+
}
230+
231+
}
232+
233+
}

src/main/java/org/skriptlang/skript/lang/structure/Structure.java

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.jetbrains.annotations.ApiStatus;
2222
import org.jetbrains.annotations.NotNull;
2323
import org.jetbrains.annotations.Nullable;
24+
import org.jetbrains.annotations.UnknownNullability;
2425
import org.skriptlang.skript.lang.entry.EntryContainer;
2526
import org.skriptlang.skript.lang.entry.EntryData;
2627
import org.skriptlang.skript.lang.entry.EntryValidator;
@@ -88,10 +89,9 @@ public final EntryContainer getEntryContainer() {
8889

8990
@Override
9091
public final boolean init(Expression<?>[] expressions, int matchedPattern, Kleenean isDelayed, ParseResult parseResult) {
91-
StructureData structureData = getParser().getData(StructureData.class);
92-
9392
Literal<?>[] literals = Arrays.copyOf(expressions, expressions.length, Literal[].class);
9493

94+
StructureData structureData = getParser().getData(StructureData.class);
9595
StructureInfo<? extends Structure> structureInfo = structureData.structureInfo;
9696
assert structureInfo != null;
9797

@@ -117,11 +117,16 @@ public final boolean init(Expression<?>[] expressions, int matchedPattern, Kleen
117117
* The initialization phase of a Structure.
118118
* Typically, this should be used for preparing fields (e.g. handling arguments, parse tags)
119119
* Logic such as trigger loading should be saved for a loading phase (e.g. {@link #load()}).
120+
*
121+
* @param args The arguments of the Structure.
122+
* @param matchedPattern The matched pattern of the Structure.
123+
* @param parseResult The parse result of the Structure.
124+
* @param entryContainer The EntryContainer of the Structure. Will not be null if the Structure provides a {@link EntryValidator}.
120125
* @return Whether initialization was successful.
121126
*/
122127
public abstract boolean init(
123128
Literal<?>[] args, int matchedPattern, ParseResult parseResult,
124-
@Nullable EntryContainer entryContainer
129+
@UnknownNullability EntryContainer entryContainer
125130
);
126131

127132
/**

src/main/resources/lang/english.lang

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -131,6 +131,14 @@ skript command:
131131

132132
# -- Log Messages --
133133
log:
134+
auto reload:
135+
reloading: Automatically reloading <gold>%s<reset>...
136+
file not found: Script '%s' was loaded without an existing file. It will not be automatically reloaded.
137+
async required: 'script loader thread size' in the config.sk must be a value greater than 0 to use auto reload
138+
error: <light red>Encountered <gold>%2$s <light red>error¦¦s¦ while automatically reloading <gold>%1$s<light red>! <gray>(<gold>%3$sms<gray>)
139+
reloaded: <lime>Successfully automatically reloaded <gold>%s<lime>. <gray>(<gold>%2$sms<gray>)
140+
script: <gold>%s<reset>
141+
134142
# runtime errors
135143
runtime:
136144
error: <light red>The script '<gray>%s<light red>' encountered an error while executing the '<gray>%s<light red>' %s<light red>:\n

0 commit comments

Comments
 (0)