Expanding DB cleanup settings, Player Leveling config pt 1

This commit is contained in:
nossr50 2019-03-13 13:09:27 -07:00
parent d139d520e7
commit 8660d86306
14 changed files with 153 additions and 31 deletions

View File

@ -119,6 +119,12 @@ Version 2.1.16
All config nodes that used to be styled with CamelCase now use underscores (_) as spaces for readability and consistency All config nodes that used to be styled with CamelCase now use underscores (_) as spaces for readability and consistency
All config nodes will now use Capital letters at the start of each nodes name and after each underscore (_) All config nodes will now use Capital letters at the start of each nodes name and after each underscore (_)
All config nodes now include a comment with the default value of the node to use as reference All config nodes now include a comment with the default value of the node to use as reference
Expanded settings relating to purging users who have not leveled or users who had not logged in for many months
Fixed a bug where players who started at level 1 would not be purged from the DB for being "powerless"
Settings related to Player Leveling are now found in "player_leveling.conf"
Player Leveling's "StartingLevel" renamed -> "Player_Starting_Level"
Scoreboard settings can now be found in "scoreboard.conf" Scoreboard settings can now be found in "scoreboard.conf"
Scoreboard's "Allow_Keep" setting was removed because it was doing something permissions should be doing instead, and I don't see why such a thing even needs a permission! Scoreboard's "Allow_Keep" setting was removed because it was doing something permissions should be doing instead, and I don't see why such a thing even needs a permission!
@ -131,7 +137,10 @@ Version 2.1.16
Scoreboard's "Display_Time" renamed -> "Display_Time_In_Seconds" Scoreboard's "Display_Time" renamed -> "Display_Time_In_Seconds"
Scoreboard.Misc.Ability locale entry renamed from "Ability" to "Super Ability", this is used only if scoreboards are enabled and super ability names are disabled in scoreboard.conf Scoreboard.Misc.Ability locale entry renamed from "Ability" to "Super Ability", this is used only if scoreboards are enabled and super ability names are disabled in scoreboard.conf
MySQL Settings can now be found in "database_settings.conf" MySQL and FlatFile Settings can now be found in "database_settings.conf"
Added new config toggle for purging power-less users
Added new config toggle for purging inactive users
Added setting for only purging users on plugin start up
MySQL User settings are now in the User Category instead of being in the Database category MySQL User settings are now in the User Category instead of being in the Database category
MySQL's "Enabled" renamed -> "Use_MySQL" MySQL's "Enabled" renamed -> "Use_MySQL"
MySQL's "Name" renamed -> "Database_Name" MySQL's "Name" renamed -> "Database_Name"

View File

