diff --git a/src/main/java/com/github/skriptdev/skript/api/skript/variables/JsonVariableStorage.java b/src/main/java/com/github/skriptdev/skript/api/skript/variables/JsonVariableStorage.java new file mode 100644 index 00000000..ff4d835e --- /dev/null +++ b/src/main/java/com/github/skriptdev/skript/api/skript/variables/JsonVariableStorage.java @@ -0,0 +1,247 @@ +package com.github.skriptdev.skript.api.skript.variables; + +import com.github.skriptdev.skript.api.utils.Utils; +import com.github.skriptdev.skript.plugin.HySk; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; +import com.hypixel.hytale.server.core.HytaleServer; +import com.hypixel.hytale.server.core.util.BsonUtil; +import io.github.syst3ms.skriptparser.config.Config.ConfigSection; +import io.github.syst3ms.skriptparser.log.ErrorType; +import io.github.syst3ms.skriptparser.log.SkriptLogger; +import io.github.syst3ms.skriptparser.variables.VariableStorage; +import io.github.syst3ms.skriptparser.variables.Variables; +import org.bson.BsonBinaryReader; +import org.bson.BsonBinaryWriter; +import org.bson.BsonDocument; +import org.bson.BsonString; +import org.bson.BsonValue; +import org.bson.codecs.BsonDocumentCodec; +import org.bson.codecs.DecoderContext; +import org.bson.codecs.EncoderContext; +import org.bson.io.BasicOutputBuffer; +import org.bson.json.JsonWriterSettings; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.io.File; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.FileWriter; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Locale; +import java.util.concurrent.ScheduledFuture; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.atomic.AtomicInteger; + +public class JsonVariableStorage extends VariableStorage { + + public enum Type { + JSON, BSON; + } + + private File file; + private Type type = null; + private BsonDocument bsonDocument; + private final AtomicInteger changes = new AtomicInteger(0); + private final int changesToSave = 500; + ScheduledFuture schedule; + private final SkriptLogger logger; + + public JsonVariableStorage(SkriptLogger logger, String name) { + super(logger, name); + this.logger = logger; + } + + @Override + protected boolean load(@NotNull ConfigSection section) { + String fileType = section.getString("file-type"); + if (fileType == null) { + this.logger.error("No 'file-type' specified for database '" + this.name + "'!", ErrorType.EXCEPTION); + return false; + } + this.type = switch (fileType.toLowerCase(Locale.ROOT)) { + case "json" -> Type.JSON; + case "bson" -> Type.BSON; + default -> { + this.logger.error("Unknown file-type '" + fileType + "' in database '" + this.name + "'", ErrorType.EXCEPTION); + yield null; + } + }; + this.logger.info("Database '" + this.name + "' loaded with filetype '" + this.type + "'"); + return this.type != null; + } + + @Override + protected void allLoaded() { + loadVariablesFromFile(); + startFileWatcher(); + } + + private void startFileWatcher() { + this.schedule = HytaleServer.SCHEDULED_EXECUTOR.scheduleAtFixedRate(() -> { + if (this.changes.get() >= this.changesToSave) { + try { + saveVariables(false); + this.logger.info("Saved " + this.changes.get() + " changes to '" + this.file.getName() + "'"); // TODO REMOVE (debug) + this.changes.set(0); + } catch (IOException e) { + this.logger.error("Failed to save variable file", ErrorType.EXCEPTION); + throw new RuntimeException(e); + } + } + }, 5, 5, TimeUnit.MINUTES); + } + + private void loadVariablesFromFile() { + this.logger.info("Loading variables from file..."); + + try { + if (this.type == Type.JSON) { + readJsonFile(); + } else if (this.type == Type.BSON) { + readBsonFile(); + } + if (this.bsonDocument == null) { + this.bsonDocument = new BsonDocument(); + } + JsonElement jsonElement = BsonUtil.translateBsonToJson(this.bsonDocument); + if (jsonElement instanceof JsonObject jsonObject) { + jsonObject.entrySet().forEach(entry -> { + String name = entry.getKey(); + JsonObject value = entry.getValue().getAsJsonObject(); + String type = value.get("type").getAsString(); + JsonElement jsonValue = value.get("value"); + + this.logger.debug("Loading variable '" + name + "' of type '" + type + "' from file. With data '" + jsonValue.toString() + "'"); + loadVariable(name, type, jsonValue); + }); + } + + } catch (IOException e) { + this.logger.error("Failed to load variables from file", ErrorType.EXCEPTION); + throw new RuntimeException(e); + } + } + + @Override + protected boolean requiresFile() { + return true; + } + + @Override + protected @Nullable File getFile(@NotNull String fileName) { + Path resolve = HySk.getInstance().getDataDirectory().resolve(fileName); + File varFile = resolve.toFile(); + if (!varFile.exists()) { + try { + if (varFile.createNewFile()) { + this.logger.info("Created " + fileName + " file!"); + } else { + this.logger.error("Failed to create " + fileName + " file!", ErrorType.EXCEPTION); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + this.file = varFile; + return varFile; + } + + @Override + protected boolean save(@NotNull String name, @Nullable String type, @Nullable JsonElement value) { + BsonDocument myDocument = BsonDocument.parse("{}"); + + if (type != null && value != null) { + try { + BsonValue bsonValue = BsonUtil.translateJsonToBson(value); + myDocument.put("type", new BsonString(type)); + + if (bsonValue instanceof BsonDocument doc) { + myDocument.put("value", doc); + } else { + myDocument.put("value", bsonValue); + } + } catch (Exception e) { + Utils.error("Failed to parse value: " + value); + } + } else { + this.bsonDocument.remove(name); + } + + this.bsonDocument.put(name, myDocument); + this.changes.incrementAndGet(); + return true; + } + + @Override + public void close() throws IOException { + Utils.log("Closing database '" + this.name + "'"); + saveVariables(true); + this.closed = true; + } + + private void saveVariables(boolean finalSave) throws IOException { + if (finalSave) { + this.schedule.cancel(true); + } + try { + Variables.getLock().lock(); + writeBsonFile(); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + Variables.getLock().unlock(); + } + } + + private void readJsonFile() throws IOException { + String jsonContent = Files.readString(this.file.toPath()); + if (jsonContent.isBlank()) { + this.bsonDocument = new BsonDocument(); + } else { + this.bsonDocument = BsonDocument.parse(jsonContent); + } + } + + private void readBsonFile() throws IOException { + if (!this.file.exists()) { + throw new FileNotFoundException("File not found: " + this.file.getAbsolutePath()); + } + + byte[] bsonBytes = Files.readAllBytes(this.file.toPath()); + if (bsonBytes.length > 0) { + try (BsonBinaryReader reader = new BsonBinaryReader(ByteBuffer.wrap(bsonBytes))) { + BsonDocument doc = new BsonDocumentCodec().decode(reader, DecoderContext.builder().build()); + this.bsonDocument = doc == null ? new BsonDocument() : doc; + } + } else { + this.bsonDocument = new BsonDocument(); + } + + } + + public void writeBsonFile() throws IOException { + if (this.type == Type.JSON) { + FileWriter fileWriter = new FileWriter(this.file); + JsonWriterSettings.Builder indent = JsonWriterSettings.builder().indent(true); + fileWriter.write(this.bsonDocument.toJson(indent.build())); + fileWriter.close(); + } else if (this.type == Type.BSON) { + BasicOutputBuffer outputBuffer = new BasicOutputBuffer(); + try (BsonBinaryWriter writer = new BsonBinaryWriter(outputBuffer)) { + new BsonDocumentCodec().encode(writer, this.bsonDocument, EncoderContext.builder().build()); + } + + byte[] bsonBytes = outputBuffer.toByteArray(); + try (FileOutputStream fos = new FileOutputStream(this.file)) { + fos.write(bsonBytes); + } + } + } + +} diff --git a/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java b/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java index 8ba28e88..cee1b64f 100644 --- a/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java +++ b/src/main/java/com/github/skriptdev/skript/api/utils/Utils.java @@ -38,7 +38,7 @@ public static void log(IMessageReceiver receiver, Level level, String message, O if (receiver == null) { HySk.getInstance().getLogger().at(level).log(message); } else { - Color color = level == Level.SEVERE ? Color.RED : level == Level.WARNING ? Color.YELLOW : Color.WHITE; + Color color = level == Level.SEVERE ? Color.RED : level == Level.WARNING ? Color.YELLOW : level == Level.FINE ? Color.PINK : Color.WHITE; Message coloredMessage = Message.raw(message).color(color); Message m = Message.empty().insert(CORE_PREFIX).insert(coloredMessage); @@ -58,9 +58,9 @@ public static void log(IMessageReceiver receiver, LogEntry logEntry) { String message = logEntry.getMessage(); switch (logEntry.getType()) { case DEBUG -> log(receiver, Level.FINE, message); - case INFO -> log(receiver, message); - case ERROR -> error(receiver, message); - case WARNING -> warn(receiver, message); + case INFO -> log(receiver, Level.INFO, message); + case ERROR -> log(receiver, Level.SEVERE, message); + case WARNING -> log(receiver, Level.WARNING, message); } } diff --git a/src/main/java/com/github/skriptdev/skript/plugin/HySk.java b/src/main/java/com/github/skriptdev/skript/plugin/HySk.java index c27e0694..b26fd0ea 100644 --- a/src/main/java/com/github/skriptdev/skript/plugin/HySk.java +++ b/src/main/java/com/github/skriptdev/skript/plugin/HySk.java @@ -1,8 +1,10 @@ package com.github.skriptdev.skript.plugin; +import com.github.skriptdev.skript.api.utils.Utils; import com.hypixel.hytale.server.core.plugin.JavaPlugin; import com.hypixel.hytale.server.core.plugin.JavaPluginInit; import com.github.skriptdev.skript.plugin.command.SkriptCommand; +import io.github.syst3ms.skriptparser.variables.Variables; import org.jetbrains.annotations.NotNull; public class HySk extends JavaPlugin { @@ -25,6 +27,13 @@ protected void start() { new SkriptCommand(getCommandRegistry()); } + @Override + protected void shutdown() { + Utils.log("Shutting down HySkript..."); + this.skript.shutdown(); + Utils.log("HySkript shutdown complete!"); + } + public Skript getSkript() { return this.skript; } diff --git a/src/main/java/com/github/skriptdev/skript/plugin/Skript.java b/src/main/java/com/github/skriptdev/skript/plugin/Skript.java index 36d000dd..3c82d49c 100644 --- a/src/main/java/com/github/skriptdev/skript/plugin/Skript.java +++ b/src/main/java/com/github/skriptdev/skript/plugin/Skript.java @@ -3,13 +3,18 @@ import com.github.skriptdev.skript.api.skript.ScriptsLoader; import com.github.skriptdev.skript.api.skript.command.ArgUtils; import com.github.skriptdev.skript.api.skript.registration.SkriptRegistration; +import com.github.skriptdev.skript.api.skript.variables.JsonVariableStorage; import com.github.skriptdev.skript.api.utils.ReflectionUtils; import com.github.skriptdev.skript.api.utils.Utils; import com.github.skriptdev.skript.plugin.elements.ElementRegistration; -import com.hypixel.hytale.server.core.console.ConsoleSender; import io.github.syst3ms.skriptparser.Parser; +import io.github.syst3ms.skriptparser.config.Config; +import io.github.syst3ms.skriptparser.config.Config.ConfigSection; +import io.github.syst3ms.skriptparser.log.ErrorType; +import io.github.syst3ms.skriptparser.log.LogEntry; import io.github.syst3ms.skriptparser.log.SkriptLogger; import io.github.syst3ms.skriptparser.registration.SkriptAddon; +import io.github.syst3ms.skriptparser.variables.Variables; import java.nio.file.Path; @@ -17,6 +22,7 @@ public class Skript extends SkriptAddon { public static Skript INSTANCE; private final HySk hySk; + private final Config skriptConfig; private final Path scriptsPath; private final SkriptLogger logger; private SkriptRegistration registration; @@ -27,10 +33,18 @@ public Skript(HySk hySk) { INSTANCE = this; this.hySk = hySk; this.scriptsPath = hySk.getDataDirectory().resolve("scripts"); - this.logger = new SkriptLogger(true); + this.logger = new SkriptLogger(); + + Path skriptConfigPath = hySk.getDataDirectory().resolve("config.sk"); + this.skriptConfig = new Config(skriptConfigPath, "/config.sk", this.logger); + this.logger.setDebug(this.skriptConfig.getBoolean("debug")); Utils.log("Setting up HySkript!"); setup(); + this.logger.finalizeLogs(); + for (LogEntry logEntry : this.logger.close()) { + Utils.log(null, logEntry); + } } private void setup() { @@ -46,14 +60,23 @@ private void setup() { printSyntaxCount(); Utils.log("HySkript setup complete!"); + // LOAD VARIABLES + loadVariables(); + // LOAD SCRIPTS this.scriptsLoader = new ScriptsLoader(this); - this.scriptsLoader.loadScripts(ConsoleSender.INSTANCE, this.scriptsPath, false); + this.scriptsLoader.loadScripts(null, this.scriptsPath, false); // FINALIZE SCRIPT LOADING Parser.getMainRegistration().getRegisterer().finishedLoading(); } + public void shutdown() { + Utils.log("Saving variables..."); + Variables.shutdown(); + Utils.log("Variable saving complete!"); + } + private void printSyntaxCount() { io.github.syst3ms.skriptparser.registration.SkriptRegistration mainRegistration = Parser.getMainRegistration(); @@ -97,4 +120,16 @@ public ScriptsLoader getScriptsLoader() { return this.scriptsLoader; } + public void loadVariables() { + Utils.log("Loading variables..."); + Variables.registerStorage(JsonVariableStorage.class, "json-database"); + ConfigSection databases = this.skriptConfig.getConfigSection("databases"); + if (databases == null) { + this.logger.error("Databases section not found in config.sk", ErrorType.STRUCTURE_ERROR); + return; + } + Variables.load(this.logger, databases); + Utils.log("Finished loading variables!"); + } + } diff --git a/src/main/java/com/github/skriptdev/skript/plugin/elements/types/Types.java b/src/main/java/com/github/skriptdev/skript/plugin/elements/types/Types.java index a69d8708..c6ad7ceb 100644 --- a/src/main/java/com/github/skriptdev/skript/plugin/elements/types/Types.java +++ b/src/main/java/com/github/skriptdev/skript/plugin/elements/types/Types.java @@ -52,6 +52,7 @@ import java.util.UUID; +@SuppressWarnings("deprecation") public class Types { public static void register(SkriptRegistration registration) { @@ -73,18 +74,16 @@ private static void registerJavaTypes(SkriptRegistration registration) { .description("Represents a UUID.") .examples("set {_uuid} to uuid of {_player}") .since("INSERT VERSION") + .toStringFunction(UUID::toString) .serializer(new TypeSerializer<>() { - // TODO no clue if this actually works, will need to test @Override - public JsonElement serialize(@NotNull Gson gson, @NotNull UUID uuid) { - String json = gson.toJson(uuid, UUID.class); - return gson.fromJson(json, JsonElement.class); + public JsonElement serialize(@NotNull Gson gson, @NotNull UUID value) { + return gson.toJsonTree(value.toString()); } @Override public UUID deserialize(@NotNull Gson gson, @NotNull JsonElement element) { - UUID uuid = gson.fromJson(element.toString(), UUID.class); - return uuid == null ? UUID.fromString(element.getAsString()) : uuid; + return UUID.fromString(element.getAsString()); } }) .register(); @@ -113,7 +112,6 @@ private static void registerServerTypes(SkriptRegistration registration) { .description("Represents a stylized message sent to a message receiver.") .since("INSERT VERSION") .serializer(new TypeSerializer<>() { - @Override public JsonElement serialize(@NotNull Gson gson, @NotNull Message value) { BsonValue encode = Message.CODEC.encode(value, new ExtraInfo()); @@ -138,7 +136,6 @@ public Message deserialize(@NotNull Gson gson, @NotNull JsonElement element) { "Often used for the rotation of entities in a world.") .since("INSERT VERSION") .serializer(new TypeSerializer<>() { - @Override public JsonElement serialize(@NotNull Gson gson, @NotNull Vector3f value) { BsonDocument encode = Vector3f.CODEC.encode(value, new ExtraInfo()); @@ -269,6 +266,18 @@ public ItemStack deserialize(@NotNull Gson gson, @NotNull JsonElement element) { .description("Represents an inventory of an entity or block.") .since("INSERT VERSION") .toStringFunction(Inventory::toString) + .serializer(new TypeSerializer<>() { + @Override + public JsonElement serialize(@NotNull Gson gson, @NotNull Inventory value) { + BsonDocument encode = Inventory.CODEC.encode(value, new ExtraInfo()); + return gson.fromJson(encode.toJson(), JsonElement.class); + } + + @Override + public Inventory deserialize(@NotNull Gson gson, @NotNull JsonElement element) { + return Inventory.CODEC.decode(BsonDocument.parse(element.toString()), new ExtraInfo()); + } + }) .register(); EnumRegistry.register(registration, InventoryActionType.class, "inventoryactiontype", "inventoryActionType@s") .name("Inventory Action Type") diff --git a/src/main/resources/config.sk b/src/main/resources/config.sk new file mode 100644 index 00000000..4d281a25 --- /dev/null +++ b/src/main/resources/config.sk @@ -0,0 +1,34 @@ +# HySkript Config + +debug: false +# Whether or not to enable debug mode (maybe print more verbose logs to console). + +# Represents the database[s] used by HySkript to save variables to. +# You can add as many databases as you want. +databases: +# dummy: (This is the name of your database) +# type: json-database +# This is the type of database, currently only 'json-database' is supported +# +# enabled: true +# Whether or not the database is enabled +# +# file-type: json +# The file type of the database, currently only 'json' and 'bson' are supported +# 'json' = Will save as a JSON file in text format (Easy to read/change if you need to). +# 'bson' = Will save as a BSON file in binary format (This is what Hytale uses, much smaller file). +# +# file: variables.json +# The file to save variables to. +# +# pattern: .* +# The regex pattern to match variables to. +# '.*' = Will save all variables. +# '(?!-).*' = Will only save variables that don't start with '-' (ex: '{-some_var}' will not be saved). + + default: + type: json-database + enabled: true + file-type: json + file: variables.json + pattern: .*