2023-05-23 01:14:17 -04:00
import Discord from 'discord.js' ;
2023-04-14 06:47:58 -04:00
import TClient from '../client.js' ;
2023-01-02 07:08:59 -05:00
import path from 'node:path' ;
2023-02-13 02:37:23 -05:00
import canvas from 'canvas' ;
2023-10-02 13:05:51 -04:00
import PalletLibrary from '../helpers/PalletLibrary.js' ;
2023-08-30 04:34:59 -04:00
import FormatPlayer from '../helpers/FormatPlayer.js' ;
2023-08-29 20:21:53 -04:00
import MessageTool from '../helpers/MessageTool.js' ;
2023-09-29 17:27:32 -04:00
import Logger from '../helpers/Logger.js' ;
2023-05-23 01:14:17 -04:00
import { readFileSync } from 'node:fs' ;
2023-09-01 00:32:11 -04:00
import { FSData } from '../typings/interfaces' ;
2022-11-17 12:58:19 -05:00
2023-08-19 21:04:14 -04:00
const serverChoices = [
{ name : 'Main Server' , value : 'mainServer' } ,
{ name : 'Second Server' , value : 'secondServer' }
]
2022-11-18 11:56:18 -05:00
export default {
2023-08-19 08:50:05 -04:00
async run ( client : TClient , interaction : Discord.ChatInputCommandInteraction < 'cached' > ) {
2023-10-02 13:05:51 -04:00
if ( client . uptime < 35000 ) return interaction . reply ( 'I have just restarted, please wait for MPLoop to finish initializing.' ) ;
2023-08-24 12:06:39 -04:00
const serverSelector = interaction . options . getString ( 'server' ) ;
2023-10-06 01:54:27 -04:00
if ( [ '468835769092669461' , '1149238561934151690' ] . includes ( interaction . channelId ) && ! MessageTool . isStaff ( interaction . member ) && [ 'status' , 'players' ] . includes ( interaction . options . getSubcommand ( ) ) ) return interaction . reply ( 'Please use <#739084625862852715> for `/mp status/players` commands to prevent clutter in this channel.' ) . then ( ( ) = > setTimeout ( ( ) = > interaction . deleteReply ( ) , 6000 ) ) ;
2023-08-19 08:50:05 -04:00
2023-10-02 18:40:03 -04:00
const database = await client . MPServer . findInCache ( interaction . guildId ) ;
2023-09-03 00:15:02 -04:00
const endpoint = await fetch ( database [ serverSelector ] . ip + '/feed/dedicated-server-stats.json?code=' + database [ serverSelector ] . code , { signal : AbortSignal.timeout ( 7500 ) , headers : { 'User-Agent' : 'Daggerbot - MPdata/undici' } } ) . then ( r = > r . json ( ) as Promise < FSData > ) ;
2023-08-19 20:17:22 -04:00
const embed = new client . embed ( ) ;
2023-08-19 08:50:05 -04:00
( {
players : async ( ) = > {
const data = JSON . parse ( readFileSync ( path . join ( ` src/database/ ${ client . MPServerCache [ serverSelector ] . name } PlayerData.json ` ) , { encoding : 'utf8' } ) ) . slice ( client . statsGraph ) ;
// handle negative days
for ( const [ i , change ] of data . entries ( ) ) if ( change < 0 ) data [ i ] = data [ i - 1 ] || data [ i + 1 ] || 0 ;
const first_graph_top = 16 ;
const second_graph_top = 16 ;
const textSize = 40 ;
const img = canvas . createCanvas ( 1500 , 750 ) ;
const ctx = img . getContext ( '2d' ) ;
const graphOrigin = [ 15 , 65 ] ;
const graphSize = [ 1300 , 630 ] ;
const nodeWidth = graphSize [ 0 ] / ( data . length - 1 ) ;
ctx . fillStyle = '#36393f' ;
ctx . fillRect ( 0 , 0 , img . width , img . height ) ;
// grey horizontal lines
ctx . lineWidth = 5 ;
const interval_candidates : [ number , number , number ] [ ] = [ ] ;
for ( let i = 4 ; i < 10 ; i ++ ) {
const interval = first_graph_top / i ;
if ( Number . isInteger ( interval ) ) {
let intervalString = interval . toString ( ) ;
const reference_number = i * Math . max ( intervalString . split ( '' ) . filter ( x = > x === '0' ) . length / intervalString . length , 0.3 ) * ( [ '1' , '2' , '4' , '5' , '6' , '8' ] . includes ( intervalString [ 0 ] ) ? 1.5 : 0.67 )
interval_candidates . push ( [ interval , i , reference_number ] ) ;
}
}
const chosen_interval = interval_candidates . sort ( ( a , b ) = > b [ 2 ] - a [ 2 ] ) [ 0 ] ;
const previousY : number [ ] = [ ] ;
ctx . strokeStyle = '#202225' ;
for ( let i = 0 ; i <= chosen_interval [ 1 ] ; i ++ ) {
const y = graphOrigin [ 1 ] + graphSize [ 1 ] - ( i * ( chosen_interval [ 0 ] / second_graph_top ) * graphSize [ 1 ] ) ;
if ( y < graphOrigin [ 1 ] ) continue ;
const even = ( ( i + 1 ) % 2 ) === 0 ;
if ( even ) ctx . strokeStyle = '#2c2f33' ;
ctx . beginPath ( ) ;
ctx . lineTo ( graphOrigin [ 0 ] , y ) ;
ctx . lineTo ( graphOrigin [ 0 ] + graphSize [ 0 ] , y ) ;
ctx . stroke ( ) ;
ctx . closePath ( ) ;
if ( even ) ctx . strokeStyle = '#202225' ;
previousY . push ( y , i * chosen_interval [ 0 ] ) ;
}
// 30d mark
ctx . setLineDash ( [ 8 , 16 ] ) ;
ctx . beginPath ( ) ;
const lastMonthStart = graphOrigin [ 0 ] + ( nodeWidth * ( data . length - 60 ) ) ;
ctx . lineTo ( lastMonthStart , graphOrigin [ 1 ] ) ;
ctx . lineTo ( lastMonthStart , graphOrigin [ 1 ] + graphSize [ 1 ] ) ;
ctx . stroke ( ) ;
ctx . closePath ( ) ;
ctx . setLineDash ( [ ] ) ;
// draw points
ctx . lineWidth = 5 ;
const gradient = ctx . createLinearGradient ( 0 , graphOrigin [ 1 ] , 0 , graphOrigin [ 1 ] + graphSize [ 1 ] ) ;
gradient . addColorStop ( 1 / 16 , '#e62c3b' ) ; // Red
gradient . addColorStop ( 5 / 16 , '#ffea00' ) ; // Yellow
gradient . addColorStop ( 12 / 16 , '#57f287' ) ; // Green
let lastCoords : number [ ] = [ ] ;
for ( let [ i , curPC /* current player count */ ] of data . entries ( ) ) {
if ( curPC < 0 ) curPC = 0 ;
const x = i * nodeWidth + graphOrigin [ 0 ] ;
const y = ( ( 1 - ( curPC / second_graph_top ) ) * graphSize [ 1 ] ) + graphOrigin [ 1 ] ;
const nexPC /* next player count */ = data [ i + 1 ] ;
const prvPC /* previous player count */ = data [ i - 1 ] ;
ctx . strokeStyle = gradient ;
ctx . beginPath ( ) ;
if ( lastCoords . length ) ctx . moveTo ( lastCoords [ 0 ] , lastCoords [ 1 ] ) ;
// if the line being drawn is horizontal, make it go until it has to go down
if ( y === lastCoords [ 1 ] ) {
let newX = x ;
for ( let j = i + 1 ; j <= data . length ; j ++ ) {
if ( data [ j ] === curPC ) newX += nodeWidth ;
else break ;
}
ctx . lineTo ( newX , y ) ;
} else ctx . lineTo ( x , y ) ;
lastCoords = [ x , y ] ;
ctx . stroke ( ) ;
ctx . closePath ( ) ;
if ( curPC !== prvPC || curPC !== nexPC ) { // Ball if vertical different to next or prev point
// ball
ctx . fillStyle = gradient ;
ctx . beginPath ( ) ;
ctx . arc ( x , y , ctx . lineWidth * 1.3 , 0 , 2 * Math . PI )
ctx . closePath ( ) ;
ctx . fill ( ) ;
} ;
}
2023-08-15 06:47:31 -04:00
2023-08-19 08:50:05 -04:00
// draw text
ctx . font = '400 ' + textSize + 'px sans-serif' ;
ctx . fillStyle = 'white' ;
// highest value
if ( ! isNaN ( previousY . at ( - 2 ) as number ) ) {
const maxx = graphOrigin [ 0 ] + graphSize [ 0 ] + textSize / 2 ;
const maxy = ( previousY . at ( - 2 ) as number ) + ( textSize / 3 ) ;
ctx . fillText ( ( previousY . at ( - 1 ) as number ) . toLocaleString ( 'en-US' ) , maxx , maxy ) ;
}
// lowest value
const lowx = graphOrigin [ 0 ] + graphSize [ 0 ] + textSize / 2 ;
const lowy = graphOrigin [ 1 ] + graphSize [ 1 ] + ( textSize / 3 ) ;
ctx . fillText ( '0 players' , lowx , lowy ) ;
// 30d
ctx . fillText ( '30 min ago' , lastMonthStart , graphOrigin [ 1 ] - ( textSize / 2 ) ) ;
// time ->
const tx = graphOrigin [ 0 ] + ( textSize / 2 ) ;
const ty = graphOrigin [ 1 ] + graphSize [ 1 ] + ( textSize ) ;
ctx . fillText ( 'time ->' , tx , ty ) ;
const playerData : string [ ] = [ ] ;
let Color = client . config . embedColor ;
if ( endpoint . slots . used === endpoint . slots . capacity ) Color = client . config . embedColorRed ;
else if ( endpoint . slots . used > 8 ) Color = client . config . embedColorYellow ;
else Color = client . config . embedColorGreen ;
2023-08-30 04:34:59 -04:00
for ( const player of endpoint . slots . players . filter ( x = > x . isUsed ) ) playerData . push ( ` ** ${ player . name } ${ FormatPlayer . decoratePlayerIcons ( player ) } ** \ nFarming for ${ FormatPlayer . uptimeFormat ( player . uptime ) } ` )
2023-08-19 08:50:05 -04:00
const slot = ` ${ endpoint . slots . used } / ${ endpoint . slots . capacity } ` ;
const ingameTime = ` ${ ( '0' + Math . floor ( ( endpoint . server . dayTime / 3600 / 1000 ) ) ) . slice ( - 2 ) } : ${ ( '0' + Math . floor ( ( endpoint . server . dayTime / 60 / 1000 ) % 60 ) ) . slice ( - 2 ) } ` ;
2023-08-19 20:17:22 -04:00
interaction . reply ( { embeds : [ new client . embed ( ) . setColor ( Color ) . setTitle ( endpoint . server . name . length > 0 ? endpoint . server . name : 'Offline' ) . setDescription ( endpoint . slots . used < 1 ? '*No players online*' : playerData . join ( '\n\n' ) ) . setImage ( 'attachment://FSStats.png' ) . setAuthor ( { name :slot } ) . setFooter ( { text : 'Current time: ' + ingameTime } ) ] , files : [ new client . attachmentBuilder ( img . toBuffer ( ) , { name : 'FSStats.png' } ) ] } )
2023-08-19 08:50:05 -04:00
} ,
status : async ( ) = > {
2023-08-19 20:17:22 -04:00
if ( ! endpoint ) return console . log ( 'Endpoint failed - status' ) ;
2023-08-19 08:50:05 -04:00
try {
2023-08-19 20:17:22 -04:00
if ( endpoint . server . name . length > 1 ) {
interaction . reply ( { embeds : [ embed . setTitle ( 'Status/Details' ) . setColor ( client . config . embedColor ) . addFields (
{ name : 'Server name' , value : ` ${ endpoint ? . server . name . length === 0 ? '\u200b' : ` \` ${ endpoint ? . server . name } \` ` } ` , inline : true } ,
{ name : 'Players' , value : ` ${ endpoint . slots . used } out of ${ endpoint . slots . capacity } ` , inline : true } ,
{ name : 'Current map' , value : ` ${ endpoint ? . server . mapName . length === 0 ? '\u200b' : endpoint . server . mapName } ` , inline : true } ,
{ name : 'Version' , value : ` ${ endpoint ? . server . version . length === 0 ? '\u200b' : endpoint . server . version } ` , inline : true } ,
{ name : 'In-game Time' , value : ` ${ ( '0' + Math . floor ( ( endpoint . server . dayTime / 3600 / 1000 ) ) ) . slice ( - 2 ) } : ${ ( '0' + Math . floor ( ( endpoint . server . dayTime / 60 / 1000 ) % 60 ) ) . slice ( - 2 ) } ` , inline : true }
2023-08-19 08:50:05 -04:00
) ] } )
2023-08-19 20:17:22 -04:00
} else if ( endpoint . server . name . length === 0 ) interaction . reply ( 'Server is currently offline.' )
2023-08-19 08:50:05 -04:00
} catch ( err ) {
console . log ( err )
2023-08-19 20:17:22 -04:00
interaction . reply ( 'Ah, you caught a rare one... Please notify <@&' + client . config . mainServer . roles . bottech + '>' )
2023-08-19 08:50:05 -04:00
}
} ,
info : async ( ) = > {
2023-08-19 20:17:22 -04:00
if ( ! endpoint ) return console . log ( 'Endpoint failed - info' )
if ( endpoint . server . name . length < 1 ) embed . setFooter ( { text : 'Server is currently offline.' } )
2023-08-29 20:21:53 -04:00
interaction . reply ( { embeds : [ embed . setColor ( client . config . embedColor ) . setDescription ( MessageTool . concatMessage (
2023-08-19 20:17:22 -04:00
` **Server name**: \` ${ endpoint ? . server . name . length === 0 ? '\u200b' : endpoint . server . name } \` ` ,
2023-08-19 08:50:05 -04:00
'**Password:** `mf4700`' ,
'**Crossplay server**' ,
2023-08-19 20:17:22 -04:00
` **Map:** ${ endpoint . server . mapName . length < 1 ? 'Null Island' : endpoint . server . mapName } ` ,
` **Mods:** [Click here]( ${ database [ serverSelector ] . ip } /mods.html) **|** [Direct Download]( ${ database [ serverSelector ] . ip } /all_mods_download?onlyActive=true) ` ,
2023-08-19 08:50:05 -04:00
'**Filters:** [Click here](https://discord.com/channels/468835415093411861/468835769092669461/926581585938120724)' ,
'Please see <#543494084363288637> for additional information.'
2023-08-29 20:21:53 -04:00
) ) ] } ) ;
2023-08-19 08:50:05 -04:00
} ,
2023-09-02 13:27:00 -04:00
url : async ( ) = > {
2023-08-19 08:50:05 -04:00
if ( client . config . mainServer . id == interaction . guildId ) {
2023-10-06 01:54:27 -04:00
if ( ! interaction . member . roles . cache . has ( client . config . mainServer . roles . mpmanager ) && ! interaction . member . roles . cache . has ( client . config . mainServer . roles . bottech ) && ! interaction . member . roles . cache . has ( client . config . mainServer . roles . admin ) ) return MessageTool . youNeedRole ( interaction , 'mpmanager' ) ;
2023-08-19 08:50:05 -04:00
}
const address = interaction . options . getString ( 'address' ) ;
if ( ! address ) {
try {
2023-10-02 18:40:03 -04:00
const Url = await client . MPServer . findInCache ( interaction . guildId ) ;
2023-09-02 13:27:00 -04:00
if ( Url [ serverSelector ] . ip && Url [ serverSelector ] . code ) return interaction . reply ( Url [ serverSelector ] . ip + '/feed/dedicated-server-stats.json?code=' + Url [ serverSelector ] . code )
2023-08-19 08:50:05 -04:00
} catch ( err ) {
2023-09-29 17:27:32 -04:00
Logger . forwardToConsole ( 'error' , 'MPDB' , err ) ;
2023-08-19 08:50:05 -04:00
interaction . reply ( ` \` \` \` ${ err } \` \` \` ` )
}
} else {
if ( ! address . match ( /dedicated-server-stats/ ) ) return interaction . reply ( 'The URL does not match `dedicated-server-stats.xml`' ) ;
const newURL = address . replace ( 'xml' , 'json' ) . split ( '/feed/dedicated-server-stats.json?code=' ) ;
try {
2023-09-29 17:27:32 -04:00
Logger . forwardToConsole ( 'log' , 'MPDB' , ` ${ serverSelector } \ 's URL for ${ interaction . guild . name } has been updated by ${ interaction . member . displayName } ( ${ interaction . member . id } ) ` ) ;
2023-09-02 13:27:00 -04:00
const affected = await client . MPServer . _content . findByIdAndUpdate ( { _id : interaction.guildId } , { $set : { [ serverSelector ] : { ip : newURL [ 0 ] , code : newURL [ 1 ] } } } )
if ( affected ) return interaction . reply ( 'URL successfully updated.' )
} catch ( err ) {
2023-09-29 17:27:32 -04:00
Logger . forwardToConsole ( 'log' , 'MPDB' , ` ${ serverSelector } \ 's URL for ${ interaction . guild . name } has been created by ${ interaction . member . displayName } ( ${ interaction . member . id } ) ` ) ;
2023-09-02 13:27:00 -04:00
await client . MPServer . _content . create ( { _id : interaction.guildId , [ serverSelector ] : { ip : newURL [ 0 ] , code : newURL [ 1 ] } } )
. then ( ( ) = > interaction . reply ( 'This server doesn\'t have any data in the database, therefore I have created it for you.' ) )
. catch ( ( err :Error ) = > interaction . reply ( ` I got hit by a flying brick while trying to populate the server data: \ n \` \` \` ${ err . message } \` \` \` ` ) )
2023-08-19 08:50:05 -04:00
}
}
2023-10-02 13:05:51 -04:00
} ,
pallets : async ( ) = > {
if ( ! endpoint ) return console . log ( 'Endpoint failed - pallets' ) ;
const filter = endpoint . vehicles . filter ( v = > v . type === 'pallet' ) ;
if ( filter . length < 1 ) return interaction . reply ( 'There are no pallets on the server.' ) ;
2023-10-02 13:14:57 -04:00
else interaction . reply ( ` There are currently ${ filter . length } pallets on the server. Here \ 's the breakdown: \` \` \` \ n ${ Object . values ( PalletLibrary ( endpoint ) ) . map ( t = > ` ${ t . name . padEnd ( 12 ) } ${ t . size } ` ) . join ( '\n' ) } \` \` \` ` )
2023-09-02 13:27:00 -04:00
}
2023-08-19 20:17:22 -04:00
} ) [ interaction . options . getSubcommand ( ) ] ( ) ;
2023-03-05 05:04:10 -05:00
} ,
2023-05-23 01:14:17 -04:00
data : new Discord . SlashCommandBuilder ( )
2023-08-19 21:04:14 -04:00
. setName ( 'mp' )
. setDescription ( 'Display MP status and other things' )
. addSubcommand ( x = > x
. setName ( 'status' )
. setDescription ( 'Display server status' )
. addStringOption ( x = > x
. setName ( 'server' )
. setDescription ( 'The server to update' )
. setRequired ( true )
2023-10-02 13:05:51 -04:00
. setChoices ( . . . serverChoices ) ) )
2023-08-19 21:04:14 -04:00
. addSubcommand ( x = > x
. setName ( 'players' )
. setDescription ( 'Display players on server' )
. addStringOption ( x = > x
. setName ( 'server' )
. setDescription ( 'The server to display players for' )
. setRequired ( true )
2023-10-02 13:05:51 -04:00
. setChoices ( . . . serverChoices ) ) )
2023-09-02 13:27:00 -04:00
. addSubcommand ( x = > x
2023-08-19 21:04:14 -04:00
. setName ( 'url' )
. setDescription ( 'View or update the server URL' )
. addStringOption ( x = > x
. setName ( 'server' )
. setDescription ( 'The server to update' )
. setRequired ( true )
2023-10-02 13:05:51 -04:00
. setChoices ( . . . serverChoices ) )
2023-08-19 21:04:14 -04:00
. addStringOption ( x = > x
. setName ( 'address' )
. setDescription ( 'The URL to the dedicated-server-stats.json file' )
2023-09-02 13:27:00 -04:00
. setRequired ( false ) ) )
2023-08-19 21:04:14 -04:00
. addSubcommand ( x = > x
. setName ( 'info' )
. setDescription ( 'Display server information' )
. addStringOption ( x = > x
. setName ( 'server' )
. setDescription ( 'The server to display information for' )
. setRequired ( true )
2023-10-02 13:05:51 -04:00
. setChoices ( . . . serverChoices ) ) )
. addSubcommand ( x = > x
. setName ( 'pallets' )
. setDescription ( 'Check total amount of pallets on the server' )
. addStringOption ( x = > x
. setName ( 'server' )
. setDescription ( 'The server to get amount of pallets from' )
. setRequired ( true )
. setChoices ( . . . serverChoices ) ) )
2023-01-22 13:14:38 -05:00
}