@ -17,7 +17,7 @@ public class McpurgeCommand implements TabExecutor {
case 0: case 0:
mcMMO.getDatabaseManager().purgePowerlessUsers(); mcMMO.getDatabaseManager().purgePowerlessUsers();
if (MainConfig.getInstance().getOldUsersCutoff() != -1) { if (mcMMO.getDatabaseCleaningSettings().getOldUserCutoffMonths() != -1) {
mcMMO.getDatabaseManager().purgeOldUsers(); mcMMO.getDatabaseManager().purgeOldUsers();
} }

View File

@ -3,6 +3,7 @@ package com.gmail.nossr50.config;
import com.gmail.nossr50.config.collectionconfigs.RepairConfig; import com.gmail.nossr50.config.collectionconfigs.RepairConfig;
import com.gmail.nossr50.config.collectionconfigs.SalvageConfig; import com.gmail.nossr50.config.collectionconfigs.SalvageConfig;
import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.config.hocon.ConfigLeveling;
import com.gmail.nossr50.config.hocon.SerializedConfigLoader; import com.gmail.nossr50.config.hocon.SerializedConfigLoader;
import com.gmail.nossr50.config.hocon.database.ConfigDatabase; import com.gmail.nossr50.config.hocon.database.ConfigDatabase;
import com.gmail.nossr50.config.hocon.scoreboard.ConfigScoreboard; import com.gmail.nossr50.config.hocon.scoreboard.ConfigScoreboard;
@ -63,6 +64,7 @@ public final class ConfigManager {
private SerializedConfigLoader<ConfigDatabase> configDatabase; private SerializedConfigLoader<ConfigDatabase> configDatabase;
private SerializedConfigLoader<ConfigScoreboard> configScoreboard; private SerializedConfigLoader<ConfigScoreboard> configScoreboard;
private SerializedConfigLoader<ConfigLeveling> configLeveling;
private MainConfig mainConfig; private MainConfig mainConfig;
private FishingTreasureConfig fishingTreasureConfig; private FishingTreasureConfig fishingTreasureConfig;
private ExcavationTreasureConfig excavationTreasureConfig; private ExcavationTreasureConfig excavationTreasureConfig;
@ -96,6 +98,7 @@ public final class ConfigManager {
//Serialized Configs //Serialized Configs
configDatabase = new SerializedConfigLoader<>(ConfigDatabase.class, "database_settings.conf", null); configDatabase = new SerializedConfigLoader<>(ConfigDatabase.class, "database_settings.conf", null);
configScoreboard = new SerializedConfigLoader<>(ConfigScoreboard.class, "scoreboard.conf", null); configScoreboard = new SerializedConfigLoader<>(ConfigScoreboard.class, "scoreboard.conf", null);
configLeveling = new SerializedConfigLoader<>(ConfigLeveling.class, "player_leveling.conf", null);
mainConfig = new MainConfig(); mainConfig = new MainConfig();
@ -313,4 +316,8 @@ public final class ConfigManager {
public ConfigDatabase getConfigDatabase() { return configDatabase.getConfig(); } public ConfigDatabase getConfigDatabase() { return configDatabase.getConfig(); }
public ConfigScoreboard getConfigScoreboard() { return configScoreboard.getConfig(); } public ConfigScoreboard getConfigScoreboard() { return configScoreboard.getConfig(); }
public ConfigLeveling getConfigLeveling() {
return configLeveling.getConfig();
}
} }

View File

@ -302,15 +302,6 @@ public class MainConfig extends ConfigValidated {
reason.add("Either Board or Print in ConfigScoreboard.Types.Inspect must be true!"); reason.add("Either Board or Print in ConfigScoreboard.Types.Inspect must be true!");
}*/ }*/
/* Database Purging */
if (getPurgeInterval() < -1) {
reason.add(DATABASE + PURGING + "." + PURGE_INTERVAL + " should be greater than, or equal to -1!");
}
if (getOldUsersCutoff() != -1 && getOldUsersCutoff() <= 0) {
reason.add(DATABASE + PURGING + "." + OLD_USER_CUTOFF + " should be greater than 0 or -1!");
}
/* Hardcore Mode */ /* Hardcore Mode */
if (getHardcoreDeathStatPenaltyPercentage() < 0.01 || getHardcoreDeathStatPenaltyPercentage() > 100) { if (getHardcoreDeathStatPenaltyPercentage() < 0.01 || getHardcoreDeathStatPenaltyPercentage() > 100) {
reason.add(HARDCORE + "." + DEATH_STAT_LOSS + "." + PENALTY_PERCENTAGE + " only accepts values from 0.01 to 100!"); reason.add(HARDCORE + "." + DEATH_STAT_LOSS + "." + PENALTY_PERCENTAGE + " only accepts values from 0.01 to 100!");
@ -535,15 +526,6 @@ public class MainConfig extends ConfigValidated {
return getIntValue(MOB_HEALTHBAR, DISPLAY_TIME); return getIntValue(MOB_HEALTHBAR, DISPLAY_TIME);
} }
/* Database Purging */
public int getPurgeInterval() {
return getIntValue(DATABASE + PURGING, PURGE_INTERVAL);
}
public int getOldUsersCutoff() {
return getIntValue(DATABASE + PURGING, OLD_USER_CUTOFF);
}
/* Backups */ /* Backups */
public boolean getBackupsEnabled() { public boolean getBackupsEnabled() {
return getBooleanValue(BACKUPS, ENABLED); return getBooleanValue(BACKUPS, ENABLED);

View File

@ -0,0 +1,20 @@
package com.gmail.nossr50.config.hocon;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
@ConfigSerializable
public class ConfigLeveling {
private static final int STARTING_LEVEL_DEFAULT = 1;
@Setting(value = "Player_Starting_Level",
comment = "Players will start at this level in all skills if they aren't already saved in the database." +
"\nHistorically this number has been 0, but this was changed in 2.1.X to 1 as I felt it was better to start from 1 than 0." +
"\nDefault value: "+STARTING_LEVEL_DEFAULT)
private int startingLevel = STARTING_LEVEL_DEFAULT;
public int getStartingLevel() {
return startingLevel;
}
}

View File

@ -10,6 +10,10 @@ public class ConfigDatabase {
* CONFIG NODES * CONFIG NODES
*/ */
@Setting(value = "Database_Cleaning",
comment = "Settings to automatically purge old users to keep database sizes small.")
private ConfigSectionCleaning configSectionCleaning = new ConfigSectionCleaning();
@Setting(value = "MySQL", comment = "Settings for using MySQL or MariaDB database" + @Setting(value = "MySQL", comment = "Settings for using MySQL or MariaDB database" +
"\nI recommend using MariaDB, its completely compatible with MySQL and runs a lot better" + "\nI recommend using MariaDB, its completely compatible with MySQL and runs a lot better" +
"\nI also recommend having the MySQL/MariaDB server in the same datacenter or LAN as your Minecraft server" + "\nI also recommend having the MySQL/MariaDB server in the same datacenter or LAN as your Minecraft server" +
@ -24,4 +28,8 @@ public class ConfigDatabase {
public ConfigSectionMySQL getConfigSectionMySQL() { public ConfigSectionMySQL getConfigSectionMySQL() {
return configSectionMySQL; return configSectionMySQL;
} }
public ConfigSectionCleaning getConfigSectionCleaning() {
return configSectionCleaning;
}
} }

View File

@ -0,0 +1,67 @@
package com.gmail.nossr50.config.hocon.database;
import ninja.leaping.configurate.objectmapping.Setting;
import ninja.leaping.configurate.objectmapping.serialize.ConfigSerializable;
@ConfigSerializable
public class ConfigSectionCleaning {
/* DEFAULT VALUES */
private static final boolean PURGE_OLD_USERS = false;
private static final boolean PURGE_POWERLESS_USERS = true;
private static final boolean ONLY_PURGE_AT_STARTUP = false;
private static final int PURGE_INTERVAL_DEFAULT = 1;
private static final int OLD_USER_CUTOFF_IN_MONTHS = 6;
/*
* CONFIG NODES
*/
@Setting(value = "Purge_Old_Users",
comment = "Turn this on to enable automatic database pruning of old users." +
"\nDefault value: "+PURGE_OLD_USERS)
private boolean purgeOldUsers = PURGE_OLD_USERS;
@Setting(value = "Purge_Powerless_Users", comment = "Powerless users are players who have not" +
" leveled up in a single skill." +
"\nDefault value: "+PURGE_POWERLESS_USERS)
private boolean purgePowerlessUsers = PURGE_POWERLESS_USERS;
@Setting(value = "Only_Purge_At_Plugin_Startup",
comment = "If set to true, then purging will only happen when the plugin first loads." +
"\nKeep in mind, this will trigger on reload as well." +
"\nThis purge is on a 2 second delay from plugin start-up and runs in an ASYNC thread." +
"\nDefault value: "+ONLY_PURGE_AT_STARTUP)
private boolean onlyPurgeAtStartup = ONLY_PURGE_AT_STARTUP;
@Setting(value = "Purge_Interval_In_Hours", comment = "How many hours between automatic purging?")
private int purgeInterval = PURGE_INTERVAL_DEFAULT;
@Setting(value = "Old_User_Cutoff_In_Months", comment = "Users who haven't connected in this many months will be purged" +
"\nDefault value: "+OLD_USER_CUTOFF_IN_MONTHS)
private int oldUserCutoffMonths = OLD_USER_CUTOFF_IN_MONTHS;
/*
* GETTER BOILERPLATE
*/
public boolean isPurgePowerlessUsers() {
return purgePowerlessUsers;
}
public boolean isPurgeOldUsers() {
return purgeOldUsers;
}
public boolean isOnlyPurgeAtStartup() {
return onlyPurgeAtStartup;
}
public int getPurgeInterval() {
return purgeInterval;
}
public int getOldUserCutoffMonths() {
return oldUserCutoffMonths;
}
}

View File

@ -5,6 +5,7 @@ import com.gmail.nossr50.datatypes.database.DatabaseType;
import com.gmail.nossr50.datatypes.database.PlayerStat; import com.gmail.nossr50.datatypes.database.PlayerStat;
import com.gmail.nossr50.datatypes.player.PlayerProfile; import com.gmail.nossr50.datatypes.player.PlayerProfile;
import com.gmail.nossr50.datatypes.skills.PrimarySkillType; import com.gmail.nossr50.datatypes.skills.PrimarySkillType;
import com.gmail.nossr50.mcMMO;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
@ -12,7 +13,7 @@ import java.util.UUID;
public interface DatabaseManager { public interface DatabaseManager {
// One month in milliseconds // One month in milliseconds
public final long PURGE_TIME = 2630000000L * MainConfig.getInstance().getOldUsersCutoff(); public final long PURGE_TIME = 2630000000L * mcMMO.getDatabaseCleaningSettings().getOldUserCutoffMonths();
// During convertUsers, how often to output a status // During convertUsers, how often to output a status
public final int progressInterval = 200; public final int progressInterval = 200;

View File

@ -58,7 +58,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
boolean powerless = true; boolean powerless = true;
for (int skill : skills.values()) { for (int skill : skills.values()) {
if (skill != 0) { if (skill > mcMMO.getPlayerLevelingSettings().getStartingLevel()) {
powerless = false; powerless = false;
break; break;
} }
@ -384,7 +384,7 @@ public final class FlatfileDatabaseManager implements DatabaseManager {
// Open the file to write the player // Open the file to write the player
out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true)); out = new BufferedWriter(new FileWriter(mcMMO.getUsersFilePath(), true));
String startingLevel = AdvancedConfig.getInstance().getStartingLevel() + ":"; String startingLevel = mcMMO.getPlayerLevelingSettings().getStartingLevel() + ":";
// Add the player to the end // Add the player to the end
out.append(playerName).append(":"); out.append(playerName).append(":");

View File

@ -119,12 +119,23 @@ public final class SQLDatabaseManager implements DatabaseManager {
connection = getConnection(PoolIdentifier.MISC); connection = getConnection(PoolIdentifier.MISC);
statement = connection.createStatement(); statement = connection.createStatement();
String startingLevel = String.valueOf(mcMMO.getPlayerLevelingSettings().getStartingLevel());
//Purge users who have not leveled from the default level
purged = statement.executeUpdate("DELETE FROM " + tablePrefix + "skills WHERE " purged = statement.executeUpdate("DELETE FROM " + tablePrefix + "skills WHERE "
+ "taming = " + startingLevel + " AND mining = " + startingLevel + " AND woodcutting = " + startingLevel + " AND repair = " + startingLevel + " "
+ "AND unarmed = " + startingLevel + " AND herbalism = " + startingLevel + " AND excavation = " + startingLevel + " AND "
+ "archery = " + startingLevel + " AND swords = " + startingLevel + " AND axes = " + startingLevel + " AND acrobatics = " + startingLevel + " "
+ "AND fishing = " + startingLevel + " AND alchemy = " + startingLevel + ";");
//Purge users who have 0 for all levels
purged += statement.executeUpdate("DELETE FROM " + tablePrefix + "skills WHERE "
+ "taming = 0 AND mining = 0 AND woodcutting = 0 AND repair = 0 " + "taming = 0 AND mining = 0 AND woodcutting = 0 AND repair = 0 "
+ "AND unarmed = 0 AND herbalism = 0 AND excavation = 0 AND " + "AND unarmed = 0 AND herbalism = 0 AND excavation = 0 AND "
+ "archery = 0 AND swords = 0 AND axes = 0 AND acrobatics = 0 " + "archery = 0 AND swords = 0 AND axes = 0 AND acrobatics = 0 "
+ "AND fishing = 0 AND alchemy = 0;"); + "AND fishing = 0 AND alchemy = 0;");
statement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "experience`.`user_id` = `s`.`user_id`)"); statement.executeUpdate("DELETE FROM `" + tablePrefix + "experience` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "experience`.`user_id` = `s`.`user_id`)");
statement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "huds`.`user_id` = `s`.`user_id`)"); statement.executeUpdate("DELETE FROM `" + tablePrefix + "huds` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "huds`.`user_id` = `s`.`user_id`)");
statement.executeUpdate("DELETE FROM `" + tablePrefix + "cooldowns` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "cooldowns`.`user_id` = `s`.`user_id`)"); statement.executeUpdate("DELETE FROM `" + tablePrefix + "cooldowns` WHERE NOT EXISTS (SELECT * FROM `" + tablePrefix + "skills` `s` WHERE `" + tablePrefix + "cooldowns`.`user_id` = `s`.`user_id`)");
@ -845,8 +856,8 @@ public final class SQLDatabaseManager implements DatabaseManager {
statement.setString(2, tablePrefix + "skills"); statement.setString(2, tablePrefix + "skills");
resultSet = statement.executeQuery(); resultSet = statement.executeQuery();
if (!resultSet.next()) { if (!resultSet.next()) {
String startingLevel = "'" + AdvancedConfig.getInstance().getStartingLevel() + "'"; String startingLevel = "'" + mcMMO.getPlayerLevelingSettings().getStartingLevel() + "'";
String totalLevel = "'" + (AdvancedConfig.getInstance().getStartingLevel() * (PrimarySkillType.values().length - PrimarySkillType.CHILD_SKILLS.size())) + "'"; String totalLevel = "'" + (mcMMO.getPlayerLevelingSettings().getStartingLevel() * (PrimarySkillType.values().length - PrimarySkillType.CHILD_SKILLS.size())) + "'";
createStatement = connection.createStatement(); createStatement = connection.createStatement();
createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "skills` (" createStatement.executeUpdate("CREATE TABLE IF NOT EXISTS `" + tablePrefix + "skills` ("
+ "`user_id` int(10) unsigned NOT NULL," + "`user_id` int(10) unsigned NOT NULL,"

View File

@ -57,7 +57,7 @@ public class PlayerProfile {
} }
for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) { for (PrimarySkillType primarySkillType : PrimarySkillType.NON_CHILD_SKILLS) {
skills.put(primarySkillType, AdvancedConfig.getInstance().getStartingLevel()); skills.put(primarySkillType, mcMMO.getPlayerLevelingSettings().getStartingLevel());
skillsXp.put(primarySkillType, 0F); skillsXp.put(primarySkillType, 0F);
} }

View File

@ -5,6 +5,8 @@ import com.gmail.nossr50.config.CoreSkillsConfig;
import com.gmail.nossr50.config.MainConfig; import com.gmail.nossr50.config.MainConfig;
import com.gmail.nossr50.config.WorldBlacklist; import com.gmail.nossr50.config.WorldBlacklist;
import com.gmail.nossr50.config.experience.ExperienceConfig; import com.gmail.nossr50.config.experience.ExperienceConfig;
import com.gmail.nossr50.config.hocon.ConfigLeveling;
import com.gmail.nossr50.config.hocon.database.ConfigSectionCleaning;
import com.gmail.nossr50.config.hocon.database.ConfigSectionMySQL; import com.gmail.nossr50.config.hocon.database.ConfigSectionMySQL;
import com.gmail.nossr50.config.hocon.scoreboard.ConfigScoreboard; import com.gmail.nossr50.config.hocon.scoreboard.ConfigScoreboard;
import com.gmail.nossr50.database.DatabaseManager; import com.gmail.nossr50.database.DatabaseManager;
@ -330,6 +332,20 @@ public class mcMMO extends JavaPlugin {
return configManager.getConfigDatabase().getConfigSectionMySQL(); return configManager.getConfigDatabase().getConfigSectionMySQL();
} }
public static ConfigLeveling getPlayerLevelingSettings()
{
return configManager.getConfigLeveling();
}
/**
* Returns settings for Database cleaning from the users config
* @return settings for Database cleaning from the users config
*/
public static ConfigSectionCleaning getDatabaseCleaningSettings()
{
return configManager.getConfigDatabase().getConfigSectionCleaning();
}
/** /**
* Returns settings for Scoreboards from the users config * Returns settings for Scoreboards from the users config
* @return settings for Scoreboards from the users config * @return settings for Scoreboards from the users config
@ -474,9 +490,9 @@ public class mcMMO extends JavaPlugin {
new BleedTimerTask().runTaskTimer(this, 1 * Misc.TICK_CONVERSION_FACTOR, 1 * (Misc.TICK_CONVERSION_FACTOR / 2)); new BleedTimerTask().runTaskTimer(this, 1 * Misc.TICK_CONVERSION_FACTOR, 1 * (Misc.TICK_CONVERSION_FACTOR / 2));
// Old & Powerless User remover // Old & Powerless User remover
long purgeIntervalTicks = MainConfig.getInstance().getPurgeInterval() * 60L * 60L * Misc.TICK_CONVERSION_FACTOR; long purgeIntervalTicks = getConfigManager().getConfigDatabase().getConfigSectionCleaning().getPurgeInterval() * 60L * 60L * Misc.TICK_CONVERSION_FACTOR;
if (purgeIntervalTicks == 0) { if (mcMMO.getDatabaseCleaningSettings().isOnlyPurgeAtStartup()) {
new UserPurgeTask().runTaskLaterAsynchronously(this, 2 * Misc.TICK_CONVERSION_FACTOR); // Start 2 seconds after startup. new UserPurgeTask().runTaskLaterAsynchronously(this, 2 * Misc.TICK_CONVERSION_FACTOR); // Start 2 seconds after startup.
} }
else if (purgeIntervalTicks > 0) { else if (purgeIntervalTicks > 0) {

View File

@ -11,9 +11,10 @@ public class UserPurgeTask extends BukkitRunnable {
@Override @Override
public void run() { public void run() {
lock.lock(); lock.lock();
mcMMO.getDatabaseManager().purgePowerlessUsers(); if(mcMMO.getDatabaseCleaningSettings().isPurgePowerlessUsers())
mcMMO.getDatabaseManager().purgePowerlessUsers();
if (MainConfig.getInstance().getOldUsersCutoff() != -1) { if (mcMMO.getDatabaseCleaningSettings().isPurgeOldUsers()) {
mcMMO.getDatabaseManager().purgeOldUsers(); mcMMO.getDatabaseManager().purgeOldUsers();
} }
lock.unlock(); lock.unlock();

View File

@ -263,7 +263,7 @@ public final class CommandRegistrationManager {
private static void registerMcpurgeCommand() { private static void registerMcpurgeCommand() {
PluginCommand command = mcMMO.p.getCommand("mcpurge"); PluginCommand command = mcMMO.p.getCommand("mcpurge");
command.setDescription(LocaleLoader.getString("Commands.Description.mcpurge", MainConfig.getInstance().getOldUsersCutoff())); command.setDescription(LocaleLoader.getString("Commands.Description.mcpurge", mcMMO.getDatabaseCleaningSettings().getOldUserCutoffMonths()));
command.setPermission("mcmmo.commands.mcpurge"); command.setPermission("mcmmo.commands.mcpurge");
command.setPermissionMessage(permissionsMessage); command.setPermissionMessage(permissionsMessage);
command.setUsage(LocaleLoader.getString("Commands.Usage.0", "mcpurge")); command.setUsage(LocaleLoader.getString("Commands.Usage.0", "mcpurge"));