diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml new file mode 100644 index 0000000..dfb2aae --- /dev/null +++ b/.github/workflows/codeql-analysis.yml @@ -0,0 +1,71 @@ +# For most projects, this workflow file will not need changing; you simply need +# to commit it to your repository. +# +# You may wish to alter this file to override the set of languages analyzed, +# or to provide custom queries or build logic. +name: "CodeQL" + +on: + push: + branches: [ master ] + pull_request: + # The branches below must be a subset of the branches above + branches: [ master ] + schedule: + - cron: '0 10 * * 1' + +jobs: + analyze: + name: Analyze + runs-on: ubuntu-latest + + strategy: + fail-fast: false + matrix: + # Override automatic language detection by changing the below list + # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] + language: [ 'java' ] + # Learn more... + # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection + + steps: + - name: Checkout repository + uses: actions/checkout@v2 + with: + # We must fetch at least the immediate parents so that if this is + # a pull request then we can checkout the head. + fetch-depth: 2 + + # If this run was triggered by a pull request event, then checkout + # the head of the pull request instead of the merge commit. + - run: git checkout HEAD^2 + if: ${{ github.event_name == 'pull_request' }} + + # Initializes the CodeQL tools for scanning. + - name: Initialize CodeQL + uses: github/codeql-action/init@v1 + with: + languages: ${{ matrix.language }} + # If you wish to specify custom queries, you can do so here or in a config file. + # By default, queries listed here will override any specified in a config file. + # Prefix the list here with "+" to use these queries and those in the config file. + # queries: ./path/to/local/query, your-org/your-repo/queries@main + + # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). + # If this step fails, then you should remove it and run the build manually (see below) + - name: Autobuild + uses: github/codeql-action/autobuild@v1 + + # ℹī¸ Command-line programs to run using the OS shell. + # 📚 https://git.io/JvXDl + + # ✏ī¸ If the Autobuild fails above, remove it and uncomment the following three lines + # and modify them (or add more) to build your code if your project + # uses a compiled language + + #- run: | + # make bootstrap + # make release + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v1 diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/player/ChromaGamerBase.java b/Chroma-Core/src/main/java/buttondevteam/lib/player/ChromaGamerBase.java index 0ce7593..5c95b5d 100755 --- a/Chroma-Core/src/main/java/buttondevteam/lib/player/ChromaGamerBase.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/player/ChromaGamerBase.java @@ -5,7 +5,6 @@ import buttondevteam.core.component.channel.Channel; import buttondevteam.lib.TBMCCoreAPI; import buttondevteam.lib.architecture.ConfigData; import buttondevteam.lib.architecture.IHaveConfig; -import com.google.common.collect.HashBiMap; import lombok.Getter; import lombok.val; import org.bukkit.Bukkit; @@ -16,6 +15,7 @@ import javax.annotation.Nullable; import java.io.File; import java.util.ArrayList; import java.util.HashMap; +import java.util.Map; import java.util.Optional; import java.util.function.Consumer; import java.util.function.Function; @@ -24,35 +24,39 @@ import java.util.function.Supplier; @ChromaGamerEnforcer public abstract class ChromaGamerBase { private static final String TBMC_PLAYERS_DIR = "TBMC/players/"; - private static final HashBiMap, String> playerTypes = HashBiMap.create(); - private static final HashMap, Supplier> constructors = new HashMap<>(); - private static final HashMap, HashMap> userCache = new HashMap<>(); private static final ArrayList>> senderConverters = new ArrayList<>(); + private static final HashMap, StaticUserData> staticDataMap = new HashMap<>(); /** * Use {@link #getConfig()} where possible; the 'id' must be always set */ - protected YamlConfiguration plugindata; + //protected YamlConfiguration plugindata; @Getter protected final IHaveConfig config = new IHaveConfig(this::save); - - public void init() { - config.reset(plugindata); - } + protected CommonUserData commonUserData; /** * Used for connecting with every type of user ({@link #connectWith(ChromaGamerBase)}) and to init the configs. */ public static void RegisterPluginUserClass(Class userclass, Supplier constructor) { - if (userclass.isAnnotationPresent(UserClass.class)) - playerTypes.put(userclass, userclass.getAnnotation(UserClass.class).foldername()); - else if (userclass.isAnnotationPresent(AbstractUserClass.class)) - playerTypes.put(userclass.getAnnotation(AbstractUserClass.class).prototype(), - userclass.getAnnotation(AbstractUserClass.class).foldername()); - else // <-- Really important + Class cl; + String folderName; + if (userclass.isAnnotationPresent(UserClass.class)) { + cl = userclass; + folderName = userclass.getAnnotation(UserClass.class).foldername(); + } else if (userclass.isAnnotationPresent(AbstractUserClass.class)) { + var ucl = userclass.getAnnotation(AbstractUserClass.class).prototype(); + if (!userclass.isAssignableFrom(ucl)) + throw new RuntimeException("The prototype class (" + ucl.getSimpleName() + ") must be a subclass of the userclass parameter (" + userclass.getSimpleName() + ")!"); + //noinspection unchecked + cl = (Class) ucl; + folderName = userclass.getAnnotation(AbstractUserClass.class).foldername(); + } else // <-- Really important throw new RuntimeException("Class not registered as a user class! Use @UserClass or TBMCPlayerBase"); - constructors.put(userclass, constructor); + var sud = new StaticUserData(folderName); + sud.getConstructors().put(cl, constructor); + staticDataMap.put(userclass, sud); } /** @@ -77,34 +81,54 @@ public abstract class ChromaGamerBase { * @return The type for the given folder name or null if not found */ public static Class getTypeForFolder(String foldername) { - return playerTypes.inverse().get(foldername); + synchronized (staticDataMap) { + return staticDataMap.entrySet().stream().filter(e -> e.getValue().getFolder().equalsIgnoreCase(foldername)) + .map(Map.Entry::getKey).findAny().orElse(null); + } } /*** - * Loads a user from disk and returns the user object. Make sure to use the subclasses' methods, where possible, like {@link TBMCPlayerBase#getPlayer(java.util.UUID, Class)} + * Retrieves a user from cache or loads it from disk. * - * @param fname Filename without .yml, usually UUID + * @param fname Filename without .yml, the user's identifier for that type * @param cl User class * @return The user object */ - public static T getUser(String fname, Class cl) { - HashMap uc; - if (userCache.containsKey(cl)) { - uc = userCache.get(cl); - if (uc.containsKey(fname)) - //noinspection unchecked - return (T) uc.get(fname); + public static synchronized T getUser(String fname, Class cl) { + StaticUserData staticUserData = null; + for (var sud : staticDataMap.entrySet()) { + if (sud.getKey().isAssignableFrom(cl)) { + staticUserData = sud.getValue(); + break; + } } - @SuppressWarnings("unchecked") T obj = (T) constructors.get(cl).get(); - final String folder = getFolderForType(cl); - final File file = new File(TBMC_PLAYERS_DIR + folder, fname + ".yml"); - file.getParentFile().mkdirs(); - obj.plugindata = YamlConfiguration.loadConfiguration(file); - obj.plugindata.set(folder + "_id", fname); + if (staticUserData == null) + throw new RuntimeException("User class not registered! Use @UserClass or @AbstractUserClass"); + var commonUserData = staticUserData.getUserDataMap().get(fname); + if (commonUserData == null) { + final String folder = staticUserData.getFolder(); + final File file = new File(TBMC_PLAYERS_DIR + folder, fname + ".yml"); + file.getParentFile().mkdirs(); + var playerData = YamlConfiguration.loadConfiguration(file); + commonUserData = new CommonUserData<>(playerData); + playerData.set(staticUserData.getFolder() + "_id", fname); + staticUserData.getUserDataMap().put(fname, commonUserData); + } + if (commonUserData.getUserCache().containsKey(cl)) + return (T) commonUserData.getUserCache().get(cl); + T obj; + if (staticUserData.getConstructors().containsKey(cl)) + //noinspection unchecked + obj = (T) staticUserData.getConstructors().get(cl).get(); + else { + try { + obj = cl.getConstructor().newInstance(); + } catch (Exception e) { + throw new RuntimeException("Failed to create new instance of user of type " + cl.getSimpleName() + "!", e); + } + } + obj.commonUserData = commonUserData; obj.init(); - synchronized (userCache) { - userCache.computeIfAbsent(cl, key -> new HashMap<>()).put(fname, obj); - } obj.scheduleUncache(); return obj; } @@ -134,9 +158,15 @@ public abstract class ChromaGamerBase { } public static void saveUsers() { - for (var users : userCache.values()) - for (var user : users.values()) - ConfigData.saveNow(user.plugindata); //Calls save() + synchronized (staticDataMap) { + for (var sud : staticDataMap.values()) + for (var cud : sud.getUserDataMap().values()) + ConfigData.saveNow(cud.getPlayerData()); //Calls save() + } + } + + protected void init() { + config.reset(commonUserData.getPlayerData()); } /** @@ -144,18 +174,23 @@ public abstract class ChromaGamerBase { */ protected void save() { try { - if (plugindata.getKeys(false).size() > 0) - plugindata.save(new File(TBMC_PLAYERS_DIR + getFolder(), getFileName() + ".yml")); + if (commonUserData.getPlayerData().getKeys(false).size() > 0) + commonUserData.getPlayerData().save(new File(TBMC_PLAYERS_DIR + getFolder(), getFileName() + ".yml")); } catch (Exception e) { TBMCCoreAPI.SendException("Error while saving player to " + getFolder() + "/" + getFileName() + ".yml!", e, MainPlugin.Instance); } } + /** + * Removes the user from the cache. This will be called automatically after some time by default. + */ public void uncache() { + final var userCache = commonUserData.getUserCache(); + //noinspection SynchronizationOnLocalVariableOrMethodParameter synchronized (userCache) { - var c = userCache.get(getClass()); - if (c != null) if (c.remove(getFileName()) != this) - throw new IllegalStateException("A different player instance was cached!"); + if (userCache.containsKey(getClass())) + if (userCache.remove(getClass()) != this) + throw new IllegalStateException("A different player instance was cached!"); } } @@ -164,60 +199,64 @@ public abstract class ChromaGamerBase { } /** - * Connect two accounts. Do not use for connecting two Minecraft accounts or similar. Also make sure you have the "id" tag set + * Connect two accounts. Do not use for connecting two Minecraft accounts or similar. Also make sure you have the "id" tag set. * * @param user The account to connect with */ public final void connectWith(T user) { // Set the ID, go through all linked files and connect them as well - if (!playerTypes.containsKey(getClass())) - throw new RuntimeException("Class not registered as a user class! Use TBMCCoreAPI.RegisterUserClass"); final String ownFolder = getFolder(); final String userFolder = user.getFolder(); if (ownFolder.equalsIgnoreCase(userFolder)) throw new RuntimeException("Do not connect two accounts of the same type! Type: " + ownFolder); - user.plugindata.set(ownFolder + "_id", plugindata.getString(ownFolder + "_id")); - plugindata.set(userFolder + "_id", user.plugindata.getString(userFolder + "_id")); + var ownData = commonUserData.getPlayerData(); + var userData = user.commonUserData.getPlayerData(); + userData.set(ownFolder + "_id", ownData.getString(ownFolder + "_id")); + ownData.set(userFolder + "_id", userData.getString(userFolder + "_id")); + config.signalChange(); + user.config.signalChange(); Consumer sync = sourcedata -> { - final String sourcefolder = sourcedata == plugindata ? ownFolder : userFolder; + final String sourcefolder = sourcedata == ownData ? ownFolder : userFolder; final String id = sourcedata.getString(sourcefolder + "_id"); - for (val entry : playerTypes.entrySet()) { // Set our ID in all files we can find, both from our connections and the new ones + for (val entry : staticDataMap.entrySet()) { // Set our ID in all files we can find, both from our connections and the new ones if (entry.getKey() == getClass() || entry.getKey() == user.getClass()) continue; - final String otherid = sourcedata.getString(entry.getValue() + "_id"); + var entryFolder = entry.getValue().getFolder(); + final String otherid = sourcedata.getString(entryFolder + "_id"); if (otherid == null) continue; ChromaGamerBase cg = getUser(otherid, entry.getKey()); - cg.plugindata.set(sourcefolder + "_id", id); // Set new IDs - cg.config.signalChange(); - for (val item : playerTypes.entrySet()) { - if (sourcedata.contains(item.getValue() + "_id")) { - cg.plugindata.set(item.getValue() + "_id", sourcedata.getString(item.getValue() + "_id")); // Set all existing IDs - cg.config.signalChange(); + var cgData = cg.commonUserData.getPlayerData(); + cgData.set(sourcefolder + "_id", id); // Set new IDs + for (val item : staticDataMap.entrySet()) { + var itemFolder = item.getValue().getFolder(); + if (sourcedata.contains(itemFolder + "_id")) { + cgData.set(itemFolder + "_id", sourcedata.getString(itemFolder + "_id")); // Set all existing IDs } } + cg.config.signalChange(); } }; - sync.accept(plugindata); - sync.accept(user.plugindata); + sync.accept(ownData); + sync.accept(userData); } /** - * Retunrs the ID for the T typed player object connected with this one or null if no connection found. + * Returns the ID for the T typed player object connected with this one or null if no connection found. * * @param cl The player class to get the ID from * @return The ID or null if not found */ public final String getConnectedID(Class cl) { - return plugindata.getString(getFolderForType(cl) + "_id"); + return commonUserData.getPlayerData().getString(getFolderForType(cl) + "_id"); } /** - * Returns this player as a plugin player. This will return a new instance unless the player is online.
- * Make sure to close both the returned and this object. A try-with-resources block or two can help.
+ * Returns a player instance of the given type that represents the same player. This will return a new instance unless the player is cached.
+ * If the class is a subclass of the current class then the same ID is used, otherwise, a connected ID is used, if found. * * @param cl The target player class - * @return The player as a {@link T} object or null if not having an account there + * @return The player as a {@link T} object or null if the user doesn't have an account there */ @SuppressWarnings("unchecked") @Nullable @@ -229,19 +268,23 @@ public abstract class ChromaGamerBase { throw new RuntimeException("The specified class " + cl.getSimpleName() + " isn't registered!"); if (newfolder.equals(getFolder())) // If in the same folder, the same filename is used return getUser(getFileName(), cl); - if (!plugindata.contains(newfolder + "_id")) + var playerData = commonUserData.getPlayerData(); + if (!playerData.contains(newfolder + "_id")) return null; - return getUser(plugindata.getString(newfolder + "_id"), cl); + return getUser(playerData.getString(newfolder + "_id"), cl); } /** - * This method returns the filename for this player data. For example, for Minecraft-related data, MC UUIDs, for Discord data, use Discord IDs, etc.
+ * This method returns the filename for this player data. For example, for Minecraft-related data, MC UUIDs, for Discord data, Discord IDs, etc.
* Does not include .yml */ public final String getFileName() { - return plugindata.getString(getFolder() + "_id"); + return commonUserData.getPlayerData().getString(getFolder() + "_id"); } + /** + * This method returns the folder that this player data is stored in. For example: "minecraft". + */ public final String getFolder() { return getFolderForType(getClass()); } diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/player/CommonUserData.java b/Chroma-Core/src/main/java/buttondevteam/lib/player/CommonUserData.java new file mode 100644 index 0000000..a9b912a --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/player/CommonUserData.java @@ -0,0 +1,19 @@ +package buttondevteam.lib.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.bukkit.configuration.file.YamlConfiguration; + +import java.util.HashMap; + +/** + * Per user, regardless of actual type + * + * @param The user class, may be abstract + */ +@Getter +@RequiredArgsConstructor +public class CommonUserData { + private final HashMap, ? extends T> userCache = new HashMap<>(); + private final YamlConfiguration playerData; +} diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/player/StaticUserData.java b/Chroma-Core/src/main/java/buttondevteam/lib/player/StaticUserData.java new file mode 100644 index 0000000..0d0a2bd --- /dev/null +++ b/Chroma-Core/src/main/java/buttondevteam/lib/player/StaticUserData.java @@ -0,0 +1,23 @@ +package buttondevteam.lib.player; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.HashMap; +import java.util.function.Supplier; + +/** + * Per user class + * + * @param The user class type, may be abstract + */ +@Getter +@RequiredArgsConstructor +public class StaticUserData { + private final HashMap, Supplier> constructors = new HashMap<>(); + /** + * Key: User ID + */ + private final HashMap> userDataMap = new HashMap<>(); + private final String folder; +} diff --git a/Chroma-Core/src/main/java/buttondevteam/lib/player/TBMCPlayerBase.java b/Chroma-Core/src/main/java/buttondevteam/lib/player/TBMCPlayerBase.java index 0629071..6a4dbd6 100755 --- a/Chroma-Core/src/main/java/buttondevteam/lib/player/TBMCPlayerBase.java +++ b/Chroma-Core/src/main/java/buttondevteam/lib/player/TBMCPlayerBase.java @@ -24,11 +24,11 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase { } public ConfigData PlayerName() { - return config.getData("PlayerName", ""); + return super.config.getData("PlayerName", ""); } /** - * Get player as a plugin player + * Get player as a plugin player. * * @param uuid The UUID of the player to get * @param cl The type of the player @@ -36,7 +36,7 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase { */ public static T getPlayer(UUID uuid, Class cl) { var player = ChromaGamerBase.getUser(uuid.toString(), cl); - if (player.uuid.equals(uuid)) + if (!player.getUUID().equals(uuid)) //It will be set from the filename because we check it for scheduling the uncache. throw new IllegalStateException("Player UUID differs after converting from and to string..."); return player; } @@ -44,7 +44,6 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase { @Override public void init() { super.init(); - uuid = UUID.fromString(getFileName()); String pluginname; if (getClass().isAnnotationPresent(PlayerClass.class)) @@ -52,42 +51,34 @@ public abstract class TBMCPlayerBase extends ChromaGamerBase { else throw new RuntimeException("Class not defined as player class! Use @PlayerClass"); - var section = super.plugindata.getConfigurationSection(pluginname); - if (section == null) section = super.plugindata.createSection(pluginname); + var playerData = commonUserData.getPlayerData(); + var section = playerData.getConfigurationSection(pluginname); + if (section == null) section = playerData.createSection(pluginname); config.reset(section); } @Override - protected void scheduleUncache() { //Don't schedule it, it will happen on quit + protected void scheduleUncache() { //Don't schedule it, it will happen on quit - if the player is online + var p = Bukkit.getPlayer(getUUID()); + if (p == null || !p.isOnline()) + super.scheduleUncache(); } /** - * This method returns a TBMC player from their name. Calling this method may return an offline player which will load it, therefore it's highly recommended to use {@link #close()} to unload the - * player data. Using try-with-resources may be the easiest way to achieve this. Example: - * - *
-	 * {@code
-	 * try(TBMCPlayer player = getFromName(p))
-	 * {
-	 * 	...
-	 * }
-	 * 
+ * This method returns a TBMC player from their name. See {@link Bukkit#getOfflinePlayer(String)}. * * @param name The player's name * @return The {@link TBMCPlayer} object for the player */ + @SuppressWarnings("deprecation") public static T getFromName(String name, Class cl) { - @SuppressWarnings("deprecation") OfflinePlayer p = Bukkit.getOfflinePlayer(name); - if (p != null) - return getPlayer(p.getUniqueId(), cl); - else - return null; + return getPlayer(p.getUniqueId(), cl); } @Override protected void save() { - Set keys = plugindata.getKeys(false); + Set keys = commonUserData.getPlayerData().getKeys(false); if (keys.size() > 1) // PlayerName is always saved, but we don't need a file for just that super.save(); }