From 122123a832f24d3b34b52c21bba4ffe591909b5a Mon Sep 17 00:00:00 2001 From: Harambe Date: Sun, 30 Aug 2026 05:53:29 +0000 Subject: [PATCH 1/3] =?UTF-8?q?=F0=9F=90=9B=20task-buffmanager:=20target?= =?UTF-8?q?=207dtd=20buffs=20by=20EOS=20id?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/BuffManager/src/commands/buffplayer/index.js | 10 ++++++++-- .../BuffManager/src/commands/debuffplayer/index.js | 10 ++++++++-- modules/BuffManager/src/commands/getbuff/index.js | 8 +++++++- .../src/cronjobs/check Buff Packages/index.js | 11 +++++++++-- .../src/hooks/Auto-Apply Buff Packages/index.js | 8 +++++++- 5 files changed, 39 insertions(+), 8 deletions(-) diff --git a/modules/BuffManager/src/commands/buffplayer/index.js b/modules/BuffManager/src/commands/buffplayer/index.js index a87d01a..a5ce915 100644 --- a/modules/BuffManager/src/commands/buffplayer/index.js +++ b/modules/BuffManager/src/commands/buffplayer/index.js @@ -1,5 +1,10 @@ import { data, takaro, TakaroUserError } from '@takaro/helpers'; +function get7dtdCommandTarget(onlinePlayer) { + if (onlinePlayer?.gameId) return String(onlinePlayer.gameId).startsWith('EOS_') ? onlinePlayer.gameId : `EOS_${onlinePlayer.gameId}`; + return `\"${onlinePlayer.name}\"`; +} + async function main() { const { gameServerId, pog, arguments: args, module, player } = data; @@ -41,6 +46,7 @@ async function main() { } console.log(`šŸ‘¤ Target player: ${targetOnlinePlayer.name}`); + const commandTarget = get7dtdCommandTarget(targetOnlinePlayer); const targetPlayerRes = await takaro.player.playerControllerSearch({ filters: { @@ -72,7 +78,7 @@ async function main() { const buffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `buffplayer "${targetOnlinePlayer.name}" ${buffName}` + command: `buffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; @@ -155,7 +161,7 @@ async function main() { const buffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `buffplayer "${targetOnlinePlayer.name}" ${buffName}` + command: `buffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; diff --git a/modules/BuffManager/src/commands/debuffplayer/index.js b/modules/BuffManager/src/commands/debuffplayer/index.js index b801848..4edcc0f 100644 --- a/modules/BuffManager/src/commands/debuffplayer/index.js +++ b/modules/BuffManager/src/commands/debuffplayer/index.js @@ -1,5 +1,10 @@ import { data, takaro, TakaroUserError } from '@takaro/helpers'; +function get7dtdCommandTarget(onlinePlayer) { + if (onlinePlayer?.gameId) return String(onlinePlayer.gameId).startsWith('EOS_') ? onlinePlayer.gameId : `EOS_${onlinePlayer.gameId}`; + return `\"${onlinePlayer.name}\"`; +} + async function main() { const { gameServerId, pog, arguments: args, module, player } = data; @@ -41,6 +46,7 @@ async function main() { } console.log(`šŸ‘¤ Target player: ${targetOnlinePlayer.name}`); + const commandTarget = get7dtdCommandTarget(targetOnlinePlayer); const targetPlayerRes = await takaro.player.playerControllerSearch({ filters: { @@ -72,7 +78,7 @@ async function main() { const debuffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `debuffplayer "${targetOnlinePlayer.name}" ${buffName}` + command: `debuffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; @@ -137,7 +143,7 @@ async function main() { const debuffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `debuffplayer "${targetOnlinePlayer.name}" ${buffName}` + command: `debuffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; diff --git a/modules/BuffManager/src/commands/getbuff/index.js b/modules/BuffManager/src/commands/getbuff/index.js index 6571dff..2f6db4e 100644 --- a/modules/BuffManager/src/commands/getbuff/index.js +++ b/modules/BuffManager/src/commands/getbuff/index.js @@ -1,5 +1,10 @@ import { data, takaro, checkPermission, TakaroUserError } from '@takaro/helpers'; +function get7dtdCommandTarget(pog, player) { + if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; + return `\"${player.name}\"`; +} + async function main() { const { gameServerId, pog, arguments: args, module, player } = data; @@ -95,11 +100,12 @@ async function main() { const buffNames = pkg.buffNames || []; console.log(`šŸ’‰ Applying ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + const commandTarget = get7dtdCommandTarget(pog, player); // Apply all buffs in parallel const buffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `buffplayer "${player.name}" ${buffName}` + command: `buffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; diff --git a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js index 2eed77d..2393378 100644 --- a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js +++ b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js @@ -1,5 +1,10 @@ import { data, takaro } from '@takaro/helpers'; +function get7dtdCommandTarget(pog, onlinePlayer) { + if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; + return `\"${onlinePlayer.name}\"`; +} + async function main() { const { gameServerId, module } = data; @@ -144,10 +149,11 @@ async function main() { // Remove the actual buffs from the player const buffNames = pkg.buffNames || []; console.log(` šŸ’Š Removing ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + const commandTarget = get7dtdCommandTarget(pog, onlinePlayer); const debuffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `debuffplayer "${onlinePlayer.name}" ${buffName}` + command: `debuffplayer ${commandTarget} ${buffName}` }) .then(() => { console.log(` āœ“ Removed ${buffName}`); @@ -221,10 +227,11 @@ async function main() { // STEP 4: Re-apply active buffs to maintain them const buffNames = pkg.buffNames || []; console.log(` šŸ’‰ Re-applying ${buffNames.length} buff(s): ${buffNames.join(', ')}`); + const commandTarget = get7dtdCommandTarget(pog, onlinePlayer); const buffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `buffplayer "${onlinePlayer.name}" ${buffName}` + command: `buffplayer ${commandTarget} ${buffName}` }) .then(() => { console.log(` āœ“ Applied ${buffName}`); diff --git a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js index ac7f4c4..cd9a8dd 100644 --- a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js +++ b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js @@ -1,5 +1,10 @@ import { data, takaro } from '@takaro/helpers'; +function get7dtdCommandTarget(pog, player) { + if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; + return `\"${player.name}\"`; +} + async function main() { const { gameServerId, eventData, player, pog, module } = data; @@ -124,11 +129,12 @@ async function main() { } console.log(` Buffs to apply: ${buffNames.join(', ')}`); + const commandTarget = get7dtdCommandTarget(pog, player); // Apply all buffs in this package in parallel const buffCommands = buffNames.map(buffName => takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { - command: `buffplayer "${player.name}" ${buffName}` + command: `buffplayer ${commandTarget} ${buffName}` }) .then((response) => { const serverResponse = response.data.data?.rawResult || 'No response'; From 319eb9221e7916463e158947a12a484f64e493d0 Mon Sep 17 00:00:00 2001 From: Harambe Date: Sun, 30 Aug 2026 06:08:04 +0000 Subject: [PATCH 2/3] =?UTF-8?q?=F0=9F=90=9B=20task-buffmanager:=20quote=20?= =?UTF-8?q?fallback=20player=20targets=20safely?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/BuffManager/src/commands/buffplayer/index.js | 2 +- modules/BuffManager/src/commands/debuffplayer/index.js | 2 +- modules/BuffManager/src/commands/getbuff/index.js | 2 +- modules/BuffManager/src/cronjobs/check Buff Packages/index.js | 2 +- modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/modules/BuffManager/src/commands/buffplayer/index.js b/modules/BuffManager/src/commands/buffplayer/index.js index a5ce915..1fd3f0a 100644 --- a/modules/BuffManager/src/commands/buffplayer/index.js +++ b/modules/BuffManager/src/commands/buffplayer/index.js @@ -2,7 +2,7 @@ import { data, takaro, TakaroUserError } from '@takaro/helpers'; function get7dtdCommandTarget(onlinePlayer) { if (onlinePlayer?.gameId) return String(onlinePlayer.gameId).startsWith('EOS_') ? onlinePlayer.gameId : `EOS_${onlinePlayer.gameId}`; - return `\"${onlinePlayer.name}\"`; + return JSON.stringify(onlinePlayer.name); } async function main() { diff --git a/modules/BuffManager/src/commands/debuffplayer/index.js b/modules/BuffManager/src/commands/debuffplayer/index.js index 4edcc0f..b7d7187 100644 --- a/modules/BuffManager/src/commands/debuffplayer/index.js +++ b/modules/BuffManager/src/commands/debuffplayer/index.js @@ -2,7 +2,7 @@ import { data, takaro, TakaroUserError } from '@takaro/helpers'; function get7dtdCommandTarget(onlinePlayer) { if (onlinePlayer?.gameId) return String(onlinePlayer.gameId).startsWith('EOS_') ? onlinePlayer.gameId : `EOS_${onlinePlayer.gameId}`; - return `\"${onlinePlayer.name}\"`; + return JSON.stringify(onlinePlayer.name); } async function main() { diff --git a/modules/BuffManager/src/commands/getbuff/index.js b/modules/BuffManager/src/commands/getbuff/index.js index 2f6db4e..fea9c3f 100644 --- a/modules/BuffManager/src/commands/getbuff/index.js +++ b/modules/BuffManager/src/commands/getbuff/index.js @@ -2,7 +2,7 @@ import { data, takaro, checkPermission, TakaroUserError } from '@takaro/helpers' function get7dtdCommandTarget(pog, player) { if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; - return `\"${player.name}\"`; + return JSON.stringify(player.name); } async function main() { diff --git a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js index 2393378..015b005 100644 --- a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js +++ b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js @@ -2,7 +2,7 @@ import { data, takaro } from '@takaro/helpers'; function get7dtdCommandTarget(pog, onlinePlayer) { if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; - return `\"${onlinePlayer.name}\"`; + return JSON.stringify(onlinePlayer.name); } async function main() { diff --git a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js index cd9a8dd..aed57c3 100644 --- a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js +++ b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js @@ -2,7 +2,7 @@ import { data, takaro } from '@takaro/helpers'; function get7dtdCommandTarget(pog, player) { if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; - return `\"${player.name}\"`; + return JSON.stringify(player.name); } async function main() { From 674476a48ccb8fc5d010b03045ff1f8981183216 Mon Sep 17 00:00:00 2001 From: Harambe Date: Sun, 30 Aug 2026 07:07:21 +0000 Subject: [PATCH 3/3] =?UTF-8?q?=E2=9C=A8=20task-buffmanager:=20add=20world?= =?UTF-8?q?=20event=20mode?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- modules/BuffManager/module.json | 48 ++++- .../src/commands/buffevent/index.js | 166 ++++++++++++++++++ .../src/cronjobs/check Buff Packages/index.js | 58 ++++++ .../src/functions/buff-manager-helpers.js | 126 +++++++++++++ .../hooks/Auto-Apply Buff Packages/index.js | 36 +++- 5 files changed, 428 insertions(+), 6 deletions(-) create mode 100644 modules/BuffManager/src/commands/buffevent/index.js create mode 100644 modules/BuffManager/src/functions/buff-manager-helpers.js diff --git a/modules/BuffManager/module.json b/modules/BuffManager/module.json index 2b54b69..54f65fb 100644 --- a/modules/BuffManager/module.json +++ b/modules/BuffManager/module.json @@ -1,7 +1,7 @@ { "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", + "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- āœ… World event mode for temporary server-wide buff packages with late-join support\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" @@ -89,6 +89,19 @@ "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 * * * *" + }, + "worldEventDefaultDuration": { + "type": "number", + "title": "World Event Default Duration (Minutes)", + "description": "Default duration for /buffevent start when no duration is provided. Set to 0 for an event that runs until /buffevent stop.", + "minimum": 0, + "default": 60 + }, + "announceWorldEvents": { + "type": "boolean", + "title": "Announce World Events", + "description": "Broadcast start and stop messages for /buffevent world buff events.", + "default": true } }, "required": [ @@ -160,6 +173,34 @@ "position": 0 } ] + }, + "buffevent": { + "trigger": "buffevent", + "description": "Start, stop, or inspect a server-wide buff event using configured buff packages.", + "helpText": "Manage world buff events. Usage: /buffevent start [durationMinutes], /buffevent stop, /buffevent status", + "function": "src/commands/buffevent/index.js", + "arguments": [ + { + "name": "action", + "type": "string", + "defaultValue": "status", + "helpText": "start, stop, or status", + "position": 0 + }, + { + "name": "packageName", + "type": "string", + "defaultValue": "", + "helpText": "Buff package commandName, or random when starting", + "position": 1 + }, + { + "name": "durationMinutes", + "type": "number", + "helpText": "Optional event duration in minutes. Omit to use module default. 0 means until stopped.", + "position": 2 + } + ] } }, "hooks": { @@ -176,5 +217,10 @@ "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" } + }, + "functions": { + "buff-manager-helpers": { + "function": "src/functions/buff-manager-helpers.js" + } } } diff --git a/modules/BuffManager/src/commands/buffevent/index.js b/modules/BuffManager/src/commands/buffevent/index.js new file mode 100644 index 0000000..4636be2 --- /dev/null +++ b/modules/BuffManager/src/commands/buffevent/index.js @@ -0,0 +1,166 @@ +import { data, takaro, TakaroUserError } from '@takaro/helpers'; +import { + applyPackageToPog, + clearWorldEvent, + findBuffPackage, + formatDuration, + getOnlinePogs, + isWorldEventExpired, + readWorldEvent, + removePackageFromPog, + resolveWorldEventPackage, + writeWorldEvent, +} from './buff-manager-helpers.js'; + +async function broadcast(gameServerId, message, enabled) { + if (!enabled) return; + try { + await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { message }); + } catch (err) { + console.log(`āš ļø Failed to broadcast world event message: ${err.message}`); + } +} + +function getEventDurationMinutes(config, pkg, argDuration) { + const requested = Number(argDuration); + if (Number.isFinite(requested) && requested >= 0) return requested; + + const configuredDefault = Number(config.worldEventDefaultDuration); + if (Number.isFinite(configuredDefault) && configuredDefault >= 0) return configuredDefault; + + const packageDuration = Number(pkg.duration); + return Number.isFinite(packageDuration) && packageDuration >= 0 ? packageDuration : 60; +} + +async function startWorldEvent(gameServerId, module, pog, config, buffPackages, packageName, durationArg) { + if (!packageName) { + throw new TakaroUserError('Usage: /buffevent start [durationMinutes]'); + } + + const pkg = resolveWorldEventPackage(buffPackages, packageName); + if (!pkg) { + const availablePackages = buffPackages.map((p) => p.commandName).join(', '); + throw new TakaroUserError(`Buff package "${packageName}" not found. Available: ${availablePackages}. Use "random" to pick one.`); + } + + const { state: existingState } = await readWorldEvent(gameServerId, module.moduleId); + if (existingState && !isWorldEventExpired(existingState)) { + const existingPkg = findBuffPackage(buffPackages, existingState.packageName); + const existingName = existingPkg?.displayName || existingState.packageName; + throw new TakaroUserError(`A world event is already active: ${existingName}. Stop it first with /buffevent stop.`); + } + + const durationMinutes = getEventDurationMinutes(config, pkg, durationArg); + const now = Date.now(); + const expiresAt = durationMinutes > 0 ? now + durationMinutes * 60000 : 0; + const eventState = { + packageName: pkg.commandName, + startedAt: now, + expiresAt, + }; + + await writeWorldEvent(gameServerId, module.moduleId, eventState); + + const onlinePogs = await getOnlinePogs(gameServerId); + let totalBuffsApplied = 0; + let playersApplied = 0; + + console.log(`šŸŒ Starting world buff event: ${pkg.displayName}`); + console.log(` Online players: ${onlinePogs.length}`); + console.log(` Duration: ${formatDuration(durationMinutes)}`); + + for (const onlinePog of onlinePogs) { + const applied = await applyPackageToPog(gameServerId, pkg, onlinePog); + if (applied > 0) playersApplied++; + totalBuffsApplied += applied; + } + + await broadcast( + gameServerId, + `World event started: ${pkg.displayName}${durationMinutes > 0 ? ` for ${formatDuration(durationMinutes)}` : ''}. Buffs apply to online players and late joiners.`, + config.announceWorldEvents !== false, + ); + + await pog.pm(`Started world event: ${pkg.displayName}. Applied ${totalBuffsApplied} buff(s) to ${playersApplied}/${onlinePogs.length} online player(s).`); +} + +async function stopWorldEvent(gameServerId, module, pog, config, buffPackages) { + const { state } = await readWorldEvent(gameServerId, module.moduleId); + if (!state) throw new TakaroUserError('No world event is active.'); + + const pkg = findBuffPackage(buffPackages, state.packageName); + if (!pkg) { + await clearWorldEvent(gameServerId, module.moduleId); + throw new TakaroUserError(`World event package "${state.packageName}" is no longer configured. Cleared the stale event state.`); + } + + const onlinePogs = await getOnlinePogs(gameServerId); + let totalBuffsRemoved = 0; + let playersRemoved = 0; + + console.log(`šŸŒ Stopping world buff event: ${pkg.displayName}`); + console.log(` Online players: ${onlinePogs.length}`); + + for (const onlinePog of onlinePogs) { + const removed = await removePackageFromPog(gameServerId, pkg, onlinePog); + if (removed > 0) playersRemoved++; + totalBuffsRemoved += removed; + } + + await clearWorldEvent(gameServerId, module.moduleId); + await broadcast(gameServerId, `World event ended: ${pkg.displayName}.`, config.announceWorldEvents !== false); + await pog.pm(`Stopped world event: ${pkg.displayName}. Removed ${totalBuffsRemoved} buff(s) from ${playersRemoved}/${onlinePogs.length} online player(s).`); +} + +async function showWorldEventStatus(gameServerId, module, pog, buffPackages) { + const { state } = await readWorldEvent(gameServerId, module.moduleId); + if (!state) { + await pog.pm('No world event is active.'); + return; + } + + if (isWorldEventExpired(state)) { + await pog.pm('A world event is expired and will be cleaned up by the next maintenance run. Use /buffevent stop to clear it now.'); + return; + } + + const pkg = findBuffPackage(buffPackages, state.packageName); + const name = pkg?.displayName || state.packageName; + const durationText = Number(state.expiresAt) > 0 + ? `expires in ${formatDuration(Math.ceil((Number(state.expiresAt) - Date.now()) / 60000))}` + : 'permanent until stopped'; + await pog.pm(`Active world event: ${name} (${durationText}).`); +} + +async function main() { + const { gameServerId, pog, arguments: args, module } = data; + const config = module.userConfig; + const buffPackages = config.buffPackages || []; + + if (buffPackages.length === 0) { + throw new TakaroUserError('No buff packages are configured.'); + } + + const action = String(args.action || 'status').trim().toLowerCase(); + const packageName = String(args.packageName || '').trim(); + const durationMinutes = args.durationMinutes; + + if (action === 'start') { + await startWorldEvent(gameServerId, module, pog, config, buffPackages, packageName, durationMinutes); + return; + } + + if (action === 'stop') { + await stopWorldEvent(gameServerId, module, pog, config, buffPackages); + return; + } + + if (action === 'status') { + await showWorldEventStatus(gameServerId, module, pog, buffPackages); + return; + } + + throw new TakaroUserError('Unknown action. Usage: /buffevent [packageName|random] [durationMinutes]'); +} + +await main(); diff --git a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js index 015b005..12a7a81 100644 --- a/modules/BuffManager/src/cronjobs/check Buff Packages/index.js +++ b/modules/BuffManager/src/cronjobs/check Buff Packages/index.js @@ -1,4 +1,11 @@ import { data, takaro } from '@takaro/helpers'; +import { + clearWorldEvent, + findBuffPackage, + isWorldEventExpired, + readWorldEvent, + removePackageFromPog, +} from './buff-manager-helpers.js'; function get7dtdCommandTarget(pog, onlinePlayer) { if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; @@ -31,6 +38,8 @@ async function main() { buffPackagesMaintained: 0, buffPackagesExpiredAndRemoved: 0, totalBuffsReapplied: 0, + worldEventBuffsMaintained: 0, + worldEventBuffsRemoved: 0, errors: 0 }; @@ -67,6 +76,53 @@ async function main() { console.log(`\nšŸ” Processing ${validPlayers.length} player(s)...\n`); + // Process active world event before per-player package maintenance. + try { + const { state: worldEvent } = await readWorldEvent(gameServerId, module.moduleId); + if (worldEvent) { + const worldEventPackage = findBuffPackage(buffPackages, worldEvent.packageName); + if (!worldEventPackage) { + console.log(`šŸŒ World event package "${worldEvent.packageName}" is no longer configured; clearing state`); + await clearWorldEvent(gameServerId, module.moduleId); + } else if (isWorldEventExpired(worldEvent)) { + console.log(`šŸŒ World event expired: ${worldEventPackage.displayName}. Removing from online players.`); + for (const { pog } of validPlayers) { + stats.worldEventBuffsRemoved += await removePackageFromPog(gameServerId, worldEventPackage, pog); + } + await clearWorldEvent(gameServerId, module.moduleId); + try { + await takaro.gameserver.gameServerControllerSendMessage(gameServerId, { + message: `World event ended: ${worldEventPackage.displayName}.` + }); + } catch (err) { + console.log(` āš ļø Failed to announce expired world event: ${err.message}`); + } + } else { + console.log(`šŸŒ Maintaining active world event: ${worldEventPackage.displayName}`); + for (const { pog } of validPlayers) { + const commandTarget = get7dtdCommandTarget(pog, pog.player || { name: pog.name || pog.gameId }); + const buffNames = worldEventPackage.buffNames || []; + const buffCommands = buffNames.map(buffName => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer ${commandTarget} ${buffName}` + }) + .then(() => ({ success: true })) + .catch(err => { + console.log(` āœ— Failed world event buff ${buffName}: ${err.message}`); + stats.errors++; + return { success: false }; + }) + ); + const results = await Promise.all(buffCommands); + stats.worldEventBuffsMaintained += results.filter(r => r.success).length; + } + } + } + } catch (err) { + console.log(`āš ļø Failed to process world event maintenance: ${err.message}`); + stats.errors++; + } + // Process each player for (const { onlinePlayer, takaroPlayer, pog } of validPlayers) { console.log(`\nšŸ‘¤ Player: ${onlinePlayer.name}`); @@ -273,6 +329,8 @@ async function main() { console.log(` - Active buff packages maintained: ${stats.buffPackagesMaintained}`); console.log(` - Expired buffs removed: ${stats.buffPackagesExpiredAndRemoved}`); console.log(` - Total buffs re-applied: ${stats.totalBuffsReapplied}`); + console.log(` - World event buffs maintained: ${stats.worldEventBuffsMaintained}`); + console.log(` - World event buffs removed: ${stats.worldEventBuffsRemoved}`); if (stats.errors > 0) { console.log(` - Errors encountered: ${stats.errors} āš ļø`); } diff --git a/modules/BuffManager/src/functions/buff-manager-helpers.js b/modules/BuffManager/src/functions/buff-manager-helpers.js new file mode 100644 index 0000000..cde6124 --- /dev/null +++ b/modules/BuffManager/src/functions/buff-manager-helpers.js @@ -0,0 +1,126 @@ +import { takaro } from '@takaro/helpers'; + +export const WORLD_EVENT_KEY = 'buff_world_event_active'; + +export function get7dtdCommandTarget(pogOrOnlinePlayer, fallbackPlayer) { + const gameId = pogOrOnlinePlayer?.gameId; + if (gameId) return String(gameId).startsWith('EOS_') ? String(gameId) : `EOS_${gameId}`; + return JSON.stringify(fallbackPlayer?.name || pogOrOnlinePlayer?.name || ''); +} + +export function findBuffPackage(buffPackages, packageName) { + const normalized = String(packageName || '').trim().toLowerCase(); + return (buffPackages || []).find((pkg) => String(pkg.commandName || '').toLowerCase() === normalized); +} + +export function resolveWorldEventPackage(buffPackages, packageName) { + const packages = buffPackages || []; + const normalized = String(packageName || '').trim().toLowerCase(); + if (normalized === 'random') { + if (packages.length === 0) return null; + return packages[Math.floor(Math.random() * packages.length)]; + } + return findBuffPackage(packages, packageName); +} + +export function formatDuration(minutes) { + const value = Math.floor(Number(minutes)); + if (!Number.isFinite(value) || value <= 0) return 'permanent'; + const hours = Math.floor(value / 60); + return hours > 0 ? `${hours}h ${value % 60}m` : `${value}m`; +} + +export function isWorldEventExpired(eventState) { + return Number(eventState?.expiresAt || 0) > 0 && Date.now() > Number(eventState.expiresAt); +} + +export async function findVariable(gameServerId, moduleId, key) { + const res = await takaro.variable.variableControllerSearch({ + filters: { + key: [key], + gameServerId: [gameServerId], + moduleId: [moduleId], + }, + limit: 1, + }); + return res.data.data.length > 0 ? res.data.data[0] : null; +} + +export async function readWorldEvent(gameServerId, moduleId) { + const variable = await findVariable(gameServerId, moduleId, WORLD_EVENT_KEY); + if (!variable) return { variable: null, state: null }; + + try { + return { variable, state: JSON.parse(variable.value) }; + } catch (err) { + console.log(`āš ļø Failed to parse world event state. Clearing invalid state: ${err.message}`); + await takaro.variable.variableControllerDelete(variable.id); + return { variable: null, state: null }; + } +} + +export async function writeWorldEvent(gameServerId, moduleId, state) { + const existing = await findVariable(gameServerId, moduleId, WORLD_EVENT_KEY); + const value = JSON.stringify(state); + if (existing) { + await takaro.variable.variableControllerUpdate(existing.id, { value }); + } else { + await takaro.variable.variableControllerCreate({ + key: WORLD_EVENT_KEY, + value, + gameServerId, + moduleId, + }); + } +} + +export async function clearWorldEvent(gameServerId, moduleId) { + const existing = await findVariable(gameServerId, moduleId, WORLD_EVENT_KEY); + if (existing) await takaro.variable.variableControllerDelete(existing.id); +} + +export async function getOnlinePogs(gameServerId) { + const pogsRes = await takaro.playerOnGameserver.playerOnGameServerControllerSearch({ + filters: { + gameServerId: [gameServerId], + online: [true], + }, + extend: ['player'], + limit: 100, + }); + return pogsRes.data.data || []; +} + +export async function applyPackageToPog(gameServerId, pkg, pog) { + const player = pog.player || { name: pog.name || pog.gameId }; + const commandTarget = get7dtdCommandTarget(pog, player); + const buffNames = pkg.buffNames || []; + const results = await Promise.all(buffNames.map((buffName) => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `buffplayer ${commandTarget} ${buffName}`, + }) + .then(() => ({ success: true, buffName })) + .catch((err) => { + console.log(` āœ— Failed to apply ${buffName} to ${player.name || pog.gameId}: ${err.message}`); + return { success: false, buffName }; + }) + )); + return results.filter((result) => result.success).length; +} + +export async function removePackageFromPog(gameServerId, pkg, pog) { + const player = pog.player || { name: pog.name || pog.gameId }; + const commandTarget = get7dtdCommandTarget(pog, player); + const buffNames = pkg.buffNames || []; + const results = await Promise.all(buffNames.map((buffName) => + takaro.gameserver.gameServerControllerExecuteCommand(gameServerId, { + command: `debuffplayer ${commandTarget} ${buffName}`, + }) + .then(() => ({ success: true, buffName })) + .catch((err) => { + console.log(` āœ— Failed to remove ${buffName} from ${player.name || pog.gameId}: ${err.message}`); + return { success: false, buffName }; + }) + )); + return results.filter((result) => result.success).length; +} diff --git a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js index aed57c3..6ab35c1 100644 --- a/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js +++ b/modules/BuffManager/src/hooks/Auto-Apply Buff Packages/index.js @@ -1,9 +1,12 @@ import { data, takaro } from '@takaro/helpers'; - -function get7dtdCommandTarget(pog, player) { - if (pog?.gameId) return String(pog.gameId).startsWith('EOS_') ? pog.gameId : `EOS_${pog.gameId}`; - return JSON.stringify(player.name); -} +import { + applyPackageToPog, + clearWorldEvent, + findBuffPackage, + get7dtdCommandTarget, + isWorldEventExpired, + readWorldEvent, +} from './buff-manager-helpers.js'; async function main() { const { gameServerId, eventData, player, pog, module } = data; @@ -28,6 +31,29 @@ async function main() { return; } + // Apply active world event packages to late joiners before normal per-player auto-apply. + try { + const { state: worldEvent } = await readWorldEvent(gameServerId, module.moduleId); + if (worldEvent) { + if (isWorldEventExpired(worldEvent)) { + console.log('šŸŒ World event is expired; clearing stale state'); + await clearWorldEvent(gameServerId, module.moduleId); + } else { + const worldEventPackage = findBuffPackage(buffPackages, worldEvent.packageName); + if (!worldEventPackage) { + console.log(`šŸŒ World event package "${worldEvent.packageName}" is no longer configured; clearing state`); + await clearWorldEvent(gameServerId, module.moduleId); + } else { + console.log(`šŸŒ Applying active world event package to late joiner: ${worldEventPackage.displayName}`); + const applied = await applyPackageToPog(gameServerId, worldEventPackage, pog); + console.log(` āœ… World event applied ${applied}/${(worldEventPackage.buffNames || []).length} buff(s)`); + } + } + } + } catch (err) { + console.log(`āš ļø Failed to process active world event for late joiner: ${err.message}`); + } + // Filter packages that should auto-apply const autoApplyPackages = buffPackages.filter(pkg => pkg.autoApplyOnConnect === true);