diff --git a/modules/BuffManager/module.json b/modules/BuffManager/module.json new file mode 100644 index 0000000..2b54b69 --- /dev/null +++ b/modules/BuffManager/module.json @@ -0,0 +1,180 @@ +{ + "name": "BuffManager", + "author": "limon", + "description": "Sell, grant, and maintain configurable 7 Days to Die buff packages through Takaro. BuffManager helps admins turn buffs into permissioned, timed, and currency-backed player perks.\n\nUse this module to:\n- Configure multiple buff packages with display names and command triggers.\n- Require roles or permissions for specific buff packages.\n- Charge Takaro currency for player-selected buffs.\n- Track expirations and reapply active buffs when players reconnect.\n- Give staff admin commands for manual buff and debuff workflows.\n\nGood fit for 7 Days to Die servers that want VIP buffs, paid perks, timed role benefits, or economy-connected buff rewards.\n\n## Module details\n\n# Dynamic Buff Manager\n\nFlexible buff management system that supports unlimited buff packages with role requirements, expiration tracking, and currency costs.\n\n## Features\n- ✅ Unlimited buff packages (add as many as you need)\n- ✅ Role-based access control per package\n- ✅ Time-based expiration system\n- ✅ Auto-apply buffs on player connect\n- ✅ Currency costs per package\n- ✅ Automatic maintenance to keep buffs active\n\n## Quick Start\n\n1. **Add a Buff Package**\n - Click \"Add Item\" under Buff Packages\n - Set Command Name (e.g., \"vipbuff\")\n - Set Display Name (e.g., \"VIP Speed Package\")\n - Add buff names from your XML (e.g., \"SpeedbuffVip\")\n\n2. **Get Role ID**\n - Go to Settings → Roles in Takaro\n - Click on the role you want (e.g., \"VIP\")\n - Look at the browser address bar: `https://your-takaro.com/settings/roles/743b9623-ad09-4cf2-86c0-3d7faed57a88`\n - Copy the UUID from the URL (the part after `/roles/`)\n - Example: `743b9623-ad09-4cf2-86c0-3d7faed57a88`\n - Paste into \"Required Role ID\" field\n - Leave empty to allow everyone\n\n3. **Configure Duration**\n - Set how long buffs should last in minutes\n - 0 = no Takaro expiry tracking. The buff is applied by `/getbuff` or auto-apply, but maintenance only maintains packages that have an expiry variable.\n - 60 = expires after 1 hour\n\n4. **Save and Install**\n\n## Configuration Fields\n\n### Buff Package Settings\n\n- **Command Name**: What players type (creates /getbuff commandName)\n- **Display Name**: Friendly name in messages\n- **Description**: Explanation of what the package does\n- **Buff Names**: Array of buff codes from your buffs.xml\n- **Required Role ID**: UUID of the role (copy from browser URL). Empty = everyone\n- **Duration (Minutes)**: Takaro tracking/maintenance window. 0 = no expiry tracking\n- **Auto-Apply on Connect**: Give buff automatically when player joins\n- **Currency Cost**: Price to use the command\n\n### Examples\n\n**VIP Package:**\n- Command: \"vipbuff\"\n- Buffs: [\"SpeedbuffVip\", \"ExpBuffVip\"]\n- Role: 743b9623-ad09-4cf2-86c0-3d7faed57a88\n- Duration: 0 (permanent)\n- Auto-apply: Yes\n- Cost: 0\n\n**Temporary Donor Buff:**\n- Command: \"donorbuff\"\n- Buffs: [\"SuperSpeed\", \"SuperJump\"]\n- Role: abc-123-donor-role-id\n- Duration: 120 (2 hours)\n- Auto-apply: No\n- Cost: 1000\n\n## How It Works\n\n1. **On Player Connect**: Module checks all packages with auto-apply enabled and matching roles\n2. **Manual Command**: Player uses /getbuff to activate a specific package\n3. **Expiration Tracking**: Module stores expiration timestamp per player per package\n4. **Maintenance**: Cronjob re-applies non-expired timed buffs on its schedule. The included cronjob is scheduled every minute; adjust the cronjob schedule in Takaro if you want a different cadence.\n\n\n## Important: BuffManager duration vs 7D2D XML duration\n\nBuffManager has two different duration concepts:\n\n- **BuffManager package duration** controls how long Takaro tracks the player as owning the package. When a player runs `/getbuff `, BuffManager stores a Takaro variable such as `buff_expiry_` with the expiry timestamp.\n- **7D2D XML buff duration** controls how long each actual in-game buff application lasts. BuffManager applies buffs by sending a normal console command such as `buffplayer \"playername\" buffName`. That command does not include a duration, so 7D2D reads the duration and stack/refresh behavior from your server/mod XML, usually `buffs.xml`.\n\nThe maintenance cronjob is a keep-alive loop for timed packages. It does not inspect the live 7D2D buff timer. It checks Takaro's saved expiry variable for online players:\n\n- If the package has not expired, maintenance sends `buffplayer \"playername\" buffName` again to refresh/reapply the buff.\n- If the package has expired, maintenance sends `debuffplayer \"playername\" buffName` and deletes the Takaro tracking variable.\n- If package duration is `0`, BuffManager does not create an expiry variable for maintenance to check. The buff can still be applied by `/getbuff`, `/buffplayer`, or auto-apply on connect, but it is not part of the timed maintenance loop.\n\nFor clean behavior, set the XML duration and stacking behavior intentionally. If maintenance runs every minute, a short XML duration such as 60-90 seconds usually works well: Takaro refreshes while the package is active, then removes the buff when the Takaro package expires. Avoid very long XML durations unless you are comfortable with possible tail time if a buff was refreshed shortly before expiry and removal does not run cleanly.\n\n## Troubleshooting\n\n**Buffs not applying:**\n- Check buff names match your buffs.xml exactly (case-sensitive)\n- Verify role ID is correct UUID from browser URL\n- Check player actually has the role assigned\n- Look at module execution events for detailed logs\n\n**Buffs disappearing:**\n- Check duration - timed packages may be expiring, or duration 0 may not be tracked by maintenance\n- Verify maintenance is enabled and the maintenance cronjob schedule is what you expect\n- Check game server console for buff command errors\n\n**Currency not deducting:**\n- Verify economy is enabled in Takaro settings\n- Check currency cost is set correctly per package", + "version": "latest", + "supportedGames": [ + "7 Days to Die" + ], + "config": { + "$schema": "http://json-schema.org/draft-07/schema#", + "type": "object", + "properties": { + "buffPackages": { + "type": "array", + "title": "Buff Packages", + "description": "Define unlimited buff packages. Each package can have its own role requirements, duration, and cost.", + "items": { + "type": "object", + "properties": { + "commandName": { + "type": "string", + "title": "Command Name", + "description": "The command trigger (e.g., 'vipbuff' for /getbuff vipbuff)", + "minLength": 1 + }, + "displayName": { + "type": "string", + "title": "Display Name", + "description": "Friendly name shown to players", + "minLength": 1 + }, + "description": { + "type": "string", + "title": "Description", + "description": "What this buff package does", + "default": "" + }, + "buffNames": { + "type": "array", + "title": "Buff Names", + "description": "List of 7D2D buff names from your server/mod XML, usually buffs.xml. BuffManager sends buffplayer with these names; XML controls each buff application duration and stack/refresh behavior.", + "items": { + "type": "string" + }, + "minItems": 1 + }, + "requiredRoleId": { + "type": "string", + "title": "Required Role ID", + "description": "Copy the role UUID from browser URL: https://takaro.io/settings/roles/743b9623-ad09-4cf2-86c0-3d7faed57a88 → Paste: 743b9623-ad09-4cf2-86c0-3d7faed57a88. Leave empty to allow all players.", + "default": "" + }, + "duration": { + "type": "number", + "title": "Duration (Minutes)", + "description": "How long Takaro tracks this package as active, in minutes. This is the purchased/owned window and the point where maintenance stops re-applying the buff. It does not change the native 7D2D XML buff duration. Set to 0 for permanent Takaro tracking; maintenance keeps re-applying it while the player is online.", + "minimum": 0, + "default": 60 + }, + "autoApplyOnConnect": { + "type": "boolean", + "title": "Auto-Apply on Connect", + "description": "Automatically give this buff when eligible players connect", + "default": true + }, + "currencyCost": { + "type": "number", + "title": "Currency Cost", + "description": "How much currency players must pay to use this command", + "minimum": 0, + "default": 0 + } + }, + "required": [ + "commandName", + "displayName", + "buffNames" + ] + } + }, + "maintenanceEnabled": { + "type": "boolean", + "title": "Enable Maintenance Cronjob", + "description": "Periodically check Takaro expiry variables for online players. Non-expired packages are re-applied with buffplayer; expired packages are removed with debuffplayer.", + "default": true + }, + "maintenanceInterval": { + "type": "string", + "title": "Maintenance Schedule", + "description": "Cron expression text for documenting the intended maintenance cadence. The bundled cronjob schedule is configured separately in the cronjob settings; maintenance is a keep-alive/check loop that re-applies active timed packages and removes expired ones.", + "default": "*/15 * * * *" + } + }, + "required": [ + "buffPackages" + ] + }, + "uiSchema": {}, + "permissions": [ + { + "canHaveCount": false, + "description": "Can manage and apply any buff package", + "permission": "BUFF_ADMIN", + "friendlyName": "Buff Administrator" + } + ], + "commands": { + "buffplayer": { + "trigger": "buffplayer", + "description": null, + "helpText": "Admin command to apply a buff package to any player", + "function": "src/commands/buffplayer/index.js", + "arguments": [ + { + "name": "playerName", + "type": "string", + "helpText": "Name of the player to buff", + "position": 0 + }, + { + "name": "packageName", + "type": "string", + "helpText": "Name of the buff package to apply", + "position": 1 + } + ] + }, + "debuffplayer": { + "trigger": "debuffplayer", + "description": "debuff your player", + "helpText": "Remove buff tracking from a player. If packageName is not specified, removes all buffs. Usage: /debuffplayer [packageName]", + "function": "src/commands/debuffplayer/index.js", + "arguments": [ + { + "name": "playerName", + "type": "string", + "helpText": "Name of the player to debuff", + "position": 0 + }, + { + "name": "packageName", + "type": "string", + "defaultValue": "", + "helpText": "The buff package name to remove", + "position": 1 + } + ] + }, + "getbuff": { + "trigger": "getbuff", + "description": null, + "helpText": "Apply a buff package to yourself. Use /getbuff or just /getbuff to see available packages", + "function": "src/commands/getbuff/index.js", + "arguments": [ + { + "name": "packageName", + "type": "string", + "defaultValue": "", + "helpText": "Name of the buff package to apply (optional - leave empty to list all)", + "position": 0 + } + ] + } + }, + "hooks": { + "Auto-Apply Buff Packages": { + "eventType": "player-connected", + "description": "Automatically applies buff packages to players based on role requirements when they connect", + "function": "src/hooks/Auto-Apply Buff Packages/index.js", + "regex": "" + } + }, + "cronJobs": { + "check Buff Packages": { + "temporalValue": "* * * * *", + "description": "Checks Takaro buff expiry variables for online players, re-applies non-expired buffs with buffplayer, and removes expired buffs with debuffplayer.", + "function": "src/cronjobs/check Buff Packages/index.js" + } + } +} diff --git a/modules/BuffManager/src/commands/buffplayer/index.js b/modules/BuffManager/src/commands/buffplayer/index.js new file mode 100644 index 0000000..a87d01a --- /dev/null +++ b/modules/BuffManager/src/commands/buffplayer/index.js @@ -0,0 +1,232 @@ +import { data, takaro, TakaroUserError } from '@takaro/helpers'; + +async function main() { + const { gameServerId, pog, arguments: args, module, player } = data; + + console.log(`⚡ /buffplayer command executed by ${player?.name || pog.gameId}`); + + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + if (buffPackages.length === 0) { + console.log('❌ No buff packages configured'); + throw new TakaroUserError('No buff packages are configured.'); + } + + const targetPlayerName = args.playerName?.trim() || ''; + const packageName = args.packageName?.trim() || ''; + + if (!targetPlayerName) { + console.log('❌ No player name provided'); + throw new TakaroUserError('Please specify a player name.'); + } + + if (!packageName) { + console.log('❌ No package name provided'); + throw new TakaroUserError('Please specify a buff package name or "all".'); + } + + const playersRes = await takaro.gameserver.gameServerControllerGetPlayers(gameServerId); + const onlinePlayers = playersRes.data.data || []; + + console.log(` Found ${onlinePlayers.length} online player(s)`); + + const targetOnlinePlayer = onlinePlayers.find(p => + p.name.toLowerCase().includes(targetPlayerName.toLowerCase()) + ); + + if (!targetOnlinePlayer) { + console.log(`❌ Player "${targetPlayerName}" not found or not online`); + throw new TakaroUserError(`Player "${targetPlayerName}" not found or not online.`); + } + + console.log(`👤 Target player: ${targetOnlinePlayer.name}`); + + const targetPlayerRes = await takaro.player.playerControllerSearch({ + filters: { + steamId: [targetOnlinePlayer.steamId] + }, + limit: 1 + }); + + if (!targetPlayerRes.data.data || targetPlayerRes.data.data.length === 0) { + console.log(`❌ Player not found in Takaro database`); + throw new TakaroUserError(`Player not found in Takaro database.`); + } + + const targetPlayer = targetPlayerRes.data.data[0]; + console.log(` Target player ID: ${targetPlayer.id}`); + + // Check if user wants to apply ALL packages + if (packageName.toLowerCase() === 'all') { + console.log(`💉 Applying ALL buff packages (${buffPackages.length} packages)`); + + let totalBuffsApplied = 0; + let packagesProcessed = 0; + + for (const pkg of buffPackages) { + console.log(`\n📦 Processing package: ${pkg.displayName}`); + + const buffNames = pkg.buffNames || []; + console.log(` Contains ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer "${targetOnlinePlayer.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Applied ${buffName}`); + console.log(` Server response: ${serverResponse}`); + return { success: true, buff: buffName }; + }) + .catch(err => { + console.error(` ❌ Failed to apply ${buffName}:`, err.message); + return { success: false, buff: buffName }; + }) + ); + + const results = await Promise.all(buffCommands); + const successCount = results.filter(r => r && r.success).length; + totalBuffsApplied += successCount; + packagesProcessed++; + + console.log(` ✅ Applied ${successCount}/${buffNames.length} buffs from this package`); + + const expiryKey = `buff_expiry_${pkg.commandName}`; + const durationMinutes = Number(pkg.duration); + const expiryTime = durationMinutes > 0 ? Date.now() + (durationMinutes * 60000) : 0; + + try { + const existingVars = await takaro.variable.variableControllerSearch({ + filters: { + key: [expiryKey], + playerId: [targetPlayer.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + if (existingVars.data.data.length > 0) { + await takaro.variable.variableControllerUpdate(existingVars.data.data[0].id, { + value: expiryTime.toString() + }); + } else { + await takaro.variable.variableControllerCreate({ + key: expiryKey, + value: expiryTime.toString(), + playerId: targetPlayer.id, + gameServerId: gameServerId, + moduleId: module.moduleId + }); + } + + if (durationMinutes > 0) { + const minutes = Math.floor(durationMinutes); + const hours = Math.floor(minutes / 60); + const timeStr = hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`; + console.log(` ⏰ Expiration ${existingVars.data.data.length > 0 ? 'updated' : 'set'} for ${pkg.commandName}: ${timeStr}`); + } else { + console.log(` ⏰ Permanent tracking ${existingVars.data.data.length > 0 ? 'updated' : 'set'} for ${pkg.commandName}`); + } + } catch (err) { + console.error(` ⚠️ Failed to set tracking variable for ${pkg.commandName}:`, err.message); + } + } + + console.log(`\n✅ Complete: Applied ${totalBuffsApplied} total buffs across ${packagesProcessed} packages`); + await pog.pm(`Applied ALL buff packages to ${targetOnlinePlayer.name}! (${totalBuffsApplied} buffs applied)`); + return; + } + + // Apply specific package + const pkg = buffPackages.find(p => p.commandName.toLowerCase() === packageName.toLowerCase()); + + if (!pkg) { + console.log(`❌ Package "${packageName}" not found`); + const availablePackages = buffPackages.map(p => p.commandName).join(', '); + throw new TakaroUserError(`Buff package "${packageName}" not found. Available: ${availablePackages}. Use "all" to apply all packages.`); + } + + console.log(`🎯 Found package: ${pkg.displayName}`); + + const buffNames = pkg.buffNames || []; + console.log(`💉 Applying ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer "${targetOnlinePlayer.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Applied ${buffName}`); + console.log(` Server response: ${serverResponse}`); + return { success: true, buff: buffName }; + }) + .catch(err => { + console.error(` ❌ Failed to apply ${buffName}:`, err.message); + return { success: false, buff: buffName }; + }) + ); + + const results = await Promise.all(buffCommands); + const successCount = results.filter(r => r && r.success).length; + + console.log(`✅ Applied ${successCount}/${buffNames.length} buffs successfully`); + + const expiryKey = `buff_expiry_${pkg.commandName}`; + const durationMinutes = Number(pkg.duration); + const expiryTime = durationMinutes > 0 ? Date.now() + (durationMinutes * 60000) : 0; + + try { + const existingVars = await takaro.variable.variableControllerSearch({ + filters: { + key: [expiryKey], + playerId: [targetPlayer.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + if (existingVars.data.data.length > 0) { + await takaro.variable.variableControllerUpdate(existingVars.data.data[0].id, { + value: expiryTime.toString() + }); + } else { + await takaro.variable.variableControllerCreate({ + key: expiryKey, + value: expiryTime.toString(), + playerId: targetPlayer.id, + gameServerId: gameServerId, + moduleId: module.moduleId + }); + } + + if (durationMinutes > 0) { + const minutes = Math.floor(durationMinutes); + const hours = Math.floor(minutes / 60); + const timeStr = hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`; + console.log(`⏰ Expiration ${existingVars.data.data.length > 0 ? 'updated' : 'set'}: ${timeStr}`); + } else { + console.log(`⏰ Permanent tracking ${existingVars.data.data.length > 0 ? 'updated' : 'set'}`); + } + } catch (err) { + console.error(`⚠️ Failed to set tracking variable:`, err.message); + } + + + // Format expiry message + let expiryMsg = ''; + if (Number(pkg.duration) > 0) { + const minutes = Math.floor(Number(pkg.duration)); + const hours = Math.floor(minutes / 60); + expiryMsg = hours > 0 + ? ` (expires in ${hours}h ${minutes % 60}m)` + : ` (expires in ${minutes}m)`; + } + + await pog.pm(`Applied ${pkg.displayName} to ${targetOnlinePlayer.name}!${expiryMsg}`); + console.log('✅ Command complete'); +} + +await main(); \ No newline at end of file diff --git a/modules/BuffManager/src/commands/debuffplayer/index.js b/modules/BuffManager/src/commands/debuffplayer/index.js new file mode 100644 index 0000000..b801848 --- /dev/null +++ b/modules/BuffManager/src/commands/debuffplayer/index.js @@ -0,0 +1,185 @@ +import { data, takaro, TakaroUserError } from '@takaro/helpers'; + +async function main() { + const { gameServerId, pog, arguments: args, module, player } = data; + + console.log(`⚡ /debuffplayer command executed by ${player?.name || pog.gameId}`); + + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + if (buffPackages.length === 0) { + console.log('❌ No buff packages configured'); + throw new TakaroUserError('No buff packages are configured.'); + } + + const targetPlayerName = args.playerName?.trim() || ''; + const packageName = args.packageName?.trim() || ''; + + if (!targetPlayerName) { + console.log('❌ No player name provided'); + throw new TakaroUserError('Please specify a player name.'); + } + + if (!packageName) { + console.log('❌ No package name provided'); + throw new TakaroUserError('Please specify a buff package name or "all".'); + } + + const playersRes = await takaro.gameserver.gameServerControllerGetPlayers(gameServerId); + const onlinePlayers = playersRes.data.data || []; + + console.log(` Found ${onlinePlayers.length} online player(s)`); + + const targetOnlinePlayer = onlinePlayers.find(p => + p.name.toLowerCase().includes(targetPlayerName.toLowerCase()) + ); + + if (!targetOnlinePlayer) { + console.log(`❌ Player "${targetPlayerName}" not found or not online`); + throw new TakaroUserError(`Player "${targetPlayerName}" not found or not online.`); + } + + console.log(`👤 Target player: ${targetOnlinePlayer.name}`); + + const targetPlayerRes = await takaro.player.playerControllerSearch({ + filters: { + steamId: [targetOnlinePlayer.steamId] + }, + limit: 1 + }); + + if (!targetPlayerRes.data.data || targetPlayerRes.data.data.length === 0) { + console.log(`❌ Player not found in Takaro database`); + throw new TakaroUserError(`Player not found in Takaro database.`); + } + + const targetPlayer = targetPlayerRes.data.data[0]; + console.log(` Target player ID: ${targetPlayer.id}`); + + // Check if user wants to remove ALL packages + if (packageName.toLowerCase() === 'all') { + console.log(`💊 Removing ALL buff packages (${buffPackages.length} packages)`); + + let totalBuffsRemoved = 0; + let packagesProcessed = 0; + + for (const pkg of buffPackages) { + console.log(`\n📦 Processing package: ${pkg.displayName}`); + + const buffNames = pkg.buffNames || []; + console.log(` Contains ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const debuffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `debuffplayer "${targetOnlinePlayer.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Removed ${buffName}`); + console.log(` Server response: ${serverResponse}`); + return { success: true, buff: buffName }; + }) + .catch(err => { + console.error(` ❌ Failed to remove ${buffName}:`, err.message); + return { success: false, buff: buffName }; + }) + ); + + const results = await Promise.all(debuffCommands); + const successCount = results.filter(r => r && r.success).length; + totalBuffsRemoved += successCount; + packagesProcessed++; + + console.log(` ✅ Removed ${successCount}/${buffNames.length} buffs from this package`); + + const expiryKey = `buff_expiry_${pkg.commandName}`; + + try { + const existingVars = await takaro.variable.variableControllerSearch({ + filters: { + key: [expiryKey], + playerId: [targetPlayer.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + if (existingVars.data.data.length > 0) { + await takaro.variable.variableControllerDelete(existingVars.data.data[0].id); + console.log(` ⏰ Expiration variable deleted for ${pkg.commandName}`); + } else { + console.log(` ⏰ No expiration variable found for ${pkg.commandName}`); + } + } catch (err) { + console.error(` ⚠️ Failed to delete expiration variable for ${pkg.commandName}:`, err.message); + } + } + + console.log(`\n✅ Complete: Removed ${totalBuffsRemoved} total buffs across ${packagesProcessed} packages`); + await pog.pm(`Removed ALL buff packages from ${targetOnlinePlayer.name}! (${totalBuffsRemoved} buffs removed)`); + return; + } + + // Remove specific package + const pkg = buffPackages.find(p => p.commandName.toLowerCase() === packageName.toLowerCase()); + + if (!pkg) { + console.log(`❌ Package "${packageName}" not found`); + const availablePackages = buffPackages.map(p => p.commandName).join(', '); + throw new TakaroUserError(`Buff package "${packageName}" not found. Available: ${availablePackages}. Use "all" to remove all packages.`); + } + + console.log(`🎯 Found package: ${pkg.displayName}`); + + const buffNames = pkg.buffNames || []; + console.log(`💊 Removing ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const debuffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `debuffplayer "${targetOnlinePlayer.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Removed ${buffName}`); + console.log(` Server response: ${serverResponse}`); + return { success: true, buff: buffName }; + }) + .catch(err => { + console.error(` ❌ Failed to remove ${buffName}:`, err.message); + return { success: false, buff: buffName }; + }) + ); + + const results = await Promise.all(debuffCommands); + const successCount = results.filter(r => r && r.success).length; + + console.log(`✅ Removed ${successCount}/${buffNames.length} buffs successfully`); + + const expiryKey = `buff_expiry_${pkg.commandName}`; + + try { + const existingVars = await takaro.variable.variableControllerSearch({ + filters: { + key: [expiryKey], + playerId: [targetPlayer.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + if (existingVars.data.data.length > 0) { + await takaro.variable.variableControllerDelete(existingVars.data.data[0].id); + console.log(`⏰ Expiration variable deleted`); + } else { + console.log(`⏰ No expiration variable found to delete`); + } + } catch (err) { + console.error(`⚠️ Failed to delete expiration variable:`, err.message); + } + + await pog.pm(`Removed ${pkg.displayName} from ${targetOnlinePlayer.name}!`); + console.log('✅ Command complete'); +} + +await main(); \ No newline at end of file diff --git a/modules/BuffManager/src/commands/getbuff/index.js b/modules/BuffManager/src/commands/getbuff/index.js new file mode 100644 index 0000000..6571dff --- /dev/null +++ b/modules/BuffManager/src/commands/getbuff/index.js @@ -0,0 +1,201 @@ +import { data, takaro, checkPermission, TakaroUserError } from '@takaro/helpers'; + +async function main() { + const { gameServerId, pog, arguments: args, module, player } = data; + + console.log(`⚡ /getbuff command executed by ${player?.name || pog.gameId}`); + + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + if (buffPackages.length === 0) { + console.log('❌ No buff packages configured'); + throw new TakaroUserError('No buff packages are configured. Please contact an administrator.'); + } + + const packageName = args.packageName?.trim() || ''; + + // If no package specified, list available packages + if (!packageName) { + console.log('📋 Listing available buff packages'); + const availablePackages = buffPackages + .map(pkg => { + const cost = pkg.currencyCost > 0 ? ` (Cost: ${pkg.currencyCost})` : ''; + const role = pkg.requiredRoleId && pkg.requiredRoleId.trim() ? ' [Role Required]' : ''; + + // Format duration stored in minutes + let durationText = ''; + if (Number(pkg.duration) > 0) { + const minutes = Math.floor(Number(pkg.duration)); + const hours = Math.floor(minutes / 60); + if (hours > 0) { + durationText = ` [Duration: ${hours}h ${minutes % 60}m]`; + } else { + durationText = ` [Duration: ${minutes}m]`; + } + } else { + durationText = ' [Permanent]'; + } + + return `• ${pkg.commandName} - ${pkg.displayName}${cost}${role}${durationText}${pkg.description ? '\n ' + pkg.description : ''}`; + }) + .join('\n'); + + await pog.pm(`Available buff packages:\n${availablePackages}\n\nUsage: getbuff `); + console.log('✅ Package list sent'); + return; + } + + // Find the requested package + const pkg = buffPackages.find(p => p.commandName.toLowerCase() === packageName.toLowerCase()); + + if (!pkg) { + console.log(`❌ Package "${packageName}" not found`); + throw new TakaroUserError(`Buff package "${packageName}" not found. Use /getbuff to see available packages.`); + } + + console.log(`🎯 Found package: ${pkg.displayName}`); + + // Check role requirement + if (pkg.requiredRoleId && pkg.requiredRoleId.trim() !== '') { + const playerRoles = pog.roles || []; + const hasRole = playerRoles.some(r => r.role.id === pkg.requiredRoleId); + + console.log(`🔐 Role check: ${hasRole ? '✓ HAS' : '✗ NO'} required role ${pkg.requiredRoleId}`); + + if (!hasRole) { + console.log('❌ Command denied - insufficient role'); + throw new TakaroUserError(`You don't have the required role to use this buff package.`); + } + } else { + console.log('🔐 Role check: ✓ No role required'); + } + + // Handle currency cost + let shouldChargeCurrency = false; + if (Number(pkg.currencyCost) > 0) { + const currentCurrency = Number(pog.currency || 0); + const cost = Number(pkg.currencyCost); + + console.log(`💰 Currency check: ${currentCurrency} >= ${cost}`); + + if (currentCurrency < cost) { + console.log('❌ Insufficient currency'); + let currencyName = 'currency'; + try { + currencyName = (await takaro.settings.settingsControllerGetOne('currencyName', gameServerId)).data.data.value || currencyName; + } catch (err) { + console.log(`⚠️ Could not fetch currency name: ${err.message}`); + } + throw new TakaroUserError(`Insufficient ${currencyName}. Need ${cost}, have ${currentCurrency}.`); + } + + shouldChargeCurrency = true; + } + + const buffNames = pkg.buffNames || []; + console.log(`💉 Applying ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + // Apply all buffs in parallel + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer "${player.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Applied ${buffName}`); + console.log(` Server response: ${serverResponse}`); + return { success: true, buff: buffName }; + }) + .catch(err => { + console.error(` ❌ Failed to apply ${buffName}:`, err.message); + if (err.response?.data?.data?.rawResult) { + console.error(` Server response: ${err.response.data.data.rawResult}`); + } + return { success: false, buff: buffName }; + }) + ); + + const results = await Promise.all(buffCommands); + const successCount = results.filter(r => r && r.success).length; + + console.log(`✅ Applied ${successCount}/${buffNames.length} buffs successfully`); + + if (successCount === 0) { + throw new TakaroUserError(`Failed to apply ${pkg.displayName}. No currency was charged.`); + } + + if (shouldChargeCurrency) { + try { + const cost = Number(pkg.currencyCost); + await takaro.playerOnGameserver.playerOnGameServerControllerDeductCurrency(gameServerId, pog.playerId, { + currency: cost + }); + console.log(`💰 Deducted ${cost} currency`); + } catch (err) { + console.error('❌ Currency deduction failed:', err.message); + throw new TakaroUserError('Buff applied, but currency deduction failed. Please contact an admin.'); + } + } + + // Set or update tracking variable. A duration of 0 means permanent Takaro tracking, + // stored as value "0", so the maintenance cron can keep re-applying it. + const expiryKey = `buff_expiry_${pkg.commandName}`; + const durationMinutes = Number(pkg.duration); + const expiryTime = durationMinutes > 0 ? Date.now() + (durationMinutes * 60000) : 0; + + try { + // Check if variable already exists + const existingVars = await takaro.variable.variableControllerSearch({ + filters: { + key: [expiryKey], + playerId: [player.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + if (existingVars.data.data.length > 0) { + await takaro.variable.variableControllerUpdate(existingVars.data.data[0].id, { + value: expiryTime.toString() + }); + } else { + await takaro.variable.variableControllerCreate({ + key: expiryKey, + value: expiryTime.toString(), + playerId: player.id, + gameServerId: gameServerId, + moduleId: module.moduleId + }); + } + + if (durationMinutes > 0) { + const minutes = Math.floor(durationMinutes); + const hours = Math.floor(minutes / 60); + const timeStr = hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`; + console.log(`⏰ Expiration ${existingVars.data.data.length > 0 ? 'updated' : 'set'}: ${timeStr}`); + } else { + console.log(`⏰ Permanent tracking ${existingVars.data.data.length > 0 ? 'updated' : 'set'}`); + } + } catch (err) { + console.error(`⚠️ Failed to set buff tracking variable:`, err.message); + // Don't throw - buff was applied successfully + } + + // Send confirmation with properly formatted duration + let expiryMsg = ''; + if (Number(pkg.duration) > 0) { + const minutes = Math.floor(Number(pkg.duration)); + const hours = Math.floor(minutes / 60); + if (hours > 0) { + expiryMsg = ` (expires in ${hours}h ${minutes % 60}m)`; + } else { + expiryMsg = ` (expires in ${minutes}m)`; + } + } + + await pog.pm(`${pkg.displayName} applied!${expiryMsg}`); + console.log('✅ Command complete'); +} + +await main(); \ No newline at end of file diff --git a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js new file mode 100644 index 0000000..2eed77d --- /dev/null +++ b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js @@ -0,0 +1,274 @@ +import { data, takaro } from '@takaro/helpers'; + +async function main() { + const { gameServerId, module } = data; + + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + console.log(`🔄 Buff Expiration Check & Maintenance starting...`); + + if (!config.maintenanceEnabled) { + console.log('⏸️ Maintenance is DISABLED in config - skipping'); + return; + } + + if (buffPackages.length === 0) { + console.log('⚠️ No buff packages configured'); + return; + } + + console.log(` Configured packages: ${buffPackages.map(p => p.displayName).join(', ')}`); + + // Statistics tracking + const stats = { + playersProcessed: 0, + buffPackagesMaintained: 0, + buffPackagesExpiredAndRemoved: 0, + totalBuffsReapplied: 0, + errors: 0 + }; + + try { + // Get online PlayerOnGameServer records directly. This is more reliable for 7D2D + // than mapping game-server player output through steamId, because 7D2D players may + // only have gameId/platformId in the online-player payload. + const pogsRes = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ + filters: { + gameServerId: [gameServerId], + online: [true] + }, + extend: ['player'], + limit: 100 + }); + const onlinePogs = pogsRes.data.data || []; + + console.log(` +👥 Found ${onlinePogs.length} online player(s)`); + + if (onlinePogs.length === 0) { + console.log(' No players online - nothing to do'); + return; + } + + const validPlayers = onlinePogs.map(pog => { + const takaroPlayer = pog.player || { id: pog.playerId, name: pog.name || pog.gameId }; + const onlinePlayer = { + name: takaroPlayer.name || pog.name || pog.gameId, + gameId: pog.gameId + }; + return { onlinePlayer, takaroPlayer, pog }; + }).filter(result => result.takaroPlayer && result.takaroPlayer.id); + + console.log(`\n🔍 Processing ${validPlayers.length} player(s)...\n`); + + // Process each player + for (const { onlinePlayer, takaroPlayer, pog } of validPlayers) { + console.log(`\n👤 Player: ${onlinePlayer.name}`); + stats.playersProcessed++; + + try { + const playerRoles = pog.roles || []; + const playerRoleIds = playerRoles.map(r => r.role.id); + console.log(` Roles: ${playerRoleIds.length > 0 ? playerRoleIds.join(', ') : 'none'}`); + + // Get all buff expiration variables for this player + console.log(` 🔍 Searching for variables with filters:`); + console.log(` - playerId: ${takaroPlayer.id}`); + console.log(` - gameServerId: ${gameServerId}`); + console.log(` - moduleId: ${module.moduleId}`); + + const variablesRes = await takaro.variable.variableControllerSearch({ + filters: { + playerId: [takaroPlayer.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + + const variables = variablesRes.data.data || []; + console.log(` 📦 Total variables found: ${variables.length}`); + if (variables.length > 0) { + console.log(` 📝 Variable keys: ${variables.map(v => v.key).join(', ')}`); + } + + const expiryVariables = variables.filter(v => v.key.startsWith('buff_expiry_')); + console.log(` ⏰ Active buff tracking variables: ${expiryVariables.length}`); + + if (expiryVariables.length === 0) { + console.log(` No active buffs to check/maintain`); + continue; + } + + // Check each buff package variable + for (const variable of expiryVariables) { + const commandName = variable.key.replace('buff_expiry_', ''); + const pkg = buffPackages.find(p => p.commandName === commandName); + + if (!pkg) { + console.log(`\n ⚠️ Package "${commandName}" no longer exists in config`); + console.log(` 🗑️ Cleaning up orphaned variable`); + try { + await takaro.variable.variableControllerDelete(variable.id); + console.log(` ✓ Deleted orphaned variable`); + } catch (err) { + console.log(` ⚠️ Failed to delete: ${err.message}`); + } + continue; + } + + console.log(`\n 📦 Package: ${pkg.displayName}`); + + try { + // STEP 1: Check if expired + const expiryTime = parseInt(variable.value); + const now = Date.now(); + const isExpired = Number(pkg.duration) > 0 && expiryTime > 0 && now > expiryTime; + + // Debug logging + console.log(` 📅 Debug: Expiry=${expiryTime === 0 ? 'permanent' : new Date(expiryTime).toISOString()}, Now=${new Date(now).toISOString()}, Duration=${pkg.duration}m`); + console.log(` 🔍 Expired check: ${isExpired} (now ${now} > expiry ${expiryTime})`) + + if (isExpired) { + const msExpired = now - expiryTime; + const minutesExpired = Math.floor(msExpired / 60000); + const hoursExpired = Math.floor(msExpired / 3600000); + + const expiredTimeStr = hoursExpired > 0 + ? `${hoursExpired}h ${minutesExpired % 60}m` + : `${minutesExpired}m`; + + console.log(` ⏰ EXPIRED ${expiredTimeStr} ago`); + console.log(` 🗑️ Removing expired buffs from player`); + + // Remove the actual buffs from the player + const buffNames = pkg.buffNames || []; + console.log(` 💊 Removing ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const debuffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `debuffplayer "${onlinePlayer.name}" ${buffName}` + }) + .then(() => { + console.log(` ✓ Removed ${buffName}`); + return { success: true }; + }) + .catch(err => { + console.error(` ❌ Failed to remove ${buffName}: ${err.message}`); + return { success: false }; + }) + ); + + const removeResults = await Promise.all(debuffCommands); + const removedCount = removeResults.filter(r => r.success).length; + console.log(` ✅ Removed ${removedCount}/${buffNames.length} buffs from player`); + + // Send expiration notification to player + if (removedCount > 0) { + try { + await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { + message: `⏰ ${pkg.displayName} has expired and been removed.`, + opts: { + recipient: { + gameId: pog.gameId + } + } + }); + console.log(` 📬 Expiration notification sent to player`); + } catch (err) { + console.log(` ⚠️ Failed to send notification: ${err.message}`); + } + } + + // Clean up expired variable + try { + await takaro.variable.variableControllerDelete(variable.id); + stats.buffPackagesExpiredAndRemoved++; + console.log(` ✓ Tracking variable deleted`); + } catch (err) { + console.log(` ⚠️ Failed to delete variable: ${err.message}`); + stats.errors++; + } + continue; // Don't re-apply expired buffs + } + + // STEP 2: Check if player still has required role + if (pkg.requiredRoleId && pkg.requiredRoleId.trim() !== '') { + const hasRole = playerRoleIds.includes(pkg.requiredRoleId); + + if (!hasRole) { + console.log(` 🔍 Player no longer has required role`); + console.log(` ⏭️ Skipping (won't re-apply)`); + continue; + } + } + + // STEP 3: Calculate time remaining (for active buffs) + if (Number(pkg.duration) > 0) { + const msRemaining = expiryTime - now; + const minutesRemaining = Math.floor(msRemaining / 60000); + const hoursRemaining = Math.floor(msRemaining / 3600000); + + const remainingTimeStr = hoursRemaining > 0 + ? `${hoursRemaining}h ${minutesRemaining % 60}m` + : `${minutesRemaining}m`; + + console.log(` ⏰ Active - Expires in ${remainingTimeStr}`); + } else { + console.log(` ⏰ Active - Permanent tracking`); + } + + // STEP 4: Re-apply active buffs to maintain them + const buffNames = pkg.buffNames || []; + console.log(` 💉 Re-applying ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer "${onlinePlayer.name}" ${buffName}` + }) + .then(() => { + console.log(` ✓ Applied ${buffName}`); + return { success: true }; + }) + .catch(err => { + console.error(` ❌ Failed ${buffName}: ${err.message}`); + stats.errors++; + return { success: false }; + }) + ); + + const results = await Promise.all(buffCommands); + const successCount = results.filter(r => r.success).length; + + stats.totalBuffsReapplied += successCount; + stats.buffPackagesMaintained++; + + console.log(` ✅ Maintained ${successCount}/${buffNames.length} buffs`); + } catch (err) { + console.error(` ❌ Error processing package ${pkg.displayName}:`, err.message); + stats.errors++; + } + } + } catch (err) { + console.error(` ❌ Error processing player ${onlinePlayer.name}:`, err.message); + stats.errors++; + } + } + } catch (err) { + console.error(`❌ Fatal error in buff check & maintenance:`, err.message); + stats.errors++; + } + + // Print summary + console.log(`\n✅ Buff Check & Maintenance Complete`); + console.log(`📊 Summary:`); + console.log(` - Players processed: ${stats.playersProcessed}`); + console.log(` - Active buff packages maintained: ${stats.buffPackagesMaintained}`); + console.log(` - Expired buffs removed: ${stats.buffPackagesExpiredAndRemoved}`); + console.log(` - Total buffs re-applied: ${stats.totalBuffsReapplied}`); + if (stats.errors > 0) { + console.log(` - Errors encountered: ${stats.errors} ⚠️`); + } +} + +await main(); \ No newline at end of file diff --git a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js new file mode 100644 index 0000000..ac7f4c4 --- /dev/null +++ b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js @@ -0,0 +1,232 @@ +import { data, takaro } from '@takaro/helpers'; + +async function main() { + const { gameServerId, eventData, player, pog, module } = data; + + if (!player) { + console.log('❌ Error: Player data is undefined'); + return; + } + + if (!pog) { + console.log('❌ Error: Player-on-gameserver (pog) data is undefined'); + return; + } + + console.log(`🔌 Player connected: ${player.name || 'Unknown'} (ID: ${player.id})`); + + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + if (buffPackages.length === 0) { + console.log('⚠️ No buff packages configured'); + return; + } + + // Filter packages that should auto-apply + const autoApplyPackages = buffPackages.filter(pkg => pkg.autoApplyOnConnect === true); + + if (autoApplyPackages.length === 0) { + console.log('⚠️ No packages with auto-apply enabled'); + return; + } + + console.log(`🔍 Checking ${autoApplyPackages.length} auto-apply package(s)`); + + const playerRoles = pog.roles || []; + const playerRoleIds = playerRoles.map(r => r.role.id); + + console.log(` Player has ${playerRoleIds.length} role(s)${playerRoleIds.length > 0 ? ': ' + playerRoleIds.join(', ') : ''}`); + + // Check each package for role requirements + const eligiblePackages = autoApplyPackages.filter(pkg => { + // If no role required, everyone is eligible + if (!pkg.requiredRoleId || pkg.requiredRoleId.trim() === '') { + console.log(` ✓ ${pkg.displayName}: No role required - eligible`); + return true; + } + + // Check if player has the required role + const hasRole = playerRoleIds.includes(pkg.requiredRoleId); + const icon = hasRole ? '✓' : '✗'; + console.log(` ${icon} ${pkg.displayName}: ${hasRole ? 'HAS' : 'MISSING'} role ${pkg.requiredRoleId}`); + return hasRole; + }); + + if (eligiblePackages.length === 0) { + console.log('⚠️ Player is not eligible for any auto-apply packages'); + return; + } + + console.log(`\n💉 Applying ${eligiblePackages.length} buff package(s) to ${player.name}`); + + // Statistics + const stats = { + packagesApplied: 0, + buffsTotalApplied: 0, + packagesSkipped: 0, + errors: 0 + }; + + // Get existing buff variables to check what's already active + let existingBuffVars = []; + try { + const varsRes = await takaro.variable.variableControllerSearch({ + filters: { + playerId: [player.id], + gameServerId: [gameServerId], + moduleId: [module.moduleId] + } + }); + existingBuffVars = (varsRes.data.data || []).filter(v => v.key.startsWith('buff_expiry_')); + console.log(` Found ${existingBuffVars.length} existing buff variable(s)`); + } catch (err) { + console.log(` ⚠️ Could not fetch existing variables: ${err.message}`); + } + + // Apply each eligible package + for (const pkg of eligiblePackages) { + console.log(`\n📦 Processing package: ${pkg.displayName}`); + + try { + // Check if this package is already active and not expired + const expiryKey = `buff_expiry_${pkg.commandName}`; + const existingVar = existingBuffVars.find(v => v.key === expiryKey); + + if (existingVar) { + const expiryTime = parseInt(existingVar.value); + const now = Date.now(); + const isPermanent = Number(pkg.duration) === 0 && expiryTime === 0; + const isStillActive = isPermanent || expiryTime > now; + + if (isStillActive) { + if (isPermanent) { + console.log(` ⏭️ Already active (permanent) - skipping`); + } else { + const msRemaining = expiryTime - now; + const minutesRemaining = Math.floor(msRemaining / 60000); + const hoursRemaining = Math.floor(msRemaining / 3600000); + const timeStr = hoursRemaining > 0 + ? `${hoursRemaining}h ${minutesRemaining % 60}m` + : `${minutesRemaining}m`; + console.log(` ⏭️ Already active (${timeStr} remaining) - skipping`); + } + stats.packagesSkipped++; + continue; + } + } + + const buffNames = pkg.buffNames || []; + if (buffNames.length === 0) { + console.log(' ⚠️ No buffs configured - skipping'); + stats.packagesSkipped++; + continue; + } + + console.log(` Buffs to apply: ${buffNames.join(', ')}`); + + // Apply all buffs in this package in parallel + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer "${player.name}" ${buffName}` + }) + .then((response) => { + const serverResponse = response.data.data?.rawResult || 'No response'; + console.log(` ✓ Applied ${buffName}`); + return { success: true }; + }) + .catch(error => { + console.log(` ✗ Failed to apply ${buffName}: ${error.message}`); + stats.errors++; + return { success: false }; + }) + ); + + const results = await Promise.all(buffCommands); + const successCount = results.filter(r => r.success).length; + + console.log(` ✅ Applied ${successCount}/${buffNames.length} buffs`); + + if (successCount > 0) { + stats.packagesApplied++; + stats.buffsTotalApplied += successCount; + } + + // Set or update tracking. Duration 0 is permanent tracking for maintenance. + try { + const durationMinutes = Number(pkg.duration); + const expiryTime = durationMinutes > 0 ? Date.now() + (durationMinutes * 60000) : 0; + + if (existingVar) { + await takaro.variable.variableControllerUpdate(existingVar.id, { + value: expiryTime.toString() + }); + } else { + await takaro.variable.variableControllerCreate({ + key: expiryKey, + value: expiryTime.toString(), + playerId: player.id, + gameServerId: gameServerId, + moduleId: module.moduleId + }); + } + + if (durationMinutes > 0) { + const minutes = Math.floor(durationMinutes); + const hours = Math.floor(minutes / 60); + const timeStr = hours > 0 ? `${hours}h ${minutes % 60}m` : `${minutes}m`; + console.log(` ⏰ Expiration ${existingVar ? 'updated' : 'set'}: ${timeStr}`); + } else { + console.log(` ⏰ Permanent tracking ${existingVar ? 'updated' : 'set'}`); + } + } catch (err) { + console.log(` ⚠️ Failed to set/update tracking: ${err.message}`); + stats.errors++; + } + } catch (err) { + console.log(` ❌ Error processing package ${pkg.displayName}: ${err.message}`); + stats.errors++; + } + } + + // Send welcome message if any buffs were applied + if (stats.packagesApplied > 0) { + try { + const packageNames = eligiblePackages + .filter(pkg => { + // Only include packages that weren't skipped + const expiryKey = `buff_expiry_${pkg.commandName}`; + const wasSkipped = existingBuffVars.find(v => v.key === expiryKey && (parseInt(v.value) === 0 || parseInt(v.value) > Date.now())); + return !wasSkipped; + }) + .map(p => p.displayName) + .join(', '); + + if (packageNames) { + await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { + message: `Welcome ${player.name}! Auto-applied buffs: ${packageNames}`, + opts: { + recipient: { + gameId: pog.gameId + } + } + }); + console.log('\n✅ Welcome message sent'); + } + } catch (error) { + console.log(`\n⚠️ Failed to send welcome message: ${error.message}`); + } + } + + // Summary + console.log('\n✅ Auto-apply complete'); + console.log(`📊 Summary:`); + console.log(` - Packages applied: ${stats.packagesApplied}`); + console.log(` - Packages skipped (already active): ${stats.packagesSkipped}`); + console.log(` - Total buffs applied: ${stats.buffsTotalApplied}`); + if (stats.errors > 0) { + console.log(` - Errors: ${stats.errors} ⚠️`); + } +} + +await main(); \ No newline at end of file diff --git a/modules/zombie-racing/src/functions/utils.js b/modules/zombie-racing/src/functions/utils.js index 1f0fbac..72d9e96 100644 --- a/modules/zombie-racing/src/functions/utils.js +++ b/modules/zombie-racing/src/functions/utils.js @@ -770,6 +770,23 @@ export async function recoverRunningRace(gameServerId, moduleId, config, systemC return completeRace(gameServerId, moduleId, config, systemConfig); } +async function acquireRaceLockWithRetry(gameServerId, moduleId, reason, timeoutMs = 20000) { + const deadline = Date.now() + timeoutMs; + let lastError; + while (Date.now() < deadline) { + try { + return await acquireRaceLock(gameServerId, moduleId, reason); + } catch (err) { + lastError = err; + if (!(err instanceof TakaroUserError) || !String(err.message || '').includes('Race state is busy')) { + throw err; + } + await new Promise((resolve) => setTimeout(resolve, 500)); + } + } + throw lastError; +} + export async function broadcastRaceCommentary(gameServerId, moduleId, config, expectedSource, stage) { const currentRaceData = await getRaceData(gameServerId, moduleId); if (currentRaceData.status !== 'running') { @@ -794,7 +811,7 @@ export async function broadcastRaceCommentary(gameServerId, moduleId, config, ex return { skipped: true, raceData: currentRaceData }; } - const lockOwner = await acquireRaceLock(gameServerId, moduleId, `race-commentary-${stage}`); + const lockOwner = await acquireRaceLockWithRetry(gameServerId, moduleId, `race-commentary-${stage}`); try { const raceData = await getRaceData(gameServerId, moduleId); if (raceData.status !== 'running') {