From b84d587b83ed8c84f22b8cea739e9ed172126f54 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Sat, 9 May 2026 16:42:51 +0100 Subject: [PATCH 01/14] Clarify copyEquipementSets text --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 22b4ad8..0c9fa3b 100644 --- a/README.md +++ b/README.md @@ -88,7 +88,7 @@ You can grant or remove tickets in game (default restriction: GM rank 2+): **Optional settings** - `keepStartingItems`: When `true`, the target character keeps existing starter/equipped items. When `false`, all target items except Hearthstone are deleted before copied gear is mailed. -- `copyEquipmentSets`: When `true`, Equipment Manager sets are copied and remapped to the new mailed item GUIDs. This requires the in-game "Use Equipment Manager" feature to be enabled. +- `copyEquipmentSets`: When `true`, Equipment Manager sets are copied and remapped to the new mailed item GUIDs. This requires the in-game "Use Equipment Manager" feature to be enabled. **Design note:** Only currently equipped items are included in equipment set copying to minimize server queries and load. This means equipment sets will only contain properly remapped items for gear that was actively equipped. Any set slots that referenced non-equipped items will show as empty (0) in the copied set. ## Copy Workflow From e7c382b73662f90f976a32234a7b8a0a13fa704e Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:08:42 +0100 Subject: [PATCH 02/14] DB and Logs Config --- CarbonCopy.lua | 57 ++++++++++++++++++++++++++++++++++++++++++- CarbonCopy_Config.lua | 22 +++++++++++++++++ 2 files changed, 78 insertions(+), 1 deletion(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 434d540..c9e3a57 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -22,6 +22,13 @@ local Config = CarbonCopyConfig.Config local cc_maps = CarbonCopyConfig.cc_maps local ticket_Cost = CarbonCopyConfig.ticket_Cost +-- Fallback / compatibility defaults for older configs. +local cc_carboncopyTable = Config.carboncopyTableName or "carboncopy" +local cc_playerLogsTable = Config.playerLogsTableName or "carboncopy_player_logs" +local cc_adminLogsTable = Config.adminLogsTableName or "carboncopy_admin_logs" +local cc_enablePlayerLogs = Config.enablePlayerLogs ~= false +local cc_enableAdminLogs = Config.enableAdminLogs ~= false + ------------------------------------------ -- NO ADJUSTMENTS REQUIRED BELOW THIS LINE @@ -34,7 +41,55 @@ cc_scriptIsBusy = 0 -- If module runs for the first time, create the db specified in Config.dbName and add the "carboncopy" table to it. CharDBQuery('CREATE DATABASE IF NOT EXISTS `'..Config.customDbName..'`;'); -CharDBQuery('CREATE TABLE IF NOT EXISTS `'..Config.customDbName..'`.`carboncopy` (`account_id` INT(11) NOT NULL, `tickets` INT(11) DEFAULT 0, `allow_copy_from_id` INT(11) DEFAULT 0, PRIMARY KEY (`account_id`) );'); +CharDBQuery('CREATE TABLE IF NOT EXISTS `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` (' + ..'`account_id` INT(11) NOT NULL, ' + ..'`tickets` INT(11) DEFAULT 0, ' + ..'`allow_copy_from_id` INT(11) DEFAULT 0, ' + ..'PRIMARY KEY (`account_id`) + ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' +) + +if cc_enablePlayerLogs then + CharDBQuery('CREATE TABLE IF NOT EXISTS `'..Config.customDbName..'`.`'..cc_playerLogsTable..'` (' + ..'`source_name` VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, ' + ..'`source_guid` INT(11) NOT NULL, ' + ..'`target_name` VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, ' + ..'`target_guid` INT(11) DEFAULT NULL, ' + ..'`source_level` TINYINT UNSIGNED DEFAULT NULL, ' + ..'`tickets_before` INT(11) DEFAULT NULL, ' + ..'`tickets_after` INT(11) DEFAULT NULL, ' + ..'`status_code` TINYINT UNSIGNED NOT NULL COMMENT "0=FREE_CONFIG_TICKET,1=SUCCESS,2=FAILED", ' + ..'`reason` VARCHAR(255) DEFAULT NULL, ' + ..'`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + ..'KEY `idx_ccpl_source_guid` (`source_guid`), ' + ..'KEY `idx_ccpl_target_guid` (`target_guid`), ' + ..'KEY `idx_ccpl_status_code` (`status_code`), ' + ..'KEY `idx_ccpl_created_at` (`created_at`) + ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' + ) +end + +if cc_enableAdminLogs then + CharDBQuery('CREATE TABLE IF NOT EXISTS `'..Config.customDbName..'`.`'..cc_adminLogsTable..'` (' + ..'`source_name` VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL, ' + ..'`source_guid` INT(11) DEFAULT NULL, ' + ..'`target_name` VARCHAR(12) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin DEFAULT NULL, ' + ..'`target_guid` INT(11) DEFAULT NULL, ' + ..'`target_level` TINYINT UNSIGNED DEFAULT NULL, ' + ..'`action_code` TINYINT UNSIGNED NOT NULL COMMENT "3=GM_ADD,4=GM_REMOVE,5=GM_LOOKUP", ' + ..'`tickets_before` INT(11) DEFAULT NULL, ' + ..'`tickets_after` INT(11) DEFAULT NULL, ' + ..'`status_code` TINYINT UNSIGNED NOT NULL COMMENT "1=SUCCESS,2=FAILED", ' + ..'`reason` VARCHAR(255) DEFAULT NULL, ' + ..'`created_at` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP, ' + ..'KEY `idx_ccal_source_guid` (`source_guid`), ' + ..'KEY `idx_ccal_target_guid` (`target_guid`), ' + ..'KEY `idx_ccal_action_code` (`action_code`), ' + ..'KEY `idx_ccal_status_code` (`status_code`), ' + ..'KEY `idx_ccal_created_at` (`created_at`) + ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' + ) +end function cc_CopyCharacter(event, player, command, chatHandler) diff --git a/CarbonCopy_Config.lua b/CarbonCopy_Config.lua index a2c55df..71d14f2 100644 --- a/CarbonCopy_Config.lua +++ b/CarbonCopy_Config.lua @@ -4,22 +4,40 @@ local ticket_Cost = {} -- Name of Eluna dB scheme Config.customDbName = 'ac_eluna' + +-- Table names inside Config.customDbName +Config.carboncopyTableName = 'carboncopy' +Config.playerLogsTableName = 'carboncopy_player_logs' +Config.adminLogsTableName = 'carboncopy_admin_logs' + +-- Enable or disable writing of logs and auto creation of log tables +Config.enablePlayerLogs = true +Config.enableAdminLogs = true + -- Min GM Level to use the .carboncopy command. Set to 0 for all players. Config.minGMRankForCopy = 0 + -- Min GM Level to add tickets to an account. Config.minGMRankForTickets = 2 + -- The amount of free tickets to grant when .carboncopy is executed for the first time on that account Config.freeTickets = 4 + -- This text is added to the mail which the new character receives alongside their copied items Config.mailText = ",\n \n here you are your gear. Have fun with the new twink!\n \n- Sincerely,\n the team of ChromieCraft!" + -- Maximum level to allow copying a character. Config.maxLevel = 79 + -- Whether the ticket amount withdrawn for a copy is always 1 (set it to "single") or depends on the level (set this to "level") Config.ticketCost = "level" + -- Keep starting / existing items on the target character instead of deleting all except hearthstone (true = yes | false = no) Config.keepStartingItems = false + -- Whether to copy equipment sets to the new character (requires "Use Equipment Manager" enabled in Interface > Features) (true = yes | false = no) Config.copyEquipmentSets = false + -- Here you can adjust the cost in tickets if Config.ticketCost is set to "level" ticket_Cost[19] = 1 ticket_Cost[29] = 1 @@ -35,12 +53,16 @@ ticket_Cost[80] = 25 -- it costs 25 tickets to copy a character at level 80 -- The maps below specify legal locations to use the .carboncopy command. -- This is used to prevent dungeon specific gear to be copied e.g. the legendaries from the Kael'thas encounter. + -- Eastern kingdoms cc_maps[1] = 0 + -- Kalimdor cc_maps[2] = 1 + -- Outland cc_maps[3] = 530 + -- Northrend cc_maps[4] = 571 From d59f9fd36cbada587bb461a3f8b97f992e7525f1 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:13:51 +0100 Subject: [PATCH 03/14] Finalise the logs and use the config Table Name instead of hardcoded values --- CarbonCopy.lua | 241 ++++++++++++++++++++++++++++++++++++++----------- 1 file changed, 188 insertions(+), 53 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index c9e3a57..ff33590 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -91,9 +91,79 @@ if cc_enableAdminLogs then ) end +local CC_STATUS_FREE_CONFIG_TICKET = 0 +local CC_STATUS_SUCCESS = 1 +local CC_STATUS_FAILED = 2 +local CC_ACTION_GM_ADD = 3 +local CC_ACTION_GM_REMOVE = 4 +local CC_ACTION_GM_LOOKUP = 5 + +local function cc_sqlQuote(value) + if value == nil then + return "NULL" + end + + local escaped = tostring(value):gsub("\\", "\\\\"):gsub("'", "''") + return "'"..escaped.."'" +end + +local function cc_sqlNumber(value) + if value == nil then + return "NULL" + end + return tostring(value) +end + +local function cc_logPlayer(sourceName, sourceGuid, targetName, targetGuid, sourceLevel, ticketsBefore, ticketsAfter, statusCode, reason) + if not cc_enablePlayerLogs then + return + end + + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_playerLogsTable..'` (' + ..'`source_name`, `source_guid`, `target_name`, `target_guid`, `source_level`, ' + ..'`tickets_before`, `tickets_after`, `status_code`, `reason`' + ..') VALUES (' + ..cc_sqlQuote(sourceName)..', ' + ..cc_sqlNumber(sourceGuid)..', ' + ..cc_sqlQuote(targetName)..', ' + ..cc_sqlNumber(targetGuid)..', ' + ..cc_sqlNumber(sourceLevel)..', ' + ..cc_sqlNumber(ticketsBefore)..', ' + ..cc_sqlNumber(ticketsAfter)..', ' + ..cc_sqlNumber(statusCode)..', ' + ..cc_sqlQuote(reason) + ..');' + ) +end + +local function cc_logAdmin(sourceName, sourceGuid, targetName, targetGuid, targetLevel, actionCode, ticketsBefore, ticketsAfter, statusCode, reason) + if not cc_enableAdminLogs then + return + end + + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_adminLogsTable..'` (' + ..'`source_name`, `source_guid`, `target_name`, `target_guid`, `target_level`, ' + ..'`action_code`, `tickets_before`, `tickets_after`, `status_code`, `reason`' + ..') VALUES (' + ..cc_sqlQuote(sourceName)..', ' + ..cc_sqlNumber(sourceGuid)..', ' + ..cc_sqlQuote(targetName)..', ' + ..cc_sqlNumber(targetGuid)..', ' + ..cc_sqlNumber(targetLevel)..', ' + ..cc_sqlNumber(actionCode)..', ' + ..cc_sqlNumber(ticketsBefore)..', ' + ..cc_sqlNumber(ticketsAfter)..', ' + ..cc_sqlNumber(statusCode)..', ' + ..cc_sqlQuote(reason) + ..');' + ) +end + function cc_CopyCharacter(event, player, command, chatHandler) local commandArray = cc_splitString(command) + local sourceName = player and player:GetName() or "console" + local sourceGuid = player and tonumber(tostring(player:GetGUID())) or nil if commandArray[2] ~= nil then commandArray[2] = commandArray[2]:gsub("[';\\, ]", "") if commandArray[3] ~= nil then @@ -112,8 +182,12 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if cc_scriptIsBusy ~= 0 then - chatHandler:SendSysMessage("The server is currently busy. Please try again in a few seconds.") + local reason = "The server is currently busy. Please try again in a few seconds." + chatHandler:SendSysMessage(reason) PrintInfo("CarbonCopy user request failed because the script has a scheduled task.") + if commandArray[1] == "carboncopy" and player ~= nil then + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) + end return false end @@ -153,12 +227,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then accountId = Data_SQL:GetUInt32(0) else - chatHandler:SendSysMessage("Player name not found. Syntax: .carboncopy tickets add $characterName $amount") + local reason = "Player name not found. Syntax: .carboncopy tickets add $characterName $amount" + chatHandler:SendSysMessage(reason) + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;') if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -166,14 +242,17 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if oldTickets >= 1000 or oldTickets < 0 then - chatHandler:SendSysMessage("Too large total amount tickets: "..commandArray[5]..". Max allowed total is +1000. Current value: "..oldTickets) + local reason = "Too large total amount tickets: "..commandArray[5]..". Max allowed total is +1000. Current value: "..oldTickets + chatHandler:SendSysMessage(reason) + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..';') - CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..commandArray[5] + oldTickets..', 0);') + CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';') + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..commandArray[5] + oldTickets..', 0);') chatHandler:SendSysMessage("The console has sucessfully used the .carboncopy tickets add command, adding "..commandArray[5].." tickets to the account "..accountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets add") cc_resetVariables() return false end @@ -198,12 +277,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then accountId = Data_SQL:GetUInt32(0) else - chatHandler:SendSysMessage("Player name not found. Syntax: .carboncopy tickets remove $characterName $amount") + local reason = "Player name not found. Syntax: .carboncopy tickets remove $characterName $amount" + chatHandler:SendSysMessage(reason) + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;') if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -211,14 +292,17 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if oldTickets <= 0 or oldTickets < tonumber(commandArray[5]) then - chatHandler:SendSysMessage("The account does not have enough tickets to remove "..commandArray[5]..". Current value: "..oldTickets) + local reason = "The account does not have enough tickets to remove "..commandArray[5]..". Current value: "..oldTickets + chatHandler:SendSysMessage(reason) + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..';') - CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..oldTickets - commandArray[5]..', 0);') + CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';') + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..oldTickets - commandArray[5]..', 0);') chatHandler:SendSysMessage("The console has sucessfully used the .carboncopy tickets remove command, removing "..commandArray[5].." tickets from the account "..accountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets - tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets remove") cc_resetVariables() return false end @@ -241,13 +325,15 @@ function cc_CopyCharacter(event, player, command, chatHandler) local lookupCharacterName = cc_normalizeCharacterName(lookupNameArg) local Data_SQL = CharDBQuery('SELECT `account` FROM `characters` WHERE `name` = "'..lookupCharacterName..'" LIMIT 1;') if Data_SQL == nil then - chatHandler:SendSysMessage("Character name not found. Check spelling.") + local reason = "Character name not found. Check spelling." + chatHandler:SendSysMessage(reason) + cc_logAdmin("console", nil, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end local lookupAccountId = Data_SQL:GetUInt32(0) - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..lookupAccountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..lookupAccountId..' LIMIT 1;') local lookupTickets if Data_SQL ~= nil then lookupTickets = Data_SQL:GetUInt32(0) @@ -256,18 +342,22 @@ function cc_CopyCharacter(event, player, command, chatHandler) end chatHandler:SendSysMessage("CarbonCopy tickets for "..lookupCharacterName.." (account "..lookupAccountId.."): "..lookupTickets) + cc_logAdmin("console", nil, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, lookupTickets, lookupTickets, CC_STATUS_SUCCESS, "Console .carboncopy tickets lookup") cc_resetVariables() return false end if player == nil then - chatHandler:SendSysMessage("This command can not be run from the console, but only from the character to copy.") + local reason = "This command can not be run from the console, but only from the character to copy." + chatHandler:SendSysMessage(reason) chatHandler:SendSysMessage("Syntax: .addcctickets $characterName $amount") -- Kept for Legacy / Compatibility return false end -- make sure the player is properly ranked if player:GetGMRank() < Config.minGMRankForCopy then - chatHandler:SendSysMessage("You lack permisisions to execute this command.") + local reason = "You lack permisisions to execute this command." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -286,12 +376,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) local accountId = player:GetAccountId() if commandArray[2] == nil then local Data_SQL - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;'); + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;'); if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), oldTickets, oldTickets, CC_STATUS_SUCCESS, "Ticket balance lookup") else oldTickets = Config.freeTickets - CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..Config.freeTickets..', 0) ;') + CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..Config.freeTickets..', 0) ;') + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), 0, Config.freeTickets, CC_STATUS_FREE_CONFIG_TICKET, "Free config ticket granted") end chatHandler:SendSysMessage("You currently have "..oldTickets.." tickets available for CarbonCopy.") cc_resetVariables() @@ -335,12 +427,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then addAccountId = Data_SQL:GetUInt32(0) else - chatHandler:SendSysMessage("Player name not found. Syntax: .carboncopy tickets add $characterName $amount") + local reason = "Player name not found. Syntax: .carboncopy tickets add $characterName $amount" + chatHandler:SendSysMessage(reason) + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..addAccountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..addAccountId..' LIMIT 1;') if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -348,14 +442,17 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if oldTickets >= 1000 or oldTickets < 0 then - chatHandler:SendSysMessage("Too large total amount tickets: "..commandArray[5]..". Max allowed total is +1000. Current value: "..oldTickets) + local reason = "Too large total amount tickets: "..commandArray[5]..". Max allowed total is +1000. Current value: "..oldTickets + chatHandler:SendSysMessage(reason) + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..addAccountId..';') - CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..addAccountId..', '..commandArray[5] + oldTickets..', 0);') + CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..addAccountId..';') + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..addAccountId..', '..commandArray[5] + oldTickets..', 0);') chatHandler:SendSysMessage("GM "..player:GetName().. " has sucessfully used the .carboncopy tickets add command, adding "..commandArray[5].." tickets to the account "..addAccountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[5]), CC_STATUS_SUCCESS, "GM .carboncopy tickets add") cc_resetVariables() return false end @@ -386,12 +483,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then removeAccountId = Data_SQL:GetUInt32(0) else - chatHandler:SendSysMessage("Player name not found. Syntax: .carboncopy tickets remove $characterName $amount") + local reason = "Player name not found. Syntax: .carboncopy tickets remove $characterName $amount" + chatHandler:SendSysMessage(reason) + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..removeAccountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..removeAccountId..' LIMIT 1;') if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -399,14 +498,17 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if oldTickets <= 0 or oldTickets < tonumber(commandArray[5]) then - chatHandler:SendSysMessage("The account does not have enough tickets to remove "..commandArray[5]..". Current value: "..oldTickets) + local reason = "The account does not have enough tickets to remove "..commandArray[5]..". Current value: "..oldTickets + chatHandler:SendSysMessage(reason) + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end - CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..removeAccountId..';') - CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..removeAccountId..', '..oldTickets - commandArray[5]..', 0);') + CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..removeAccountId..';') + CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..removeAccountId..', '..oldTickets - commandArray[5]..', 0);') chatHandler:SendSysMessage("GM "..player:GetName().. " has sucessfully used the .carboncopy tickets remove command, removing "..commandArray[5].." tickets from the account "..removeAccountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets - tonumber(commandArray[5]), CC_STATUS_SUCCESS, "GM .carboncopy tickets remove") cc_resetVariables() return false end @@ -429,13 +531,15 @@ function cc_CopyCharacter(event, player, command, chatHandler) local lookupCharacterName = cc_normalizeCharacterName(lookupNameArg) local Data_SQL = CharDBQuery('SELECT `account` FROM `characters` WHERE `name` = "'..lookupCharacterName..'" LIMIT 1;') if Data_SQL == nil then - chatHandler:SendSysMessage("Character name not found. Check spelling.") + local reason = "Character name not found. Check spelling." + chatHandler:SendSysMessage(reason) + cc_logAdmin(sourceName, sourceGuid, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end local lookupAccountId = Data_SQL:GetUInt32(0) - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..lookupAccountId..' LIMIT 1;') + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..lookupAccountId..' LIMIT 1;') local lookupTickets if Data_SQL ~= nil then lookupTickets = Data_SQL:GetUInt32(0) @@ -444,13 +548,16 @@ function cc_CopyCharacter(event, player, command, chatHandler) end chatHandler:SendSysMessage("CarbonCopy tickets for "..lookupCharacterName.." (account "..lookupAccountId.."): "..lookupTickets) + cc_logAdmin(sourceName, sourceGuid, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, lookupTickets, lookupTickets, CC_STATUS_SUCCESS, "GM .carboncopy tickets lookup") cc_resetVariables() return false end -- check maxLevel if player:GetLevel() > Config.maxLevel then - chatHandler:SendSysMessage("The character you want to copy from is too high level. Max level is "..Config.maxLevel..". Aborting.") + local reason = "The character you want to copy from is too high level. Max level is "..Config.maxLevel..". Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -462,14 +569,18 @@ function cc_CopyCharacter(event, player, command, chatHandler) --check for target character to be on same account local Data_SQL = CharDBQuery('SELECT `account` FROM `characters` WHERE `name` = "'..targetName..'" LIMIT 1;'); if Data_SQL == nil then - chatHandler:SendSysMessage("Name not found. Check spelling. Aborting.") + local reason = "Name not found. Check spelling. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end local targetAccountId = Data_SQL:GetUInt32(0) Data_SQL = nil if targetAccountId ~= accountId then - chatHandler:SendSysMessage("The requested character is not on the same account. Aborting.") + local reason = "The requested character is not on the same account. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -479,27 +590,31 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then newCharacter = Data_SQL:GetUInt32(0) else - chatHandler:SendSysMessage("Name not found. Check spelling. Aborting.") + local reason = "Name not found. Check spelling. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) cc_resetVariables() return false end Data_SQL = nil --check for available tickets - local Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..';'); + local Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';'); local availableTickets local requiredTickets if Data_SQL ~= nil then availableTickets = Data_SQL:GetUInt32(0) Data_SQL = nil else - CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..Config.freeTickets..', 0) ;') + CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..Config.freeTickets..', 0) ;') availableTickets = Config.freeTickets end if Config.ticketCost == "single" then if availableTickets ~= nil and availableTickets <= 0 then - chatHandler:SendSysMessage("You do not have enough Carbon Copy tickets to execute this command. Aborting.") + local reason = "You do not have enough Carbon Copy tickets to execute this command. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -512,12 +627,16 @@ function cc_CopyCharacter(event, player, command, chatHandler) until ticket_Cost[n] ~= nil requiredTickets = ticket_Cost[n] if availableTickets ~= nil and availableTickets <= 0 then - chatHandler:SendSysMessage("You do not have enough Carbon Copy tickets to execute this command. Aborting.") + local reason = "You do not have enough Carbon Copy tickets to execute this command. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end if availableTickets < requiredTickets then - chatHandler:SendSysMessage("You do not have enough Carbon Copy tickets to execute this command. Aborting.") + local reason = "You do not have enough Carbon Copy tickets to execute this command. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -538,12 +657,16 @@ function cc_CopyCharacter(event, player, command, chatHandler) Data_SQL = nil if sourceRace ~= targetRace then - chatHandler:SendSysMessage("The requested character is not the same race as this character. Aborting.") + local reason = "The requested character is not the same race as this character. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end if sourceClass ~= targetClass then - chatHandler:SendSysMessage("The requested character is not the same class as this character. Aborting.") + local reason = "The requested character is not the same class as this character. Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -554,7 +677,9 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then cc_cinematic = Data_SQL:GetUInt16(0) if cc_cinematic == 1 then - chatHandler:SendSysMessage("The requested character has been logged in already (or has already been Carbon Copied). Aborting.") + local reason = "The requested character has been logged in already (or has already been Carbon Copied). Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_cinematic = nil cc_resetVariables() return false @@ -569,7 +694,9 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL ~= nil then cc_online = Data_SQL:GetUInt16(0) if cc_online == 1 then - chatHandler:SendSysMessage("The requested character has been logged in already (or has already been Carbon Copied). Aborting.") + local reason = "The requested character has been logged in already (or has already been Carbon Copied). Aborting." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_online = nil cc_resetVariables() return false @@ -582,7 +709,9 @@ function cc_CopyCharacter(event, player, command, chatHandler) local cc_mapId cc_mapId = player:GetMapId() if not cc_has_value(cc_maps, cc_mapId) then - chatHandler:SendSysMessage("You are not in an allowed map. Try again outside/not in a dungeon.") + local reason = "You are not in an allowed map. Try again outside/not in a dungeon." + chatHandler:SendSysMessage(reason) + cc_logPlayer(sourceName, sourceGuid, targetName, newCharacter, player:GetLevel(), availableTickets, availableTickets, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -598,6 +727,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) --fetch all required variables before 1st yield cc_playerString = player:GetClassAsString(0) + cc_logPlayer(sourceName, sourceGuid, targetName, cc_newCharacter, player:GetLevel(), availableTickets, availableTickets - requiredTickets, CC_STATUS_SUCCESS, "Copy started") PrintInfo("1) The player with GUID "..cc_playerGUID.." has succesfully initiated the .carboncopy command. Target character: "..cc_newCharacter); chatHandler:SendSysMessage("Copy started. You have been charged "..requiredTickets.." ticket(s) for this action. There are "..availableTickets - requiredTickets.." ticket()s left.") local stayMsgEnglish = "STAY logged in for one minute!" @@ -628,10 +758,10 @@ function cc_CopyCharacter(event, player, command, chatHandler) -- deduct tickets if Config.ticketCost == "single" then - local Data_SQL = CharDBQuery('UPDATE `'..Config.customDbName..'`.`carboncopy` SET tickets = tickets -1 WHERE `account_id` = '..accountId..';'); + local Data_SQL = CharDBQuery('UPDATE `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` SET tickets = tickets -1 WHERE `account_id` = '..accountId..';'); Data_SQL = nil elseif Config.ticketCost == "level" then - local Data_SQL = CharDBQuery('UPDATE `'..Config.customDbName..'`.`carboncopy` SET tickets = tickets -'..requiredTickets..' WHERE `account_id` = '..accountId..';'); + local Data_SQL = CharDBQuery('UPDATE `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` SET tickets = tickets -'..requiredTickets..' WHERE `account_id` = '..accountId..';'); Data_SQL = nil end @@ -908,7 +1038,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) end Data_SQL = nil local Data_SQL - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;'); + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;'); if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -924,10 +1054,11 @@ function cc_CopyCharacter(event, player, command, chatHandler) -- the `allow_copy_from_id` column is hardcoded to 0 for now. Only copies to the same account are possible. local Data_SQL - Data_SQL = CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..';'); - Data_SQL = CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0);'); + Data_SQL = CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';'); + Data_SQL = CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0);'); Data_SQL = nil chatHandler:SendSysMessage("GM "..player:GetName().. " has sucessfully used the .addcctickets command, adding "..commandArray[3].." tickets to the account "..accountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin(sourceName, sourceGuid, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[3]), CC_STATUS_SUCCESS, "GM .addcctickets") cc_resetVariables() return false else @@ -960,7 +1091,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) end Data_SQL = nil local Data_SQL - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;'); + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;'); if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -976,10 +1107,11 @@ function cc_CopyCharacter(event, player, command, chatHandler) -- the `allow_copy_from_id` column is hardcoded to 0 for now. Only copies to the same account are possible. local Data_SQL - Data_SQL = CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..';'); - Data_SQL = CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0);'); + Data_SQL = CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';'); + Data_SQL = CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0);'); Data_SQL = nil chatHandler:SendSysMessage("The console has sucessfully used the .addcctickets command, adding "..commandArray[3].." tickets to the account "..accountId.." which belongs to player "..normalisedCharacterName..".") + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[3]), CC_STATUS_SUCCESS, "Console .addcctickets") cc_resetVariables() return false end @@ -989,6 +1121,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) Data_SQL = AuthDBQuery('SELECT `id` FROM `account` WHERE `username` = "'..commandArray[2]..'";') if Data_SQL == nil then PrintError("CCACCOUNTTICKETS to "..commandArray[2].." has failed.") + cc_logAdmin("console", nil, commandArray[2], nil, nil, CC_ACTION_GM_ADD, nil, nil, CC_STATUS_FAILED, "CCACCOUNTTICKETS account not found") cc_resetVariables() return false else @@ -1001,7 +1134,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) return false end - Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`carboncopy` WHERE `account_id` = '..accountId..' LIMIT 1;'); + Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;'); if Data_SQL ~= nil then oldTickets = Data_SQL:GetUInt32(0) else @@ -1010,11 +1143,13 @@ function cc_CopyCharacter(event, player, command, chatHandler) if oldTickets >= 1000 or oldTickets < 0 then chatHandler:SendSysMessage("Too large total amount of tickets: "..commandArray[3]..". Max allowed total is +1000. Current value: "..oldTickets) + cc_logAdmin("console", nil, commandArray[2], nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets, CC_STATUS_FAILED, "CCACCOUNTTICKETS total exceeds max") cc_resetVariables() return false end - CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`carboncopy` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0) ;') + CharDBExecute('REPLACE INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..commandArray[3] + oldTickets..', 0) ;') + cc_logAdmin("console", nil, commandArray[2], nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[3]), CC_STATUS_SUCCESS, "CCACCOUNTTICKETS success") cc_resetVariables() return false elseif player ~= nil then From 3235c112831069efda7ebea06a7f21aac08301c6 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:29:25 +0100 Subject: [PATCH 04/14] Update CarbonCopy.lua --- CarbonCopy.lua | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index ff33590..7d2f614 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -45,7 +45,7 @@ CharDBQuery('CREATE TABLE IF NOT EXISTS `'..Config.customDbName..'`.`'..cc_carbo ..'`account_id` INT(11) NOT NULL, ' ..'`tickets` INT(11) DEFAULT 0, ' ..'`allow_copy_from_id` INT(11) DEFAULT 0, ' - ..'PRIMARY KEY (`account_id`) + ..'PRIMARY KEY (`account_id`)' ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' ) @@ -64,7 +64,7 @@ if cc_enablePlayerLogs then ..'KEY `idx_ccpl_source_guid` (`source_guid`), ' ..'KEY `idx_ccpl_target_guid` (`target_guid`), ' ..'KEY `idx_ccpl_status_code` (`status_code`), ' - ..'KEY `idx_ccpl_created_at` (`created_at`) + ..'KEY `idx_ccpl_created_at` (`created_at`)' ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' ) end @@ -86,7 +86,7 @@ if cc_enableAdminLogs then ..'KEY `idx_ccal_target_guid` (`target_guid`), ' ..'KEY `idx_ccal_action_code` (`action_code`), ' ..'KEY `idx_ccal_status_code` (`status_code`), ' - ..'KEY `idx_ccal_created_at` (`created_at`) + ..'KEY `idx_ccal_created_at` (`created_at`)' ..') ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci ROW_FORMAT=DEFAULT;' ) end @@ -252,7 +252,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';') CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..commandArray[5] + oldTickets..', 0);') chatHandler:SendSysMessage("The console has sucessfully used the .carboncopy tickets add command, adding "..commandArray[5].." tickets to the account "..accountId.." which belongs to player "..normalisedCharacterName..".") - cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets add") + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_ADD, oldTickets, oldTickets + tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets add "..normalisedCharacterName.." "..commandArray[5]) cc_resetVariables() return false end @@ -302,7 +302,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) CharDBQuery('DELETE FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..';') CharDBQuery('INSERT INTO `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` VALUES ('..accountId..', '..oldTickets - commandArray[5]..', 0);') chatHandler:SendSysMessage("The console has sucessfully used the .carboncopy tickets remove command, removing "..commandArray[5].." tickets from the account "..accountId.." which belongs to player "..normalisedCharacterName..".") - cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets - tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets remove") + cc_logAdmin("console", nil, normalisedCharacterName, nil, nil, CC_ACTION_GM_REMOVE, oldTickets, oldTickets - tonumber(commandArray[5]), CC_STATUS_SUCCESS, "Console .carboncopy tickets remove "..normalisedCharacterName.." "..commandArray[5]) cc_resetVariables() return false end @@ -342,7 +342,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) end chatHandler:SendSysMessage("CarbonCopy tickets for "..lookupCharacterName.." (account "..lookupAccountId.."): "..lookupTickets) - cc_logAdmin("console", nil, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, lookupTickets, lookupTickets, CC_STATUS_SUCCESS, "Console .carboncopy tickets lookup") + cc_logAdmin("console", nil, lookupCharacterName, nil, nil, CC_ACTION_GM_LOOKUP, lookupTickets, lookupTickets, CC_STATUS_SUCCESS, "Console .carboncopy tickets lookup "..lookupCharacterName) cc_resetVariables() return false end From beb1abcb733149a7aaccacc51e28b60775ff107e Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:29:29 +0100 Subject: [PATCH 05/14] Create migrate_single_table_to_innodb.sh --- migrate_single_table_to_innodb.sh | 236 ++++++++++++++++++++++++++++++ 1 file changed, 236 insertions(+) create mode 100644 migrate_single_table_to_innodb.sh diff --git a/migrate_single_table_to_innodb.sh b/migrate_single_table_to_innodb.sh new file mode 100644 index 0000000..c206a8e --- /dev/null +++ b/migrate_single_table_to_innodb.sh @@ -0,0 +1,236 @@ +#!/usr/bin/env bash +set -euo pipefail + +# Migrate exactly one table from MyISAM to InnoDB. +# Defaults target CarbonCopy table in ac_eluna. +# +# Examples: +# ./migrate_single_table_to_innodb.sh -u +# ./migrate_single_table_to_innodb.sh -u root -p 'secret' +# ./migrate_single_table_to_innodb.sh -u root -d ac_eluna -t carboncopy_player_logs +# ./migrate_single_table_to_innodb.sh -u root --dry-run + +HOST="127.0.0.1" +PORT="3306" +USER="" +PASSWORD="" +SOCKET="" +DB_NAME="ac_eluna" +TABLE_NAME="carboncopy" +BACKUP_DIR="./db_backups" +SKIP_BACKUP=0 +DRY_RUN=0 + +usage() { + cat <<'EOF' +Usage: + migrate_single_table_to_innodb.sh -u USER [options] + +Required: + -u, --user USER Database user + +Options: + -h, --host HOST Database host (default: 127.0.0.1) + -P, --port PORT Database port (default: 3306) + -p, --password PASSWORD Database password (if omitted, prompts securely) + -S, --socket PATH MySQL socket path (optional) + -d, --database NAME Database/schema name (default: ac_eluna) + -t, --table NAME Table name (default: carboncopy) + -b, --backup-dir PATH Backup output folder (default: ./db_backups) + --skip-backup Skip mysqldump backup step + --dry-run Print ALTER statement only + --help Show this help +EOF +} + +require_cmd() { + if ! command -v "$1" >/dev/null 2>&1; then + echo "ERROR: Required command not found: $1" >&2 + exit 1 + fi +} + +mysql_base_args() { + local args=() + args+=("-h" "$HOST" "-P" "$PORT" "-u" "$USER" "--batch" "--raw" "--silent") + if [[ -n "$SOCKET" ]]; then + args+=("-S" "$SOCKET") + fi + printf '%s\n' "${args[@]}" +} + +mysql_exec() { + local sql="$1" + MYSQL_PWD="$PASSWORD" mysql $(mysql_base_args | tr '\n' ' ') -e "$sql" +} + +mysqldump_base_args() { + local args=() + args+=("-h" "$HOST" "-P" "$PORT" "-u" "$USER") + if [[ -n "$SOCKET" ]]; then + args+=("-S" "$SOCKET") + fi + printf '%s\n' "${args[@]}" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + -u|--user) + USER="${2:-}" + shift 2 + ;; + -h|--host) + HOST="${2:-}" + shift 2 + ;; + -P|--port) + PORT="${2:-}" + shift 2 + ;; + -p|--password) + PASSWORD="${2:-}" + shift 2 + ;; + -S|--socket) + SOCKET="${2:-}" + shift 2 + ;; + -d|--database) + DB_NAME="${2:-}" + shift 2 + ;; + -t|--table) + TABLE_NAME="${2:-}" + shift 2 + ;; + -b|--backup-dir) + BACKUP_DIR="${2:-}" + shift 2 + ;; + --skip-backup) + SKIP_BACKUP=1 + shift + ;; + --dry-run) + DRY_RUN=1 + shift + ;; + --help) + usage + exit 0 + ;; + *) + echo "ERROR: Unknown argument: $1" >&2 + usage + exit 1 + ;; + esac +done + +if [[ -z "$USER" ]]; then + echo "ERROR: --user is required." >&2 + usage + exit 1 +fi + +if [[ -z "$PASSWORD" ]]; then + read -r -s -p "DB password for $USER: " PASSWORD + echo +fi + +require_cmd mysql +if [[ $SKIP_BACKUP -eq 0 ]]; then + require_cmd mysqldump +fi + +echo "Checking DB connectivity..." +mysql_exec "SELECT 1;" >/dev/null + +TABLE_EXISTS_SQL=" +SELECT COUNT(*) +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='${DB_NAME//\'/\'\'}' + AND TABLE_NAME='${TABLE_NAME//\'/\'\'}'; +" +EXISTS_COUNT="$(mysql_exec "$TABLE_EXISTS_SQL" | tail -n 1)" + +if [[ "$EXISTS_COUNT" != "1" ]]; then + echo "ERROR: Table not found: ${DB_NAME}.${TABLE_NAME}" >&2 + exit 1 +fi + +ENGINE_SQL=" +SELECT ENGINE +FROM information_schema.TABLES +WHERE TABLE_SCHEMA='${DB_NAME//\'/\'\'}' + AND TABLE_NAME='${TABLE_NAME//\'/\'\'}' +LIMIT 1; +" +CURRENT_ENGINE="$(mysql_exec "$ENGINE_SQL" | tail -n 1)" + +ALTER_SQL="ALTER TABLE \`${DB_NAME}\`.\`${TABLE_NAME}\` ENGINE=InnoDB ROW_FORMAT=DEFAULT;" + +echo "Target table: ${DB_NAME}.${TABLE_NAME}" +echo "Current engine: ${CURRENT_ENGINE}" + +if [[ "$CURRENT_ENGINE" == "InnoDB" ]]; then + echo "Already InnoDB. Nothing to do." + exit 0 +fi + +if [[ "$CURRENT_ENGINE" != "MyISAM" ]]; then + echo "WARNING: Current engine is ${CURRENT_ENGINE} (not MyISAM)." + echo "Will still execute requested conversion to InnoDB." +fi + +if [[ $DRY_RUN -eq 1 ]]; then + echo "Dry run statement:" + echo "$ALTER_SQL" + exit 0 +fi + +if [[ $SKIP_BACKUP -eq 0 ]]; then + mkdir -p "$BACKUP_DIR" + TS="$(date +%Y%m%d_%H%M%S)" + BACKUP_FILE="$BACKUP_DIR/${DB_NAME}_${TABLE_NAME}_pre_innodb_${TS}.sql" + DUMP_STDERR_FILE="$(mktemp)" + echo "Creating backup: $BACKUP_FILE" + + DUMP_EXIT=0 + MYSQL_PWD="$PASSWORD" mysqldump $(mysqldump_base_args | tr '\n' ' ') \ + --databases "$DB_NAME" --tables "$TABLE_NAME" \ + --routines --events --triggers --hex-blob --single-transaction \ + > "$BACKUP_FILE" 2>"$DUMP_STDERR_FILE" || DUMP_EXIT=$? + + # Always print any stderr so the user sees it. + if [[ -s "$DUMP_STDERR_FILE" ]]; then + cat "$DUMP_STDERR_FILE" >&2 + fi + + # Treat as failure if mysqldump exited non-zero OR stderr contains "Error:". + # mysqldump can exit 0 on privilege warnings while still printing errors. + BACKUP_OK=1 + if [[ $DUMP_EXIT -ne 0 ]] || grep -qi 'error:' "$DUMP_STDERR_FILE"; then + BACKUP_OK=0 + fi + rm -f "$DUMP_STDERR_FILE" + + if [[ $BACKUP_OK -eq 0 ]]; then + echo "ERROR: Backup failed. Aborting — engine not changed." >&2 + rm -f "$BACKUP_FILE" + exit 1 + fi +fi + +echo "Running conversion..." +mysql_exec "$ALTER_SQL" >/dev/null + +NEW_ENGINE="$(mysql_exec "$ENGINE_SQL" | tail -n 1)" +echo "New engine: ${NEW_ENGINE}" + +if [[ "$NEW_ENGINE" != "InnoDB" ]]; then + echo "ERROR: Conversion did not complete as expected." >&2 + exit 2 +fi + +echo "Done: ${DB_NAME}.${TABLE_NAME} is now InnoDB." From bfecca39c2f19bd2982d300017d453046d2d231e Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:46:24 +0100 Subject: [PATCH 06/14] Update CarbonCopy.lua --- CarbonCopy.lua | 51 +++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 46 insertions(+), 5 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 7d2f614..54f34f9 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -38,6 +38,9 @@ local cc_enableAdminLogs = Config.enableAdminLogs ~= false cc_oldItemGuids = {} cc_newItemGuids = {} cc_scriptIsBusy = 0 +cc_newCharacterName = nil +cc_copyTicketsBefore = nil +cc_copyTicketsAfter = nil -- If module runs for the first time, create the db specified in Config.dbName and add the "carboncopy" table to it. CharDBQuery('CREATE DATABASE IF NOT EXISTS `'..Config.customDbName..'`;'); @@ -374,6 +377,9 @@ function cc_CopyCharacter(event, player, command, chatHandler) -- print the available tickets local accountId = player:GetAccountId() + local _ticketsAtEntrySQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;') + local ticketsAtEntry = _ticketsAtEntrySQL ~= nil and _ticketsAtEntrySQL:GetUInt32(0) or Config.freeTickets + _ticketsAtEntrySQL = nil if commandArray[2] == nil then local Data_SQL Data_SQL = CharDBQuery('SELECT `tickets` FROM `'..Config.customDbName..'`.`'..cc_carboncopyTable..'` WHERE `account_id` = '..accountId..' LIMIT 1;'); @@ -557,7 +563,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) if player:GetLevel() > Config.maxLevel then local reason = "The character you want to copy from is too high level. Max level is "..Config.maxLevel..". Aborting." chatHandler:SendSysMessage(reason) - cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) + cc_logPlayer(sourceName, sourceGuid, nil, nil, player:GetLevel(), ticketsAtEntry, ticketsAtEntry, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -571,7 +577,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) if Data_SQL == nil then local reason = "Name not found. Check spelling. Aborting." chatHandler:SendSysMessage(reason) - cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), ticketsAtEntry, ticketsAtEntry, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -580,7 +586,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) if targetAccountId ~= accountId then local reason = "The requested character is not on the same account. Aborting." chatHandler:SendSysMessage(reason) - cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), ticketsAtEntry, ticketsAtEntry, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -592,7 +598,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) else local reason = "Name not found. Check spelling. Aborting." chatHandler:SendSysMessage(reason) - cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), nil, nil, CC_STATUS_FAILED, reason) + cc_logPlayer(sourceName, sourceGuid, targetName, nil, player:GetLevel(), ticketsAtEntry, ticketsAtEntry, CC_STATUS_FAILED, reason) cc_resetVariables() return false end @@ -721,6 +727,9 @@ function cc_CopyCharacter(event, player, command, chatHandler) Ban(1, targetName, 15, "CarbonCopy", "CarbonCopy" ) cc_scriptIsBusy = 1 cc_newCharacter = newCharacter + cc_newCharacterName = targetName + cc_copyTicketsBefore = availableTickets + cc_copyTicketsAfter = availableTickets - requiredTickets -- save the source character to db to prevent recent changes from being not applied player:SaveToDB() @@ -1207,9 +1216,15 @@ function cc_fixItems() end end - GetPlayerByGUID(cc_playerGUID):SendBroadcastMessage("Character copy done. You can log out now.") + local copyingPlayer = GetPlayerByGUID(cc_playerGUID) + if copyingPlayer ~= nil then + copyingPlayer:SendBroadcastMessage("Character copy done. You can log out now.") + end PrintInfo("2) Item enchants/gems copied for new character with GUID "..cc_newCharacter); cc_newCharacter = 0 + cc_newCharacterName = nil + cc_copyTicketsBefore = nil + cc_copyTicketsAfter = nil cc_oldItemGuids = {} cc_newItemGuids = {} cc_scriptIsBusy = 0 @@ -1280,5 +1295,31 @@ function cc_resumeSubRoutine(eventId, delay, repeats) end local PLAYER_EVENT_ON_COMMAND = 42 +local PLAYER_EVENT_ON_LOGOUT = 4 + +-- If the player who initiated a copy disconnects mid-copy, release the busy lock. +-- The coroutine will still finish its DB work, but the lock would never reset otherwise. +local function cc_OnPlayerLogout(event, player) + if cc_scriptIsBusy ~= 0 and cc_playerGUID ~= nil then + local guid = tonumber(tostring(player:GetGUID())) + if guid == cc_playerGUID then + PrintInfo("CarbonCopy: Source player disconnected mid-copy (GUID "..cc_playerGUID.."). Releasing busy lock.") + cc_logPlayer( + player:GetName(), guid, + cc_newCharacterName, cc_newCharacter ~= 0 and cc_newCharacter or nil, + player:GetLevel(), + cc_copyTicketsBefore, cc_copyTicketsAfter, + CC_STATUS_FAILED, "Source player disconnected mid-copy" + ) + cc_scriptIsBusy = 0 + cc_playerGUID = nil + cc_newCharacterName = nil + cc_copyTicketsBefore = nil + cc_copyTicketsAfter = nil + end + end +end + -- function to be called when the command hook fires RegisterPlayerEvent(PLAYER_EVENT_ON_COMMAND, cc_CopyCharacter) +RegisterPlayerEvent(PLAYER_EVENT_ON_LOGOUT, cc_OnPlayerLogout) From db34dc448f9dd79e25b0038960a643ab5d3ff873 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:50:44 +0100 Subject: [PATCH 07/14] Update CarbonCopy.lua --- CarbonCopy.lua | 132 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 54f34f9..0d66e25 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -162,6 +162,118 @@ local function cc_logAdmin(sourceName, sourceGuid, targetName, targetGuid, targe ) end +-- cc_execLogsCommand: shared handler for ".carboncopy logs" from console or GM. +-- Flags (all optional, any order after "carboncopy logs"): +-- --source name|guid filter by source character name or GUID +-- --target name|guid filter by target character name or GUID +-- --day YYYY-MM-DD show only entries from this date (server time) +-- --limit N max rows returned, 1-100 (default 20) +-- --oldest sort oldest-first (default newest-first) +local function cc_execLogsCommand(chatHandler, commandArray, startIdx) + local filterSource = nil + local filterTarget = nil + local filterDay = nil + local limitN = 20 + local orderOldest = false + + local i = startIdx + while i <= #commandArray do + local flag = string.lower(commandArray[i]) + if flag == "--source" then + i = i + 1 + if commandArray[i] ~= nil then filterSource = commandArray[i] end + elseif flag == "--target" then + i = i + 1 + if commandArray[i] ~= nil then filterTarget = commandArray[i] end + elseif flag == "--day" then + i = i + 1 + if commandArray[i] ~= nil then filterDay = commandArray[i] end + elseif flag == "--limit" then + i = i + 1 + if commandArray[i] ~= nil then + local n = tonumber(commandArray[i]) + if n ~= nil and n >= 1 and n <= 100 then + limitN = math.floor(n) + else + chatHandler:SendSysMessage("--limit must be 1-100. Using default 20.") + end + end + elseif flag == "--oldest" then + orderOldest = true + else + chatHandler:SendSysMessage("Unknown flag: "..commandArray[i]..". Ignored.") + end + i = i + 1 + end + + local conditions = {} + + if filterSource ~= nil then + if tonumber(filterSource) ~= nil then + table.insert(conditions, '`source_guid` = '..math.floor(tonumber(filterSource))) + else + table.insert(conditions, '`source_name` = '..cc_sqlQuote(cc_normalizeCharacterName(filterSource))) + end + end + + if filterTarget ~= nil then + if tonumber(filterTarget) ~= nil then + table.insert(conditions, '`target_guid` = '..math.floor(tonumber(filterTarget))) + else + table.insert(conditions, '`target_name` = '..cc_sqlQuote(cc_normalizeCharacterName(filterTarget))) + end + end + + if filterDay ~= nil then + if filterDay:match("^%d%d%d%d%-%d%d%-%d%d$") then + table.insert(conditions, 'DATE(`created_at`) = '..cc_sqlQuote(filterDay)) + else + chatHandler:SendSysMessage("--day must be YYYY-MM-DD. Day filter ignored.") + end + end + + local whereClause = (#conditions > 0) and (" WHERE "..table.concat(conditions, " AND ")) or "" + local orderDir = orderOldest and "ASC" or "DESC" + + local sql = 'SELECT `source_name`,`source_guid`,`source_level`,`target_name`,`target_guid`,' + ..'`tickets_before`,`tickets_after`,`status_code`,`reason`,`created_at`' + ..' FROM `'..Config.customDbName..'`.`'..cc_playerLogsTable..'`' + ..whereClause + ..' ORDER BY `created_at` '..orderDir + ..' LIMIT '..limitN..';' + + local rows = CharDBQuery(sql) + if rows == nil then + chatHandler:SendSysMessage("No player log entries found.") + return + end + + local count = 0 + repeat + local sName = rows:GetString(0) + local sGuid = rows:GetUInt32(1) + local sLevel = rows:GetUInt8(2) + local tName = rows:GetString(3) + local tGuid = rows:GetUInt32(4) + local tickBef = rows:GetInt32(5) + local tickAft = rows:GetInt32(6) + local reason = rows:GetString(8) + local createdAt = rows:GetString(9) + + local line = "["..createdAt.."] "..sName.." ("..sGuid..") [lv"..sLevel.."]" + if tName ~= nil and tName ~= "" then + line = line.." -> "..tName.." ("..tGuid..")" + end + line = line.." | tickets("..tickBef.."=>"..tickAft..")" + line = line.." | "..reason + + chatHandler:SendSysMessage(line) + count = count + 1 + until not rows:NextRow() + + chatHandler:SendSysMessage("--- "..count.." result(s) | limit "..limitN.." | order: "..(orderOldest and "oldest" or "newest").." ---") +end + function cc_CopyCharacter(event, player, command, chatHandler) local commandArray = cc_splitString(command) @@ -350,6 +462,13 @@ function cc_CopyCharacter(event, player, command, chatHandler) return false end + if player == nil and ccSubCommandConsole == "logs" then + chatHandler:SendSysMessage("Syntax: .carboncopy logs [--source name|guid] [--target name|guid] [--day YYYY-MM-DD] [--limit N] [--oldest]") + cc_execLogsCommand(chatHandler, commandArray, 3) + cc_resetVariables() + return false + end + if player == nil then local reason = "This command can not be run from the console, but only from the character to copy." chatHandler:SendSysMessage(reason) @@ -371,6 +490,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) chatHandler:SendSysMessage("Syntax: .carboncopy tickets lookup $characterName") chatHandler:SendSysMessage("Syntax: .carboncopy tickets add $characterName $amount") chatHandler:SendSysMessage("Syntax: .carboncopy tickets remove $characterName $amount") + chatHandler:SendSysMessage("Syntax: .carboncopy logs [--source name|guid] [--target name|guid] [--day YYYY-MM-DD] [--limit N] [--oldest]") cc_resetVariables() return false end @@ -559,6 +679,18 @@ function cc_CopyCharacter(event, player, command, chatHandler) return false end + if ccSubCommand == "logs" then + if player:GetGMRank() < Config.minGMRankForTickets then + chatHandler:SendSysMessage("You lack permissions to execute this command.") + cc_resetVariables() + return false + end + chatHandler:SendSysMessage("Syntax: .carboncopy logs [--source name|guid] [--target name|guid] [--day YYYY-MM-DD] [--limit N] [--oldest]") + cc_execLogsCommand(chatHandler, commandArray, 3) + cc_resetVariables() + return false + end + -- check maxLevel if player:GetLevel() > Config.maxLevel then local reason = "The character you want to copy from is too high level. Max level is "..Config.maxLevel..". Aborting." From 3e7ff242bcd74477d56a7c7eb9bb0e1276179a02 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 16:56:15 +0100 Subject: [PATCH 08/14] Update CarbonCopy.lua --- CarbonCopy.lua | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 0d66e25..93a7cec 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -242,9 +242,23 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) ..' ORDER BY `created_at` '..orderDir ..' LIMIT '..limitN..';' + -- Build active-filter summary for header + local activeFilters = {} + if filterSource ~= nil then table.insert(activeFilters, "source:"..filterSource) end + if filterTarget ~= nil then table.insert(activeFilters, "target:"..filterTarget) end + if filterDay ~= nil then table.insert(activeFilters, "day:"..filterDay) end + local filterSummary = (#activeFilters > 0) and table.concat(activeFilters, " ") or "none" + + -- Header + chatHandler:SendSysMessage("========== CarbonCopy Player Logs ==========") + chatHandler:SendSysMessage("Filters: "..filterSummary.." | limit:"..limitN.." | "..(orderOldest and "oldest first" or "newest first")) + chatHandler:SendSysMessage(" Time (MM-DD HH:MM) Source (guid) [lv] -> Target (guid) tix:before=>after Reason") + chatHandler:SendSysMessage("--------------------------------------------") + local rows = CharDBQuery(sql) if rows == nil then - chatHandler:SendSysMessage("No player log entries found.") + chatHandler:SendSysMessage(" (no entries found matching the filters)") + chatHandler:SendSysMessage("============================================") return end @@ -260,18 +274,25 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) local reason = rows:GetString(8) local createdAt = rows:GetString(9) - local line = "["..createdAt.."] "..sName.." ("..sGuid..") [lv"..sLevel.."]" + -- Shorten timestamp to "MM-DD HH:MM" from "YYYY-MM-DD HH:MM:SS" + local timeStr = createdAt:sub(6, 16) + + local srcPart = sName.." ("..sGuid..") [lv"..sLevel.."]" + + local tgtPart = "" if tName ~= nil and tName ~= "" then - line = line.." -> "..tName.." ("..tGuid..")" + tgtPart = " -> "..tName.." ("..tGuid..")" end - line = line.." | tickets("..tickBef.."=>"..tickAft..")" - line = line.." | "..reason - chatHandler:SendSysMessage(line) + local tickPart = " tix:"..tickBef.."=>"..tickAft + + chatHandler:SendSysMessage("["..timeStr.."] "..srcPart..tgtPart..tickPart.." "..reason) count = count + 1 until not rows:NextRow() - chatHandler:SendSysMessage("--- "..count.." result(s) | limit "..limitN.." | order: "..(orderOldest and "oldest" or "newest").." ---") + chatHandler:SendSysMessage("--------------------------------------------") + chatHandler:SendSysMessage(count.." result(s) | limit "..limitN.." | "..(orderOldest and "oldest first" or "newest first")) + chatHandler:SendSysMessage("============================================") end function cc_CopyCharacter(event, player, command, chatHandler) From 6c04b8f23cddde1b2862fca8a4d064381c02063f Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 17:02:17 +0100 Subject: [PATCH 09/14] Update CarbonCopy.lua --- CarbonCopy.lua | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 93a7cec..3350e63 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -274,19 +274,27 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) local reason = rows:GetString(8) local createdAt = rows:GetString(9) - -- Shorten timestamp to "MM-DD HH:MM" from "YYYY-MM-DD HH:MM:SS" - local timeStr = createdAt:sub(6, 16) + -- Shorten timestamp to "YYYY-MM-DD HH:MM" from "YYYY-MM-DD HH:MM:SS" + local timeStr = createdAt:sub(1, 16) - local srcPart = sName.." ("..sGuid..") [lv"..sLevel.."]" + local srcPart = sName.." (GUID: "..sGuid..") [lv"..sLevel.."]" local tgtPart = "" if tName ~= nil and tName ~= "" then - tgtPart = " -> "..tName.." ("..tGuid..")" + tgtPart = " | "..tName.." (GUID: "..tGuid..")" end - local tickPart = " tix:"..tickBef.."=>"..tickAft + local tickPart + if tickBef == tickAft then + tickPart = tostring(tickBef) + else + tickPart = tickBef.." => "..tickAft + end + + local statusCode = rows:GetUInt8(7) + local reasonPart = reason.." (Code: "..statusCode..")" - chatHandler:SendSysMessage("["..timeStr.."] "..srcPart..tgtPart..tickPart.." "..reason) + chatHandler:SendSysMessage(timeStr.." | "..srcPart..tgtPart.." | "..tickPart.." | "..reasonPart) count = count + 1 until not rows:NextRow() From e2251d96daa909d61fd0ab8e8d8cc0b296540c16 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 17:06:54 +0100 Subject: [PATCH 10/14] Update CarbonCopy.lua --- CarbonCopy.lua | 41 ++++++++++++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 9 deletions(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 3350e63..c0ccb53 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -163,16 +163,26 @@ local function cc_logAdmin(sourceName, sourceGuid, targetName, targetGuid, targe end -- cc_execLogsCommand: shared handler for ".carboncopy logs" from console or GM. --- Flags (all optional, any order after "carboncopy logs"): --- --source name|guid filter by source character name or GUID --- --target name|guid filter by target character name or GUID --- --day YYYY-MM-DD show only entries from this date (server time) --- --limit N max rows returned, 1-100 (default 20) --- --oldest sort oldest-first (default newest-first) local function cc_execLogsCommand(chatHandler, commandArray, startIdx) + -- Help subcommand + local firstArg = commandArray[startIdx] and string.lower(commandArray[startIdx]) or nil + if firstArg == "help" or firstArg == "--help" then + chatHandler:SendSysMessage("===== .carboncopy logs flags (all optional) =====") + chatHandler:SendSysMessage(" --source name|guid filter by source name or GUID") + chatHandler:SendSysMessage(" --target name|guid filter by target name or GUID") + chatHandler:SendSysMessage(" --day YYYY-MM-DD entries from this date only") + chatHandler:SendSysMessage(" --code N filter by status code:") + chatHandler:SendSysMessage(" 0=free ticket 1=success 2=failed") + chatHandler:SendSysMessage(" --limit N max rows 1-100 (default 20)") + chatHandler:SendSysMessage(" --oldest sort oldest-first (default: newest)") + chatHandler:SendSysMessage("================================================") + return + end + local filterSource = nil local filterTarget = nil local filterDay = nil + local filterCode = nil local limitN = 20 local orderOldest = false @@ -188,6 +198,16 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) elseif flag == "--day" then i = i + 1 if commandArray[i] ~= nil then filterDay = commandArray[i] end + elseif flag == "--code" then + i = i + 1 + if commandArray[i] ~= nil then + local n = tonumber(commandArray[i]) + if n ~= nil and n >= 0 then + filterCode = math.floor(n) + else + chatHandler:SendSysMessage("--code must be a non-negative number. Code filter ignored.") + end + end elseif flag == "--limit" then i = i + 1 if commandArray[i] ~= nil then @@ -201,7 +221,7 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) elseif flag == "--oldest" then orderOldest = true else - chatHandler:SendSysMessage("Unknown flag: "..commandArray[i]..". Ignored.") + chatHandler:SendSysMessage("Unknown flag: "..commandArray[i]..". Use 'help' or '--help' for usage.") end i = i + 1 end @@ -232,6 +252,10 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) end end + if filterCode ~= nil then + table.insert(conditions, '`status_code` = '..filterCode) + end + local whereClause = (#conditions > 0) and (" WHERE "..table.concat(conditions, " AND ")) or "" local orderDir = orderOldest and "ASC" or "DESC" @@ -247,6 +271,7 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) if filterSource ~= nil then table.insert(activeFilters, "source:"..filterSource) end if filterTarget ~= nil then table.insert(activeFilters, "target:"..filterTarget) end if filterDay ~= nil then table.insert(activeFilters, "day:"..filterDay) end + if filterCode ~= nil then table.insert(activeFilters, "code:"..filterCode) end local filterSummary = (#activeFilters > 0) and table.concat(activeFilters, " ") or "none" -- Header @@ -492,7 +517,6 @@ function cc_CopyCharacter(event, player, command, chatHandler) end if player == nil and ccSubCommandConsole == "logs" then - chatHandler:SendSysMessage("Syntax: .carboncopy logs [--source name|guid] [--target name|guid] [--day YYYY-MM-DD] [--limit N] [--oldest]") cc_execLogsCommand(chatHandler, commandArray, 3) cc_resetVariables() return false @@ -714,7 +738,6 @@ function cc_CopyCharacter(event, player, command, chatHandler) cc_resetVariables() return false end - chatHandler:SendSysMessage("Syntax: .carboncopy logs [--source name|guid] [--target name|guid] [--day YYYY-MM-DD] [--limit N] [--oldest]") cc_execLogsCommand(chatHandler, commandArray, 3) cc_resetVariables() return false From 4a18b5f0ebe00f03ad8533b1cd17accf98feebbe Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 17:08:24 +0100 Subject: [PATCH 11/14] Update README.md --- README.md | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) diff --git a/README.md b/README.md index 0c9fa3b..edafee7 100644 --- a/README.md +++ b/README.md @@ -33,14 +33,39 @@ There a showcase of the video "[CarbonCopy Feature](https://youtu.be/zDdODWPlbLU - `.carboncopy tickets lookup CharacterName` - See how many tickets a character has. - `.carboncopy tickets add CharacterName Amount` - Add tickets to a character's account. - `.carboncopy tickets remove CharacterName Amount` - Remove tickets from a character's account. +- `.carboncopy logs help` or `.carboncopy logs --help` - Show logs command flags. +- `.carboncopy logs` - Show player copy logs (newest first, limit 20). +- `.carboncopy logs --source nameOrGuid --target nameOrGuid --day YYYY-MM-DD --code N --limit N --oldest` - Filter player copy logs. - `.addcctickets help` - [Kept for: Legacy/Compatibility] - `.addcctickets CharacterName Amount`- [Kept for: Legacy/Compatibility] Add tickets to a character's account. - `CCACCOUNTTICKETS accountName amount` - Used by [SOAP](https://www.azerothcore.org/wiki/remote-access#soap) for [acore-cms](https://github.com/azerothcore/acore-cms). +### Logs Command Flags + +All flags are optional and can be combined: + +- `--source name|guid` - Filter by source character name or GUID. +- `--target name|guid` - Filter by target character name or GUID. +- `--day YYYY-MM-DD` - Show entries from this date only. +- `--code N` - Filter by status code (`0=free ticket`, `1=success`, `2=failed`). +- `--limit N` - Max rows returned (`1-100`, default `20`). +- `--oldest` - Sort oldest first (default is newest first). + +Output format: + +- `YYYY-MM-DD HH:MM | source_name (GUID: source_guid) [lvX] | target_name (GUID: target_guid) | tickets | reason (Code: N)` +- If target is empty, the target section is omitted. +- Tickets are shown as `before => after` when changed, or just a single number when unchanged. + ## Configuration ``` customDbName = "ac_eluna" -- Name of the database schema used for CarbonCopy data. +carboncopyTableName = "carboncopy" -- Main ticket table name. +playerLogsTableName = "carboncopy_player_logs" -- Player logs table name. +adminLogsTableName = "carboncopy_admin_logs" -- Admin logs table name. +enablePlayerLogs = true -- Enable inserts into player logs table. +enableAdminLogs = true -- Enable inserts into admin logs table. minGMRankForCopy = 0 -- Minimum GM rank required to use .carboncopy. minGMRankForTickets = 2 -- Minimum GM rank required to add or remove tickets. freeTickets = 4 -- Tickets granted when an account uses CarbonCopy for the first time. @@ -74,6 +99,9 @@ You need to grant account related tickets in the `carboncopy` table: - `tickets` is the number of times an account can copy a character. - `allow_copy_from_id` is reserved for future use. +If `enablePlayerLogs` is true, player copy attempts are recorded in `playerLogsTableName`. +If `enableAdminLogs` is true, GM/console ticket actions are recorded in `adminLogsTableName`. + You can also grant tickets from console or SOAP: - `CCACCOUNTTICKETS accountName amount` From 3c0235ecf8d477a08598d1cd92f23b640498fdcc Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Tue, 12 May 2026 17:23:11 +0100 Subject: [PATCH 12/14] Update CarbonCopy.lua --- CarbonCopy.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index c0ccb53..6d5e72a 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -185,6 +185,7 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) local filterCode = nil local limitN = 20 local orderOldest = false + local parseError = false local i = startIdx while i <= #commandArray do @@ -222,10 +223,16 @@ local function cc_execLogsCommand(chatHandler, commandArray, startIdx) orderOldest = true else chatHandler:SendSysMessage("Unknown flag: "..commandArray[i]..". Use 'help' or '--help' for usage.") + parseError = true + break end i = i + 1 end + if parseError then + return + end + local conditions = {} if filterSource ~= nil then From 4d1f020cb08860b8fab23defb22914bf5711c41f Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Thu, 14 May 2026 18:53:26 +0100 Subject: [PATCH 13/14] Add command to display config values --- CarbonCopy.lua | 75 ++++++++++++++++++++++++++++++++++++++++++++++++++ README.md | 1 + 2 files changed, 76 insertions(+) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 6d5e72a..63ccc22 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -162,6 +162,62 @@ local function cc_logAdmin(sourceName, sourceGuid, targetName, targetGuid, targe ) end +local function cc_sortedKeys(tbl) + local keys = {} + for key in pairs(tbl) do + table.insert(keys, key) + end + + table.sort(keys, function(a, b) + local typeA = type(a) + local typeB = type(b) + if typeA == typeB then + if typeA == "number" then + return a < b + end + return tostring(a) < tostring(b) + end + + return typeA < typeB + end) + + return keys +end + +local function cc_sendTableDump(chatHandler, title, tbl) + chatHandler:SendSysMessage(title) + for _, key in ipairs(cc_sortedKeys(tbl)) do + chatHandler:SendSysMessage(" ["..tostring(key).."] = "..tostring(tbl[key])) + end +end + +local function cc_formatDisplayValue(value) + if type(value) ~= "string" then + return tostring(value) + end + + local compact = value:gsub("%s+", " ") + compact = compact:gsub("^%s+", "") + compact = compact:gsub("%s+$", "") + return compact +end + +local function cc_execConfigCommand(chatHandler) + chatHandler:SendSysMessage("===== CarbonCopy configuration =====") + + for _, key in ipairs(cc_sortedKeys(Config)) do + local value = Config[key] + if type(value) ~= "table" then + chatHandler:SendSysMessage(" "..tostring(key).." = "..cc_formatDisplayValue(value)) + end + end + + cc_sendTableDump(chatHandler, " cc_maps:", cc_maps) + cc_sendTableDump(chatHandler, " ticket_Cost:", ticket_Cost) + + chatHandler:SendSysMessage("===================================") +end + -- cc_execLogsCommand: shared handler for ".carboncopy logs" from console or GM. local function cc_execLogsCommand(chatHandler, commandArray, startIdx) -- Help subcommand @@ -529,6 +585,12 @@ function cc_CopyCharacter(event, player, command, chatHandler) return false end + if player == nil and (ccSubCommandConsole == "config" or ccSubCommandConsole == "settings") then + cc_execConfigCommand(chatHandler) + cc_resetVariables() + return false + end + if player == nil then local reason = "This command can not be run from the console, but only from the character to copy." chatHandler:SendSysMessage(reason) @@ -547,6 +609,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) -- provide syntax help if commandArray[2] == "help" then chatHandler:SendSysMessage("Syntax: .carboncopy $newCharacterName") + chatHandler:SendSysMessage("Syntax: .carboncopy config") chatHandler:SendSysMessage("Syntax: .carboncopy tickets lookup $characterName") chatHandler:SendSysMessage("Syntax: .carboncopy tickets add $characterName $amount") chatHandler:SendSysMessage("Syntax: .carboncopy tickets remove $characterName $amount") @@ -577,6 +640,18 @@ function cc_CopyCharacter(event, player, command, chatHandler) end local ccSubCommand = string.lower(commandArray[2]) + if ccSubCommand == "config" or ccSubCommand == "settings" then + if player:GetGMRank() < Config.minGMRankForTickets then + chatHandler:SendSysMessage("You lack permisisions to execute this command.") + cc_resetVariables() + return false + end + + cc_execConfigCommand(chatHandler) + cc_resetVariables() + return false + end + if ccSubCommand == "tickets" then if commandArray[3] == nil then chatHandler:SendSysMessage("Syntax: .carboncopy tickets lookup $characterName") diff --git a/README.md b/README.md index edafee7..edeb9a9 100644 --- a/README.md +++ b/README.md @@ -33,6 +33,7 @@ There a showcase of the video "[CarbonCopy Feature](https://youtu.be/zDdODWPlbLU - `.carboncopy tickets lookup CharacterName` - See how many tickets a character has. - `.carboncopy tickets add CharacterName Amount` - Add tickets to a character's account. - `.carboncopy tickets remove CharacterName Amount` - Remove tickets from a character's account. +- `.carboncopy config` or `.carboncopy settings` - Show all active CarbonCopy config values. - `.carboncopy logs help` or `.carboncopy logs --help` - Show logs command flags. - `.carboncopy logs` - Show player copy logs (newest first, limit 20). - `.carboncopy logs --source nameOrGuid --target nameOrGuid --day YYYY-MM-DD --code N --limit N --oldest` - Filter player copy logs. From 77094a206656af7798a69d563508139c1b53c2f4 Mon Sep 17 00:00:00 2001 From: FlyingArowana Date: Thu, 14 May 2026 19:39:28 +0100 Subject: [PATCH 14/14] Prevent levle up dings achivement spam and donot copy totaltime --- CarbonCopy.lua | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/CarbonCopy.lua b/CarbonCopy.lua index 63ccc22..a5f41fd 100644 --- a/CarbonCopy.lua +++ b/CarbonCopy.lua @@ -1047,7 +1047,7 @@ function cc_CopyCharacter(event, player, command, chatHandler) local QueryString QueryString = 'UPDATE `characters` AS t1 ' QueryString = QueryString..'INNER JOIN `characters` AS t2 ON t2.guid = '..cc_playerGUID..' ' - QueryString = QueryString..'SET t1.level = t2.level, t1.xp = t2.xp, t1.taximask = t2.taximask, t1.totaltime = t2.totaltime, ' + QueryString = QueryString..'SET t1.level = t2.level, t1.xp = t2.xp, t1.taximask = t2.taximask, ' QueryString = QueryString..'t1.leveltime = t2.leveltime, t1.stable_slots = t2.stable_slots, t1.health = t2.health, ' QueryString = QueryString..'t1.power1 = t2.power1, t1.power2 = t2.power2, t1.power3 = t2.power3, t1.power4 = t2.power4, ' QueryString = QueryString..'t1.power5 = t2.power5, t1.power6 = t2.power6, t1.power7 = t2.power7, t1.talentGroupsCount = t2.talentGroupsCount, ' @@ -1057,6 +1057,14 @@ function cc_CopyCharacter(event, player, command, chatHandler) Data_SQL = nil local Data_SQL = CharDBQuery('UPDATE characters SET cinematic = 1 WHERE guid = '..cc_newCharacter..';'); + coroutine.yield() + -- Copy achivements level ups 10, 20, 30, 40, 50, 60, 70 or 80 if they exist and use carboncopy sucess timestamp when it was "obtained". + local level_achievements = {6, 7, 8, 9, 10, 11, 12, 13} + local achievement_list = table.concat(level_achievements, ",") + local Data_SQL = CharDBQuery('DELETE FROM character_achievement WHERE guid = '..cc_newCharacter..' AND achievement IN ('..achievement_list..');') + local Data_SQL = CharDBQuery('INSERT INTO character_achievement (guid, achievement, date) SELECT '..cc_newCharacter..', achievement, UNIX_TIMESTAMP() FROM character_achievement WHERE guid = '..cc_playerGUID..' AND achievement IN ('..achievement_list..');') + Data_SQL = nil + coroutine.yield() -- Copy character_homebind local Data_SQL = CharDBQuery('DELETE FROM character_homebind WHERE guid = '..cc_newCharacter..';') @@ -1268,6 +1276,10 @@ function cc_CopyCharacter(event, player, command, chatHandler) end CreateLuaEvent(cc_fixItems, 3000) -- do it after 3 seconds cc_deleteTempTables(cc_playerGUID) + + cc_chatHandler:SendSysMessage("Character copy complete! The character "..targetName.." has been successfully copied.") + cc_logPlayer(sourceName, sourceGuid, targetName, cc_newCharacter, player:GetLevel(), oldTickets - 1, oldTickets, CC_STATUS_SUCCESS, "Character copied") + cc_resetVariables() return false