From 4c0d29148a6ffe00156ac98c558ffbca5ae2113e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 28 Dec 2023 19:33:15 +0000 Subject: [PATCH 001/101] Begin automating presidential election state management. --- presidential-election.js | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 presidential-election.js diff --git a/presidential-election.js b/presidential-election.js new file mode 100644 index 0000000..6134bf3 --- /dev/null +++ b/presidential-election.js @@ -0,0 +1,28 @@ +const moment = require('./moment'); + +const chatroomId = '1188932067359211550'; + +async function Update() { + const t = moment(); + if (t.isAfter('2024-01-03')) { + // This code is in development and needs to be updated before the next election. + return; + } + if (t.isAfter('2024-01-02')) { + // Delete election results message and hide the chatroom, if not already. + return; + } + if (t.isAfter('2024-01-01')) { + // Post election results and delete ballots, if not already. + return; + } + if (t.isAfter('2023-12-28')) { + // Start election. Post ballots and unhide the chatroom, if not already. + return; + } +} + +module.exports = { + CheckReactionForPresidentialElectionVote, + Update, +}; From 9844f171c39498020f0a092786ba30b7a57a39fa Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Jan 2024 23:48:31 +0000 Subject: [PATCH 002/101] Monthly update. --- bot-commands.js | 18 +++++++++--------- huddles.js | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 8f13b90..9d221d1 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -78,14 +78,14 @@ async function HandleServerVoteCommand(discordMessage) { const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); const message = await channel.send( - 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for December 2023.\n\n' + + 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for Jan 2024.\n\n' + 'Every top 100 US monthly vanilla server is included.' ); await message.react('❤️'); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 45); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 36); await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 110); await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 98); - await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 41); + await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 22); await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 128); await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 125); await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); @@ -111,7 +111,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in December 2023. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can.'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in Jan 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; @@ -246,23 +246,23 @@ async function HandleVoiceActiveUsersCommand(discordMessage) { async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { const name = user.getNicknameOrTitleWithInsignia(); - await discordMessage.channel.send(`Sending special mission orders to ${name}`); + await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of December 2023. Report to Rustafied.com - US Long III\n`; + content += `Here are your secret orders for the month of January 2024. Report to Rustafied.com - US Long III\n`; content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. if (user.rank <= 5) { content += `Generals Code 1111\n`; } if (user.rank <= 9) { - content += `Officer Code 1111\n`; + content += `Officers Code 1111\n`; content += `Grunt Code 1111\n`; } if (user.rank <= 13) { content += `Gate Code 1111\n\n`; } - content += `Run straight to A1. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/servers to automatically protect your base from getting raided by the gov. New advancements are coming soon that will revolutionize Rust.\n\n`; + content += `Run straight to D14. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/servers to automatically protect your base from getting raided by the gov.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); diff --git a/huddles.js b/huddles.js index d86598c..a34f44f 100644 --- a/huddles.js +++ b/huddles.js @@ -267,7 +267,7 @@ function GetLowestRankingMembersFromVoiceChannel(channel, n) { return sortableMembers.slice(-n); } -let overflowLimit = 25; +let overflowLimit = 28; function SetOverflowLimit(newLimit) { const maxLimit = 90; From 2a771cc280336d4687e853db7805d09900ff551f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Jan 2024 23:49:09 +0000 Subject: [PATCH 003/101] Lottery activates at a tax base of 100 yen. --- yen.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/yen.js b/yen.js index 9041760..a97531e 100644 --- a/yen.js +++ b/yen.js @@ -370,7 +370,7 @@ async function DoLottery() { for (const i in taxBase) { totalTaxBase += taxBase[i]; } - const minimumTaxBaseForLottery = 1010; + const minimumTaxBaseForLottery = 100; if (totalTaxBase < minimumTaxBaseForLottery) { return; } From 8bba48ffa2e090784e76ac7a6cc17c3260fdeb9a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 12 Jan 2024 03:04:28 +0000 Subject: [PATCH 004/101] Remove unused variable. --- huddles.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/huddles.js b/huddles.js index a34f44f..2518fcd 100644 --- a/huddles.js +++ b/huddles.js @@ -379,11 +379,9 @@ async function Overflow(guild) { console.log(`${mainChannels.length} Main voice channels detected.`); console.log(`overflowLimit ${overflowLimit}`); const overflowMembers = []; - let totalPopOfAllMainRooms = 0; for (const channel of mainChannels) { const pop = channel.members.size; console.log('Main room with pop', pop); - totalPopOfAllMainRooms += pop; if (pop < overflowLimit) { await SetOpenPerms(channel); } else { From 5b16affd6743864935b403d39bed6f980e5bd2c4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 12 Jan 2024 03:59:24 +0000 Subject: [PATCH 005/101] Overflow limit depends on room type. --- bot-commands.js | 17 -------------- huddles.js | 62 +++++++++++++++++-------------------------------- 2 files changed, 21 insertions(+), 58 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 9d221d1..25fff01 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -666,22 +666,6 @@ async function HandleAfkCommand(discordMessage) { } } -async function HandleOverflowCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author || author.commissar_id !== 7) { - // Auth: this command for developer use only. - return; - } - const tokens = discordMessage.content.split(' '); - if (tokens.length !== 2) { - await discordMessage.channel.send('USAGE: !overflow 20'); - return; - } - const newLimit = tokens[1]; - const finalLimit = huddles.SetOverflowLimit(newLimit); - await discordMessage.channel.send(`Overflow limit set to ${finalLimit}`); -} - // Handle any unrecognized commands, possibly replying with an error message. async function HandleUnknownCommand(discordMessage) { // TODO: add permission checks. Only high enough ranks should get a error @@ -722,7 +706,6 @@ async function Dispatch(discordMessage) { '!lottery': yen.DoLottery, '!money': yen.HandleYenCommand, '!nick': HandleNickCommand, - '!overflow': HandleOverflowCommand, '!orders': HandleOrdersCommand, '!orderstest': HandleOrdersTestCommand, '!pardon': Ban.HandlePardonCommand, diff --git a/huddles.js b/huddles.js index 2518fcd..4e31d9a 100644 --- a/huddles.js +++ b/huddles.js @@ -13,11 +13,11 @@ const RoleID = require('./role-id'); const UserCache = require('./user-cache'); const huddles = [ - { name: 'Main', userLimit: 99, position: 1000 }, - { name: 'Duo', userLimit: 2, position: 2000 }, - { name: 'Trio', userLimit: 3, position: 3000 }, - //{ name: 'Quad', userLimit: 4, position: 4000 }, - //{ name: 'Squad', userLimit: 8, position: 7000 }, + { name: 'Main', userLimit: 30, position: 1000 }, + { name: 'Duo', userLimit: 4, position: 2000 }, + { name: 'Trio', userLimit: 5, position: 3000 }, + { name: 'Quad', userLimit: 6, position: 4000 }, + { name: 'Squad', userLimit: 10, position: 7000 }, ]; function GetAllMatchingVoiceChannels(guild, huddle) { @@ -99,9 +99,6 @@ async function UpdateVoiceChannelsForOneHuddleType(guild, huddle) { return; } console.log('Found', matchingChannels.length, 'matching channels.'); - //for (const channel of matchingChannels) { - // await channel.setPosition(huddle.position); - //} const emptyChannels = matchingChannels.filter(ch => ch.members.size === 0); console.log(emptyChannels.length, 'empty channels of this type.'); if (emptyChannels.length === 0) { @@ -267,28 +264,6 @@ function GetLowestRankingMembersFromVoiceChannel(channel, n) { return sortableMembers.slice(-n); } -let overflowLimit = 28; - -function SetOverflowLimit(newLimit) { - const maxLimit = 90; - if (!newLimit) { - newLimit = maxLimit; - } - try { - newLimit = parseInt(newLimit); - } catch (error) { - newLimit = maxLimit; - } - if (newLimit > maxLimit) { - newLimit = maxLimit; - } - if (newLimit < 2) { - newLimit = maxLimit; - } - overflowLimit = newLimit; - return newLimit; -} - // Sets a channel to be accessible to everyone. async function SetOpenPerms(channel) { const connect = PermissionFlagsBits.Connect; @@ -366,22 +341,27 @@ async function SetRankLimit(channel, rankLimit, scoreThreshold) { await channel.permissionOverwrites.set(perms); } -// Enforces a population cap on the Main voice chat rooms by moving low-ranking members around. -// Returns true if it had to move anyone, and false if no moves are needed. +// Enforces a soft population cap on all voice chat rooms by moving low-ranking +// members around. Returns true if it had to move anyone, and false if no moves +// are needed. async function Overflow(guild) { console.log(`Overflow`); const mainChannels = []; + const voiceChannels = []; for (const [id, channel] of guild.channels.cache) { + if (channel.type === 2) { + voiceChannels.push(channel); + } if (channel.type === 2 && !channel.parent && channel.name === 'Main') { mainChannels.push(channel); } } - console.log(`${mainChannels.length} Main voice channels detected.`); - console.log(`overflowLimit ${overflowLimit}`); + console.log(`${voiceChannels.length} voice channels detected.`); const overflowMembers = []; - for (const channel of mainChannels) { + for (const channel of voiceChannels) { const pop = channel.members.size; - console.log('Main room with pop', pop); + const overflowLimit = channel.userLimit - 2; + console.log('Voice room with pop', pop, 'limit', overflowLimit); if (pop < overflowLimit) { await SetOpenPerms(channel); } else { @@ -411,14 +391,15 @@ async function Overflow(guild) { const memberToMove = overflowMembers[0]; const cu = UserCache.GetCachedUserByDiscordId(memberToMove.id); const name = cu.getNicknameOrTitleWithInsignia(); - // Now identify which is the best other Main room to move them to. - // For now choose the fullest other Main room the member is + // Now identify which is the best other voice room to move them to. + // For now choose the fullest other voice room the member is // allowed to join. In the future personalize this so it uses the // coplay time to choose the most familiar group to place the member with. console.log(`Looking for destination for ${name}`); let bestDestination; let bestDestinationPop = 0; - for (const channel of mainChannels) { + for (const channel of voiceChannels) { + const overflowLimit = channel.userLimit - 2; let superiorCount = 0; for (const [id, member] of channel.members) { const voiceUser = UserCache.GetCachedUserByDiscordId(member.id); @@ -450,7 +431,7 @@ async function Overflow(guild) { } // If we end up with at least 2 overflow members and nowhere to put them, // then move them to an empty Main room together. - console.log('Trying to find an empty main channel to populate.'); + console.log('Trying to find an empty v channel to populate.'); let emptyMainChannel; for (const channel of mainChannels) { if (channel.members.size === 0) { @@ -504,5 +485,4 @@ function ScheduleUpdate() { module.exports = { ScheduleUpdate, - SetOverflowLimit, }; From 99343e17ef8c4c33500afabce4d901eb8533032e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 17 Jan 2024 08:14:30 +0000 Subject: [PATCH 006/101] Add steam id to user records. --- commissar-user.js | 12 +++++++++++- user-cache.js | 2 ++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/commissar-user.js b/commissar-user.js index daa789f..e1886bb 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -29,7 +29,8 @@ class CommissarUser { ban_conviction_time, ban_pardon_time, presidential_election_vote, - presidential_election_message_id) { + presidential_election_message_id, + steam_id) { this.commissar_id = commissar_id; this.discord_id = discord_id; this.nickname = nickname; @@ -54,6 +55,7 @@ class CommissarUser { this.ban_pardon_time = ban_pardon_time; this.presidential_election_vote = presidential_election_vote; this.presidential_election_message_id = presidential_election_message_id; + this.steam_id = steam_id; } async setDiscordId(discord_id) { @@ -253,6 +255,14 @@ class CommissarUser { this.presidential_election_message_id = presidential_election_message_id; await this.updateFieldInDatabase('presidential_election_message_id', this.presidential_election_message_id); } + + async setSteamId(steam_id) { + if (steam_id === this.steam_id) { + return; + } + this.steam_id = steam_id; + await this.updateFieldInDatabase('steam_id', this.steam_id); + } async updateFieldInDatabase(fieldName, fieldValue) { //console.log(`DB update ${fieldName} = ${fieldValue} for ${this.nickname} (ID:${this.commissar_id}).`); diff --git a/user-cache.js b/user-cache.js index 6c53be9..3ca26c1 100644 --- a/user-cache.js +++ b/user-cache.js @@ -38,6 +38,7 @@ async function LoadAllUsersFromDatabase() { row.ban_pardon_time, row.presidential_election_vote, row.presidential_election_message_id, + row.steam_id, ); newCache[row.commissar_id] = newUser; }); @@ -115,6 +116,7 @@ async function CreateNewDatabaseUser(discordMember) { null, null, null, 0, null, null, null, null, null, + null, ); commissarUserCache[commissar_id] = newUser; return newUser; From 0cd6377226b9551f6060b156190ea76dbdcb603d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 17 Jan 2024 08:46:03 +0000 Subject: [PATCH 007/101] ID badge automation. --- role-id.js | 1 + server.js | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/role-id.js b/role-id.js index 44968d8..26ae93c 100644 --- a/role-id.js +++ b/role-id.js @@ -8,6 +8,7 @@ module.exports = { Defendant: '918232560813871114', General: '318985002266263552', Grunt: '319302277837881344', + IdBadge: '947942301039231016', Lieutenant: '825491800478449705', Major: '825490288218734674', Marshal: '829432221042999356', diff --git a/server.js b/server.js index 2e1de1f..d5c73f6 100644 --- a/server.js +++ b/server.js @@ -3,10 +3,12 @@ const Ban = require('./ban'); const BanVoteCache = require('./ban-vote-cache'); const BotCommands = require('./bot-commands'); const Clock = require('./clock'); +const config = require('./config'); const DB = require('./database'); const deepEqual = require('deep-equal'); const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('discord.js'); const DiscordUtil = require('./discord-util'); +const fetch = require('./fetch'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); @@ -88,6 +90,10 @@ async function UpdateMemberAppearance(member) { } else { await DiscordUtil.RemoveRole(member, RoleID.RetiredGeneral); } + // ID Badge for all members with a linked steam account. + if (cu.steam_id) { + await DiscordUtil.AddRole(member, RoleID.IdBadge); + } } const afkLoungeId = '703716669452714054'; @@ -237,10 +243,37 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } +async function UpdateProximityChat() { + if (!config.rustCultApiToken) { + console.log('Cannot update prox because no api token.'); + return; + } + const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; + const response = await fetch(url); + if (!response) { + console.log('Cannot update prox because no response received.'); + return; + } + if (typeof response !== 'string') { + console.log('Cannot update prox because response is not a string.'); + return; + } + const linkedAccounts = JSON.parse(response); + for (const account of linkedAccounts) { + if (account && account.discordId && account.steamId) { + const cu = UserCache.GetCachedUserByDiscordId(account.discordId); + if (cu) { + await cu.setSteamId(account.steamId); + } + } + } +} + // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); startTime = new Date().getTime(); + await UpdateProximityChat(); await yen.DoLottery(); await Rank.UpdateUserRanks(); await UpdateVoiceActiveMembersForMainDiscordGuild(); From 85c01083f3269bd4be2ff5b6d829686abf1904ff Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 18 Jan 2024 04:06:38 +0000 Subject: [PATCH 008/101] Rank limit those without linked steam accounts to 4 dot raank. --- rank.js | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/rank.js b/rank.js index 452dc63..9db2459 100644 --- a/rank.js +++ b/rank.js @@ -16,8 +16,10 @@ async function UpdateUserRanks() { } // When we run out of ranks, this line defaults to the last/least rank. rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); - await AnnounceIfPromotion(user, rank); - await user.setRank(rank); + const cap = user.steam_id ? 0 : 10; + const cappedRank = Math.max(rank, cap); + await AnnounceIfPromotion(user, cappedRank); + await user.setRank(cappedRank); usersAtRank++; } } From 38ad89b746a74f8261c73064a90776a240008ff9 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 18 Jan 2024 04:27:38 +0000 Subject: [PATCH 009/101] Get steam name from rustcult API --- commissar-user.js | 12 +++++++++++- server.js | 1 + setup-database.sql | 1 + user-cache.js | 3 ++- 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/commissar-user.js b/commissar-user.js index e1886bb..45603af 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -30,7 +30,8 @@ class CommissarUser { ban_pardon_time, presidential_election_vote, presidential_election_message_id, - steam_id) { + steam_id, + steam_name) { this.commissar_id = commissar_id; this.discord_id = discord_id; this.nickname = nickname; @@ -56,6 +57,7 @@ class CommissarUser { this.presidential_election_vote = presidential_election_vote; this.presidential_election_message_id = presidential_election_message_id; this.steam_id = steam_id; + this.steam_name = steam_name; } async setDiscordId(discord_id) { @@ -263,6 +265,14 @@ class CommissarUser { this.steam_id = steam_id; await this.updateFieldInDatabase('steam_id', this.steam_id); } + + async setSteamName(steam_name) { + if (steam_name === this.steam_name) { + return; + } + this.steam_name = steam_name; + await this.updateFieldInDatabase('steam_name', this.steam_name); + } async updateFieldInDatabase(fieldName, fieldValue) { //console.log(`DB update ${fieldName} = ${fieldValue} for ${this.nickname} (ID:${this.commissar_id}).`); diff --git a/server.js b/server.js index d5c73f6..20772f4 100644 --- a/server.js +++ b/server.js @@ -264,6 +264,7 @@ async function UpdateProximityChat() { const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (cu) { await cu.setSteamId(account.steamId); + await cu.setSteamName(account.steamName); } } } diff --git a/setup-database.sql b/setup-database.sql index 54ce874..6f75d6d 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -8,6 +8,7 @@ CREATE TABLE users commissar_id INT NOT NULL AUTO_INCREMENT, -- Our clan's own set of IDs so we don't have to rely on Discord IDs. discord_id VARCHAR(32), -- Discord ID. steam_id VARCHAR(32), -- Steam ID. + steam_name VARCHAR(128), -- Steam display name. battlemetrics_id VARCHAR(32), -- User ID on Battlemetrics.com. nickname VARCHAR(32), -- Last known nickname. nick VARCHAR(32), -- A user-supplied preferred nickname. diff --git a/user-cache.js b/user-cache.js index 3ca26c1..b5848e3 100644 --- a/user-cache.js +++ b/user-cache.js @@ -39,6 +39,7 @@ async function LoadAllUsersFromDatabase() { row.presidential_election_vote, row.presidential_election_message_id, row.steam_id, + row.steam_name, ); newCache[row.commissar_id] = newUser; }); @@ -116,7 +117,7 @@ async function CreateNewDatabaseUser(discordMember) { null, null, null, 0, null, null, null, null, null, - null, + null, null, ); commissarUserCache[commissar_id] = newUser; return newUser; From db323d948be2a35a9550f5c4134d0a68cd01a0fe Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 18 Jan 2024 22:55:41 +0000 Subject: [PATCH 010/101] Discord names now tied to steam name. --- commissar-user.js | 5 +++-- server.js | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/commissar-user.js b/commissar-user.js index 45603af..6f945c0 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -267,6 +267,7 @@ class CommissarUser { } async setSteamName(steam_name) { + steam_name = FilterUsername(steam_name); if (steam_name === this.steam_name) { return; } @@ -312,7 +313,7 @@ class CommissarUser { return `${prefix} ${job.title}`; } else { // User-supplied nickname overrides their Discord-wide nickname. - return this.nick || this.nickname; + return this.steam_name || this.nick || this.nickname; } } @@ -326,7 +327,7 @@ class CommissarUser { } getNicknameWithInsignia() { - const name = this.nick || this.nickname || 'John Doe'; + const name = this.steam_name || this.nick || this.nickname || 'John Doe'; const insignia = this.getInsignia(); return `${name} ${insignia}`; } diff --git a/server.js b/server.js index 20772f4..bfd1dc6 100644 --- a/server.js +++ b/server.js @@ -72,7 +72,7 @@ async function UpdateMemberAppearance(member) { const displayName = cu.getNicknameOrTitleWithInsignia(); if (member.nickname !== displayName && member.user.id !== member.guild.ownerId) { console.log(`Updating nickname ${displayName}.`); - member.setNickname(displayName); + await member.setNickname(displayName); } // Update role (including rank color). UpdateMemberRankRoles(member, rankData, cu.good_standing); @@ -389,7 +389,7 @@ async function Start() { discordClient.on('userUpdate', async (oldUser, newUser) => { console.log('userUpdate', newUser.username); const cu = await UserCache.GetCachedUserByDiscordId(newUser.id); - await cu.setNickname(newUser.username); + //await cu.setNickname(newUser.username); }); // When a guild member changes their nickname or other details. @@ -399,7 +399,7 @@ async function Start() { if (!cu) { return; } - await cu.setNickname(newMember.user.username); + //await cu.setNickname(newMember.user.username); await cu.setCitizen(true); }); From f52d282a3a0c4ab0249fbdbace6d1021d9e1dfe4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 19 Jan 2024 07:47:33 +0000 Subject: [PATCH 011/101] Half done proximity VC. --- huddles.js | 166 ++++++++++++++++++++++++++++++++++++++++++++++++++++- server.js | 30 ---------- 2 files changed, 163 insertions(+), 33 deletions(-) diff --git a/huddles.js b/huddles.js index 4e31d9a..5b39d25 100644 --- a/huddles.js +++ b/huddles.js @@ -6,8 +6,10 @@ // introduce auto-scaling to Discord voice chat rooms so there are always // the right amount of rooms no matter how busy. +const config = require('./config'); const { PermissionFlagsBits } = require('discord.js'); const DiscordUtil = require('./discord-util'); +const fetch = require('./fetch'); const RankMetadata = require('./rank-definitions'); const RoleID = require('./role-id'); const UserCache = require('./user-cache'); @@ -22,8 +24,6 @@ const huddles = [ function GetAllMatchingVoiceChannels(guild, huddle) { const matchingChannels = []; - // Necessary in case a string key is passed in. Object keys are - // sometimes showing up as strings. for (const [id, channel] of guild.channels.cache) { if (channel.type === 2 && channel.name === huddle.name && @@ -459,14 +459,174 @@ async function Overflow(guild) { return true; } +// Details of last seen in-game movement, keyed by discord ID. +const lastSeenCache = {}; + +async function UpdateProximityChat() { + if (!config.rustCultApiToken) { + console.log('Cannot update prox because no api token.'); + return; + } + const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; + const response = await fetch(url); + if (!response) { + console.log('Cannot update prox because no response received.'); + return; + } + if (typeof response !== 'string') { + console.log('Cannot update prox because response is not a string.'); + return; + } + const linkedAccounts = JSON.parse(response); + console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.cm API.'); + for (const account of linkedAccounts) { + if (account && account.discordId) { + if (account.steamId) { + const cu = UserCache.GetCachedUserByDiscordId(account.discordId); + if (cu) { + await cu.setSteamId(account.steamId); + await cu.setSteamName(account.steamName); + } + } + if (account.server && account.x && account.y) { + lastSeenCache[account.discordId] = account; + } + } + } + console.log(Object.keys(lastSeenCache).length, 'cached member locations.'); + // Get all Proximity VC rooms & members in them. + const guild = await DiscordUtil.GetMainDiscordGuild(); + const proxChannels = {}; + const proxMembers = {}; + for (const [channelId, channel] of guild.channels.cache) { + if (channel.type === 2 && channel.name === 'Proximity') { + proxChannels[channelId] = channel; + for (const [memberId, member] of channel.members) { + proxMembers[memberId] = member; + } + } + } + console.log('Found', Object.keys(proxChannels).length, 'prox channels'); + console.log('Found', Object.keys(proxMembers).length, 'prox members'); + // Make distance matrix. + const distanceMatrix = {}; + for (const i in proxMembers) { + distanceMatrix[i] = {}; + for (const j in proxMembers) { + const a = lastSeenCache[i]; + const b = lastSeenCache[j]; + if (a && b && a.server && b.server && a.server === b.server) { + const dx = a.x - b.x; + const dy = a.y - b.y; + const distance = Math.sqrt(dx * dx + dy * dy); + distanceMatrix[i][j] = distance; + } else { + // Different server or missing server = infinite distance. + distanceMatrix[i][j] = 999999; + } + } + } + console.log('distanceMatrix', distanceMatrix); + // Distance between clusters of Discord IDs. + function ClusterDistance(a, b) { + let minDist = null; + for (const i of a) { + for (const j of b) { + const d = distanceMatrix[i][j]; + if (minDist === null || d < minDist) { + minDist = d; + } + } + } + return minDist; + } + // Cluster diameter. ie: max distance between two points. + function ClusterDiameter(c) { + let maxDist = null; + const n = c.length; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const ci = c[i]; + const cj = c[j]; + const d = distanceMatrix[ci][cj]; + if (maxDist === null || d > maxDist) { + maxDist = d; + } + } + } + return maxDist; + } + // Diameter of 2 clusters combined. + function TwoClusterDiameter(a, b) { + const c = a.concat(b); + return ClusterDiameter(c); + } + // Initialize clusters. Start with n clusters: one per member in proximity VC. + const clusters = []; + for (const discordId in proxMembers) { + clusters.push([discordId]); + } + // Merge clusters until no longer possible. + let bestDistance; + do { + const n = clusters.length; + let bestI; + let bestJ; + bestDistance = null; + for (let i = 0; i < n; i++) { + for (let j = i + 1; j < n; j++) { + const a = clusters[i]; + const b = clusters[j]; + const distance = ClusterDistance(a, b); + if (distance < 438) { + const diameter = TwoClusterDiameter(a, b); + if (diameter < 584) { + if (bestDistance === null || distance < bestDistance) { + bestDistance = distance; + bestI = i; + bestJ = j; + } + } + } + } + } + if (bestDistance !== null) { + const a = clusters[bestI]; + const b = clusters[bestJ]; + // Combine two closest clusters. + const newCluster = a.concat(b); + clusters.splice(bestJ, 1); + clusters.splice(bestI, 1); + clusters.push(newCluster); + } + } while (bestDistance !== null); + // Put solos and randos together into one lobby. + const clustersWithLobby = [[]]; + for (const cluster of clusters) { + if (cluster.length > 1) { + // Cluster with 2 or more members. + clustersWithLobby.push(cluster); + } else { + // Solo cluster. Isolated player detected. Add to lobby. + const solo = cluster[0]; + clustersWithLobby[0].push(solo); + } + } + console.log('Prox clusters', clustersWithLobby); + // Permute clusters to minimize number of drags. + // Perms. Offline members by nearest neighbor. + // Drag. +} + // To avoid race conditions on the cheap, use a system of routine updates. // To schedule an update, a boolean flag is flipped. That way, the next time // the cycle goes around, it knows that an update is needed. Redundant or // overlapping updates are avoided this way. let isUpdateNeeded = false; -setInterval(Update, 5 * 1000); +setInterval(Update, 9 * 1000); async function Update() { + await UpdateProximityChat(); if (!isUpdateNeeded) { return; } diff --git a/server.js b/server.js index bfd1dc6..be35aef 100644 --- a/server.js +++ b/server.js @@ -3,12 +3,10 @@ const Ban = require('./ban'); const BanVoteCache = require('./ban-vote-cache'); const BotCommands = require('./bot-commands'); const Clock = require('./clock'); -const config = require('./config'); const DB = require('./database'); const deepEqual = require('deep-equal'); const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('discord.js'); const DiscordUtil = require('./discord-util'); -const fetch = require('./fetch'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); @@ -243,38 +241,10 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } -async function UpdateProximityChat() { - if (!config.rustCultApiToken) { - console.log('Cannot update prox because no api token.'); - return; - } - const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; - const response = await fetch(url); - if (!response) { - console.log('Cannot update prox because no response received.'); - return; - } - if (typeof response !== 'string') { - console.log('Cannot update prox because response is not a string.'); - return; - } - const linkedAccounts = JSON.parse(response); - for (const account of linkedAccounts) { - if (account && account.discordId && account.steamId) { - const cu = UserCache.GetCachedUserByDiscordId(account.discordId); - if (cu) { - await cu.setSteamId(account.steamId); - await cu.setSteamName(account.steamName); - } - } - } -} - // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); startTime = new Date().getTime(); - await UpdateProximityChat(); await yen.DoLottery(); await Rank.UpdateUserRanks(); await UpdateVoiceActiveMembersForMainDiscordGuild(); From 8ebe59c91c46db47e2e1cab3a206f2c17409ce41 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 25 Jan 2024 08:19:52 +0000 Subject: [PATCH 012/101] Proximity chat --- huddles.js | 208 +++++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 169 insertions(+), 39 deletions(-) diff --git a/huddles.js b/huddles.js index 5b39d25..025677e 100644 --- a/huddles.js +++ b/huddles.js @@ -15,11 +15,11 @@ const RoleID = require('./role-id'); const UserCache = require('./user-cache'); const huddles = [ - { name: 'Main', userLimit: 30, position: 1000 }, - { name: 'Duo', userLimit: 4, position: 2000 }, - { name: 'Trio', userLimit: 5, position: 3000 }, - { name: 'Quad', userLimit: 6, position: 4000 }, - { name: 'Squad', userLimit: 10, position: 7000 }, + { name: 'Main', userLimit: 99, position: 1000 }, + { name: 'Duo', userLimit: 2, position: 2000 }, + { name: 'Trio', userLimit: 3, position: 3000 }, + { name: 'Quad', userLimit: 4, position: 4000 }, + { name: 'Squad', userLimit: 8, position: 7000 }, ]; function GetAllMatchingVoiceChannels(guild, huddle) { @@ -51,22 +51,20 @@ async function CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate) { userLimit: huddle.userLimit, }; console.log('Creating channel.'); - await guild.channels.create(options); - console.log('Done'); + return await guild.channels.create(options); } async function CreateNewVoiceChannel(guild, huddle) { - const level3Bitrate = 384000; + const level3Bitrate = 256000; const level2Bitrate = 128000; try { - await CreateNewVoiceChannelWithBitrate(guild, huddle, level3Bitrate); + return await CreateNewVoiceChannelWithBitrate(guild, huddle, level3Bitrate); } catch (err) { try { - await CreateNewVoiceChannelWithBitrate(guild, huddle, level2Bitrate); + return await CreateNewVoiceChannelWithBitrate(guild, huddle, level2Bitrate); } catch (err) { - // If channel creation fails, assume that it's because of the bitrate and try again. - // This will save us if the server loses Discord Nitro levels. - await CreateNewVoiceChannelWithBitrate(guild, huddle); + console.log('Failed to create voice channel.'); + return null; } } } @@ -161,7 +159,6 @@ function CompareRooms(a, b) { return -1; } // This is the scoring rule for rooms that are neither empty nor full. - // The room with the most senior member wins. const ah = ScoreRoom(a); const bh = ScoreRoom(b); if (ah < bh) { @@ -172,6 +169,13 @@ function CompareRooms(a, b) { } // Rules from here on down are mainly intended for sorting the empty // VC rooms at the bottom amongst themselves. + // Rooms named Proximity sort up. + if (a.name !== 'Proximity' && b.name === 'Proximity') { + return 1; + } + if (a.name === 'Proximity' && b.name !== 'Proximity') { + return -1; + } // Rooms named Main sort up. if (a.name !== 'Main' && b.name === 'Main') { return 1; @@ -509,21 +513,23 @@ async function UpdateProximityChat() { console.log('Found', Object.keys(proxChannels).length, 'prox channels'); console.log('Found', Object.keys(proxMembers).length, 'prox members'); // Make distance matrix. + function Distance(a, b) { + if (!a || !b || !a.server || !b.server || a.server !== b.server) { + // Different server or missing server = infinite distance. + return 999999; + } + const dx = a.x - b.x; + const dy = a.y - b.y; + const distance = Math.sqrt(dx * dx + dy * dy); + return distance; + } const distanceMatrix = {}; for (const i in proxMembers) { + const a = lastSeenCache[i]; distanceMatrix[i] = {}; for (const j in proxMembers) { - const a = lastSeenCache[i]; const b = lastSeenCache[j]; - if (a && b && a.server && b.server && a.server === b.server) { - const dx = a.x - b.x; - const dy = a.y - b.y; - const distance = Math.sqrt(dx * dx + dy * dy); - distanceMatrix[i][j] = distance; - } else { - // Different server or missing server = infinite distance. - distanceMatrix[i][j] = 999999; - } + distanceMatrix[i][j] = Distance(a, b); } } console.log('distanceMatrix', distanceMatrix); @@ -567,12 +573,11 @@ async function UpdateProximityChat() { clusters.push([discordId]); } // Merge clusters until no longer possible. - let bestDistance; - do { + while (true) { const n = clusters.length; let bestI; let bestJ; - bestDistance = null; + let bestDistance = null; for (let i = 0; i < n; i++) { for (let j = i + 1; j < n; j++) { const a = clusters[i]; @@ -590,16 +595,17 @@ async function UpdateProximityChat() { } } } - if (bestDistance !== null) { - const a = clusters[bestI]; - const b = clusters[bestJ]; - // Combine two closest clusters. - const newCluster = a.concat(b); - clusters.splice(bestJ, 1); - clusters.splice(bestI, 1); - clusters.push(newCluster); + if (bestDistance === null) { + break; } - } while (bestDistance !== null); + // Combine two closest clusters. + const a = clusters[bestI]; + const b = clusters[bestJ]; + const newCluster = a.concat(b); + clusters.splice(bestJ, 1); + clusters.splice(bestI, 1); + clusters.push(newCluster); + } // Put solos and randos together into one lobby. const clustersWithLobby = [[]]; for (const cluster of clusters) { @@ -613,9 +619,133 @@ async function UpdateProximityChat() { } } console.log('Prox clusters', clustersWithLobby); + // Create new channel(s) if needed. + // Don't delete extra channels here. Do that at the end. + while (Object.keys(proxChannels).length < clustersWithLobby.length) { + const newChannel = await CreateNewVoiceChannel(guild, { name: 'Proximity', userLimit: 99 }); + proxChannels[newChannel.id] = newChannel; + } + // Helper functions for generating permutations of the channel list. + function ForAllPermutationsRecursive(permuted, remaining, callback) { + const n = remaining.length; + if (n === 0) { + callback(permuted); + } + for (let i = 0; i < n; i++) { + const newPermuted = permuted.slice(); + newPermuted.push(remaining[i]); + const newRemaining = remaining.slice(); + newRemaining.splice(i, 1); + ForAllPermutationsRecursive(newPermuted, newRemaining, callback); + } + } + function ForAllPermutations(arr, callback) { + ForAllPermutationsRecursive([], arr, callback); + } // Permute clusters to minimize number of drags. - // Perms. Offline members by nearest neighbor. - // Drag. + const proxChannelsAsList = Object.values(proxChannels); + let bestPermutation; + let minDrags; + let enforcementPlanWithMinimumDrags; + ForAllPermutations(proxChannelsAsList, (perm) => { + console.log('Imagining permutation'); + const plan = {}; + for (let i = 0; i < perm.length; i++) { + const channel = perm[i]; + const discordIdsInChannel = {}; + for (const [memberId, member] of channel.members) { + discordIdsInChannel[memberId] = true; + } + const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; + for (const discordId of cluster) { + if (!(discordId in discordIdsInChannel)) { + plan[discordId] = channel.id; + } + } + } + const dragCount = Object.keys(plan).length; + if (!bestPermutation || dragCount < minDrags) { + minDrags = dragCount; + enforcementPlanWithMinimumDrags = plan; + bestPermutation = perm; + } + }); + console.log('Calculated enforcement plan requires', minDrags, 'drags'); + // Open perms for the lobby (ie: channel zero). + const lobby = bestPermutation[0]; + await SetOpenPerms(lobby); + // Private perms for the rest of the prox channels that are not the lobby. + for (let i = 1; i < bestPermutation.length; i++) { + const connect = PermissionFlagsBits.Connect; + const view = PermissionFlagsBits.ViewChannel; + const perms = [ + { id: guild.roles.everyone.id, deny: [connect, view] }, + { id: RoleID.Grunt, allow: [view] }, + { id: RoleID.Officer, allow: [view] }, + { id: RoleID.General, allow: [view] }, + { id: RoleID.Marshal, allow: [view] }, + { id: RoleID.Bots, allow: [view, connect] }, + ]; + const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; + for (const discordId of cluster) { + perms.push({ id: discordId, allow: [view, connect] }); + } + // Add perms for users who are not in proximity VC but who are geographically + // nearby in-game to let them know which prox VC room they can join. + for (const discordId in lastSeenCache) { + // Don't make duplicate perms for users already in prox VC rooms. + if (discordId in proxMembers) { + continue; + } + const a = lastSeenCache[discordId]; + if (!a.server || !a.x || !a.y) { + continue; + } + // Get min distance to a cluster member. + let minDist = null; + for (const c of cluster) { + const b = lastSeenCache[c]; + const d = Distance(a, b); + if (minDist === null || d < minDist) { + minDist = d; + } + } + if (!guild.members.cache.has(discordId)) { + continue; + } + // Give perms if close enough. + if (minDist !== null && minDist < 400) { + perms.push({ id: discordId, allow: [view, connect] }); + } + } + // Send the accumulated perms to the discord channel. + const channel = bestPermutation[i]; + console.log('Setting perms', perms); + await channel.permissionOverwrites.set(perms); + } + // Drag people who need to be dragged. + for (const discordId in enforcementPlanWithMinimumDrags) { + const member = proxMembers[discordId]; + if (!member) { + continue; + } + const channelId = enforcementPlanWithMinimumDrags[discordId]; + if (!channelId) { + continue; + } + const channel = proxChannels[channelId]; + if (!channel) { + continue; + } + console.log('Dragging a member'); + await member.voice.setChannel(channel); + } + // Delete an extra channel if there are any. + if (Object.keys(proxChannels).length > clustersWithLobby.length) { + console.log('Deleting leftover Prox channel.'); + const channelToDelete = bestPermutation[bestPermutation.length - 1]; + await channelToDelete.delete(); + } } // To avoid race conditions on the cheap, use a system of routine updates. @@ -634,7 +764,7 @@ async function Update() { for (const huddle of huddles) { await UpdateVoiceChannelsForOneHuddleType(guild, huddle); } - const overflowMovedAnyone = await Overflow(guild); + const overflowMovedAnyone = false; // await Overflow(guild); const roomsInOrder = await MoveOneRoomIfNeeded(guild); isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; } From ab0042868c031f9e07dbfa09e424bf22429dcd7b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 03:02:24 +0000 Subject: [PATCH 013/101] Monthly elections update. --- bot-commands.js | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 25fff01..2bd883b 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -78,18 +78,18 @@ async function HandleServerVoteCommand(discordMessage) { const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); const message = await channel.send( - 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for Jan 2024.\n\n' + + 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for February 2024.\n\n' + 'Every top 100 US monthly vanilla server is included.' ); await message.react('❤️'); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 36); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 110); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 98); - await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 22); - await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 128); - await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 125); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 21); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 128); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 184); + await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 19); + await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 120); + await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 116); await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); - await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 35); + await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 45); await MakeOneServerVoteOption(channel, 'Reddit.com/r/PlayRust - US Monthly', 'https://www.battlemetrics.com/servers/rust/3345988', 28); await MakeOneServerVoteOption(channel, 'Rustoria.co - US Long', 'https://www.battlemetrics.com/servers/rust/9594576', 3); } @@ -111,7 +111,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in Jan 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in February 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; From cd59b494a706709e35e144b3d869f8fde1426e1a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 03:05:02 +0000 Subject: [PATCH 014/101] Do not fall down when rustcult API is rebooting. --- huddles.js | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/huddles.js b/huddles.js index 025677e..6a11be5 100644 --- a/huddles.js +++ b/huddles.js @@ -472,7 +472,13 @@ async function UpdateProximityChat() { return; } const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; - const response = await fetch(url); + let response; + try { + response = await fetch(url); + } catch (error) { + console.log('Cannot update prox because error while querying rustcult API.'); + return; + } if (!response) { console.log('Cannot update prox because no response received.'); return; From f554f1dce70d956ec8bafe33182bc8993fe7761e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 03:07:07 +0000 Subject: [PATCH 015/101] Max cluster diameter 5 grids instead of 4 --- huddles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/huddles.js b/huddles.js index 6a11be5..c92c579 100644 --- a/huddles.js +++ b/huddles.js @@ -591,7 +591,7 @@ async function UpdateProximityChat() { const distance = ClusterDistance(a, b); if (distance < 438) { const diameter = TwoClusterDiameter(a, b); - if (diameter < 584) { + if (diameter < 730) { if (bestDistance === null || distance < bestDistance) { bestDistance = distance; bestI = i; From 33285b37bd548810387c7778000cd6f1e836c468 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 05:06:46 +0000 Subject: [PATCH 016/101] Only drag players that are moving in game. --- huddles.js | 78 +++++++++++++++++++++++++++++++++++------------------- 1 file changed, 51 insertions(+), 27 deletions(-) diff --git a/huddles.js b/huddles.js index c92c579..d8dfcfe 100644 --- a/huddles.js +++ b/huddles.js @@ -463,13 +463,10 @@ async function Overflow(guild) { return true; } -// Details of last seen in-game movement, keyed by discord ID. -const lastSeenCache = {}; - -async function UpdateProximityChat() { +async function GetAllDiscordAccountsFromRustCultApi() { if (!config.rustCultApiToken) { console.log('Cannot update prox because no api token.'); - return; + return null; } const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; let response; @@ -477,29 +474,47 @@ async function UpdateProximityChat() { response = await fetch(url); } catch (error) { console.log('Cannot update prox because error while querying rustcult API.'); - return; + return null; } if (!response) { console.log('Cannot update prox because no response received.'); - return; + return null; } if (typeof response !== 'string') { console.log('Cannot update prox because response is not a string.'); - return; + return null; } - const linkedAccounts = JSON.parse(response); - console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.cm API.'); - for (const account of linkedAccounts) { - if (account && account.discordId) { - if (account.steamId) { - const cu = UserCache.GetCachedUserByDiscordId(account.discordId); - if (cu) { - await cu.setSteamId(account.steamId); - await cu.setSteamName(account.steamName); + return response; +} + +// Details of last seen in-game movement, keyed by discord ID. +const lastSeenCache = {}; + +async function UpdateProximityChat() { + const draggableDiscordIds = {}; + const response = await GetAllDiscordAccountsFromRustCultApi(); + if (response) { + const linkedAccounts = JSON.parse(response); + console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.cm API.'); + for (const account of linkedAccounts) { + if (account && account.discordId) { + if (account.steamId) { + const cu = UserCache.GetCachedUserByDiscordId(account.discordId); + if (cu) { + await cu.setSteamId(account.steamId); + await cu.setSteamName(account.steamName); + } + } + if (account.server && account.x && account.y) { + lastSeenCache[account.discordId] = account; + } + const sslm = account.secondsSinceLastMovement; + const ssbc = account.secondsSinceBreadcrumb; + if ((sslm || sslm === 0) && (ssbc || ssbc === 0)) { + if (sslm < 10 && ssbc < 30) { + draggableDiscordIds[account.discordId] = true; + } } - } - if (account.server && account.x && account.y) { - lastSeenCache[account.discordId] = account; } } } @@ -651,11 +666,13 @@ async function UpdateProximityChat() { // Permute clusters to minimize number of drags. const proxChannelsAsList = Object.values(proxChannels); let bestPermutation; + let minFails; let minDrags; - let enforcementPlanWithMinimumDrags; + let bestPlan; ForAllPermutations(proxChannelsAsList, (perm) => { console.log('Imagining permutation'); const plan = {}; + let failCount = 0; for (let i = 0; i < perm.length; i++) { const channel = perm[i]; const discordIdsInChannel = {}; @@ -665,18 +682,25 @@ async function UpdateProximityChat() { const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; for (const discordId of cluster) { if (!(discordId in discordIdsInChannel)) { - plan[discordId] = channel.id; + if (discordId in draggableDiscordIds) { + plan[discordId] = channel.id; + } else { + failCount++; + } } } } const dragCount = Object.keys(plan).length; - if (!bestPermutation || dragCount < minDrags) { + if (!bestPermutation || + failCount < minFails || + (failCount === minFails && dragCount < minDrags)) { + minFails = failCount; minDrags = dragCount; - enforcementPlanWithMinimumDrags = plan; + bestPlan = plan; bestPermutation = perm; } }); - console.log('Calculated enforcement plan requires', minDrags, 'drags'); + console.log('Calculated enforcement plan requires', minDrags, 'drags and has', minFails, 'fails.'); // Open perms for the lobby (ie: channel zero). const lobby = bestPermutation[0]; await SetOpenPerms(lobby); @@ -730,12 +754,12 @@ async function UpdateProximityChat() { await channel.permissionOverwrites.set(perms); } // Drag people who need to be dragged. - for (const discordId in enforcementPlanWithMinimumDrags) { + for (const discordId in bestPlan) { const member = proxMembers[discordId]; if (!member) { continue; } - const channelId = enforcementPlanWithMinimumDrags[discordId]; + const channelId = bestPlan[discordId]; if (!channelId) { continue; } From bf3e3c29fd28a71f2fdfa34eaec3e61bed9e79cd Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 06:13:47 +0000 Subject: [PATCH 017/101] Dynamic names for prox channels: Village & Roaming --- huddles.js | 27 +++++++++++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/huddles.js b/huddles.js index d8dfcfe..fa9b499 100644 --- a/huddles.js +++ b/huddles.js @@ -492,10 +492,11 @@ const lastSeenCache = {}; async function UpdateProximityChat() { const draggableDiscordIds = {}; + const villageDiscordIds = {}; const response = await GetAllDiscordAccountsFromRustCultApi(); if (response) { const linkedAccounts = JSON.parse(response); - console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.cm API.'); + console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.com API.'); for (const account of linkedAccounts) { if (account && account.discordId) { if (account.steamId) { @@ -515,16 +516,25 @@ async function UpdateProximityChat() { draggableDiscordIds[account.discordId] = true; } } + if (account.howManyBasesNearby && account.howManyBasesNearby >= 10) { + villageDiscordIds[account.discordId] = true; + } } } } console.log(Object.keys(lastSeenCache).length, 'cached member locations.'); // Get all Proximity VC rooms & members in them. + const proxRoomNames = { + Lobby: true, + Proximity: true, + Roaming: true, + Village: true, + }; const guild = await DiscordUtil.GetMainDiscordGuild(); const proxChannels = {}; const proxMembers = {}; for (const [channelId, channel] of guild.channels.cache) { - if (channel.type === 2 && channel.name === 'Proximity') { + if (channel.type === 2 && channel.name in proxRoomNames) { proxChannels[channelId] = channel; for (const [memberId, member] of channel.members) { proxMembers[memberId] = member; @@ -704,6 +714,10 @@ async function UpdateProximityChat() { // Open perms for the lobby (ie: channel zero). const lobby = bestPermutation[0]; await SetOpenPerms(lobby); + const lobbyName = 'Proximity'; + if (lobby.name !== lobbyName) { + await lobby.setName(lobbyName); + } // Private perms for the rest of the prox channels that are not the lobby. for (let i = 1; i < bestPermutation.length; i++) { const connect = PermissionFlagsBits.Connect; @@ -717,8 +731,12 @@ async function UpdateProximityChat() { { id: RoleID.Bots, allow: [view, connect] }, ]; const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; + let villagePeopleDetected = false; for (const discordId of cluster) { perms.push({ id: discordId, allow: [view, connect] }); + if (discordId in villageDiscordIds) { + villagePeopleDetected = true; + } } // Add perms for users who are not in proximity VC but who are geographically // nearby in-game to let them know which prox VC room they can join. @@ -752,6 +770,11 @@ async function UpdateProximityChat() { const channel = bestPermutation[i]; console.log('Setting perms', perms); await channel.permissionOverwrites.set(perms); + // Set the channel name. Village or Roaming. + const newChannelName = villagePeopleDetected ? 'Village' : 'Roaming'; + if (channel.name !== newChannelName) { + await channel.setName(newChannelName); + } } // Drag people who need to be dragged. for (const discordId in bestPlan) { From 2247222b481377154d4739ceebc237d19c5bc411 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 07:29:21 +0000 Subject: [PATCH 018/101] Debugging channel rename issue. --- huddles.js | 49 +++++++++++++++++++++++++++++++++---------------- 1 file changed, 33 insertions(+), 16 deletions(-) diff --git a/huddles.js b/huddles.js index fa9b499..0f7820c 100644 --- a/huddles.js +++ b/huddles.js @@ -531,9 +531,10 @@ async function UpdateProximityChat() { Village: true, }; const guild = await DiscordUtil.GetMainDiscordGuild(); + const allChannels = await guild.channels.fetch(); const proxChannels = {}; const proxMembers = {}; - for (const [channelId, channel] of guild.channels.cache) { + for (const [channelId, channel] of allChannels) { if (channel.type === 2 && channel.name in proxRoomNames) { proxChannels[channelId] = channel; for (const [memberId, member] of channel.members) { @@ -716,10 +717,11 @@ async function UpdateProximityChat() { await SetOpenPerms(lobby); const lobbyName = 'Proximity'; if (lobby.name !== lobbyName) { - await lobby.setName(lobbyName); + // Setting channel names is slow for some reason. + //await lobby.setName(lobbyName); } // Private perms for the rest of the prox channels that are not the lobby. - for (let i = 1; i < bestPermutation.length; i++) { + for (let i = 1; i < clustersWithLobby.length; i++) { const connect = PermissionFlagsBits.Connect; const view = PermissionFlagsBits.ViewChannel; const perms = [ @@ -769,13 +771,24 @@ async function UpdateProximityChat() { // Send the accumulated perms to the discord channel. const channel = bestPermutation[i]; console.log('Setting perms', perms); - await channel.permissionOverwrites.set(perms); + try { + console.log('BEGIN SET PERMS'); + await channel.permissionOverwrites.set(perms); + console.log('END SET PERMS'); + } catch (error) { + console.log('Error while setting perms on prox channel.'); + // Do nothing. + } // Set the channel name. Village or Roaming. const newChannelName = villagePeopleDetected ? 'Village' : 'Roaming'; if (channel.name !== newChannelName) { - await channel.setName(newChannelName); + // Setting channel names is incredibly slow for some reason. + //console.log('BEGIN SET CHANNEL NAME'); + //await channel.setName(newChannelName); + //console.log('END SET CHANNEL NAME'); } } + console.log('Done setting perms'); // Drag people who need to be dragged. for (const discordId in bestPlan) { const member = proxMembers[discordId]; @@ -794,6 +807,7 @@ async function UpdateProximityChat() { await member.voice.setChannel(channel); } // Delete an extra channel if there are any. + console.log('Thinking about deleting prox channel.', Object.keys(proxChannels).length, clustersWithLobby.length); if (Object.keys(proxChannels).length > clustersWithLobby.length) { console.log('Deleting leftover Prox channel.'); const channelToDelete = bestPermutation[bestPermutation.length - 1]; @@ -806,20 +820,23 @@ async function UpdateProximityChat() { // the cycle goes around, it knows that an update is needed. Redundant or // overlapping updates are avoided this way. let isUpdateNeeded = false; -setInterval(Update, 9 * 1000); +setTimeout(Update, 9000); async function Update() { + console.log('Starting proximity chat update'); await UpdateProximityChat(); - if (!isUpdateNeeded) { - return; - } - const guild = await DiscordUtil.GetMainDiscordGuild(); - for (const huddle of huddles) { - await UpdateVoiceChannelsForOneHuddleType(guild, huddle); - } - const overflowMovedAnyone = false; // await Overflow(guild); - const roomsInOrder = await MoveOneRoomIfNeeded(guild); - isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; + console.log('Proximity chat update done'); + if (isUpdateNeeded) { + const guild = await DiscordUtil.GetMainDiscordGuild(); + for (const huddle of huddles) { + await UpdateVoiceChannelsForOneHuddleType(guild, huddle); + } + const overflowMovedAnyone = false; // await Overflow(guild); + const roomsInOrder = await MoveOneRoomIfNeeded(guild); + isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; + } + console.log('Done huddles update. Scheduling next update.'); + setTimeout(Update, 9000); } function ScheduleUpdate() { From ba5d9d04f1867772b014d96cfc68d10b2e436ebf Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 08:22:00 +0000 Subject: [PATCH 019/101] Skip rustcult.com API if no prox members. --- huddles.js | 46 +++++++++++++++++++++++++--------------------- 1 file changed, 25 insertions(+), 21 deletions(-) diff --git a/huddles.js b/huddles.js index 0f7820c..bb9a484 100644 --- a/huddles.js +++ b/huddles.js @@ -491,8 +491,33 @@ async function GetAllDiscordAccountsFromRustCultApi() { const lastSeenCache = {}; async function UpdateProximityChat() { + // Get all Proximity VC rooms & members in them. + const proxRoomNames = { + Lobby: true, + Proximity: true, + Roaming: true, + Village: true, + }; + const guild = await DiscordUtil.GetMainDiscordGuild(); + const allChannels = await guild.channels.fetch(); + const proxChannels = {}; + const proxMembers = {}; + for (const [channelId, channel] of allChannels) { + if (channel.type === 2 && channel.name in proxRoomNames) { + proxChannels[channelId] = channel; + for (const [memberId, member] of channel.members) { + proxMembers[memberId] = member; + } + } + } + console.log('Found', Object.keys(proxChannels).length, 'prox channels'); + console.log('Found', Object.keys(proxMembers).length, 'prox members'); + if (Object.keys(proxChannels).length === 1 && Object.keys(proxMembers).length === 0) { + return; + } const draggableDiscordIds = {}; const villageDiscordIds = {}; + // Hit the rustcult.com API to get updated player positions. const response = await GetAllDiscordAccountsFromRustCultApi(); if (response) { const linkedAccounts = JSON.parse(response); @@ -523,27 +548,6 @@ async function UpdateProximityChat() { } } console.log(Object.keys(lastSeenCache).length, 'cached member locations.'); - // Get all Proximity VC rooms & members in them. - const proxRoomNames = { - Lobby: true, - Proximity: true, - Roaming: true, - Village: true, - }; - const guild = await DiscordUtil.GetMainDiscordGuild(); - const allChannels = await guild.channels.fetch(); - const proxChannels = {}; - const proxMembers = {}; - for (const [channelId, channel] of allChannels) { - if (channel.type === 2 && channel.name in proxRoomNames) { - proxChannels[channelId] = channel; - for (const [memberId, member] of channel.members) { - proxMembers[memberId] = member; - } - } - } - console.log('Found', Object.keys(proxChannels).length, 'prox channels'); - console.log('Found', Object.keys(proxMembers).length, 'prox members'); // Make distance matrix. function Distance(a, b) { if (!a || !b || !a.server || !b.server || a.server !== b.server) { From e0c6a2cbb3787ac84160eeb61fe958f800ec2b48 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 08:43:28 +0000 Subject: [PATCH 020/101] Rate limit channel renames --- discord-util.js | 20 ++++++++++++++++++++ huddles.js | 12 ++---------- 2 files changed, 22 insertions(+), 10 deletions(-) diff --git a/discord-util.js b/discord-util.js index 19336de..9441e2c 100644 --- a/discord-util.js +++ b/discord-util.js @@ -275,6 +275,25 @@ async function moveMemberToAfk(member) { return; } +const lastTimeNameChangedByChannelId = {}; + +async function TryToSetChannelNameWithRateLimit(channel, newName) { + if (channel.name === newName) { + return; + } + const t = Date.now(); + const s = lastTimeNameChangedByChannelId[channel.id] || 0; + const elapsed = t - s; + const tenMinutes = 10 * 60 * 1000; + if (elapsed < tenMinutes) { + return; + } + lastTimeNameChangedByChannelId[channel.id] = t; + // Do not await. This call is known to hang for a long time when rate limited. + // Best thing in such cases is to move on. + channel.setName(newName); +} + module.exports = { AddRole, Connect, @@ -290,6 +309,7 @@ module.exports = { ParseExactlyOneMentionedDiscordMember, RemoveRole, SendLongList, + TryToSetChannelNameWithRateLimit, UpdateHarmonicCentralityChatChannel, moveMemberToAfk }; diff --git a/huddles.js b/huddles.js index bb9a484..f3a70ff 100644 --- a/huddles.js +++ b/huddles.js @@ -720,10 +720,7 @@ async function UpdateProximityChat() { const lobby = bestPermutation[0]; await SetOpenPerms(lobby); const lobbyName = 'Proximity'; - if (lobby.name !== lobbyName) { - // Setting channel names is slow for some reason. - //await lobby.setName(lobbyName); - } + await DiscordUtil.TryToSetChannelNameWithRateLimit(lobby, lobbyName); // Private perms for the rest of the prox channels that are not the lobby. for (let i = 1; i < clustersWithLobby.length; i++) { const connect = PermissionFlagsBits.Connect; @@ -785,12 +782,7 @@ async function UpdateProximityChat() { } // Set the channel name. Village or Roaming. const newChannelName = villagePeopleDetected ? 'Village' : 'Roaming'; - if (channel.name !== newChannelName) { - // Setting channel names is incredibly slow for some reason. - //console.log('BEGIN SET CHANNEL NAME'); - //await channel.setName(newChannelName); - //console.log('END SET CHANNEL NAME'); - } + await DiscordUtil.TryToSetChannelNameWithRateLimit(channel, newChannelName); } console.log('Done setting perms'); // Drag people who need to be dragged. From 236444f95bc6600ae187ed15d38121bf385f0500 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 27 Jan 2024 08:53:35 +0000 Subject: [PATCH 021/101] Hit rustcult API no matter if the prox channels are empty for now. --- huddles.js | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/huddles.js b/huddles.js index f3a70ff..c44c106 100644 --- a/huddles.js +++ b/huddles.js @@ -512,9 +512,12 @@ async function UpdateProximityChat() { } console.log('Found', Object.keys(proxChannels).length, 'prox channels'); console.log('Found', Object.keys(proxMembers).length, 'prox members'); - if (Object.keys(proxChannels).length === 1 && Object.keys(proxMembers).length === 0) { - return; - } + // Ideally we want to bail early if there's no work to do, but there are some things + // like a chatroom's name switching after minutes of being rate limited that + // still need to happen in weird corner cases even with no people in the VC rooms. + //if (Object.keys(proxChannels).length === 1 && Object.keys(proxMembers).length === 0) { + // return; + //} const draggableDiscordIds = {}; const villageDiscordIds = {}; // Hit the rustcult.com API to get updated player positions. From 487e76cc1458e7ea6f62722be920d9fcb60f15d5 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 1 Feb 2024 16:35:45 +0000 Subject: [PATCH 022/101] Made prox chat the default by giving it the Main room. --- discord-util.js | 2 +- huddles.js | 12 +++++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/discord-util.js b/discord-util.js index 9441e2c..0e7fd85 100644 --- a/discord-util.js +++ b/discord-util.js @@ -284,7 +284,7 @@ async function TryToSetChannelNameWithRateLimit(channel, newName) { const t = Date.now(); const s = lastTimeNameChangedByChannelId[channel.id] || 0; const elapsed = t - s; - const tenMinutes = 10 * 60 * 1000; + const tenMinutes = 6 * 60 * 1000; if (elapsed < tenMinutes) { return; } diff --git a/huddles.js b/huddles.js index c44c106..2b7e51d 100644 --- a/huddles.js +++ b/huddles.js @@ -15,12 +15,15 @@ const RoleID = require('./role-id'); const UserCache = require('./user-cache'); const huddles = [ - { name: 'Main', userLimit: 99, position: 1000 }, { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, { name: 'Quad', userLimit: 4, position: 4000 }, { name: 'Squad', userLimit: 8, position: 7000 }, ]; +const mainRoomControlledByProximity = true; +if (!mainRoomControlledByProximity) { + huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); +} function GetAllMatchingVoiceChannels(guild, huddle) { const matchingChannels = []; @@ -492,12 +495,16 @@ const lastSeenCache = {}; async function UpdateProximityChat() { // Get all Proximity VC rooms & members in them. + const lobbyName = mainRoomControlledByProximity ? 'Main' : 'Proximity'; const proxRoomNames = { Lobby: true, Proximity: true, Roaming: true, Village: true, }; + if (mainRoomControlledByProximity) { + proxRoomNames['Main'] = true; + } const guild = await DiscordUtil.GetMainDiscordGuild(); const allChannels = await guild.channels.fetch(); const proxChannels = {}; @@ -661,7 +668,7 @@ async function UpdateProximityChat() { // Create new channel(s) if needed. // Don't delete extra channels here. Do that at the end. while (Object.keys(proxChannels).length < clustersWithLobby.length) { - const newChannel = await CreateNewVoiceChannel(guild, { name: 'Proximity', userLimit: 99 }); + const newChannel = await CreateNewVoiceChannel(guild, { name: lobbyName, userLimit: 99 }); proxChannels[newChannel.id] = newChannel; } // Helper functions for generating permutations of the channel list. @@ -722,7 +729,6 @@ async function UpdateProximityChat() { // Open perms for the lobby (ie: channel zero). const lobby = bestPermutation[0]; await SetOpenPerms(lobby); - const lobbyName = 'Proximity'; await DiscordUtil.TryToSetChannelNameWithRateLimit(lobby, lobbyName); // Private perms for the rest of the prox channels that are not the lobby. for (let i = 1; i < clustersWithLobby.length; i++) { From bf696274c86392268fc571c8cae296dbb12a17bf Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 1 Feb 2024 18:27:18 +0000 Subject: [PATCH 023/101] Rate limit the channel perms update. --- discord-util.js | 17 +++++++++++++++++ huddles.js | 45 ++++++++++++++++++++++++++++++++++++--------- server.js | 4 ++++ 3 files changed, 57 insertions(+), 9 deletions(-) diff --git a/discord-util.js b/discord-util.js index 0e7fd85..8dfe681 100644 --- a/discord-util.js +++ b/discord-util.js @@ -294,6 +294,22 @@ async function TryToSetChannelNameWithRateLimit(channel, newName) { channel.setName(newName); } +const lastTimePermsChangedByChannelId = {}; + +async function TryToSetChannelPermsWithRateLimit(channel, newPerms) { + const t = Date.now(); + const s = lastTimePermsChangedByChannelId[channel.id] || 0; + const elapsed = t - s; + const tenMinutes = 6 * 60 * 1000; + if (elapsed < tenMinutes) { + return; + } + lastTimePermsChangedByChannelId[channel.id] = t; + // Do not await. This call is known to hang for a long time when rate limited. + // Best thing in such cases is to move on. + channel.permissionOverwrites.set(newPerms); +} + module.exports = { AddRole, Connect, @@ -310,6 +326,7 @@ module.exports = { RemoveRole, SendLongList, TryToSetChannelNameWithRateLimit, + TryToSetChannelPermsWithRateLimit, UpdateHarmonicCentralityChatChannel, moveMemberToAfk }; diff --git a/huddles.js b/huddles.js index 2b7e51d..1be2bac 100644 --- a/huddles.js +++ b/huddles.js @@ -497,26 +497,36 @@ async function UpdateProximityChat() { // Get all Proximity VC rooms & members in them. const lobbyName = mainRoomControlledByProximity ? 'Main' : 'Proximity'; const proxRoomNames = { - Lobby: true, Proximity: true, Roaming: true, Village: true, }; - if (mainRoomControlledByProximity) { - proxRoomNames['Main'] = true; - } const guild = await DiscordUtil.GetMainDiscordGuild(); const allChannels = await guild.channels.fetch(); + let lobbyChannel; const proxChannels = {}; const proxMembers = {}; for (const [channelId, channel] of allChannels) { - if (channel.type === 2 && channel.name in proxRoomNames) { + if (channel.type !== 2) { + continue; + } + if (channel.name in proxRoomNames) { proxChannels[channelId] = channel; for (const [memberId, member] of channel.members) { proxMembers[memberId] = member; } + } else if (channel.name === lobbyName) { + lobbyChannel = channel; + for (const [memberId, member] of channel.members) { + proxMembers[memberId] = member; + } } } + if (!lobbyChannel) { + console.log('No prox lobby channel found. Bailing.'); + return; + } + console.log('Found', lobbyChannel ? 1 : 0, 'lobby channels'); console.log('Found', Object.keys(proxChannels).length, 'prox channels'); console.log('Found', Object.keys(proxMembers).length, 'prox members'); // Ideally we want to bail early if there's no work to do, but there are some things @@ -667,7 +677,7 @@ async function UpdateProximityChat() { console.log('Prox clusters', clustersWithLobby); // Create new channel(s) if needed. // Don't delete extra channels here. Do that at the end. - while (Object.keys(proxChannels).length < clustersWithLobby.length) { + while (Object.keys(proxChannels).length < clustersWithLobby.length - 1) { const newChannel = await CreateNewVoiceChannel(guild, { name: lobbyName, userLimit: 99 }); proxChannels[newChannel.id] = newChannel; } @@ -704,7 +714,7 @@ async function UpdateProximityChat() { for (const [memberId, member] of channel.members) { discordIdsInChannel[memberId] = true; } - const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; + const cluster = i < (clustersWithLobby.length - 1) ? clustersWithLobby[i + 1] : []; for (const discordId of cluster) { if (!(discordId in discordIdsInChannel)) { if (discordId in draggableDiscordIds) { @@ -715,6 +725,21 @@ async function UpdateProximityChat() { } } } + // Do lobby calculation. + const discordIdsInLobby = {}; + for (const [memberId, member] of lobbyChannel.members) { + discordIdsInLobby[memberId] = true; + } + const lobbyCluster = clustersWithLobby[0]; + for (const discordId of lobbyCluster) { + if (!(discordId in discordIdsInLobby)) { + if (discordId in draggableDiscordIds) { + plan[discordId] = lobbyChannel.id; + } else { + failCount++; + } + } + } const dragCount = Object.keys(plan).length; if (!bestPermutation || failCount < minFails || @@ -779,11 +804,13 @@ async function UpdateProximityChat() { } } // Send the accumulated perms to the discord channel. - const channel = bestPermutation[i]; + const channel = bestPermutation[i - 1]; console.log('Setting perms', perms); try { console.log('BEGIN SET PERMS'); - await channel.permissionOverwrites.set(perms); + // Do not await. This is rate limited so we just move on. + DiscordUtil.TryToSetChannelPermsWithRateLimit(channel, perms); + //await channel.permissionOverwrites.set(perms); console.log('END SET PERMS'); } catch (error) { console.log('Error while setting perms on prox channel.'); diff --git a/server.js b/server.js index be35aef..b9b853b 100644 --- a/server.js +++ b/server.js @@ -386,6 +386,10 @@ async function Start() { // Do nothing. }); + //discordClient.on('rateLimit', (rateLimitData) => { + //console.log('RATELIMIT ###', rateLimitData); + //}); + const upvoteMenuBuilder = new ContextMenuCommandBuilder() .setName('Upvote') .setType(ApplicationCommandType.User); From 99f92ff7d20382dc75f2f04a6bf05c47760d7813 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 14 Feb 2024 16:46:16 +0000 Subject: [PATCH 024/101] Orders update, prox chat bug fix. --- bot-commands.js | 47 ++++++++++++++++++++++++++++++++++++++++------ filter-username.js | 2 +- huddles.js | 24 +++++++++++++---------- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 2bd883b..f05aedf 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -244,25 +244,55 @@ async function HandleVoiceActiveUsersCommand(discordMessage) { await discordMessage.channel.send(`${voiceActiveUsers} users active in voice chat in the last ${daysToLookback} days.`); } -async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { +async function SendWipeBadgeOrders(user, discordMessage, discordMember) { const name = user.getNicknameOrTitleWithInsignia(); await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of January 2024. Report to Rustafied.com - US Long III\n`; + content += `Here are your secret orders for the month of February 2024. Report to Rustafied.com - US Long III\n`; content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. if (user.rank <= 5) { content += `Generals Code 1111\n`; } if (user.rank <= 9) { content += `Officers Code 1111\n`; - content += `Grunt Code 1111\n`; + content += `Non wipe badge code 1111\n`; } if (user.rank <= 13) { + content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } - content += `Run straight to D14. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/servers to automatically protect your base from getting raided by the gov.\n\n`; + content += `Run straight to E4. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ for the best possible experience with the new proximity chat. It also works when you join team in-game. The main objective of the proximity bot is to keep people that are close to each other in the same call. It will automatically moves you to a separate room if you are more than three grids away, unless you are solo. If you die and respawn at beach you will be moved to the solo call. If there is any call with people close to you, your two calls will merge. If you have trouble getting or staying in the right call make sure you are connected in https://rustcult.com/. Static VCs that are excluded from the moving around are: +the *wipe badge call*, *duo*, *trio* *quad* and *squad* call.\n\n`; + content += `Yours truly,\n`; + content += `The Government <3`; + console.log('Content length', content.length, 'characters.'); + try { + await discordMember.send({ + content, + files: [{ + attachment: 'nov-2023-village-heatmap.png', + name: 'nov-2023-village-heatmap.png' + }] + }); + } catch (error) { + console.log('Failed to send orders to', name); + } +} + +async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { + const name = user.getNicknameOrTitleWithInsignia(); + await discordMessage.channel.send(`Sending orders to ${name}`); + const rankNameAndInsignia = user.getRankNameAndInsignia(); + let content = `${rankNameAndInsignia},\n\n`; + content += `Here are your secret orders for the month of February 2024. Report to Rustafied.com - US Long III\n`; + content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. + if (user.rank <= 9) { + content += `Non wipe badge code 1111\n`; + } + content += `Run straight to G1. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ for the best possible experience with the new proximity chat. It also works if you join team in-game.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); @@ -282,7 +312,12 @@ async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { async function SendOrdersToOneCommissarUser(user, discordMessage) { const guild = await DiscordUtil.GetMainDiscordGuild(); const discordMember = await guild.members.fetch(user.discord_id); - await SendNonWipeBadgeOrders(user, discordMessage, discordMember); + const hasWipeBadge = await DiscordUtil.GuildMemberHasRole(discordMember, RoleID.WipeBadge); + if (hasWipeBadge) { + await SendWipeBadgeOrders(user, discordMessage, discordMember); + } else { + await SendNonWipeBadgeOrders(user, discordMessage, discordMember); + } } async function SendOrdersToTheseCommissarUsers(users, discordMessage) { diff --git a/filter-username.js b/filter-username.js index 170a42f..9d5849d 100644 --- a/filter-username.js +++ b/filter-username.js @@ -1,6 +1,6 @@ // Filters Discord usernames to replace or remove problematic characters. function FilterUsername(username) { - const allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_` ()!?\'*+/\\:=~èáéíóúüñà'; + const allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_` ()[]!?\'*+/\\:=~èáéíóúüñà'; const substitutions = { 'ғ': 'f', 'u': 'U', diff --git a/huddles.js b/huddles.js index 1be2bac..ce2fcc5 100644 --- a/huddles.js +++ b/huddles.js @@ -20,7 +20,7 @@ const huddles = [ { name: 'Quad', userLimit: 4, position: 4000 }, { name: 'Squad', userLimit: 8, position: 7000 }, ]; -const mainRoomControlledByProximity = true; +const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); } @@ -273,17 +273,22 @@ function GetLowestRankingMembersFromVoiceChannel(channel, n) { // Sets a channel to be accessible to everyone. async function SetOpenPerms(channel) { + if (!channel) { + return; + } + const guild = await DiscordUtil.GetMainDiscordGuild(); const connect = PermissionFlagsBits.Connect; const view = PermissionFlagsBits.ViewChannel; const perms = [ - { id: channel.guild.roles.everyone.id, deny: [connect, view] }, + { id: guild.roles.everyone.id, deny: [connect, view] }, { id: RoleID.Grunt, allow: [connect, view] }, { id: RoleID.Officer, allow: [connect, view] }, { id: RoleID.General, allow: [connect, view] }, { id: RoleID.Marshal, allow: [connect, view] }, { id: RoleID.Bots, allow: [view, connect] }, ]; - await channel.permissionOverwrites.set(perms); + // Do not await. Fire and forget with rate limit. + DiscordUtil.TryToSetChannelPermsWithRateLimit(channel, perms); } // Calculates the rank-level perms to use for rank-limiting a voice channel. @@ -510,13 +515,13 @@ async function UpdateProximityChat() { if (channel.type !== 2) { continue; } - if (channel.name in proxRoomNames) { - proxChannels[channelId] = channel; + if (channel.name === lobbyName) { + lobbyChannel = channel; for (const [memberId, member] of channel.members) { proxMembers[memberId] = member; } - } else if (channel.name === lobbyName) { - lobbyChannel = channel; + } else if (channel.name in proxRoomNames) { + proxChannels[channelId] = channel; for (const [memberId, member] of channel.members) { proxMembers[memberId] = member; } @@ -752,9 +757,8 @@ async function UpdateProximityChat() { }); console.log('Calculated enforcement plan requires', minDrags, 'drags and has', minFails, 'fails.'); // Open perms for the lobby (ie: channel zero). - const lobby = bestPermutation[0]; - await SetOpenPerms(lobby); - await DiscordUtil.TryToSetChannelNameWithRateLimit(lobby, lobbyName); + await SetOpenPerms(lobbyChannel); + await DiscordUtil.TryToSetChannelNameWithRateLimit(lobbyChannel, lobbyName); // Private perms for the rest of the prox channels that are not the lobby. for (let i = 1; i < clustersWithLobby.length; i++) { const connect = PermissionFlagsBits.Connect; From 79751fd8ba5cfc3d7f173770b5a08aa41a34471d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 26 Feb 2024 22:07:41 +0000 Subject: [PATCH 025/101] Remove nick command. --- bot-commands.js | 17 +---------------- 1 file changed, 1 insertion(+), 16 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index f05aedf..6d4544f 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -535,22 +535,7 @@ async function HandleCommitteeCommand(discordMessage) { } async function HandleNickCommand(discordMessage) { - const tokens = discordMessage.content.split(' '); - if (tokens.length < 2) { - await discordMessage.channel.send(`ERROR: wrong number of arguments. USAGE: !nick NewNicknam3`); - return; - } - const raw = discordMessage.content.substring(6); - const filtered = FilterUsername(raw); - if (filtered.length === 0) { - await discordMessage.channel.send(`ERROR: no weird nicknames.`); - return; - } - const discordId = discordMessage.author.id; - const cu = await UserCache.GetCachedUserByDiscordId(discordId); - await cu.setNick(filtered); - const newName = cu.getNicknameOrTitleWithInsignia(); - await discordMessage.channel.send(`Changed name to ${newName}`); + await discordMessage.channel.send(`To set your nickname, link your Steam account https://rustcult.com\n\nYour discord name is your Steam name. It can take up to 12 hours to update.`); } // Do as if the user just joined the discord. For manually resolving people who From a911c46acec6c7c82c525cc35f35da6d49896bb0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 26 Feb 2024 22:09:34 +0000 Subject: [PATCH 026/101] Update month --- bot-commands.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 6d4544f..aacb4b1 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -78,7 +78,7 @@ async function HandleServerVoteCommand(discordMessage) { const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); const message = await channel.send( - 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for February 2024.\n\n' + + 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for March 2024.\n\n' + 'Every top 100 US monthly vanilla server is included.' ); await message.react('❤️'); @@ -111,7 +111,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in February 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in March 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; From d9c90bc7bb96e856da7b3ae979e9e76cc12a12f7 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 1 Mar 2024 07:18:14 +0000 Subject: [PATCH 027/101] Update month name --- bot-commands.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/bot-commands.js b/bot-commands.js index aacb4b1..efce5e4 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -111,7 +111,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in March 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in March 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; From 14d54c29c698923222f074397870d281f2ef4e4a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 7 Mar 2024 00:07:32 +0000 Subject: [PATCH 028/101] Trying a new layout for VC rooms based on rank. --- huddles.js | 50 +++++++++++++++++---------------------------- rank-definitions.js | 2 +- server.js | 14 +++++++++++++ 3 files changed, 34 insertions(+), 32 deletions(-) diff --git a/huddles.js b/huddles.js index ce2fcc5..1c0c37b 100644 --- a/huddles.js +++ b/huddles.js @@ -22,7 +22,7 @@ const huddles = [ ]; const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { - huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); + //huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); } function GetAllMatchingVoiceChannels(guild, huddle) { @@ -172,20 +172,22 @@ function CompareRooms(a, b) { } // Rules from here on down are mainly intended for sorting the empty // VC rooms at the bottom amongst themselves. - // Rooms named Proximity sort up. - if (a.name !== 'Proximity' && b.name === 'Proximity') { - return 1; + const roomOrder = ['★', '❱❱❱❱', '❱❱❱', '❱❱', '❱', '⦁⦁⦁⦁', '⦁⦁⦁', '⦁⦁', '⦁']; + for (const roomName of roomOrder) { + if (a.name.startsWith(roomName) && !b.name.startsWith(roomName)) { + return -1; + } + if (!a.name.startsWith(roomName) && b.name.startsWith(roomName)) { + return 1; + } } - if (a.name === 'Proximity' && b.name !== 'Proximity') { + // Rooms with a unicode chevron character in the name sort up (officer rank insignia). + if (a.name.includes('❱') && !b.name.includes('❱')) { return -1; } - // Rooms named Main sort up. - if (a.name !== 'Main' && b.name === 'Main') { + if (!a.name.includes('❱') && b.name.includes('❱')) { return 1; } - if (a.name === 'Main' && b.name !== 'Main') { - return -1; - } // Rooms with lower capacity sort up. if (a.userLimit > b.userLimit) { return 1; @@ -193,20 +195,6 @@ function CompareRooms(a, b) { if (a.userLimit < b.userLimit) { return -1; } - // Rooms named Officers Only go next. - if (a.name !== 'Officers Only' && b.name === 'Officers Only') { - return 1; - } - if (a.name === 'Officers Only' && b.name !== 'Officers Only') { - return -1; - } - // Rooms named Generals Only go next. - if (a.name !== 'Generals Only' && b.name === 'Generals Only') { - return 1; - } - if (a.name === 'Generals Only' && b.name !== 'Generals Only') { - return -1; - } // Should all other criteria fail to break the tie, then alphabetic ordering is the last resort. return a.name.localeCompare(b.name); } @@ -863,13 +851,13 @@ async function Update() { await UpdateProximityChat(); console.log('Proximity chat update done'); if (isUpdateNeeded) { - const guild = await DiscordUtil.GetMainDiscordGuild(); - for (const huddle of huddles) { - await UpdateVoiceChannelsForOneHuddleType(guild, huddle); - } - const overflowMovedAnyone = false; // await Overflow(guild); - const roomsInOrder = await MoveOneRoomIfNeeded(guild); - isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; + //const guild = await DiscordUtil.GetMainDiscordGuild(); + //for (const huddle of huddles) { + // await UpdateVoiceChannelsForOneHuddleType(guild, huddle); + //} + //const overflowMovedAnyone = false; // await Overflow(guild); + //const roomsInOrder = await MoveOneRoomIfNeeded(guild); + //isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; } console.log('Done huddles update. Scheduling next update.'); setTimeout(Update, 9000); diff --git a/rank-definitions.js b/rank-definitions.js index 62cb19b..2e9c2d9 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -84,7 +84,7 @@ module.exports = [ title: 'Staff Sergeant', }, { - count: 50, + count: 30, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], title: 'Sergeant', diff --git a/server.js b/server.js index b9b853b..c83d23a 100644 --- a/server.js +++ b/server.js @@ -241,6 +241,18 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } +async function HourlyCensus() { + const guild = await DiscordUtil.GetMainDiscordGuild(); + const recruitRoomId = '1197740625534140557'; + const recruitRoom = await guild.channels.fetch(recruitRoomId); + if (!recruitRoom) { + return; + } + const roomName = `${guild.memberCount} ⦁`; + // Do not await. Discord has rate limits for changing channel names. Fire and forget. + recruitRoom.setName(roomName); +} + // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); @@ -415,6 +427,8 @@ async function Start() { //await recruiting.UpdateRecruitingLeaderboard(); // Routine update schedules itself to run again after it finishes. // That way it avoids running over itself if it runs longer than a minute. + await HourlyCensus(); + setInterval(HourlyCensus, 60 * 60 * 1000); await RoutineUpdate(); } From ad4c73621b5d1503ce6505c49eff61dc77313844 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 15 Mar 2024 22:51:40 +0000 Subject: [PATCH 029/101] Bunch of maintenance changes. --- bot-commands.js | 23 +++++++--------- filter-username.js | 2 +- huddles.js | 67 +++++++++++++++++++++++++++++----------------- server.js | 14 ---------- 4 files changed, 54 insertions(+), 52 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index efce5e4..3aafcce 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -249,22 +249,21 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of February 2024. Report to Rustafied.com - US Long III\n`; - content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. + content += `Here are your secret orders for the month of March 2024. Report to Rustopia.gg - US Large\n`; + content += '```client.connect USLarge.Rustopia.gg```\n'; // Only one newline after triple backticks. if (user.rank <= 5) { content += `Generals Code 1111\n`; } if (user.rank <= 9) { content += `Officers Code 1111\n`; - content += `Non wipe badge code 1111\n`; } if (user.rank <= 13) { content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } - content += `Run straight to E4. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/ for the best possible experience with the new proximity chat. It also works when you join team in-game. The main objective of the proximity bot is to keep people that are close to each other in the same call. It will automatically moves you to a separate room if you are more than three grids away, unless you are solo. If you die and respawn at beach you will be moved to the solo call. If there is any call with people close to you, your two calls will merge. If you have trouble getting or staying in the right call make sure you are connected in https://rustcult.com/. Static VCs that are excluded from the moving around are: -the *wipe badge call*, *duo*, *trio* *quad* and *squad* call.\n\n`; + content += `Run straight to A1. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around all your bases even if you never have the app open.\n\n`; + content += `Want yen? Become a Government Contractor. Mr. President is paying 100 yen per box of stone at community base all day on wipe day. Offer is good for at least 10 boxes of stone so there is time for you to cash in. 10 yen per row of stone for smaller deliveries.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); @@ -286,13 +285,11 @@ async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of February 2024. Report to Rustafied.com - US Long III\n`; - content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. - if (user.rank <= 9) { - content += `Non wipe badge code 1111\n`; - } - content += `Run straight to G1. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/ for the best possible experience with the new proximity chat. It also works if you join team in-game.\n\n`; + content += `Here are your secret orders for the month of March 2024. Report to Rustopia.gg - US Large\n`; + content += '```client.connect USLarge.Rustopia.gg```\n'; // Only one newline after triple backticks. + content += `Get the gov build location from one of your trusted friends with a high rank in The Government. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around all your bases even if you never have the app open.\n\n`; + content += `Want yen? Become a Government Contractor. Mr. President is paying 100 yen per box of stone at community base all day on wipe day. Offer is good for at least 10 boxes of stone so there is time for you to cash in. 10 yen per row of stone for smaller deliveries.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); diff --git a/filter-username.js b/filter-username.js index 9d5849d..12862d1 100644 --- a/filter-username.js +++ b/filter-username.js @@ -1,6 +1,6 @@ // Filters Discord usernames to replace or remove problematic characters. function FilterUsername(username) { - const allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_` ()[]!?\'*+/\\:=~èáéíóúüñà'; + const allowedChars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789.-_` ()[]!?\'*+/\\:=~#èáéíóúüñà'; const substitutions = { 'ғ': 'f', 'u': 'U', diff --git a/huddles.js b/huddles.js index 1c0c37b..f5be524 100644 --- a/huddles.js +++ b/huddles.js @@ -22,7 +22,7 @@ const huddles = [ ]; const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { - //huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); + huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); } function GetAllMatchingVoiceChannels(guild, huddle) { @@ -172,7 +172,7 @@ function CompareRooms(a, b) { } // Rules from here on down are mainly intended for sorting the empty // VC rooms at the bottom amongst themselves. - const roomOrder = ['★', '❱❱❱❱', '❱❱❱', '❱❱', '❱', '⦁⦁⦁⦁', '⦁⦁⦁', '⦁⦁', '⦁']; + const roomOrder = ['Main', 'Duo', 'Trio', 'Quad', 'Squad']; for (const roomName of roomOrder) { if (a.name.startsWith(roomName) && !b.name.startsWith(roomName)) { return -1; @@ -181,13 +181,6 @@ function CompareRooms(a, b) { return 1; } } - // Rooms with a unicode chevron character in the name sort up (officer rank insignia). - if (a.name.includes('❱') && !b.name.includes('❱')) { - return -1; - } - if (!a.name.includes('❱') && b.name.includes('❱')) { - return 1; - } // Rooms with lower capacity sort up. if (a.userLimit > b.userLimit) { return 1; @@ -839,28 +832,54 @@ async function UpdateProximityChat() { } } +async function UpdateSteamAccountInfo() { + // Hit the rustcult.com API to get updated player positions. + const response = await GetAllDiscordAccountsFromRustCultApi(); + if (!response) { + return; + } + const linkedAccounts = JSON.parse(response); + console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.com API.'); + for (const account of linkedAccounts) { + if (!account) { + return; + } + if (!account.discordId) { + return; + } + if (!account.steamId) { + return; + } + const cu = UserCache.GetCachedUserByDiscordId(account.discordId); + if (!cu) { + return; + } + await cu.setSteamId(account.steamId); + await cu.setSteamName(account.steamName); + } +} + +// Update steam account info once after startup, then hourly after that. +setTimeout(UpdateSteamAccountInfo, 15 * 1000); +setInterval(UpdateSteamAccountInfo, 60 * 60 * 1000); + // To avoid race conditions on the cheap, use a system of routine updates. // To schedule an update, a boolean flag is flipped. That way, the next time // the cycle goes around, it knows that an update is needed. Redundant or // overlapping updates are avoided this way. let isUpdateNeeded = false; -setTimeout(Update, 9000); +setTimeout(HuddlesUpdate, 9000); -async function Update() { - console.log('Starting proximity chat update'); - await UpdateProximityChat(); - console.log('Proximity chat update done'); +async function HuddlesUpdate() { if (isUpdateNeeded) { - //const guild = await DiscordUtil.GetMainDiscordGuild(); - //for (const huddle of huddles) { - // await UpdateVoiceChannelsForOneHuddleType(guild, huddle); - //} - //const overflowMovedAnyone = false; // await Overflow(guild); - //const roomsInOrder = await MoveOneRoomIfNeeded(guild); - //isUpdateNeeded = overflowMovedAnyone || !roomsInOrder; - } - console.log('Done huddles update. Scheduling next update.'); - setTimeout(Update, 9000); + const guild = await DiscordUtil.GetMainDiscordGuild(); + for (const huddle of huddles) { + await UpdateVoiceChannelsForOneHuddleType(guild, huddle); + } + const roomsInOrder = await MoveOneRoomIfNeeded(guild); + isUpdateNeeded = !roomsInOrder; + } + setTimeout(HuddlesUpdate, 1000); } function ScheduleUpdate() { diff --git a/server.js b/server.js index c83d23a..b9b853b 100644 --- a/server.js +++ b/server.js @@ -241,18 +241,6 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } -async function HourlyCensus() { - const guild = await DiscordUtil.GetMainDiscordGuild(); - const recruitRoomId = '1197740625534140557'; - const recruitRoom = await guild.channels.fetch(recruitRoomId); - if (!recruitRoom) { - return; - } - const roomName = `${guild.memberCount} ⦁`; - // Do not await. Discord has rate limits for changing channel names. Fire and forget. - recruitRoom.setName(roomName); -} - // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); @@ -427,8 +415,6 @@ async function Start() { //await recruiting.UpdateRecruitingLeaderboard(); // Routine update schedules itself to run again after it finishes. // That way it avoids running over itself if it runs longer than a minute. - await HourlyCensus(); - setInterval(HourlyCensus, 60 * 60 * 1000); await RoutineUpdate(); } From bf0168cf0eefc98dcf50bca1249d47a1d9e060be Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 04:19:00 +0000 Subject: [PATCH 030/101] Amnesty command. --- bot-commands.js | 76 ++++++++++++++++++++++++++++++++++++++++--------- 1 file changed, 63 insertions(+), 13 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 3aafcce..1e11699 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -7,6 +7,7 @@ const DiscordUtil = require('./discord-util'); const FilterUsername = require('./filter-username'); const huddles = require('./huddles'); const RandomPin = require('./random-pin'); +const RankMetadata = require('./rank-definitions'); const RoleID = require('./role-id'); const Sleep = require('./sleep'); const UserCache = require('./user-cache'); @@ -187,24 +188,73 @@ async function HandlePrivateRoomVoteCommand(discordMessage) { await channel.setParent(voteSectionId); } -async function HandleAmnestyVoteCommand(discordMessage) { +function GenerateAkaStringForUser(cu) { + const peakRank = cu.peak_rank || 13; + const peakRankInsignia = RankMetadata[peakRank].insignia; + const names = [ + cu.steam_name, + cu.nick, + cu.nickname, + cu.steam_id, + cu.discord_id, + peakRankInsignia, + ]; + const filteredNames = []; + for (const name of names) { + if (name && name.length > 0) { + filteredNames.push(name); + } + } + const joined = filteredNames.join(' / '); + return joined; +} + +async function HandleAmnestyCommand(discordMessage) { const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author || author.commissar_id !== 7) { // Auth: this command for developer use only. return; } + await discordMessage.channel.send(`The Generals have voted to unban the following individuals from The Government:`); + let unbanCountForGov = 0; + await UserCache.ForEach(async (cu) => { + if (!cu.good_standing && !cu.ban_vote_start_time && !cu.ban_pardon_time) { + await cu.setGoodStanding(true); + await cu.setBanVoteStartTime(null); + await cu.setBanVoteChatroom(null); + await cu.setBanVoteMessage(null); + await cu.setBanConvictionTime(null); + await cu.setBanPardonTime(null); + const aka = GenerateAkaStringForUser(cu); + await discordMessage.channel.send(`Unbanned ${aka}`); + await Sleep(1000); + unbanCountForGov++; + } + }); const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.create({ name: 'amnesty-vote' }); - const message = await channel.send( - `__**Amnesty for weighedsea**__\n\n` + - `Should we unban weighedsea?\n\n` + - `Vote Yes to unban weighedsea. Vote No to keep weighedsea banned, or if you disagree with this vote being held in the first place.\n\n` + - `This guy was banned about a year ago. What we can determine is that he was banned during the Broken Dairy Queen Shooting Saga, early on, for calling it out as a lie. I guess in that moment it seemed insensitive. In light of the situation later blowing up in Broken's face as an obvious fabrication, it now looks like we banned weighed for nothing.\n\n` + - `A 2/3 majority is needed for this motion to pass. These matters will always be decided by the Generals. If we choose to grant this amnesty, then we can always ban him again later.`); - await message.react('✅'); - await message.react('❌'); - const voteSectionId = '1043778293612163133'; - await channel.setParent(voteSectionId); + const bans = await guild.bans.fetch(); + let unbanCountForDiscord = 0; + for (const [banId, ban] of bans) { + const discordId = ban.user.id; + if (!discordId) { + continue; + } + const cu = await UserCache.GetCachedUserByDiscordId(discordId); + if (!cu) { + continue; + } + if (!cu.ban_pardon_time) { + await guild.bans.remove(discordId); + const aka = GenerateAkaStringForUser(cu); + await discordMessage.channel.send(`Unbanned ${aka}`); + await Sleep(1000); + unbanCountForDiscord++; + } + } + await discordMessage.channel.send(`${unbanCountForGov} gov users unbanned`); + await discordMessage.channel.send(`${unbanCountForDiscord} discord users unbanned`); + const total = unbanCountForGov + unbanCountForDiscord; + await discordMessage.channel.send(`These ${total} bans have been pardoned by order of the Generals`); } async function HandleTermLengthVoteCommand(discordMessage) { @@ -696,7 +746,7 @@ async function HandleUnknownCommand(discordMessage) { async function Dispatch(discordMessage) { const handlers = { '!afk': HandleAfkCommand, - '!amnestyvote': HandleAmnestyVoteCommand, + '!amnesty': HandleAmnestyCommand, '!apprehend': Ban.HandleBanCommand, '!arrest': Ban.HandleBanCommand, '!art': Artillery, From b5a907a1aa5a878bb54060469a3792fb97e0da47 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 05:58:38 +0000 Subject: [PATCH 031/101] Rate limit for ban court messages. --- ban.js | 43 ++++++++++++++++++++++++++++++++++++++++++- server.js | 1 + 2 files changed, 43 insertions(+), 1 deletion(-) diff --git a/ban.js b/ban.js index e00819e..1e51d69 100644 --- a/ban.js +++ b/ban.js @@ -62,7 +62,8 @@ async function UpdateTrial(cu) { console.log('Failed to find or create ban court channel', roomName); return; } - await channel.setRateLimitPerUser(600); + // No more rate limit because it's being enforced by the gov bot now. + //await channel.setRateLimitPerUser(600); await cu.setBanVoteChatroom(channel.id); // Update or create the ban vote message itself. The votes are reactions to this message. let message; @@ -427,10 +428,50 @@ async function HandleConvictCommand(discordMessage) { } } +// +async function RateLimitBanCourtMessage(discordMessage) { + const timeframeHours = 4; + const maxMessagesPerChannelPerTimeframe = 4; + const defendantUser = await UserCache.GetCachedUserByBanVoteChannelId(discordMessage.channel.id); + if (!defendantUser) { + // This is not a ban courtroom. Do nothing. + return; + } + const guild = await DiscordUtil.GetMainDiscordGuild(); + if (!defendantUser.ban_vote_chatroom) { + // No courtroom for some reason. Bail. + return; + } + const channel = await guild.channels.resolve(defendantUser.ban_vote_chatroom); + if (!channel) { + // Could not find the channel. Bail. + return; + } + const messages = await channel.messages.fetch({ limit: 20, cache: false }); + const currentTime = Date.now(); + let recentMessageCount = 0; + for (const [messageId, message] of messages) { + if (message.author.id === discordMessage.author.id) { + const ageMillis = currentTime - message.createdTimestamp; + const ageHours = ageMillis / (60 * 60 * 1000); + if (ageHours < timeframeHours) { + recentMessageCount++; + } + } + } + // Recent message count includes the currently posted message, discordMessage. + if (recentMessageCount > maxMessagesPerChannelPerTimeframe) { + const explanation = `The wheels of justice turn slowly. There is a limit of 4 messages every 4 hours per juror per trial. Your contributions to ban court are appreciated. Feel free to edit your messages to add more. This message is automated and helps The Government keep #case-files reasonably short. Thank you and sorry for deleting your message. --The Bot`; + await discordMessage.author.send(explanation); + await discordMessage.delete(); + } +} + module.exports = { HandleBanCommand, HandleConvictCommand, HandlePardonCommand, HandlePossibleReaction, + RateLimitBanCourtMessage, UpdateTrial, }; diff --git a/server.js b/server.js index b9b853b..da8dc83 100644 --- a/server.js +++ b/server.js @@ -335,6 +335,7 @@ async function Start() { } await cu.setCitizen(true); await BotCommands.Dispatch(message); + await Ban.RateLimitBanCourtMessage(message); }); // This Discord event fires when someone joins or leaves a voice chat channel, or mutes, From 8a8da8f45a197349f2826c564a71dc5e51382bed Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 07:16:52 +0000 Subject: [PATCH 032/101] Bug fix. Enforce ban court rate limit even if user has bot blocked. --- ban.js | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/ban.js b/ban.js index 1e51d69..3750128 100644 --- a/ban.js +++ b/ban.js @@ -451,6 +451,7 @@ async function RateLimitBanCourtMessage(discordMessage) { const currentTime = Date.now(); let recentMessageCount = 0; for (const [messageId, message] of messages) { + //console.log(messageId, message.author.username, message.createdTimestamp, message.content); if (message.author.id === discordMessage.author.id) { const ageMillis = currentTime - message.createdTimestamp; const ageHours = ageMillis / (60 * 60 * 1000); @@ -462,8 +463,16 @@ async function RateLimitBanCourtMessage(discordMessage) { // Recent message count includes the currently posted message, discordMessage. if (recentMessageCount > maxMessagesPerChannelPerTimeframe) { const explanation = `The wheels of justice turn slowly. There is a limit of 4 messages every 4 hours per juror per trial. Your contributions to ban court are appreciated. Feel free to edit your messages to add more. This message is automated and helps The Government keep #case-files reasonably short. Thank you and sorry for deleting your message. --The Bot`; - await discordMessage.author.send(explanation); - await discordMessage.delete(); + try { + await discordMessage.author.send(explanation); + } catch (error) { + console.log('Failed to DM member for too frequent messages in ban court'); + } + try { + await discordMessage.delete(); + } catch (error) { + console.log('Failed to delete a message in ban court'); + } } } From c3934438c208f69de6126e234ac2143ccb66e7e6 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 07:19:37 +0000 Subject: [PATCH 033/101] Text chat activity will mark a user as active for 3 months now, not only voice --- server.js | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server.js b/server.js index da8dc83..cfbe913 100644 --- a/server.js +++ b/server.js @@ -334,6 +334,7 @@ async function Start() { return; } await cu.setCitizen(true); + await cu.seenNow(); await BotCommands.Dispatch(message); await Ban.RateLimitBanCourtMessage(message); }); @@ -380,6 +381,7 @@ async function Start() { return; } await cu.setCitizen(true); + await cu.seenNow(); await Ban.HandlePossibleReaction(messageReaction, user, true); }); From 493b1ee988239cada110362b7c698b8d02df197b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 12:14:15 +0000 Subject: [PATCH 034/101] Immunity for the inactive members. --- ban.js | 12 +++++++++++- user-cache.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/ban.js b/ban.js index 3750128..51f2a19 100644 --- a/ban.js +++ b/ban.js @@ -288,7 +288,17 @@ async function HandleBanCommand(discordMessage) { return; } if (mentionedUser.ban_vote_start_time) { - await discordMessage.channel.send(`${mentionedUser.getNicknameOrTitleWithInsignia()} is already on trial.`); + await discordMessage.channel.send(`${mentionedUser.getNicknameOrTitleWithInsignia()} is already on trial`); + return; + } + if (!mentionedUser.last_seen) { + await discordMessage.channel.send(`${mentionedUser.getNicknameOrTitleWithInsignia()} is immune until they send a text message or join voice chat for the first time`); + return; + } + const lastSeen = moment(mentionedUser.last_seen); + if (moment().subtract(20, 'days').isAfter(lastSeen)) { + const daysOfInactivity = Math.round(moment().diff(lastSeen, 'days')); + await discordMessage.channel.send(`${mentionedUser.getNicknameOrTitleWithInsignia()} is immune because their last message or voice acivity was ${daysOfInactivity} days ago.`); return; } await discordMessage.channel.send(`${mentionedUser.getRankNameAndInsignia()} has been sent to Ban Court!`); diff --git a/user-cache.js b/user-cache.js index b5848e3..ecf3882 100644 --- a/user-cache.js +++ b/user-cache.js @@ -98,7 +98,7 @@ async function CreateNewDatabaseUser(discordMember) { const nickname = FilterUsername(discordMember.user.username); console.log(`Create a new DB user for ${nickname}`); const rank = RankMetadata.length - 1; - const last_seen = moment().format(); + const last_seen = null; const office = null; const fields = {discord_id, nickname, rank, last_seen}; const result = await DB.Query('INSERT INTO users SET ?', fields); From c8bbffb4e29cec1d8a307738358186a9ab802500 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 23 Mar 2024 12:18:46 +0000 Subject: [PATCH 035/101] Faster ban trials. --- ban.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/ban.js b/ban.js index 51f2a19..1ea3d5c 100644 --- a/ban.js +++ b/ban.js @@ -158,7 +158,7 @@ async function UpdateTrial(cu) { let baselineVoteDurationDays; let nextStateChangeMessage; if (guilty) { - baselineVoteDurationDays = 7; + baselineVoteDurationDays = 3; const n = VoteDuration.HowManyMoreNoVotes(yesVoteCount, noVoteCount, VoteDuration.SimpleMajority); nextStateChangeMessage = `${n} more NO votes to unban`; if (cu.good_standing) { @@ -174,7 +174,7 @@ async function UpdateTrial(cu) { } } } else { - baselineVoteDurationDays = 2; + baselineVoteDurationDays = 1; const n = VoteDuration.HowManyMoreYesVotes(yesVoteCount, noVoteCount, VoteDuration.SimpleMajority); nextStateChangeMessage = `${n} more YES votes to ban`; if (!cu.good_standing) { From 1595def0133000e34814291fd422cbd6ef672c73 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 25 Mar 2024 01:04:29 +0000 Subject: [PATCH 036/101] Added steam name crawling to the bot to make it more robust than depending on rustcult.com. --- commissar-user.js | 11 ++++-- server.js | 86 ++++++++++++++++++++++++++++++++++++++++++++++ setup-database.sql | 1 + user-cache.js | 24 ++++++++++++- 4 files changed, 119 insertions(+), 3 deletions(-) diff --git a/commissar-user.js b/commissar-user.js index 6f945c0..d343f4d 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -58,6 +58,7 @@ class CommissarUser { this.presidential_election_message_id = presidential_election_message_id; this.steam_id = steam_id; this.steam_name = steam_name; + this.steam_name_update_time = null; } async setDiscordId(discord_id) { @@ -274,7 +275,13 @@ class CommissarUser { this.steam_name = steam_name; await this.updateFieldInDatabase('steam_name', this.steam_name); } - + + async setSteamNameUpdatedNow() { + const t = moment().format(); + this.steam_name_update_time = t; + await this.updateFieldInDatabase('steam_name_update_time', t); + } + async updateFieldInDatabase(fieldName, fieldValue) { //console.log(`DB update ${fieldName} = ${fieldValue} for ${this.nickname} (ID:${this.commissar_id}).`); const sql = `UPDATE users SET ${fieldName} = ? WHERE commissar_id = ?`; @@ -294,7 +301,7 @@ class CommissarUser { } return this.rank; } - + getGenderPrefix() { if (this.gender === 'F') { return 'Madam'; diff --git a/server.js b/server.js index cfbe913..aa1485e 100644 --- a/server.js +++ b/server.js @@ -7,6 +7,7 @@ const DB = require('./database'); const deepEqual = require('deep-equal'); const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('discord.js'); const DiscordUtil = require('./discord-util'); +const fetch = require('./fetch'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); @@ -213,8 +214,19 @@ async function UpdateAllCitizens() { const selectedUsers = activeUsers.concat(inactiveUsers); console.log(`Updating ${selectedUsers.length} users`); const guild = await DiscordUtil.GetMainDiscordGuild(); + const maxLoopDuration = 10 * 1000; + const startTime = Date.now(); + let howManyUsersGotUpdatedCounter = 0; for (const cu of selectedUsers) { await UpdateUser(cu, guild); + howManyUsersGotUpdatedCounter++; + const elapsedTime = Date.now() - startTime; + if (elapsedTime > maxLoopDuration) { + break; + } + } + if (howManyUsersGotUpdatedCounter < selectedUsers.length) { + console.log(`Update cycle timed out after updating ${howManyUsersGotUpdatedCounter} users`); } } @@ -241,6 +253,79 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } +// Crawl and update the steam names of some steam-connected users. +// This routine happens often so not every user has to get updated +// every cycle. +async function UpdateSomeSteamNames() { + const u = UserCache.GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName(); + if (!u) { + return; + } + console.log('UpdateSomeSteamNames', u.steam_name, u.steam_id); + const steamWebApiKey = '22A69A4E939F0D8EC4689D6CAA5D79EE'; + const url = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${steamWebApiKey}&steamids=${u.steam_id}`; + let response; + try { + response = await fetch(url); + } catch (error) { + console.log(error); + } + if (!response) { + console.log('No response'); + return; + } + let json; + try { + json = JSON.parse(response); + } catch (error) { + console.log(error); + } + if (!json) { + console.log('Failed to parse json'); + return; + } + if (!json.response) { + console.log('Json has wrong format'); + return; + } + if (!json.response.players) { + console.log('Could not get list of players'); + return; + } + const players = json.response.players; + if (!players.length || players.length === 0) { + console.log('No valid player records received'); + return; + } + const p = players[0]; + if (p.steamid !== u.steam_id) { + console.log('Steam ID does not match response'); + return; + } + const n = p.personaname; + if (!n) { + console.log('No personaname (steam name) in response'); + return; + } + if (typeof n !== 'string') { + console.log('Steam name is not a string'); + return; + } + if (n.length === 0) { + console.log('Steam name has zero length'); + return; + } + // At this point we crawled the user's steam name successfully so we mark them + // as updated to send them to the back of the crawling queue. + await u.setSteamNameUpdatedNow(); + if (u.steam_name === n) { + console.log('Steam name already up to date'); + return; + } + console.log('Updating steam name of', u.steam_id, 'from', u.steam_name, 'to', n); + await u.setSteamName(n); +} + // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); @@ -254,6 +339,7 @@ async function RoutineUpdate() { await DB.WriteTimeTogetherRecords(timeCappedRecords); await DB.ConsolidateTimeMatrix(); await UpdateHarmonicCentrality(); + await UpdateSomeSteamNames(); await UpdateAllCitizens(); await recruiting.ScanInvitesForChanges(); await BanVoteCache.ExpungeVotesWithNoOngoingTrial(); diff --git a/setup-database.sql b/setup-database.sql index 6f75d6d..2f16d98 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -9,6 +9,7 @@ CREATE TABLE users discord_id VARCHAR(32), -- Discord ID. steam_id VARCHAR(32), -- Steam ID. steam_name VARCHAR(128), -- Steam display name. + steam_name_update_time TIMESTAMP, battlemetrics_id VARCHAR(32), -- User ID on Battlemetrics.com. nickname VARCHAR(32), -- Last known nickname. nick VARCHAR(32), -- A user-supplied preferred nickname. diff --git a/user-cache.js b/user-cache.js index ecf3882..2cb48ba 100644 --- a/user-cache.js +++ b/user-cache.js @@ -40,6 +40,7 @@ async function LoadAllUsersFromDatabase() { row.presidential_election_message_id, row.steam_id, row.steam_name, + row.steam_name_update_time, ); newCache[row.commissar_id] = newUser; }); @@ -117,7 +118,7 @@ async function CreateNewDatabaseUser(discordMember) { null, null, null, 0, null, null, null, null, null, - null, null, + null, null, null, ); commissarUserCache[commissar_id] = newUser; return newUser; @@ -263,6 +264,26 @@ function CountPresidentialElectionVotes() { return votes; } +function GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName() { + let chosenUser = null; + let oldestUpdateTime; + for (const i in commissarUserCache) { + const u = commissarUserCache[i]; + if (!u.steam_id) { + continue; + } + if (!u.steam_name_update_time) { + return u; + } + const t = moment(u.steam_name_update_time); + if (!oldestUpdateTime || t.isBefore(oldestUpdateTime)) { + oldestUpdateTime = t; + chosenUser = u; + } + } + return chosenUser; +} + module.exports = { BulkCentralityUpdate, CountVoiceActiveUsers, @@ -274,6 +295,7 @@ module.exports = { GetCachedUserByBanVoteMessageId, GetCachedUserByCommissarId, GetCachedUserByDiscordId, + GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName, GetMostCentralUsers, GetOrCreateUserByDiscordId, GetUsersSortedByLastSeen, From d40276f3f6d7223a2684265088626ffac4411467 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 28 Mar 2024 02:44:13 +0000 Subject: [PATCH 037/101] Added skeleton chain of command module. --- bot-commands.js | 35 +++++++++++++++++------------------ chain-of-command.js | 27 +++++++++++++++++++++++++++ server.js | 14 ++++++++------ 3 files changed, 52 insertions(+), 24 deletions(-) create mode 100644 chain-of-command.js diff --git a/bot-commands.js b/bot-commands.js index 1e11699..e53600f 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -65,7 +65,7 @@ async function HandleGenderCommand(discordMessage) { async function MakeOneServerVoteOption(channel, serverName, battlemetricsLink, peakRank) { //const text = `__**${serverName}**__\n${battlemetricsLink}\n_Peak rank #${peakRank} ★ ${playerDensity} players / sq km ★ ${bpWipe}_`; - const text = `__**${serverName}**__\n${battlemetricsLink}\n_Peak rank #${peakRank}`; + const text = `__**${serverName}**__\n${battlemetricsLink}\n_Peak rank #${peakRank}_`; const message = await channel.send(text); await message.react('✅'); } @@ -78,21 +78,20 @@ async function HandleServerVoteCommand(discordMessage) { } const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); - const message = await channel.send( - 'The Government will play on whichever server gets the most votes. This will be our main home Rust server for March 2024.\n\n' + - 'Every top 100 US monthly vanilla server is included.' - ); + const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our main home Rust server for April 2024.'); await message.react('❤️'); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 21); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 128); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 184); - await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 19); - await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 120); - await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 116); - await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); - await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 45); - await MakeOneServerVoteOption(channel, 'Reddit.com/r/PlayRust - US Monthly', 'https://www.battlemetrics.com/servers/rust/3345988', 28); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 24); + await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 17); + await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 133); + await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 4); + await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 32); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 124); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 118); + await MakeOneServerVoteOption(channel, 'Reddit.com/r/PlayRust - US Monthly', 'https://www.battlemetrics.com/servers/rust/3345988', 33); + await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 108); await MakeOneServerVoteOption(channel, 'Rustoria.co - US Long', 'https://www.battlemetrics.com/servers/rust/9594576', 3); + await MakeOneServerVoteOption(channel, 'US Rustinity 2x Monthly Large Vanilla+', 'https://www.battlemetrics.com/servers/rust/10477772', 14); + await MakeOneServerVoteOption(channel, 'PICKLE QUAD MONTHLY US', 'https://www.battlemetrics.com/servers/rust/3477804', 230); } async function MakeOnePresidentVoteOption(channel, playerName) { @@ -112,14 +111,14 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in March 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in April 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; for (const user of generalRankUsers) { - if (user.commissar_id === 7) { - continue; - } + //if (user.commissar_id === 7) { + // continue; + //} const name = user.getNicknameOrTitleWithInsignia(); candidateNames.push(name); } diff --git a/chain-of-command.js b/chain-of-command.js new file mode 100644 index 0000000..536748f --- /dev/null +++ b/chain-of-command.js @@ -0,0 +1,27 @@ +const db = require('./database'); + +async function CalculateChainOfCommand() { + console.log('Chain of command'); + await LoadDiscordEdges(); +} + +async function LoadDiscordEdges() { + const relationships = await db.GetTimeMatrix(); + return relationships; +} + +async function LoadRustEdges() { + +} + +async function LoadDiscordVertices() { + +} + +async function LoadRustVertices() { + +} + +module.exports = { + CalculateChainOfCommand, +}; diff --git a/server.js b/server.js index aa1485e..785679a 100644 --- a/server.js +++ b/server.js @@ -3,6 +3,7 @@ const Ban = require('./ban'); const BanVoteCache = require('./ban-vote-cache'); const BotCommands = require('./bot-commands'); const Clock = require('./clock'); +const com = require('./chain-of-command'); const DB = require('./database'); const deepEqual = require('deep-equal'); const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('discord.js'); @@ -329,23 +330,24 @@ async function UpdateSomeSteamNames() { // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); - startTime = new Date().getTime(); - await yen.DoLottery(); - await Rank.UpdateUserRanks(); - await UpdateVoiceActiveMembersForMainDiscordGuild(); + const startTime = new Date().getTime(); await huddles.ScheduleUpdate(); + await UpdateVoiceActiveMembersForMainDiscordGuild(); const recordsToSync = timeTogetherStream.popTimeTogether(9000); const timeCappedRecords = await FilterTimeTogetherRecordsToEnforceTimeCap(recordsToSync); await DB.WriteTimeTogetherRecords(timeCappedRecords); await DB.ConsolidateTimeMatrix(); await UpdateHarmonicCentrality(); + await com.CalculateChainOfCommand(); + await Rank.UpdateUserRanks(); await UpdateSomeSteamNames(); await UpdateAllCitizens(); + await yen.DoLottery(); await recruiting.ScanInvitesForChanges(); await BanVoteCache.ExpungeVotesWithNoOngoingTrial(); await AutoUpdate(); - endTime = new Date().getTime(); - elapsed = endTime - startTime; + const endTime = new Date().getTime(); + const elapsed = endTime - startTime; console.log(`Update Time: ${elapsed} ms`); setTimeout(RoutineUpdate, 60 * 1000); } From 64655fb06b99b37f402af95085deb346f79ac6dc Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 29 Mar 2024 06:17:47 +0000 Subject: [PATCH 038/101] Roll the rank points up the tree. --- chain-of-command.js | 258 +++++++++++++++++++++++++++++++++++++++++--- package-lock.json | 27 +++++ package.json | 1 + user-cache.js | 21 ++++ 4 files changed, 291 insertions(+), 16 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 536748f..9c73890 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,25 +1,251 @@ const db = require('./database'); +const fs = require('fs'); +const kruskal = require('kruskal-mst'); +const UserCache = require('./user-cache'); async function CalculateChainOfCommand() { console.log('Chain of command'); - await LoadDiscordEdges(); + // Initialize the social graph made up up vertices (people) and edges (relationships). + const vertices = {}; + const edges = {}; + // Populate vertex data from discord. + const discordVertices = UserCache.GetAllUsersAsFlatList(); + let minHC = null; + let maxHC = null; + let sumHC = 0; + for (const v of discordVertices) { + const i = v.steam_id || v.discord_id || v.commissar_id; + const hc = v.citizen ? v.harmonic_centrality : 0; + vertices[i] = { + discord_id: v.discord_id, + harmonic_centrality: v.harmonic_centrality, + steam_id: v.steam_id, + vertex_id: i, + }; + if (minHC === null || v.harmonic_centrality < minHC) { + minHC = v.harmonic_centrality; + } + if (maxHC === null || v.harmonic_centrality > maxHC) { + maxHC = v.harmonic_centrality; + } + sumHC += v.harmonic_centrality; + } + console.log('Harmonic centrality summary stats'); + console.log('#', discordVertices.length); + console.log('min', minHC); + console.log('max', maxHC); + console.log('sum', sumHC); + console.log('max%', 100 * maxHC / sumHC, '%'); + console.log('mean', sumHC / discordVertices.length); + console.log(Object.keys(vertices).length, 'combined vertices'); + // Populate edge data from discord. + const discordEdges = await db.GetTimeMatrix(); + let minDiscord = null; + let maxDiscord = null; + let sumDiscord = 0; + for (const e of discordEdges) { + const loUser = UserCache.GetCachedUserByCommissarId(e.lo_user_id); + const hiUser = UserCache.GetCachedUserByCommissarId(e.hi_user_id); + const loid = loUser.steam_id || loUser.discord_id || loUser.commissar_id; + const hiid = hiUser.steam_id || hiUser.discord_id || hiUser.commissar_id; + const a = loid < hiid ? loid : hiid; + const b = loid < hiid ? hiid : loid; + if (!(a in edges)) { + edges[a] = {}; + } + edges[a][b] = { + discord_coplay_time: e.discounted_diluted_seconds, + }; + if (minDiscord === null || e.discounted_diluted_seconds < minDiscord) { + minDiscord = e.discounted_diluted_seconds; + } + if (maxDiscord === null || e.discounted_diluted_seconds > maxDiscord) { + maxDiscord = e.discounted_diluted_seconds; + } + sumDiscord += e.discounted_diluted_seconds; + } + console.log('Discord coplay time summary stats'); + console.log('#', discordEdges.length); + console.log('min', minDiscord); + console.log('max', maxDiscord); + console.log('sum', sumDiscord); + console.log('max%', 100 * maxDiscord / sumDiscord, '%'); + console.log('mean', sumDiscord / discordEdges.length); + console.log(Object.keys(vertices).length, 'combined vertices'); + // Populate vertex data from Rust. + let minActivity = null; + let maxActivity = null; + let sumActivity = 0; + const rustVertexLines = ReadLinesFromCsvFile('in-game-activity-points-march-2024.csv'); + for (const line of rustVertexLines) { + if (line.length !== 2) { + continue; + } + const i = line[0]; + const activity = parseFloat(line[1]); + if (!(i in vertices)) { + vertices[i] = {}; + } + vertices[i].in_game_activity = activity; + vertices[i].steam_id = i; + if (minActivity === null || activity < minActivity) { + minActivity = activity; + } + if (maxActivity === null || activity > maxActivity) { + maxActivity = activity; + } + sumActivity += activity; + } + console.log('Rust in-game activity summary stats'); + console.log('#', rustVertexLines.length); + console.log('min', minActivity); + console.log('max', maxActivity); + console.log('sum', sumActivity); + console.log('max%', 100 * maxActivity / sumActivity, '%'); + console.log('mean', sumActivity / rustVertexLines.length); + console.log(Object.keys(vertices).length, 'combined vertices'); + // Populate edge data from Rust. + let minRust = null; + let maxRust = null; + let sumRust = 0; + const rustEdgeLines = ReadLinesFromCsvFile('in-game-relationships-march-2024.csv'); + for (const line of rustEdgeLines) { + if (line.length !== 3) { + continue; + } + const i = line[0]; + const j = line[1]; + const t = parseFloat(line[2]); + const a = i < j ? i : j; + const b = i < j ? j : i; + if (!(a in edges)) { + edges[a] = {}; + } + if (!(b in edges[a])) { + edges[a][b] = {}; + } + edges[a][b].rust_coplay_time = t; + if (minRust === null || t < minRust) { + minRust = t; + } + if (maxRust === null || t > maxRust) { + maxRust = t; + } + sumRust += t; + } + console.log('Rust in-game relationships summary stats'); + console.log('#', rustEdgeLines.length); + console.log('min', minRust); + console.log('max', maxRust); + console.log('sum', sumRust); + console.log('max%', 100 * maxRust / sumRust, '%'); + console.log('mean', sumRust / rustEdgeLines.length); + console.log(Object.keys(vertices).length, 'combined vertices'); + console.log(Object.keys(edges).length, 'edge buckets'); + // Calculate final vertex weights as a weighted combination of + // vertex features from multiple sources. + for (const i in vertices) { + const v = vertices[i]; + const hc = v.harmonic_centrality || 0; + const iga = v.in_game_activity || 0; + v.cross_platform_activity = hc + iga; + } + // Calculate final edge weights as a weighted combination of + // edge features from multiple sources. + const edgesFormattedForKruskal = []; + for (const i in edges) { + for (const j in edges[i]) { + const e = edges[i][j]; + const d = e.discord_coplay_time || 0; + const r = e.rust_coplay_time || 0; + const t = d + 2 * r; + e.cross_platform_relationship_strength = t / 3600; + if (t > 0) { + e.cross_platform_relationship_distance = 1 / t; + edgesFormattedForKruskal.push({ + from: i, + to: j, + weight: e.cross_platform_relationship_distance, + }); + } + } + } + // Calculate Minimum Spanning Tree (MST) of the relationship graph. + console.log('Calculating MST'); + const forest = kruskal.kruskal(edgesFormattedForKruskal); + console.log('MST forest has', forest.length, 'edges'); + // Index the edges of the MST by vertex for efficiency. + for (const edge of forest) { + const from = vertices[edge.from]; + if (!from.mstEdges) { + from.mstEdges = []; + } + from.mstEdges.push(edge.to); + const to = vertices[edge.to]; + if (!to.mstEdges) { + to.mstEdges = []; + } + to.mstEdges.push(edge.from); + } + // Roll up the points starting from edges of the graph. + while (true) { + let next; + let minScore; + let remainingVertices = 0; + for (const i in vertices) { + const v = vertices[i]; + if (v.leadershipScore || v.leadershipScore === 0) { + continue; + } + remainingVertices++; + const mstEdges = v.mstEdges || []; + let scoredNeighbors = 0; + let scoreSum = v.cross_platform_activity || 0; + for (const j of mstEdges) { + const u = vertices[j]; + const hasScore = u.leadershipScore || u.leadershipScore === 0; + if (hasScore) { + scoredNeighbors++; + scoreSum += u.leadershipScore; + } + } + const degree = mstEdges.length; + const unscoredNeighbors = degree - scoredNeighbors; + if (unscoredNeighbors < 2) { + if (!next || scoreSum < minScore) { + next = v; + minScore = scoreSum; + } + } + } + if (!next) { + // No more nodes left unscored. Terminate the loop. + break; + } + next.leadershipScore = minScore; + let cu; + cu = UserCache.GetCachedUserByDiscordId(next.discord_id); + if (!cu) { + cu = UserCache.GetCachedUserBySteamId(next.steam_id); + } + const displayName = cu ? cu.getNicknameOrTitleWithInsignia() : next.vertex_id || next.steam_id || next.discord_id || 'Unknown Player'; + const formattedScore = Math.round(minScore).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + //console.log(formattedScore, displayName, '(', remainingVertices, 'vertices remaining', ')'); + } } -async function LoadDiscordEdges() { - const relationships = await db.GetTimeMatrix(); - return relationships; -} - -async function LoadRustEdges() { - -} - -async function LoadDiscordVertices() { - -} - -async function LoadRustVertices() { - +// Helper function that reads and parses a CSV file into memory. +// Only use for small files. This function is memory inefficient. +// Returns an array of arrays. +function ReadLinesFromCsvFile(filename) { + const fileContents = fs.readFileSync(filename).toString(); + const lines = fileContents.split('\n'); + const tokenizedLines = []; + for (const line of lines) { + const tokens = line.split(','); + tokenizedLines.push(tokens); + } + return tokenizedLines; } module.exports = { diff --git a/package-lock.json b/package-lock.json index d01ebad..d45703f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,6 +18,7 @@ "discord.js": "^14.11.0", "exchange-rates-api": "^1.1.0", "gamedig": "^2.0.23", + "kruskal-mst": "^1.0.0", "mocha": "^10.2.0", "moment": "^2.29.1", "mysql": "^2.17.1", @@ -1141,6 +1142,11 @@ "node": ">=16.9.0" } }, + "node_modules/disjoint-set-ds": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/disjoint-set-ds/-/disjoint-set-ds-1.0.1.tgz", + "integrity": "sha512-bWpDqFyFM8qCtuAGyGjsr8oytoFO4GNargvwzCp8ny7jRQOgxhjgn1YG6fUKBP1fldFlIkHSv8OT/Xa8RnJF0A==" + }, "node_modules/dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -2308,6 +2314,14 @@ "json-buffer": "3.0.1" } }, + "node_modules/kruskal-mst": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kruskal-mst/-/kruskal-mst-1.0.0.tgz", + "integrity": "sha512-7NiPrIX0T9UghfHvAZ6fDUvCBNnmBPECkm3v0xqSfRJFyrtUp6C4tSGEUL84VANnwmyFJUBuobyAZFWQnnKiCg==", + "dependencies": { + "disjoint-set-ds": "^1.0.0" + } + }, "node_modules/locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", @@ -4732,6 +4746,11 @@ "ws": "^8.13.0" } }, + "disjoint-set-ds": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/disjoint-set-ds/-/disjoint-set-ds-1.0.1.tgz", + "integrity": "sha512-bWpDqFyFM8qCtuAGyGjsr8oytoFO4GNargvwzCp8ny7jRQOgxhjgn1YG6fUKBP1fldFlIkHSv8OT/Xa8RnJF0A==" + }, "dom-serializer": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.3.2.tgz", @@ -5584,6 +5603,14 @@ "json-buffer": "3.0.1" } }, + "kruskal-mst": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/kruskal-mst/-/kruskal-mst-1.0.0.tgz", + "integrity": "sha512-7NiPrIX0T9UghfHvAZ6fDUvCBNnmBPECkm3v0xqSfRJFyrtUp6C4tSGEUL84VANnwmyFJUBuobyAZFWQnnKiCg==", + "requires": { + "disjoint-set-ds": "^1.0.0" + } + }, "locate-path": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", diff --git a/package.json b/package.json index 3d74f24..98a4255 100644 --- a/package.json +++ b/package.json @@ -37,6 +37,7 @@ "discord.js": "^14.11.0", "exchange-rates-api": "^1.1.0", "gamedig": "^2.0.23", + "kruskal-mst": "^1.0.0", "mocha": "^10.2.0", "moment": "^2.29.1", "mysql": "^2.17.1", diff --git a/user-cache.js b/user-cache.js index 2cb48ba..50e7d38 100644 --- a/user-cache.js +++ b/user-cache.js @@ -85,6 +85,16 @@ function GetCachedUserByDiscordId(discord_id) { return null; } +// Get a cached user record by Steam ID. +function GetCachedUserBySteamId(steam_id) { + for (const [commissarId, user] of Object.entries(commissarUserCache)) { + if (user.steam_id === steam_id) { + return user; + } + } + return null; +} + async function GetOrCreateUserByDiscordId(discordMember) { const cu = await GetCachedUserByDiscordId(discordMember.user.id); if (cu) { @@ -153,6 +163,15 @@ function GetMostCentralUsers(topN) { return flat.slice(0, topN); } +function GetAllUsersAsFlatList() { + const flat = []; + for (const i in commissarUserCache) { + const u = commissarUserCache[i]; + flat.push(u); + } + return flat; +} + async function BulkCentralityUpdate(centralityScores) { // Sequential for loop used on purpose. This loop awaits each user update // in turn. @@ -291,10 +310,12 @@ module.exports = { ForEach, GetAllCitizenCommissarIds, GetAllNicknames, + GetAllUsersAsFlatList, GetCachedUserByBanVoteChannelId, GetCachedUserByBanVoteMessageId, GetCachedUserByCommissarId, GetCachedUserByDiscordId, + GetCachedUserBySteamId, GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName, GetMostCentralUsers, GetOrCreateUserByDiscordId, From fa9b2cb22166345102059c35496fba1241c29b9f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 29 Mar 2024 13:02:52 +0000 Subject: [PATCH 039/101] Print more info about the tree. --- chain-of-command.js | 72 ++++++++++++++++++++++++++++++++++++++------- huddles.js | 1 + user-cache.js | 28 ++++++++++++++++++ 3 files changed, 91 insertions(+), 10 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 9c73890..4b80a2e 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,10 +1,20 @@ const db = require('./database'); const fs = require('fs'); const kruskal = require('kruskal-mst'); +const moment = require('moment'); const UserCache = require('./user-cache'); async function CalculateChainOfCommand() { console.log('Chain of command'); + // Load recently active steam IDs from a file. + const recentlyActiveSteamIds = {}; + for (const line of ReadLinesFromCsvFile('recently-active-steam-ids-march-2024.csv')) { + if (line.length !== 1) { + continue; + } + const s = line[0]; + recentlyActiveSteamIds[s] = true; + } // Initialize the social graph made up up vertices (people) and edges (relationships). const vertices = {}; const edges = {}; @@ -14,6 +24,15 @@ async function CalculateChainOfCommand() { let maxHC = null; let sumHC = 0; for (const v of discordVertices) { + const activeInGame = v.steam_id in recentlyActiveSteamIds; + if (!v.last_seen && !activeInGame) { + continue; + } + const lastSeen = moment(v.last_seen); + const limit = moment().subtract(90, 'days'); + if (lastSeen.isBefore(limit) && !activeInGame) { + continue; + } const i = v.steam_id || v.discord_id || v.commissar_id; const hc = v.citizen ? v.harmonic_centrality : 0; vertices[i] = { @@ -50,6 +69,12 @@ async function CalculateChainOfCommand() { const hiid = hiUser.steam_id || hiUser.discord_id || hiUser.commissar_id; const a = loid < hiid ? loid : hiid; const b = loid < hiid ? hiid : loid; + if (!(a in vertices)) { + continue; + } + if (!(b in vertices)) { + continue; + } if (!(a in edges)) { edges[a] = {}; } @@ -82,12 +107,16 @@ async function CalculateChainOfCommand() { continue; } const i = line[0]; + if (!(i in recentlyActiveSteamIds)) { + continue; + } const activity = parseFloat(line[1]); if (!(i in vertices)) { vertices[i] = {}; } vertices[i].in_game_activity = activity; vertices[i].steam_id = i; + vertices[i].vertex_id = i; if (minActivity === null || activity < minActivity) { minActivity = activity; } @@ -118,6 +147,12 @@ async function CalculateChainOfCommand() { const t = parseFloat(line[2]); const a = i < j ? i : j; const b = i < j ? j : i; + if (!(a in vertices)) { + continue; + } + if (!(b in vertices)) { + continue; + } if (!(a in edges)) { edges[a] = {}; } @@ -148,7 +183,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = hc + iga; + v.cross_platform_activity = (0.2 * hc + iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -158,8 +193,8 @@ async function CalculateChainOfCommand() { const e = edges[i][j]; const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; - const t = d + 2 * r; - e.cross_platform_relationship_strength = t / 3600; + const t = (d + 2 * r) / 3600; + e.cross_platform_relationship_strength = t; if (t > 0) { e.cross_platform_relationship_distance = 1 / t; edgesFormattedForKruskal.push({ @@ -223,14 +258,31 @@ async function CalculateChainOfCommand() { break; } next.leadershipScore = minScore; - let cu; - cu = UserCache.GetCachedUserByDiscordId(next.discord_id); - if (!cu) { - cu = UserCache.GetCachedUserBySteamId(next.steam_id); - } - const displayName = cu ? cu.getNicknameOrTitleWithInsignia() : next.vertex_id || next.steam_id || next.discord_id || 'Unknown Player'; + const displayName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(next.vertex_id); const formattedScore = Math.round(minScore).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); - //console.log(formattedScore, displayName, '(', remainingVertices, 'vertices remaining', ')'); + let boss; + const subordinates = []; + const mstEdges = next.mstEdges || []; + for (const i of mstEdges) { + const v = vertices[i]; + if (!v.leadershipScore && v.leadershipScore !== 0) { + boss = v; + } else { + subordinates.push(v); + } + } + let bossName = 'NONE'; + if (boss) { + bossName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(boss.vertex_id); + } + subordinates.sort((a, b) => b.leadershipScore - a.leadershipScore); + const subNames = []; + for (const sub of subordinates) { + const subName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(sub.vertex_id); + subNames.push(subName); + } + const allSubs = subNames.join(' '); + //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); } } diff --git a/huddles.js b/huddles.js index f5be524..149df40 100644 --- a/huddles.js +++ b/huddles.js @@ -528,6 +528,7 @@ async function UpdateProximityChat() { if (response) { const linkedAccounts = JSON.parse(response); console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.com API.'); + //console.log(linkedAccounts); for (const account of linkedAccounts) { if (account && account.discordId) { if (account.steamId) { diff --git a/user-cache.js b/user-cache.js index 50e7d38..e0c072b 100644 --- a/user-cache.js +++ b/user-cache.js @@ -95,6 +95,32 @@ function GetCachedUserBySteamId(steam_id) { return null; } +// Try to find a user given any known ID. +function TryToFindUserGivenAnyKnownId(i) { + for (const [commissarId, user] of Object.entries(commissarUserCache)) { + if (user.steam_id === i) { + return user; + } + if (user.discord_id === i) { + return user; + } + if (user.commissar_id === i) { + return user; + } + } + return null; +} + +// Try to find a display name for a user given any known ID. +function TryToFindDisplayNameForUserGivenAnyKnownId(i) { + const u = TryToFindUserGivenAnyKnownId(i); + if (u) { + return u.getNicknameOrTitleWithInsignia(); + } else { + return i; + } +} + async function GetOrCreateUserByDiscordId(discordMember) { const cu = await GetCachedUserByDiscordId(discordMember.user.id); if (cu) { @@ -322,4 +348,6 @@ module.exports = { GetUsersSortedByLastSeen, GetUsersWithRankAndScoreHigherThan, LoadAllUsersFromDatabase, + TryToFindDisplayNameForUserGivenAnyKnownId, + TryToFindUserGivenAnyKnownId, }; From f864d555f1e0295ab3a9f3fa518d668712ca9746 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 31 Mar 2024 12:39:49 +0000 Subject: [PATCH 040/101] Load additional steam names from rustcult. --- chain-of-command.js | 51 ++++++++++++++++++++++++++++++++++----------- user-cache.js | 2 +- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 4b80a2e..bf81d08 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -4,17 +4,12 @@ const kruskal = require('kruskal-mst'); const moment = require('moment'); const UserCache = require('./user-cache'); +const recentlyActiveSteamIds = {}; + async function CalculateChainOfCommand() { console.log('Chain of command'); // Load recently active steam IDs from a file. - const recentlyActiveSteamIds = {}; - for (const line of ReadLinesFromCsvFile('recently-active-steam-ids-march-2024.csv')) { - if (line.length !== 1) { - continue; - } - const s = line[0]; - recentlyActiveSteamIds[s] = true; - } + ReadSteamAccountsFromFile('recently-active-steam-ids-march-2024.csv'); // Initialize the social graph made up up vertices (people) and edges (relationships). const vertices = {}; const edges = {}; @@ -258,7 +253,7 @@ async function CalculateChainOfCommand() { break; } next.leadershipScore = minScore; - const displayName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(next.vertex_id); + const displayName = GetDisplayName(next.vertex_id); const formattedScore = Math.round(minScore).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); let boss; const subordinates = []; @@ -273,16 +268,16 @@ async function CalculateChainOfCommand() { } let bossName = 'NONE'; if (boss) { - bossName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(boss.vertex_id); + bossName = GetDisplayName(boss.vertex_id); } subordinates.sort((a, b) => b.leadershipScore - a.leadershipScore); const subNames = []; for (const sub of subordinates) { - const subName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(sub.vertex_id); + const subName = GetDisplayName(sub.vertex_id); subNames.push(subName); } const allSubs = subNames.join(' '); - //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); + console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); } } @@ -300,6 +295,38 @@ function ReadLinesFromCsvFile(filename) { return tokenizedLines; } +// Helper function that reads and parses a CSV file into memory. +// This is only for a particular file that contains steam IDs and +// steam names. The reason why the regular CSV parser is no good +// for this situation is because sometimes steam names contain commas. +// This parser is specialized for the special case of 2 columns with +// ids and names so it is not fooled by commas in steam names. +function ReadSteamAccountsFromFile(filename) { + const fileContents = fs.readFileSync(filename).toString(); + const lines = fileContents.split('\n'); + for (const line of lines) { + const commaIndex = line.indexOf(','); + if (commaIndex < 0) { + continue; + } + const steamId = line.substring(0, commaIndex); + const steamName = line.substring(commaIndex + 1); + recentlyActiveSteamIds[steamId] = steamName; + } +} + +function GetDisplayName(vertexId) { + let displayName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(vertexId); + if (displayName) { + // This user is known to commissar. Use their known name. + return displayName; + } else { + // This user is unknown to commissar. They are a rustcult.com user only. + // Import their name from outside commissar. + return recentlyActiveSteamIds[vertexId] || 'John Doe'; + } +} + module.exports = { CalculateChainOfCommand, }; diff --git a/user-cache.js b/user-cache.js index e0c072b..6805ca3 100644 --- a/user-cache.js +++ b/user-cache.js @@ -117,7 +117,7 @@ function TryToFindDisplayNameForUserGivenAnyKnownId(i) { if (u) { return u.getNicknameOrTitleWithInsignia(); } else { - return i; + return null; } } From c6bfac0466d25f69c61102355b4a5de1e85fd034 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 1 Apr 2024 21:08:52 +0000 Subject: [PATCH 041/101] Calculate summary tree. Might have bugs. --- chain-of-command.js | 93 ++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 92 insertions(+), 1 deletion(-) diff --git a/chain-of-command.js b/chain-of-command.js index bf81d08..0e6049d 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -218,7 +218,9 @@ async function CalculateChainOfCommand() { to.mstEdges.push(edge.from); } // Roll up the points starting from edges of the graph. + const verticesSortedByScore = []; while (true) { + // Each iteration of the loop the first thing to do is find the next vertex to score. let next; let minScore; let remainingVertices = 0; @@ -252,11 +254,14 @@ async function CalculateChainOfCommand() { // No more nodes left unscored. Terminate the loop. break; } + // If we get here, then a new vertex has been chosen to score next. Calculate each vertex's + // boss and subordinates, turning the otherwise directionless graph into a top-down tree. next.leadershipScore = minScore; const displayName = GetDisplayName(next.vertex_id); const formattedScore = Math.round(minScore).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); let boss; const subordinates = []; + next.subordinates = []; const mstEdges = next.mstEdges || []; for (const i of mstEdges) { const v = vertices[i]; @@ -264,11 +269,13 @@ async function CalculateChainOfCommand() { boss = v; } else { subordinates.push(v); + next.subordinates.push(i); } } let bossName = 'NONE'; if (boss) { bossName = GetDisplayName(boss.vertex_id); + next.boss = boss.vertex_id; } subordinates.sort((a, b) => b.leadershipScore - a.leadershipScore); const subNames = []; @@ -277,8 +284,51 @@ async function CalculateChainOfCommand() { subNames.push(subName); } const allSubs = subNames.join(' '); - console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); + //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); + verticesSortedByScore.push(next); } + //verticesSortedByScore.reverse(); + const n = verticesSortedByScore.length; + const howManyTopLeadersToExpand = 1; + for (let i = n - howManyTopLeadersToExpand; i < n; i++) { + verticesSortedByScore[i].expand = true; + } + for (const v of verticesSortedByScore) { + const expandedChildren = []; + let nonExpandedChildren = []; + for (const subId of v.subordinates) { + const sub = vertices[subId]; + if (sub.expand) { + expandedChildren.push(sub.summaryTree); + } else { + // If this node is not expanded then neither are its children. + nonExpandedChildren = nonExpandedChildren.concat(sub.summaryTree.members); + } + } + // TODO: sort the non-expanded children to properly interleave members from different branches in rank order. + if (expandedChildren.length === 0) { + nonExpandedChildren.unshift(v.vertex_id); + v.summaryTree = { + members: nonExpandedChildren, + }; + } else { + expandedChildren.push({ + members: nonExpandedChildren, + }); + v.summaryTree = { + children: expandedChildren, + members: [v.vertex_id], + }; + } + } + const king = verticesSortedByScore[n - 1]; + const serializedSummaryTree = JSON.stringify(king.summaryTree, null, 2); + //console.log('Summary tree (', serializedSummaryTree.length, 'chars )'); + //console.log(serializedSummaryTree); + + console.log('CountNodesOfTree', CountNodesOfTree(king.summaryTree)); + console.log('CountLeafNodesOfTree', CountLeafNodesOfTree(king.summaryTree)); + console.log('MaxDepthOfTree', MaxDepthOfTree(king.summaryTree)); } // Helper function that reads and parses a CSV file into memory. @@ -327,6 +377,47 @@ function GetDisplayName(vertexId) { } } +function CountNodesOfTree(t) { + let nodeCount = 1; + const children = t.children || []; + for (const child of children) { + nodeCount += CountNodesOfTree(child); + } + return nodeCount + 1; +} + +function MarkTreeAsMainComponent(t) { + t.is_in_largest_component = true; + const children = t.children || []; + for (const child of children) { + nodeCount += CountNodesOfTree(child); + } + return nodeCount; +} + +function CountLeafNodesOfTree(t) { + if (!t.children) { + return 1; + } + let leafCount = 0; + for (const child of t.children) { + leafCount += CountLeafNodesOfTree(child); + } + return leafCount; +} + +function MaxDepthOfTree(t) { + if (!t.children) { + return 1; + } + let maxDepth = -1; + for (const child of t.children) { + const d = MaxDepthOfTree(child); + maxDepth = Math.max(d + 1, maxDepth); + } + return maxDepth; +} + module.exports = { CalculateChainOfCommand, }; From b5c1963e5ba8658bf0154bb47559cba060cafe20 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 2 Apr 2024 13:55:31 +0000 Subject: [PATCH 042/101] Draw the summarized tree. --- chain-of-command.js | 213 ++++++++++++++++++++++++++++++++++++-------- commissar-user.js | 11 ++- package-lock.json | 30 +++---- package.json | 2 +- rank-definitions.js | 14 +++ 5 files changed, 214 insertions(+), 56 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 0e6049d..fb2b5ec 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,3 +1,4 @@ +const { createCanvas } = require('canvas'); const db = require('./database'); const fs = require('fs'); const kruskal = require('kruskal-mst'); @@ -178,7 +179,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (0.2 * hc + iga) / 3600; + v.cross_platform_activity = (0.5 * hc + iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -287,48 +288,181 @@ async function CalculateChainOfCommand() { //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); verticesSortedByScore.push(next); } - //verticesSortedByScore.reverse(); + // Find any isolated kings and plug them directly into the king of kings. This unites all + // the disconnected components of the graph into one. const n = verticesSortedByScore.length; - const howManyTopLeadersToExpand = 1; - for (let i = n - howManyTopLeadersToExpand; i < n; i++) { - verticesSortedByScore[i].expand = true; + const king = verticesSortedByScore[n - 1]; + for (const v of verticesSortedByScore) { + if (v.boss) { + continue; + } + if (v === king) { + continue; + } + v.boss = king.vertex_id; + king.subordinates.push(v.vertex_id); + king.leadershipScore += v.leadershipScore; + } + // Sort each node's subordinates. + for (const v of verticesSortedByScore) { + v.subordinates.sort((a, b) => { + const aScore = vertices[a].leadershipScore; + const bScore = vertices[b].leadershipScore; + return bScore - aScore; + }); } + // Calculate the descendants of each node. for (const v of verticesSortedByScore) { - const expandedChildren = []; - let nonExpandedChildren = []; + v.descendants = [v.vertex_id]; for (const subId of v.subordinates) { const sub = vertices[subId]; - if (sub.expand) { - expandedChildren.push(sub.summaryTree); - } else { - // If this node is not expanded then neither are its children. - nonExpandedChildren = nonExpandedChildren.concat(sub.summaryTree.members); + v.descendants = v.descendants.concat(sub.descendants); + } + } + // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive + // tree that is more compact to render and easier to read. + function RenderTree(howManyTopLeadersToExpand, pixelWidth, pixelHeight, outputImageFilename) { + for (let i = n - howManyTopLeadersToExpand; i < n; i++) { + const v = verticesSortedByScore[i]; + if (v.subordinates.length > 0) { + v.expand = true; } } - // TODO: sort the non-expanded children to properly interleave members from different branches in rank order. - if (expandedChildren.length === 0) { - nonExpandedChildren.unshift(v.vertex_id); - v.summaryTree = { - members: nonExpandedChildren, - }; - } else { - expandedChildren.push({ - members: nonExpandedChildren, + for (const v of verticesSortedByScore) { + const expandedChildren = []; + let nonExpandedChildren = []; + for (const subId of v.subordinates) { + const sub = vertices[subId]; + if (sub.expand) { + expandedChildren.push(sub.summaryTree); + } else { + // If this node is not expanded then neither are its children. + nonExpandedChildren = nonExpandedChildren.concat(sub.descendants); + } + } + // Sort the non-expanded children to properly interleave members from different branches in rank order. + nonExpandedChildren.sort((a, b) => { + const aScore = vertices[a].leadershipScore; + const bScore = vertices[b].leadershipScore; + return bScore - aScore; }); - v.summaryTree = { - children: expandedChildren, - members: [v.vertex_id], - }; + if (expandedChildren.length === 0) { + if (nonExpandedChildren.length === 0) { + v.summaryTree = { + members: [v.vertex_id], + }; + } else { + v.summaryTree = { + members: [v.vertex_id], + children: [{ + members: nonExpandedChildren, + }], + }; + } + } else { + if (nonExpandedChildren.length > 0) { + expandedChildren.push({ + members: nonExpandedChildren, + }); + } + v.summaryTree = { + children: expandedChildren, + members: [v.vertex_id], + }; + } + } + const wholeSummaryTree = king.summaryTree; + const serializedSummaryTree = JSON.stringify(wholeSummaryTree, null, 2); + console.log('Summary tree (', serializedSummaryTree.length, 'chars )'); + //console.log(serializedSummaryTree); + console.log('CountNodesOfTree', CountNodesOfTree(wholeSummaryTree)); + console.log('CountMembersInTree', CountMembersInTree(wholeSummaryTree)); + console.log('CountLeafNodesOfTree', CountLeafNodesOfTree(wholeSummaryTree)); + const maxDepth = MaxDepthOfTree(wholeSummaryTree); + console.log('MaxDepthOfTree', MaxDepthOfTree(wholeSummaryTree)); + const largeFontSize = 26; + const smallFontSize = largeFontSize / 2; + const horizontalMargin = 8; + const canvas = createCanvas(pixelWidth, pixelHeight); + const ctx = canvas.getContext('2d'); + ctx.fillStyle = '#313338'; + ctx.fillRect(0, 0, canvas.width, canvas.height); + function DrawTree(tree, leftX, rightX, topY) { + const leafNodes = CountLeafNodesOfTree(tree); + const horizontalPixelsPerLeafNode = (rightX - leftX) / leafNodes; + // Draw members. + const centerX = Math.floor((leftX + rightX) / 2) + 0.5; + let bottomY = topY; + for (let i = 0; i < tree.members.length; i++) { + const vertexId = tree.members[i]; + const cu = UserCache.TryToFindUserGivenAnyKnownId(vertexId); + const color = cu ? cu.getRankColor() : '#4285F4'; + const fontSize = tree.children ? largeFontSize : smallFontSize; + const rowHeight = 2 * fontSize; + const nameY = topY + (i * rowHeight) + rowHeight / 2; + const maxColumnWidth = rightX - leftX - horizontalMargin; + let displayName = GetDisplayName(vertexId).replaceAll('⦁', '•').replaceAll('❱', '›'); + bottomY += rowHeight; + if (bottomY > canvas.height - 2 * largeFontSize) { + const numHidden = tree.members.length - i; + if (numHidden > 1) { + displayName = `+${numHidden} more`; + } + } + ctx.font = `${fontSize}px Uni Sans Heavy`; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillStyle = color; + ctx.fillText(displayName, centerX, nameY, maxColumnWidth); + if (bottomY > canvas.height - 2 * largeFontSize) { + // Reached bottom of the page. Stop drawing names. + break; + } + } + // Draw children recursively. + const childTopY = bottomY + 2 * largeFontSize; + const bracketY = Math.floor((bottomY + childTopY) / 2) + 0.5; + ctx.strokeStyle = '#D2D5DA'; + let leafNodesDrawn = 0; + let leftBracketX = rightX; + let rightBracketX = leftX; + const children = tree.children || []; + for (const child of children) { + const childLeafNodes = CountLeafNodesOfTree(child); + const childLeftX = leftX + leafNodesDrawn * horizontalPixelsPerLeafNode; + const childRightX = childLeftX + childLeafNodes * horizontalPixelsPerLeafNode; + const childCenterX = Math.floor((childLeftX + childRightX) / 2) + 0.5; + leftBracketX = Math.min(leftBracketX, childCenterX); + rightBracketX = Math.max(rightBracketX, childCenterX); + // Draw the vertical white line that points down towards the child. + ctx.beginPath(); + ctx.moveTo(childCenterX, bracketY); + ctx.lineTo(childCenterX, Math.floor(childTopY - 12) + 0.5); + ctx.stroke(); + DrawTree(child, childLeftX, childRightX, childTopY); + leafNodesDrawn += childLeafNodes; + } + if (tree.children) { + // Vertical line pointing up at the parent. + ctx.beginPath(); + ctx.moveTo(centerX, Math.floor(bottomY + 12) + 0.5); + ctx.lineTo(centerX, bracketY); + ctx.stroke(); + // Horizontal line. Bracket that joins siblings. + ctx.beginPath(); + ctx.moveTo(Math.floor(leftBracketX) + 0.5, bracketY); + ctx.lineTo(Math.floor(rightBracketX) + 0.5, bracketY); + ctx.stroke(); + } } + DrawTree(wholeSummaryTree, 0, canvas.width, largeFontSize); + const out = fs.createWriteStream(__dirname + '/' + outputImageFilename); + const stream = canvas.createPNGStream(); + stream.pipe(out); + out.on('finish', () => console.log('Wrote', outputImageFilename)); } - const king = verticesSortedByScore[n - 1]; - const serializedSummaryTree = JSON.stringify(king.summaryTree, null, 2); - //console.log('Summary tree (', serializedSummaryTree.length, 'chars )'); - //console.log(serializedSummaryTree); - - console.log('CountNodesOfTree', CountNodesOfTree(king.summaryTree)); - console.log('CountLeafNodesOfTree', CountLeafNodesOfTree(king.summaryTree)); - console.log('MaxDepthOfTree', MaxDepthOfTree(king.summaryTree)); + RenderTree(15, 1920, 1080, 'chain-of-command-general.png'); + RenderTree(50, 7000, 1080, 'chain-of-command-officer.png'); } // Helper function that reads and parses a CSV file into memory. @@ -373,7 +507,8 @@ function GetDisplayName(vertexId) { } else { // This user is unknown to commissar. They are a rustcult.com user only. // Import their name from outside commissar. - return recentlyActiveSteamIds[vertexId] || 'John Doe'; + const n = recentlyActiveSteamIds[vertexId] || 'John Doe'; + return `${n} ⦁`; } } @@ -383,16 +518,16 @@ function CountNodesOfTree(t) { for (const child of children) { nodeCount += CountNodesOfTree(child); } - return nodeCount + 1; + return nodeCount; } -function MarkTreeAsMainComponent(t) { - t.is_in_largest_component = true; +function CountMembersInTree(t) { + let memberCount = t.members.length; const children = t.children || []; for (const child of children) { - nodeCount += CountNodesOfTree(child); + memberCount += CountMembersInTree(child); } - return nodeCount; + return memberCount; } function CountLeafNodesOfTree(t) { diff --git a/commissar-user.js b/commissar-user.js index d343f4d..4d20ec1 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -324,9 +324,18 @@ class CommissarUser { } } + getRankColor() { + if (!this.citizen) { + return '#4285F4'; + } + const rank = this.getRank(); + const rankData = RankMetadata[rank]; + return rankData.color; + } + getInsignia() { if (!this.citizen) { - return '●'; + return '⦁'; } const rank = this.getRank(); const rankData = RankMetadata[rank]; diff --git a/package-lock.json b/package-lock.json index d45703f..7f85b82 100644 --- a/package-lock.json +++ b/package-lock.json @@ -11,7 +11,7 @@ "dependencies": { "aws-sdk": "^2.780.0", "binomial-cdf": "^2.0.0", - "canvas": "^2.6.1", + "canvas": "^2.11.2", "deep-equal": "^2.0.4", "diff": "^5.1.0", "discord-html-transcripts": "^3.1.4", @@ -696,13 +696,13 @@ } }, "node_modules/canvas": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", - "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "hasInstallScript": true, "dependencies": { "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.14.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" }, "engines": { @@ -2632,9 +2632,9 @@ } }, "node_modules/nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" }, "node_modules/nanoid": { "version": "3.3.3", @@ -4407,12 +4407,12 @@ "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==" }, "canvas": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.8.0.tgz", - "integrity": "sha512-gLTi17X8WY9Cf5GZ2Yns8T5lfBOcGgFehDFb+JQwDqdOoBOcECS9ZWMEAqMSVcMYwXD659J8NyzjRY/2aE+C2Q==", + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", "requires": { "@mapbox/node-pre-gyp": "^1.0.0", - "nan": "^2.14.0", + "nan": "^2.17.0", "simple-get": "^3.0.3" } }, @@ -5844,9 +5844,9 @@ } }, "nan": { - "version": "2.15.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.15.0.tgz", - "integrity": "sha512-8ZtvEnA2c5aYCZYd1cvgdnU6cqwixRoYg70xPLWUws5ORTa/lnw+u4amixRS/Ac5U5mQVgp9pnlSUnbNWFaWZQ==" + "version": "2.19.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.19.0.tgz", + "integrity": "sha512-nO1xXxfh/RWNxfd/XPfbIfFk5vgLsAxUR9y5O0cHMJu/AW9U95JLXqthYHjEp+8gQ5p96K9jUp8nbVOxCdRbtw==" }, "nanoid": { "version": "3.3.3", diff --git a/package.json b/package.json index 98a4255..ca0253a 100644 --- a/package.json +++ b/package.json @@ -30,7 +30,7 @@ "dependencies": { "aws-sdk": "^2.780.0", "binomial-cdf": "^2.0.0", - "canvas": "^2.6.1", + "canvas": "^2.11.2", "deep-equal": "^2.0.4", "diff": "^5.1.0", "discord-html-transcripts": "^3.1.4", diff --git a/rank-definitions.js b/rank-definitions.js index 2e9c2d9..793d415 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -3,6 +3,7 @@ const RoleID = require('./role-id'); module.exports = [ { banPower: true, + color: '#189b17', count: 0, insignia: '⚑', roles: [ @@ -15,6 +16,7 @@ module.exports = [ }, { banPower: true, + color: '#189b17', count: 0, insignia: '⚑', roles: [ @@ -27,6 +29,7 @@ module.exports = [ }, { banPower: true, + color: '#F4B400', count: 0, insignia: '★★★★', roles: [RoleID.General, RoleID.Admin], @@ -34,6 +37,7 @@ module.exports = [ }, { banPower: true, + color: '#F4B400', count: 0, insignia: '★★★', roles: [RoleID.General, RoleID.Admin], @@ -41,6 +45,7 @@ module.exports = [ }, { banPower: true, + color: '#F4B400', count: 0, insignia: '★★', roles: [RoleID.General, RoleID.Admin], @@ -48,54 +53,63 @@ module.exports = [ }, { banPower: true, + color: '#F4B400', count: 15, insignia: '★', roles: [RoleID.General, RoleID.Admin], title: 'General', }, { + color: '#DB4437', count: 8, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], title: 'Colonel', }, { + color: '#DB4437', count: 8, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], title: 'Major', }, { + color: '#DB4437', count: 9, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], title: 'Captain', }, { + color: '#DB4437', count: 10, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], title: 'Lieutenant', }, { + color: '#4285F4', count: 20, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], title: 'Staff Sergeant', }, { + color: '#4285F4', count: 30, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], title: 'Sergeant', }, { + color: '#4285F4', count: 400, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], title: 'Corporal', }, { + color: '#4285F4', count: 1000 * 1000, insignia: '⦁', roles: [RoleID.Recruit, RoleID.Grunt], From 18a540d5caa229373bc7f56eda845515fbc3a527 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 4 Apr 2024 01:07:16 +0000 Subject: [PATCH 043/101] Various post-release updates. --- ban.js | 2 +- chain-of-command.js | 91 +++++++++++++++++++++++++++++++++++++++------ huddles.js | 4 +- rank-definitions.js | 22 +++++------ server.js | 6 +-- user-cache.js | 2 +- 6 files changed, 96 insertions(+), 31 deletions(-) diff --git a/ban.js b/ban.js index 1ea3d5c..029d571 100644 --- a/ban.js +++ b/ban.js @@ -154,7 +154,7 @@ async function UpdateTrial(cu) { const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); - const totalVoters = 50; + const totalVoters = 80; let baselineVoteDurationDays; let nextStateChangeMessage; if (guilty) { diff --git a/chain-of-command.js b/chain-of-command.js index fb2b5ec..1ea2d1b 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,8 +1,10 @@ const { createCanvas } = require('canvas'); const db = require('./database'); +const DiscordUtil = require('./discord-util'); const fs = require('fs'); const kruskal = require('kruskal-mst'); const moment = require('moment'); +const RankMetadata = require('./rank-definitions'); const UserCache = require('./user-cache'); const recentlyActiveSteamIds = {}; @@ -179,7 +181,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (0.5 * hc + iga) / 3600; + v.cross_platform_activity = (hc + iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -319,9 +321,43 @@ async function CalculateChainOfCommand() { v.descendants = v.descendants.concat(sub.descendants); } } + // Assign discrete ranks to each player. + let rank = 0; + let usersAtRank = 0; + for (let i = n - 1; i >=0; i--) { + while (usersAtRank >= RankMetadata[rank].count) { + rank++; + usersAtRank = 0; + } + // When we run out of ranks, this line defaults to the last/least rank. + rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); + const v = verticesSortedByScore[i]; + v.rank = rank; + const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); + if (cu) { + // Disable promotions during the transition to the new ranks. + //await AnnounceIfPromotion(user, cappedRank); + await cu.setRank(rank); + //console.log(cu.getNicknameOrTitleWithInsignia(), rank); + } + usersAtRank++; + } + // Assign the bottom rank to any known users that do not appear in the tree. + await UserCache.ForEach(async (user) => { + if (user.steam_id in vertices) { + return; + } + if (user.discord_id in vertices) { + return; + } + if (user.commissar_id in vertices) { + return; + } + await user.setRank(RankMetadata.length - 1); + }); // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive // tree that is more compact to render and easier to read. - function RenderTree(howManyTopLeadersToExpand, pixelWidth, pixelHeight, outputImageFilename) { + async function RenderSummaryTree(howManyTopLeadersToExpand, pixelWidth, pixelHeight, outputImageFilename) { for (let i = n - howManyTopLeadersToExpand; i < n; i++) { const v = verticesSortedByScore[i]; if (v.subordinates.length > 0) { @@ -380,7 +416,7 @@ async function CalculateChainOfCommand() { console.log('CountLeafNodesOfTree', CountLeafNodesOfTree(wholeSummaryTree)); const maxDepth = MaxDepthOfTree(wholeSummaryTree); console.log('MaxDepthOfTree', MaxDepthOfTree(wholeSummaryTree)); - const largeFontSize = 26; + const largeFontSize = 18; const smallFontSize = largeFontSize / 2; const horizontalMargin = 8; const canvas = createCanvas(pixelWidth, pixelHeight); @@ -395,13 +431,24 @@ async function CalculateChainOfCommand() { let bottomY = topY; for (let i = 0; i < tree.members.length; i++) { const vertexId = tree.members[i]; - const cu = UserCache.TryToFindUserGivenAnyKnownId(vertexId); - const color = cu ? cu.getRankColor() : '#4285F4'; + const v = vertices[vertexId]; + const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); + const rank = v.rank || (RankMetadata.length - 1); + const rankData = RankMetadata[rank]; + let color = rankData.color || '#4285F4'; + let insignia = rankData.insignia || '•'; + if (cu) { + if (cu.office) { + color = '#189b17'; + insignia = '⚑'; + } + } + insignia = insignia.replaceAll('⦁', '•').replaceAll('❱', '›') const fontSize = tree.children ? largeFontSize : smallFontSize; const rowHeight = 2 * fontSize; const nameY = topY + (i * rowHeight) + rowHeight / 2; const maxColumnWidth = rightX - leftX - horizontalMargin; - let displayName = GetDisplayName(vertexId).replaceAll('⦁', '•').replaceAll('❱', '›'); + let displayName = GetDisplayName(vertexId) + ' ' + insignia; bottomY += rowHeight; if (bottomY > canvas.height - 2 * largeFontSize) { const numHidden = tree.members.length - i; @@ -459,10 +506,33 @@ async function CalculateChainOfCommand() { const out = fs.createWriteStream(__dirname + '/' + outputImageFilename); const stream = canvas.createPNGStream(); stream.pipe(out); - out.on('finish', () => console.log('Wrote', outputImageFilename)); + // Wait for the image file to finish writing to disk. + return new Promise((resolve, reject) => { + out.on('finish', () => { + console.log('Wrote', outputImageFilename); + resolve(); + }); + }); } - RenderTree(15, 1920, 1080, 'chain-of-command-general.png'); - RenderTree(50, 7000, 1080, 'chain-of-command-officer.png'); + await RenderSummaryTree(20, 1920, 1080, 'chain-of-command-general.png'); + await RenderSummaryTree(80, 6400, 1080, 'chain-of-command-officer.png'); + const guild = await DiscordUtil.GetMainDiscordGuild(); + const channel = await guild.channels.fetch('711850971072036946'); + await channel.bulkDelete(99); + await channel.send({ + content: `**The Government Chain of Command**\nThe ranks update every 60 seconds. The structure comes from your relationships in-game and in discord. Who you base with, roam with, raid with, spend time in discord with, etc. Your rank score = (your in-game activity) + (your discord activity) + (all your followers in-game activity) + (all your followers discord activity). To climb the ranks, be a leader. Invite others into your base. Lead raids. Plan events. Be yourself and love your friends. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`, + files: [{ + attachment: 'chain-of-command-general.png', + name: 'chain-of-command-general.png' + }], + }); + await channel.send({ + content: `**More Detailed View**`, + files: [{ + attachment: 'chain-of-command-officer.png', + name: 'chain-of-command-officer.png' + }], + }); } // Helper function that reads and parses a CSV file into memory. @@ -507,8 +577,7 @@ function GetDisplayName(vertexId) { } else { // This user is unknown to commissar. They are a rustcult.com user only. // Import their name from outside commissar. - const n = recentlyActiveSteamIds[vertexId] || 'John Doe'; - return `${n} ⦁`; + return recentlyActiveSteamIds[vertexId] || 'John Doe'; } } diff --git a/huddles.js b/huddles.js index 149df40..e4d989e 100644 --- a/huddles.js +++ b/huddles.js @@ -17,8 +17,8 @@ const UserCache = require('./user-cache'); const huddles = [ { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - { name: 'Quad', userLimit: 4, position: 4000 }, - { name: 'Squad', userLimit: 8, position: 7000 }, + //{ name: 'Quad', userLimit: 4, position: 4000 }, + //{ name: 'Squad', userLimit: 8, position: 7000 }, ]; const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { diff --git a/rank-definitions.js b/rank-definitions.js index 793d415..970ab7b 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -30,7 +30,7 @@ module.exports = [ { banPower: true, color: '#F4B400', - count: 0, + count: 1, insignia: '★★★★', roles: [RoleID.General, RoleID.Admin], title: 'General', @@ -38,7 +38,7 @@ module.exports = [ { banPower: true, color: '#F4B400', - count: 0, + count: 3, insignia: '★★★', roles: [RoleID.General, RoleID.Admin], title: 'General', @@ -46,7 +46,7 @@ module.exports = [ { banPower: true, color: '#F4B400', - count: 0, + count: 5, insignia: '★★', roles: [RoleID.General, RoleID.Admin], title: 'General', @@ -54,56 +54,56 @@ module.exports = [ { banPower: true, color: '#F4B400', - count: 15, + count: 7, insignia: '★', roles: [RoleID.General, RoleID.Admin], title: 'General', }, { color: '#DB4437', - count: 8, + count: 15, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], title: 'Colonel', }, { color: '#DB4437', - count: 8, + count: 15, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], title: 'Major', }, { color: '#DB4437', - count: 9, + count: 15, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], title: 'Captain', }, { color: '#DB4437', - count: 10, + count: 15, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], title: 'Lieutenant', }, { color: '#4285F4', - count: 20, + count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], title: 'Staff Sergeant', }, { color: '#4285F4', - count: 30, + count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], title: 'Sergeant', }, { color: '#4285F4', - count: 400, + count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], title: 'Corporal', diff --git a/server.js b/server.js index 785679a..baba9bb 100644 --- a/server.js +++ b/server.js @@ -12,7 +12,6 @@ const fetch = require('./fetch'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); -const Rank = require('./rank'); const RankMetadata = require('./rank-definitions'); const recruiting = require('./recruiting'); const RoleID = require('./role-id'); @@ -157,8 +156,6 @@ async function UpdateHarmonicCentrality() { } const centralityScoresById = await HarmonicCentrality(candidates); await UserCache.BulkCentralityUpdate(centralityScoresById); - const mostCentral = await UserCache.GetMostCentralUsers(400); - await DiscordUtil.UpdateHarmonicCentralityChatChannel(mostCentral); } async function UpdateUser(cu, guild) { @@ -215,7 +212,7 @@ async function UpdateAllCitizens() { const selectedUsers = activeUsers.concat(inactiveUsers); console.log(`Updating ${selectedUsers.length} users`); const guild = await DiscordUtil.GetMainDiscordGuild(); - const maxLoopDuration = 10 * 1000; + const maxLoopDuration = 90 * 1000; const startTime = Date.now(); let howManyUsersGotUpdatedCounter = 0; for (const cu of selectedUsers) { @@ -339,7 +336,6 @@ async function RoutineUpdate() { await DB.ConsolidateTimeMatrix(); await UpdateHarmonicCentrality(); await com.CalculateChainOfCommand(); - await Rank.UpdateUserRanks(); await UpdateSomeSteamNames(); await UpdateAllCitizens(); await yen.DoLottery(); diff --git a/user-cache.js b/user-cache.js index 6805ca3..a5a7aa8 100644 --- a/user-cache.js +++ b/user-cache.js @@ -115,7 +115,7 @@ function TryToFindUserGivenAnyKnownId(i) { function TryToFindDisplayNameForUserGivenAnyKnownId(i) { const u = TryToFindUserGivenAnyKnownId(i); if (u) { - return u.getNicknameOrTitleWithInsignia(); + return u.getNicknameOrTitle(); } else { return null; } From 55c07183be4db57e100321e46118046c7449a113 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Apr 2024 16:20:09 +0000 Subject: [PATCH 044/101] Make the tree look more tacticool and milsim like. --- bot-commands.js | 30 ++++++++++------ chain-of-command.js | 85 ++++++++++++++++++++++++++++----------------- 2 files changed, 72 insertions(+), 43 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index e53600f..a2e2af1 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -298,8 +298,8 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of March 2024. Report to Rustopia.gg - US Large\n`; - content += '```client.connect USLarge.Rustopia.gg```\n'; // Only one newline after triple backticks. + content += `Here are your secret orders for the month of April 2024. Report to Rustafied.com - US Long III\n`; + content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. if (user.rank <= 5) { content += `Generals Code 1111\n`; } @@ -310,9 +310,9 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } - content += `Run straight to A1. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around all your bases even if you never have the app open.\n\n`; - content += `Want yen? Become a Government Contractor. Mr. President is paying 100 yen per box of stone at community base all day on wipe day. Offer is good for at least 10 boxes of stone so there is time for you to cash in. 10 yen per row of stone for smaller deliveries.\n\n`; + content += `Run straight to E24. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around your bases even if you never have the app open.\n\n`; + content += `Check out the new https://discord.com/channels/305840605328703500/711850971072036946. We are addressing the most longstanding problem in gov: having to put up with people you don't like to hang out with the ones you do like. Pair with rustcult.com for the best possible experience.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); @@ -334,11 +334,13 @@ async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of March 2024. Report to Rustopia.gg - US Large\n`; - content += '```client.connect USLarge.Rustopia.gg```\n'; // Only one newline after triple backticks. - content += `Get the gov build location from one of your trusted friends with a high rank in The Government. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around all your bases even if you never have the app open.\n\n`; - content += `Want yen? Become a Government Contractor. Mr. President is paying 100 yen per box of stone at community base all day on wipe day. Offer is good for at least 10 boxes of stone so there is time for you to cash in. 10 yen per row of stone for smaller deliveries.\n\n`; + content += `Here are your secret orders for the month of April 2024. Report to Rustafied.com - US Long III\n`; + content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. + content += `Grunt Code 1111\n`; + content += `Gate Code 1111\n\n`; + content += `Run straight to V7. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around your bases even if you never have the app open.\n\n`; + content += `Check out the new https://discord.com/channels/305840605328703500/711850971072036946. We are addressing the most longstanding problem in gov: having to put up with people you don't like to hang out with the ones you do like. Pair with rustcult.com for the best possible experience.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); @@ -358,6 +360,9 @@ async function SendNonWipeBadgeOrders(user, discordMessage, discordMember) { async function SendOrdersToOneCommissarUser(user, discordMessage) { const guild = await DiscordUtil.GetMainDiscordGuild(); const discordMember = await guild.members.fetch(user.discord_id); + if (discordMember.user.bot) { + return; + } const hasWipeBadge = await DiscordUtil.GuildMemberHasRole(discordMember, RoleID.WipeBadge); if (hasWipeBadge) { await SendWipeBadgeOrders(user, discordMessage, discordMember); @@ -368,8 +373,11 @@ async function SendOrdersToOneCommissarUser(user, discordMessage) { async function SendOrdersToTheseCommissarUsers(users, discordMessage) { await discordMessage.channel.send(`Sending orders to ${users.length} members. Restart the bot now if this is not right.`); - await Sleep(10 * 1000); + await Sleep(1 * 1000); for (const user of users) { + if (!user) { + continue; + } await SendOrdersToOneCommissarUser(user, discordMessage); await Sleep(5 * 1000); } diff --git a/chain-of-command.js b/chain-of-command.js index 1ea2d1b..c6261a4 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -181,7 +181,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (hc + iga) / 3600; + v.cross_platform_activity = (2 * hc + iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -357,7 +357,7 @@ async function CalculateChainOfCommand() { }); // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive // tree that is more compact to render and easier to read. - async function RenderSummaryTree(howManyTopLeadersToExpand, pixelWidth, pixelHeight, outputImageFilename) { + async function RenderSummaryTree(howManyTopLeadersToExpand, pixelHeight, outputImageFilename) { for (let i = n - howManyTopLeadersToExpand; i < n; i++) { const v = verticesSortedByScore[i]; if (v.subordinates.length > 0) { @@ -413,19 +413,21 @@ async function CalculateChainOfCommand() { //console.log(serializedSummaryTree); console.log('CountNodesOfTree', CountNodesOfTree(wholeSummaryTree)); console.log('CountMembersInTree', CountMembersInTree(wholeSummaryTree)); - console.log('CountLeafNodesOfTree', CountLeafNodesOfTree(wholeSummaryTree)); + const leafNodeCount = CountLeafNodesOfTree(wholeSummaryTree); + console.log('CountLeafNodesOfTree', leafNodeCount); const maxDepth = MaxDepthOfTree(wholeSummaryTree); console.log('MaxDepthOfTree', MaxDepthOfTree(wholeSummaryTree)); - const largeFontSize = 18; - const smallFontSize = largeFontSize / 2; + const fontSize = 18; + const rowHeight = Math.floor(fontSize * 3 / 2); const horizontalMargin = 8; + const horizontalPixelsPerLeafNode = 140; + const pixelWidth = leafNodeCount * horizontalPixelsPerLeafNode; const canvas = createCanvas(pixelWidth, pixelHeight); const ctx = canvas.getContext('2d'); ctx.fillStyle = '#313338'; ctx.fillRect(0, 0, canvas.width, canvas.height); function DrawTree(tree, leftX, rightX, topY) { const leafNodes = CountLeafNodesOfTree(tree); - const horizontalPixelsPerLeafNode = (rightX - leftX) / leafNodes; // Draw members. const centerX = Math.floor((leftX + rightX) / 2) + 0.5; let bottomY = topY; @@ -435,7 +437,7 @@ async function CalculateChainOfCommand() { const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); const rank = v.rank || (RankMetadata.length - 1); const rankData = RankMetadata[rank]; - let color = rankData.color || '#4285F4'; + let color = rankData.color || '#4285F4'; let insignia = rankData.insignia || '•'; if (cu) { if (cu.office) { @@ -444,36 +446,51 @@ async function CalculateChainOfCommand() { } } insignia = insignia.replaceAll('⦁', '•').replaceAll('❱', '›') - const fontSize = tree.children ? largeFontSize : smallFontSize; - const rowHeight = 2 * fontSize; const nameY = topY + (i * rowHeight) + rowHeight / 2; const maxColumnWidth = rightX - leftX - horizontalMargin; - let displayName = GetDisplayName(vertexId) + ' ' + insignia; + let displayName = GetDisplayName(vertexId).replace(/(\r\n|\n|\r)/gm, '');; + // Try removing characters from end of the display name to make it fit. + while (true) { + const nameAndInsignia = displayName + ' ' + insignia; + const textWidth = ctx.measureText(nameAndInsignia).width; + if (textWidth < maxColumnWidth) { + break; + } else { + displayName = displayName.substring(0, displayName.length - 1); + } + } + if (displayName.length === 0) { + displayName = 'John Doe'; + } + displayName = displayName.trim() + ' ' + insignia; bottomY += rowHeight; - if (bottomY > canvas.height - 2 * largeFontSize) { + if (bottomY > canvas.height - rowHeight) { const numHidden = tree.members.length - i; if (numHidden > 1) { displayName = `+${numHidden} more`; } } - ctx.font = `${fontSize}px Uni Sans Heavy`; + ctx.fillStyle = color; ctx.textAlign = 'center'; ctx.textBaseline = 'middle'; - ctx.fillStyle = color; + ctx.font = `${fontSize}px Uni Sans Heavy`; ctx.fillText(displayName, centerX, nameY, maxColumnWidth); - if (bottomY > canvas.height - 2 * largeFontSize) { + if (bottomY > canvas.height - rowHeight) { // Reached bottom of the page. Stop drawing names. break; } } // Draw children recursively. - const childTopY = bottomY + 2 * largeFontSize; - const bracketY = Math.floor((bottomY + childTopY) / 2) + 0.5; - ctx.strokeStyle = '#D2D5DA'; let leafNodesDrawn = 0; let leftBracketX = rightX; let rightBracketX = leftX; const children = tree.children || []; + let childTopY = bottomY; + if (children.length > 1) { + childTopY += 2 * rowHeight; + } + const bracketY = Math.floor((bottomY + childTopY) / 2) + 0.5; + ctx.strokeStyle = '#D2D5DA'; for (const child of children) { const childLeafNodes = CountLeafNodesOfTree(child); const childLeftX = leftX + leafNodesDrawn * horizontalPixelsPerLeafNode; @@ -481,18 +498,21 @@ async function CalculateChainOfCommand() { const childCenterX = Math.floor((childLeftX + childRightX) / 2) + 0.5; leftBracketX = Math.min(leftBracketX, childCenterX); rightBracketX = Math.max(rightBracketX, childCenterX); - // Draw the vertical white line that points down towards the child. - ctx.beginPath(); - ctx.moveTo(childCenterX, bracketY); - ctx.lineTo(childCenterX, Math.floor(childTopY - 12) + 0.5); - ctx.stroke(); + // Draw the child sub-trees recursively. DrawTree(child, childLeftX, childRightX, childTopY); leafNodesDrawn += childLeafNodes; + // Draw the vertical white line that points down towards the child. + if (children.length > 1) { + ctx.beginPath(); + ctx.moveTo(childCenterX, bracketY); + ctx.lineTo(childCenterX, bracketY + 8 + 0.5); + ctx.stroke(); + } } - if (tree.children) { + if (children.length > 1) { // Vertical line pointing up at the parent. ctx.beginPath(); - ctx.moveTo(centerX, Math.floor(bottomY + 12) + 0.5); + ctx.moveTo(centerX, bracketY - 8 + 0.5); ctx.lineTo(centerX, bracketY); ctx.stroke(); // Horizontal line. Bracket that joins siblings. @@ -502,7 +522,7 @@ async function CalculateChainOfCommand() { ctx.stroke(); } } - DrawTree(wholeSummaryTree, 0, canvas.width, largeFontSize); + DrawTree(wholeSummaryTree, 0, canvas.width, rowHeight); const out = fs.createWriteStream(__dirname + '/' + outputImageFilename); const stream = canvas.createPNGStream(); stream.pipe(out); @@ -514,25 +534,26 @@ async function CalculateChainOfCommand() { }); }); } - await RenderSummaryTree(20, 1920, 1080, 'chain-of-command-general.png'); - await RenderSummaryTree(80, 6400, 1080, 'chain-of-command-officer.png'); + await RenderSummaryTree(20, 800, 'chain-of-command-generals.png'); + await RenderSummaryTree(70, 800, 'chain-of-command-officers.png'); const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.fetch('711850971072036946'); await channel.bulkDelete(99); await channel.send({ - content: `**The Government Chain of Command**\nThe ranks update every 60 seconds. The structure comes from your relationships in-game and in discord. Who you base with, roam with, raid with, spend time in discord with, etc. Your rank score = (your in-game activity) + (your discord activity) + (all your followers in-game activity) + (all your followers discord activity). To climb the ranks, be a leader. Invite others into your base. Lead raids. Plan events. Be yourself and love your friends. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`, + content: `**The Government Chain of Command**`, files: [{ - attachment: 'chain-of-command-general.png', - name: 'chain-of-command-general.png' + attachment: 'chain-of-command-generals.png', + name: 'chain-of-command-generals.png' }], }); await channel.send({ content: `**More Detailed View**`, files: [{ - attachment: 'chain-of-command-officer.png', - name: 'chain-of-command-officer.png' + attachment: 'chain-of-command-officers.png', + name: 'chain-of-command-officers.png' }], }); + await channel.send(`Updates every 60 seconds. Your rank score = your activity in Rust + your activity in Discord + all your followers activity in Rust + all your followers activity in Discord. The structure comes from your relationships. Who you most often base with, roam with, raid with, and spend time with in Discord. To climb the ranks, be a leader. Build a base and bag people in. Lead raids. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`); } // Helper function that reads and parses a CSV file into memory. From a8fda93f1e5fe0fce604ef8f3d35827e6d99165e Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 8 Apr 2024 16:42:30 +0000 Subject: [PATCH 045/101] Rebalanced activity rank points towards discord. --- chain-of-command.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index c6261a4..4b25c0a 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -181,7 +181,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (2 * hc + iga) / 3600; + v.cross_platform_activity = (hc + 0.1 * iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -191,7 +191,7 @@ async function CalculateChainOfCommand() { const e = edges[i][j]; const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; - const t = (d + 2 * r) / 3600; + const t = (0.5 * d + r) / 3600; e.cross_platform_relationship_strength = t; if (t > 0) { e.cross_platform_relationship_distance = 1 / t; From 994b6b236b26a7a93b195ffd472fc77da30cc282 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 13 Apr 2024 22:15:10 +0000 Subject: [PATCH 046/101] Kick banned people out of ranks and enable more huddle rooms. --- chain-of-command.js | 18 ++++++++++++++++-- huddles.js | 5 +++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 4b25c0a..2509f6f 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -22,6 +22,10 @@ async function CalculateChainOfCommand() { let maxHC = null; let sumHC = 0; for (const v of discordVertices) { + if (v.ban_conviction_time && v.ban_pardon_time) { + // Exclude banned members from the ranks until they do their time. + continue; + } const activeInGame = v.steam_id in recentlyActiveSteamIds; if (!v.last_seen && !activeInGame) { continue; @@ -108,6 +112,16 @@ async function CalculateChainOfCommand() { if (!(i in recentlyActiveSteamIds)) { continue; } + const cu = UserCache.GetCachedUserBySteamId(i); + if (cu) { + if (cu.ban_conviction_time && cu.ban_pardon_time) { + // Exclude known banned users from the graph until they serve their time. + // This will still let through the steam accounts of non-linked users + // banned from discord but nothing much can be done about that automatically. + // The solution in that case is to manually link the person's account for them. + continue; + } + } const activity = parseFloat(line[1]); if (!(i in vertices)) { vertices[i] = {}; @@ -181,7 +195,7 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (hc + 0.1 * iga) / 3600; + v.cross_platform_activity = (0.8 * hc + 0.2 * iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -191,7 +205,7 @@ async function CalculateChainOfCommand() { const e = edges[i][j]; const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; - const t = (0.5 * d + r) / 3600; + const t = (0.2 * d + r) / 3600; e.cross_platform_relationship_strength = t; if (t > 0) { e.cross_platform_relationship_distance = 1 / t; diff --git a/huddles.js b/huddles.js index e4d989e..5a8f740 100644 --- a/huddles.js +++ b/huddles.js @@ -17,8 +17,9 @@ const UserCache = require('./user-cache'); const huddles = [ { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - //{ name: 'Quad', userLimit: 4, position: 4000 }, - //{ name: 'Squad', userLimit: 8, position: 7000 }, + { name: 'Quad', userLimit: 4, position: 4000 }, + { name: 'Six Pack', userLimit: 6, position: 6000 }, + { name: 'Squad', userLimit: 8, position: 7000 }, ]; const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { From 031cb61d116a66a6de843f9d48218cc7c048aa3b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 14 Apr 2024 14:24:07 +0000 Subject: [PATCH 047/101] Added 10 new ranks above General. --- ban.js | 8 ++-- bot-commands.js | 10 ++--- chain-of-command.js | 35 ++++++++------- commissar-user.js | 3 ++ huddles.js | 1 + rank-definitions.js | 102 ++++++++++++++++++++++++++++++++++++++++++++ role-id.js | 1 + server.js | 4 +- 8 files changed, 138 insertions(+), 26 deletions(-) diff --git a/ban.js b/ban.js index 029d571..2a56ce1 100644 --- a/ban.js +++ b/ban.js @@ -8,8 +8,8 @@ const VoteDuration = require('./vote-duration'); const threeTicks = '```'; -const banCommandRank = 5; // General 1 -const banVoteRank = 9; // Lieutenant +const banCommandRank = 15; // General 1 +const banVoteRank = 19; // Lieutenant function SentenceLengthAsString(years) { if (years <= 0) { @@ -112,7 +112,7 @@ async function UpdateTrial(cu) { const yesPercentage = voteCount > 0 ? yesVoteCount / voteCount : 0; if (member) { const before = await channel.permissionOverwrites.resolve(member.id); - if (cu.peak_rank >= 10 && voteCount >= 5 && yesPercentage >= 0.909) { + if (cu.peak_rank >= 20 && voteCount >= 5 && yesPercentage >= 0.909) { await channel.permissionOverwrites.create(member, { Connect: true, SendMessages: false, @@ -154,7 +154,7 @@ async function UpdateTrial(cu) { const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); - const totalVoters = 80; + const totalVoters = 100; let baselineVoteDurationDays; let nextStateChangeMessage; if (guilty) { diff --git a/bot-commands.js b/bot-commands.js index a2e2af1..eec2959 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -188,7 +188,7 @@ async function HandlePrivateRoomVoteCommand(discordMessage) { } function GenerateAkaStringForUser(cu) { - const peakRank = cu.peak_rank || 13; + const peakRank = cu.peak_rank || 23; const peakRankInsignia = RankMetadata[peakRank].insignia; const names = [ cu.steam_name, @@ -300,13 +300,13 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { let content = `${rankNameAndInsignia},\n\n`; content += `Here are your secret orders for the month of April 2024. Report to Rustafied.com - US Long III\n`; content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. - if (user.rank <= 5) { + if (user.rank <= 15) { content += `Generals Code 1111\n`; } - if (user.rank <= 9) { + if (user.rank <= 19) { content += `Officers Code 1111\n`; } - if (user.rank <= 13) { + if (user.rank <= 23) { content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } @@ -698,7 +698,7 @@ const sentToAFkTimes = {}; async function HandleAfkCommand(discordMessage) { const authorId = discordMessage.author.id; const author = await UserCache.GetCachedUserByDiscordId(authorId); - if (!author || author.rank > 5) { + if (!author || author.rank > 15) { await discordMessage.channel.send( `Error: Only generals can do that.` ) diff --git a/chain-of-command.js b/chain-of-command.js index 2509f6f..69312c3 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -275,7 +275,7 @@ async function CalculateChainOfCommand() { // boss and subordinates, turning the otherwise directionless graph into a top-down tree. next.leadershipScore = minScore; const displayName = GetDisplayName(next.vertex_id); - const formattedScore = Math.round(minScore).toString().replace(/\B(?=(\d{3})+(?!\d))/g, ","); + const formattedScore = Math.round(minScore).toString(); // To put commas in formatted score .replace(/\B(?=(\d{3})+(?!\d))/g, ","); let boss; const subordinates = []; next.subordinates = []; @@ -302,6 +302,7 @@ async function CalculateChainOfCommand() { } const allSubs = subNames.join(' '); //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); + //console.log(formattedScore + ',' + displayName); verticesSortedByScore.push(next); } // Find any isolated kings and plug them directly into the king of kings. This unites all @@ -335,26 +336,30 @@ async function CalculateChainOfCommand() { v.descendants = v.descendants.concat(sub.descendants); } } + // Helper function to look up what rank someone should be by their score. + function ScoreToRank(score) { + for (let i = 0; i < RankMetadata.length; i++) { + const r = RankMetadata[i]; + if (!r.minScore) { + continue; + } + if (score > r.minScore) { + return i; + } + } + // Default to the most junior rank just to be safe. + return RankMetadata.length - 1; + } // Assign discrete ranks to each player. - let rank = 0; - let usersAtRank = 0; - for (let i = n - 1; i >=0; i--) { - while (usersAtRank >= RankMetadata[rank].count) { - rank++; - usersAtRank = 0; - } - // When we run out of ranks, this line defaults to the last/least rank. - rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); - const v = verticesSortedByScore[i]; - v.rank = rank; + for (const v of verticesSortedByScore) { + v.rank = ScoreToRank(v.leadershipScore); const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); if (cu) { // Disable promotions during the transition to the new ranks. //await AnnounceIfPromotion(user, cappedRank); - await cu.setRank(rank); - //console.log(cu.getNicknameOrTitleWithInsignia(), rank); + //console.log(v.rank, cu.nickname); + await cu.setRank(v.rank); } - usersAtRank++; } // Assign the bottom rank to any known users that do not appear in the tree. await UserCache.ForEach(async (user) => { diff --git a/commissar-user.js b/commissar-user.js index 4d20ec1..32ecb6f 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -314,6 +314,9 @@ class CommissarUser { getNicknameOrTitle() { const rank = this.getRank(); + if (!rank && rank !== 0) { + return this.steam_name || this.nick || this.nickname; + } const job = RankMetadata[rank]; if (job.titleOverride) { const prefix = this.getGenderPrefix(); diff --git a/huddles.js b/huddles.js index 5a8f740..0266a03 100644 --- a/huddles.js +++ b/huddles.js @@ -45,6 +45,7 @@ async function CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate) { permissionOverwrites: [ { id: guild.roles.everyone, deny: perms }, { id: RoleID.Admin, allow: perms }, + { id: RoleID.Commander, allow: perms }, { id: RoleID.General, allow: perms }, { id: RoleID.Officer, allow: perms }, { id: RoleID.Grunt, allow: perms }, diff --git a/rank-definitions.js b/rank-definitions.js index 970ab7b..1cde880 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -27,12 +27,103 @@ module.exports = [ title: 'Vice President', titleOverride: true, }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 5120000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 2560000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 1280000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 640000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 320000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 160000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 80000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 40000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 20000, + title: 'Commander', + }, + { + banPower: true, + color: '#189b17', + count: 0, + insignia: '♦', + roles: [RoleID.Commander, RoleID.Admin], + minScore: 10000, + title: 'Commander', + }, { banPower: true, color: '#F4B400', count: 1, insignia: '★★★★', roles: [RoleID.General, RoleID.Admin], + minScore: 5000, title: 'General', }, { @@ -41,6 +132,7 @@ module.exports = [ count: 3, insignia: '★★★', roles: [RoleID.General, RoleID.Admin], + minScore: 2400, title: 'General', }, { @@ -49,6 +141,7 @@ module.exports = [ count: 5, insignia: '★★', roles: [RoleID.General, RoleID.Admin], + minScore: 1200, title: 'General', }, { @@ -57,6 +150,7 @@ module.exports = [ count: 7, insignia: '★', roles: [RoleID.General, RoleID.Admin], + minScore: 600, title: 'General', }, { @@ -64,6 +158,7 @@ module.exports = [ count: 15, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], + minScore: 300, title: 'Colonel', }, { @@ -71,6 +166,7 @@ module.exports = [ count: 15, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], + minScore: 200, title: 'Major', }, { @@ -78,6 +174,7 @@ module.exports = [ count: 15, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], + minScore: 150, title: 'Captain', }, { @@ -85,6 +182,7 @@ module.exports = [ count: 15, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], + minScore: 100, title: 'Lieutenant', }, { @@ -92,6 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], + minScore: 70, title: 'Staff Sergeant', }, { @@ -99,6 +198,7 @@ module.exports = [ count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], + minScore: 50, title: 'Sergeant', }, { @@ -106,6 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], + minScore: 30, title: 'Corporal', }, { @@ -113,6 +214,7 @@ module.exports = [ count: 1000 * 1000, insignia: '⦁', roles: [RoleID.Recruit, RoleID.Grunt], + minScore: 0, title: 'Recruit', }, ]; diff --git a/role-id.js b/role-id.js index 26ae93c..a344327 100644 --- a/role-id.js +++ b/role-id.js @@ -4,6 +4,7 @@ module.exports = { Bots: '319352533422309376', Captain: '825491798654582804', Colonel: '825489993132671006', + Commander: '1228834925067894794', Corporal: '825491805117874197', Defendant: '918232560813871114', General: '318985002266263552', diff --git a/server.js b/server.js index baba9bb..4ca10fc 100644 --- a/server.js +++ b/server.js @@ -82,8 +82,8 @@ async function UpdateMemberAppearance(member) { } // Retired Generals. const hasRankData = (cu.rank || cu.rank === 0) && (cu.peak_rank || cu.peak_rank === 0); - const hasBeenAGeneralEver = cu.peak_rank <= 5; - const isCurrentlyAGeneral = cu.rank <= 5; + const hasBeenAGeneralEver = cu.peak_rank <= 15; + const isCurrentlyAGeneral = cu.rank <= 15; if (hasRankData && hasBeenAGeneralEver && !isCurrentlyAGeneral) { await DiscordUtil.AddRole(member, RoleID.RetiredGeneral); } else { From bbf76d7180d41ae5a73b36b016acf5b9d551f28b Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 15 Apr 2024 18:09:29 +0000 Subject: [PATCH 048/101] Rebalance the rank cutoff scores. --- rank-definitions.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/rank-definitions.js b/rank-definitions.js index 1cde880..9d5ed9a 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -190,7 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 70, + minScore: 60, title: 'Staff Sergeant', }, { @@ -198,7 +198,7 @@ module.exports = [ count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - minScore: 50, + minScore: 30, title: 'Sergeant', }, { @@ -206,7 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 30, + minScore: 15, title: 'Corporal', }, { From fdfccfc1cf78a4208e771dcf730ec54dea24760a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 15 Apr 2024 18:35:58 +0000 Subject: [PATCH 049/101] Re-enable promotions with rate limit. --- chain-of-command.js | 61 +++++++++++++++++++++++++++++++++++++++++ rank-definitions.js | 6 ++-- rank.js | 67 --------------------------------------------- 3 files changed, 64 insertions(+), 70 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 69312c3..67c8a5e 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -5,6 +5,7 @@ const fs = require('fs'); const kruskal = require('kruskal-mst'); const moment = require('moment'); const RankMetadata = require('./rank-definitions'); +const Sleep = require('./sleep'); const UserCache = require('./user-cache'); const recentlyActiveSteamIds = {}; @@ -358,6 +359,7 @@ async function CalculateChainOfCommand() { // Disable promotions during the transition to the new ranks. //await AnnounceIfPromotion(user, cappedRank); //console.log(v.rank, cu.nickname); + await AnnounceIfPromotion(cu, v.rank); await cu.setRank(v.rank); } } @@ -575,6 +577,65 @@ async function CalculateChainOfCommand() { await channel.send(`Updates every 60 seconds. Your rank score = your activity in Rust + your activity in Discord + all your followers activity in Rust + all your followers activity in Discord. The structure comes from your relationships. Who you most often base with, roam with, raid with, and spend time with in Discord. To climb the ranks, be a leader. Build a base and bag people in. Lead raids. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`); } +// A temporary in-memory cache of the highest rank seen per user. +// This is used to avoid spamming promotion notices if a user's +// rank oscilates up and down rapidly. +let maxRankByCommissarId = {}; + +// Clear the recent max rank cache every few hours. +setInterval(() => { + console.log('Clearing maxRankByCommissarId'); + maxRankByCommissarId = {}; +}, 8 * 60 * 60 * 1000); + +// Announce a promotion in #public chat, if applicable. +// +// user - a commissar user. +// newRank - integer rank index of the user's new rank. +async function AnnounceIfPromotion(user, newRank) { + if (!user || + user.rank === undefined || user.rank === null || + newRank === undefined || newRank === null || + !Number.isInteger(user.rank) || !Number.isInteger(newRank) || + newRank >= user.rank) { + // No promotion detected. Bail. + return; + } + if (!user.last_seen) { + return; + } + const lastSeen = moment(user.last_seen); + if (moment().subtract(72, 'hours').isAfter(lastSeen)) { + // No announcements for people who are invactive the last 24 hours. + return; + } + const lowestPossibleRank = RankMetadata.length - 1; + const maxRecentRank = maxRankByCommissarId[user.commissar_id] || lowestPossibleRank; + // Lower rank index represents a higher-status rank. + // If could do it again I would. But that's how it is. + const newMaxRank = Math.min(newRank, maxRecentRank); + if (newMaxRank >= maxRecentRank) { + return; + } + maxRankByCommissarId[user.commissar_id] = newMaxRank; + // If we get past here, a promotion has been detected. + // Announce it in #public chat. + const name = user.getNicknameOrTitleWithInsignia(); + const oldMeta = RankMetadata[user.rank]; + const newMeta = RankMetadata[newRank]; + const message = ( + `${user.nickname} ${newMeta.insignia} is promoted from ` + + `${oldMeta.title} ${oldMeta.insignia} to ` + + `${newMeta.title} ${newMeta.insignia}` + ); + console.log(message); + // Delay for a few seconds to spread out the promotion messages and + // also achieve a crude non-guaranteed sorting by rank. + const delayMillis = 1000 * (newRank + Math.random() / 2) + 100; + await Sleep(delayMillis); + await DiscordUtil.MessagePublicChatChannel(message); +} + // Helper function that reads and parses a CSV file into memory. // Only use for small files. This function is memory inefficient. // Returns an array of arrays. diff --git a/rank-definitions.js b/rank-definitions.js index 9d5ed9a..05d0361 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -190,7 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 60, + minScore: 50, title: 'Staff Sergeant', }, { @@ -198,7 +198,7 @@ module.exports = [ count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - minScore: 30, + minScore: 20, title: 'Sergeant', }, { @@ -206,7 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 15, + minScore: 5, title: 'Corporal', }, { diff --git a/rank.js b/rank.js index 9db2459..e69de29 100644 --- a/rank.js +++ b/rank.js @@ -1,67 +0,0 @@ -const DiscordUtil = require('./discord-util'); -const moment = require('moment'); -const RankMetadata = require('./rank-definitions'); -const Sleep = require('./sleep'); -const UserCache = require('./user-cache'); - -// Update the ranks of all users. -async function UpdateUserRanks() { - const users = UserCache.GetMostCentralUsers(); - let rank = 0; - let usersAtRank = 0; - for (const user of users) { - while (usersAtRank >= RankMetadata[rank].count) { - rank++; - usersAtRank = 0; - } - // When we run out of ranks, this line defaults to the last/least rank. - rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); - const cap = user.steam_id ? 0 : 10; - const cappedRank = Math.max(rank, cap); - await AnnounceIfPromotion(user, cappedRank); - await user.setRank(cappedRank); - usersAtRank++; - } -} - -// Announce a promotion in #public chat, if applicable. -// -// user - a commissar user. -// newRank - integer rank index of the user's new rank. -async function AnnounceIfPromotion(user, newRank) { - if (!user || - user.rank === undefined || user.rank === null || - newRank === undefined || newRank === null || - !Number.isInteger(user.rank) || !Number.isInteger(newRank) || - newRank >= user.rank) { - // No promotion detected. Bail. - return; - } - if (!user.last_seen) { - return; - } - const lastSeen = moment(user.last_seen); - if (moment().subtract(24, 'hours').isAfter(lastSeen)) { - // No announcements for people who are invactive the last 24 hours. - return; - } - // If we get past here, a promotion has been detected. - // Announce it in #public chat. - const oldMeta = RankMetadata[user.rank]; - const newMeta = RankMetadata[newRank]; - const message = ( - `${user.nickname} ${newMeta.insignia} is promoted from ` + - `${oldMeta.title} ${oldMeta.insignia} to ` + - `${newMeta.title} ${newMeta.insignia}` - ); - console.log(message); - // Delay for a few seconds to spread out the promotion messages and - // also achieve a crude non-guaranteed sorting by rank. - const delayMillis = 1000 * (newRank + Math.random() / 2) + 100; - await Sleep(delayMillis); - await DiscordUtil.MessagePublicChatChannel(message); -} - -module.exports = { - UpdateUserRanks, -}; From 98d99fd3cfb16209d35b9d2c661ba98095133a82 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 15 Apr 2024 18:36:18 +0000 Subject: [PATCH 050/101] Delete dead code file. --- rank.js | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 rank.js diff --git a/rank.js b/rank.js deleted file mode 100644 index e69de29..0000000 From 68674851c25fe1dce881c2251e3ce72d8009746f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 16 Apr 2024 13:13:16 +0000 Subject: [PATCH 051/101] Fixed bug that gave retired president to all new joiners. --- chain-of-command.js | 21 +++++++++++++-------- user-cache.js | 2 +- 2 files changed, 14 insertions(+), 9 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 67c8a5e..31df8e2 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -200,6 +200,11 @@ async function CalculateChainOfCommand() { } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. + const relationshipsToPrint = { + '76561198294876014': 'BBQ', + '76561198355439651': 'KEY', + '76561198956010410': 'JIB', + }; const edgesFormattedForKruskal = []; for (const i in edges) { for (const j in edges[i]) { @@ -207,6 +212,11 @@ async function CalculateChainOfCommand() { const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; const t = (0.2 * d + r) / 3600; + if ((i in relationshipsToPrint) && (j in relationshipsToPrint)) { + const iName = relationshipsToPrint[i]; + const jName = relationshipsToPrint[j]; + console.log(iName, jName, t); + } e.cross_platform_relationship_strength = t; if (t > 0) { e.cross_platform_relationship_distance = 1 / t; @@ -321,6 +331,8 @@ async function CalculateChainOfCommand() { king.subordinates.push(v.vertex_id); king.leadershipScore += v.leadershipScore; } + // Print out the king's score. + console.log('Top leadership score:', Math.round(king.leadershipScore)); // Sort each node's subordinates. for (const v of verticesSortedByScore) { v.subordinates.sort((a, b) => { @@ -356,9 +368,6 @@ async function CalculateChainOfCommand() { v.rank = ScoreToRank(v.leadershipScore); const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); if (cu) { - // Disable promotions during the transition to the new ranks. - //await AnnounceIfPromotion(user, cappedRank); - //console.log(v.rank, cu.nickname); await AnnounceIfPromotion(cu, v.rank); await cu.setRank(v.rank); } @@ -623,11 +632,7 @@ async function AnnounceIfPromotion(user, newRank) { const name = user.getNicknameOrTitleWithInsignia(); const oldMeta = RankMetadata[user.rank]; const newMeta = RankMetadata[newRank]; - const message = ( - `${user.nickname} ${newMeta.insignia} is promoted from ` + - `${oldMeta.title} ${oldMeta.insignia} to ` + - `${newMeta.title} ${newMeta.insignia}` - ); + const message = `${name} is promoted from ${oldMeta.title} ${oldMeta.insignia} to ${newMeta.title} ${newMeta.insignia}`; console.log(message); // Delay for a few seconds to spread out the promotion messages and // also achieve a crude non-guaranteed sorting by rank. diff --git a/user-cache.js b/user-cache.js index a5a7aa8..1e57b81 100644 --- a/user-cache.js +++ b/user-cache.js @@ -148,7 +148,7 @@ async function CreateNewDatabaseUser(discordMember) { rank, last_seen, office, - 0, 12, null, + 0, rank, null, true, true, null, null, null, null, null, null, From 19d95fe8399828550402f370f23046e11a45c604 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 17 Apr 2024 01:09:52 +0000 Subject: [PATCH 052/101] Dust off some old disused columns from the database to store friend roles and VC room IDs. --- commissar-user.js | 10 ++++++++++ setup-database.sql | 1 + user-cache.js | 3 ++- 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/commissar-user.js b/commissar-user.js index 32ecb6f..183de07 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -18,6 +18,7 @@ class CommissarUser { gender, citizen, good_standing, + friend_role_id, friend_category_id, friend_text_chat_id, friend_voice_room_id, @@ -44,6 +45,7 @@ class CommissarUser { this.gender = gender; this.citizen = citizen; this.good_standing = good_standing; + this.friend_role_id = friend_role_id; this.friend_category_id = friend_category_id; this.friend_text_chat_id = friend_text_chat_id; this.friend_voice_room_id = friend_voice_room_id; @@ -163,6 +165,14 @@ class CommissarUser { await this.updateFieldInDatabase('good_standing', this.good_standing); } + async setFriendRoleId(friend_role_id) { + if (friend_role_id === this.friend_role_id) { + return; + } + this.friend_role_id = friend_role_id; + await this.updateFieldInDatabase('friend_role_id', this.friend_role_id); + } + async setFriendCategorityId(friend_category_id) { if (friend_category_id === this.friend_category_id) { return; diff --git a/setup-database.sql b/setup-database.sql index 2f16d98..201d2b6 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -24,6 +24,7 @@ CREATE TABLE users -- It says so in the Bible. Everyone knows God created exactly 26 genders! citizen BOOLEAN DEFAULT TRUE, -- Is this user currently a member of the main Discord guild? good_standing BOOLEAN DEFAULT TRUE, -- The preliminary outcome of a pending ban vote trial. + friend_role_id VARCHAR(32), -- ID of the Discord Role for a user's friends. friend_category_id VARCHAR(32), -- ID of the Discord category/section for a user's friends. friend_text_chat_id VARCHAR(32), -- ID of the private Discord text chatroom for a user's friends. friend_voice_room_id VARCHAR(32), -- ID of the private Discord voice room for a user's friends. diff --git a/user-cache.js b/user-cache.js index 1e57b81..21ea283 100644 --- a/user-cache.js +++ b/user-cache.js @@ -26,6 +26,7 @@ async function LoadAllUsersFromDatabase() { row.gender, row.citizen, row.good_standing, + row.friend_role_id, row.friend_category_id, row.friend_text_chat_id, row.friend_voice_room_id, @@ -150,7 +151,7 @@ async function CreateNewDatabaseUser(discordMember) { office, 0, rank, null, true, true, - null, null, null, + null, null, null, null, null, null, null, 0, null, null, null, null, null, From 6947342da40eb281c8cca3f727d3bbb0cbd38771 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Thu, 18 Apr 2024 22:06:22 +0000 Subject: [PATCH 053/101] Investigate account linking bug and make server vote. --- bot-commands.js | 24 ++++++++++++------------ chain-of-command.js | 34 +++++++++++++++++++++++++++++++++- huddles.js | 1 + 3 files changed, 46 insertions(+), 13 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index eec2959..9b527de 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -78,20 +78,20 @@ async function HandleServerVoteCommand(discordMessage) { } const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); - const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our main home Rust server for April 2024.'); + const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for May 2024.'); await message.react('❤️'); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 24); - await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 17); - await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 133); - await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 4); - await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 32); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 124); - await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 118); - await MakeOneServerVoteOption(channel, 'Reddit.com/r/PlayRust - US Monthly', 'https://www.battlemetrics.com/servers/rust/3345988', 33); + await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 11); + await MakeOneServerVoteOption(channel, 'Rustopia US Large', 'https://www.battlemetrics.com/servers/rust/14876729', 15); + await MakeOneServerVoteOption(channel, 'Reddit.com/r/PlayRust - US Monthly', 'https://www.battlemetrics.com/servers/rust/3345988', 26); + await MakeOneServerVoteOption(channel, 'Rusty Moose |US Small|', 'https://www.battlemetrics.com/servers/rust/2933470', 34); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long', 'https://www.battlemetrics.com/servers/rust/1477148', 88); + await MakeOneServerVoteOption(channel, 'PICKLE VANILLA MONTHLY', 'https://www.battlemetrics.com/servers/rust/4403307', 91); + await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long II', 'https://www.battlemetrics.com/servers/rust/2036399', 144); await MakeOneServerVoteOption(channel, 'Rustopia.gg - US Small', 'https://www.battlemetrics.com/servers/rust/14876730', 108); - await MakeOneServerVoteOption(channel, 'Rustoria.co - US Long', 'https://www.battlemetrics.com/servers/rust/9594576', 3); - await MakeOneServerVoteOption(channel, 'US Rustinity 2x Monthly Large Vanilla+', 'https://www.battlemetrics.com/servers/rust/10477772', 14); - await MakeOneServerVoteOption(channel, 'PICKLE QUAD MONTHLY US', 'https://www.battlemetrics.com/servers/rust/3477804', 230); + await MakeOneServerVoteOption(channel, 'Rustoria.co - US Long', 'https://www.battlemetrics.com/servers/rust/9594576', 2); + await MakeOneServerVoteOption(channel, 'US Rustinity 2x Monthly Large Vanilla+', 'https://www.battlemetrics.com/servers/rust/10477772', 16); + await MakeOneServerVoteOption(channel, 'PICKLE QUAD MONTHLY US', 'https://www.battlemetrics.com/servers/rust/3477804', 203); } async function MakeOnePresidentVoteOption(channel, playerName) { diff --git a/chain-of-command.js b/chain-of-command.js index 31df8e2..e8fb360 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -385,6 +385,39 @@ async function CalculateChainOfCommand() { } await user.setRank(RankMetadata.length - 1); }); + // Make sure the top leaders all have their own leader role and VC. If any + // are missing, create them. + const guild = await DiscordUtil.GetMainDiscordGuild(); + const numTopLeadersToMaintainVoiceRoomsFor = 3; + const k = numTopLeadersToMaintainVoiceRoomsFor; + for (let i = n - k; i < n; i++) { + const v = verticesSortedByScore[i]; + if (!v) { + continue; + } + if (!v.vertex_id) { + continue; + } + const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); + if (!cu) { + continue; + } + if (!cu.friend_role_id) { + const name = cu.getNicknameOrTitleWithInsignia(); + const rankData = RankMetadata[cu.rank]; + const color = rankData.color; + try { + const newFriendRole = await guild.roles.create({ name, color }); + await cu.setFriendRoleId(newFriendRole.id); + } catch (error) { + console.log('Failed to create a friend role for', name); + console.log(error); + } + } + if (!cu.friend_voice_room_id) { + + } + } // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive // tree that is more compact to render and easier to read. async function RenderSummaryTree(howManyTopLeadersToExpand, pixelHeight, outputImageFilename) { @@ -566,7 +599,6 @@ async function CalculateChainOfCommand() { } await RenderSummaryTree(20, 800, 'chain-of-command-generals.png'); await RenderSummaryTree(70, 800, 'chain-of-command-officers.png'); - const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.fetch('711850971072036946'); await channel.bulkDelete(99); await channel.send({ diff --git a/huddles.js b/huddles.js index 0266a03..c942b92 100644 --- a/huddles.js +++ b/huddles.js @@ -853,6 +853,7 @@ async function UpdateSteamAccountInfo() { if (!account.steamId) { return; } + //console.log(account); const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (!cu) { return; From 00b5317819a57e6e4caca4fe2b5963a37c37f93d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 19 Apr 2024 19:42:53 +0000 Subject: [PATCH 054/101] New guy demotion. --- chain-of-command.js | 47 +++++++++++++++++++++++++++++++++------------ rank-definitions.js | 42 ++++++++++++++++++++-------------------- setup-database.sql | 2 +- 3 files changed, 57 insertions(+), 34 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index e8fb360..024c0a0 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -106,7 +106,7 @@ async function CalculateChainOfCommand() { let sumActivity = 0; const rustVertexLines = ReadLinesFromCsvFile('in-game-activity-points-march-2024.csv'); for (const line of rustVertexLines) { - if (line.length !== 2) { + if (line.length !== 4) { continue; } const i = line[0]; @@ -130,6 +130,8 @@ async function CalculateChainOfCommand() { vertices[i].in_game_activity = activity; vertices[i].steam_id = i; vertices[i].vertex_id = i; + vertices[i].distinct_date_count = parseInt(line[2]); + vertices[i].distinct_month_count = parseInt(line[3]); if (minActivity === null || activity < minActivity) { minActivity = activity; } @@ -190,13 +192,29 @@ async function CalculateChainOfCommand() { console.log('mean', sumRust / rustEdgeLines.length); console.log(Object.keys(vertices).length, 'combined vertices'); console.log(Object.keys(edges).length, 'edge buckets'); + // Helper function that calculates the "new guy" demotion. This + // stops brand new members from power-leveling too quickly no + // matter their relationships and activity level. + function CalculateNewGuyDemotion(distinctDateCount, distinctMonthCount) { + const d = distinctDateCount || 1; + const m = distinctMonthCount || 1; + const newGuyDays = 45; + const newGuyMonths = 6; + const intercept = 0.2; + const slope = 1 - intercept; + const dayDemotion = Math.min(d / newGuyDays, 1) * slope + intercept; + const monthDemotion = Math.min(m / newGuyMonths, 1) * slope + intercept; + const totalDemotion = dayDemotion * monthDemotion; + return totalDemotion; + } // Calculate final vertex weights as a weighted combination of // vertex features from multiple sources. for (const i in vertices) { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; - v.cross_platform_activity = (0.8 * hc + 0.2 * iga) / 3600; + const newGuyDemotion = CalculateNewGuyDemotion(v.distinct_date_count, v.distinct_month_count); + v.cross_platform_activity = newGuyDemotion * (0.8 * hc + 0.2 * iga) / 3600; } // Calculate final edge weights as a weighted combination of // edge features from multiple sources. @@ -211,7 +229,12 @@ async function CalculateChainOfCommand() { const e = edges[i][j]; const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; - const t = (0.2 * d + r) / 3600; + const a = vertices[i]; + const b = vertices[j]; + const iDemotion = CalculateNewGuyDemotion(a.distinct_date_count, a.distinct_month_count); + const jDemotion = CalculateNewGuyDemotion(b.distinct_date_count, b.distinct_month_count); + const edgeDemotion = iDemotion * jDemotion; + const t = edgeDemotion * (0.2 * d + r) / 3600; if ((i in relationshipsToPrint) && (j in relationshipsToPrint)) { const iName = relationshipsToPrint[i]; const jName = relationshipsToPrint[j]; @@ -368,7 +391,8 @@ async function CalculateChainOfCommand() { v.rank = ScoreToRank(v.leadershipScore); const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); if (cu) { - await AnnounceIfPromotion(cu, v.rank); + // Do not await the promotion announcement. Fire and forget. + await AnnounceIfPromotion(cu, cu.rank, v.rank); await cu.setRank(v.rank); } } @@ -633,12 +657,15 @@ setInterval(() => { // // user - a commissar user. // newRank - integer rank index of the user's new rank. -async function AnnounceIfPromotion(user, newRank) { +async function AnnounceIfPromotion(user, oldRank, newRank) { if (!user || user.rank === undefined || user.rank === null || + oldRank === undefined || oldRank === null || newRank === undefined || newRank === null || - !Number.isInteger(user.rank) || !Number.isInteger(newRank) || - newRank >= user.rank) { + !Number.isInteger(user.rank) || + !Number.isInteger(newRank) || + !Number.isInteger(oldRank) || + newRank >= oldRank) { // No promotion detected. Bail. return; } @@ -662,14 +689,10 @@ async function AnnounceIfPromotion(user, newRank) { // If we get past here, a promotion has been detected. // Announce it in #public chat. const name = user.getNicknameOrTitleWithInsignia(); - const oldMeta = RankMetadata[user.rank]; + const oldMeta = RankMetadata[oldRank]; const newMeta = RankMetadata[newRank]; const message = `${name} is promoted from ${oldMeta.title} ${oldMeta.insignia} to ${newMeta.title} ${newMeta.insignia}`; console.log(message); - // Delay for a few seconds to spread out the promotion messages and - // also achieve a crude non-guaranteed sorting by rank. - const delayMillis = 1000 * (newRank + Math.random() / 2) + 100; - await Sleep(delayMillis); await DiscordUtil.MessagePublicChatChannel(message); } diff --git a/rank-definitions.js b/rank-definitions.js index 05d0361..0ac2735 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -33,7 +33,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 5120000, + minScore: 5000000, title: 'Commander', }, { @@ -42,7 +42,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 2560000, + minScore: 2000000, title: 'Commander', }, { @@ -51,7 +51,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 1280000, + minScore: 1000000, title: 'Commander', }, { @@ -60,7 +60,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 640000, + minScore: 500000, title: 'Commander', }, { @@ -69,7 +69,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 320000, + minScore: 240000, title: 'Commander', }, { @@ -78,7 +78,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 160000, + minScore: 120000, title: 'Commander', }, { @@ -87,7 +87,7 @@ module.exports = [ count: 0, insignia: '♦♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 80000, + minScore: 60000, title: 'Commander', }, { @@ -96,7 +96,7 @@ module.exports = [ count: 0, insignia: '♦♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 40000, + minScore: 30000, title: 'Commander', }, { @@ -105,7 +105,7 @@ module.exports = [ count: 0, insignia: '♦♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 20000, + minScore: 15000, title: 'Commander', }, { @@ -114,7 +114,7 @@ module.exports = [ count: 0, insignia: '♦', roles: [RoleID.Commander, RoleID.Admin], - minScore: 10000, + minScore: 7000, title: 'Commander', }, { @@ -123,7 +123,7 @@ module.exports = [ count: 1, insignia: '★★★★', roles: [RoleID.General, RoleID.Admin], - minScore: 5000, + minScore: 3500, title: 'General', }, { @@ -132,7 +132,7 @@ module.exports = [ count: 3, insignia: '★★★', roles: [RoleID.General, RoleID.Admin], - minScore: 2400, + minScore: 1750, title: 'General', }, { @@ -141,7 +141,7 @@ module.exports = [ count: 5, insignia: '★★', roles: [RoleID.General, RoleID.Admin], - minScore: 1200, + minScore: 875, title: 'General', }, { @@ -150,7 +150,7 @@ module.exports = [ count: 7, insignia: '★', roles: [RoleID.General, RoleID.Admin], - minScore: 600, + minScore: 420, title: 'General', }, { @@ -158,7 +158,7 @@ module.exports = [ count: 15, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], - minScore: 300, + minScore: 210, title: 'Colonel', }, { @@ -166,7 +166,7 @@ module.exports = [ count: 15, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], - minScore: 200, + minScore: 120, title: 'Major', }, { @@ -174,7 +174,7 @@ module.exports = [ count: 15, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], - minScore: 150, + minScore: 50, title: 'Captain', }, { @@ -182,7 +182,7 @@ module.exports = [ count: 15, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], - minScore: 100, + minScore: 8, title: 'Lieutenant', }, { @@ -190,7 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 50, + minScore: 4, title: 'Staff Sergeant', }, { @@ -198,7 +198,7 @@ module.exports = [ count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - minScore: 20, + minScore: 2, title: 'Sergeant', }, { @@ -206,7 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 5, + minScore: 1, title: 'Corporal', }, { diff --git a/setup-database.sql b/setup-database.sql index 201d2b6..77ad1e5 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -13,7 +13,7 @@ CREATE TABLE users battlemetrics_id VARCHAR(32), -- User ID on Battlemetrics.com. nickname VARCHAR(32), -- Last known nickname. nick VARCHAR(32), -- A user-supplied preferred nickname. - rank INT NOT NULL DEFAULT 12, -- Rank. 0 = President, 1 = VP, 2 = 4-star General, etc. + rank INT NOT NULL DEFAULT 22, -- Rank. 0 = President, 1 = VP, 2 = 4-star General, etc. last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Last time active in voice chat. office VARCHAR(32), -- Which office (executive title) the user occupies, if any. harmonic_centrality FLOAT DEFAULT 0, -- A measure of this user's social influence. From e83fae17ebb8216e8898f4d7de1946448cd4c923 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 21 Apr 2024 19:46:58 +0000 Subject: [PATCH 055/101] Various small improvements to General private comms. --- chain-of-command.js | 219 +++++++++++++++++++++++++++++++++++++++++--- commissar-user.js | 6 +- huddles.js | 4 +- rank-definitions.js | 12 +-- 4 files changed, 214 insertions(+), 27 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 024c0a0..c377827 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,14 +1,23 @@ const { createCanvas } = require('canvas'); const db = require('./database'); +const { PermissionFlagsBits } = require('discord.js'); const DiscordUtil = require('./discord-util'); const fs = require('fs'); const kruskal = require('kruskal-mst'); const moment = require('moment'); const RankMetadata = require('./rank-definitions'); +const RoleID = require('./role-id'); const Sleep = require('./sleep'); const UserCache = require('./user-cache'); const recentlyActiveSteamIds = {}; +let channelPermsModifiedRecently = {}; + +setInterval(() => { + // Clear the perms modified flags every few minutes, enabling them + // to be modified again. + channelPermsModifiedRecently = {}; +}, 9 * 60 * 1000); async function CalculateChainOfCommand() { console.log('Chain of command'); @@ -409,37 +418,217 @@ async function CalculateChainOfCommand() { } await user.setRank(RankMetadata.length - 1); }); + // Initialize each vertex's friend badges to empty. + for (const v of verticesSortedByScore) { + v.badges = {}; + } // Make sure the top leaders all have their own leader role and VC. If any // are missing, create them. - const guild = await DiscordUtil.GetMainDiscordGuild(); - const numTopLeadersToMaintainVoiceRoomsFor = 3; + console.log('Create and update friend role and rooms for top leaders'); + const numTopLeadersToMaintainVoiceRoomsFor = 17; const k = numTopLeadersToMaintainVoiceRoomsFor; - for (let i = n - k; i < n; i++) { - const v = verticesSortedByScore[i]; - if (!v) { + const guild = await DiscordUtil.GetMainDiscordGuild(); + const allFriendRoles = {}; + for (const v of verticesSortedByScore) { + const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); + if (!cu) { + continue; + } + if (cu.rank > 15) { + // Higher rank index means a lower rank. 15 is General 1. + continue; + } + const name = cu.getNicknameOrTitleWithInsignia(); + const rankData = RankMetadata[cu.rank]; + const color = rankData.color; + if (cu.friend_role_id) { + try { + v.friendRole = await guild.roles.fetch(cu.friend_role_id); + } catch (error) { + console.log('Failed to fetch friend role for', name); + console.log(error); + continue; + } + } else { + try { + v.friendRole = await guild.roles.create({ name, color }); + await cu.setFriendRoleId(v.friendRole.id); + } catch (error) { + console.log('Failed to create a friend role for', name); + console.log(error); + continue; + } + } + if (!v.friendRole) { + console.log('No valid friend role or failed to create.'); continue; } - if (!v.vertex_id) { + allFriendRoles[v.friendRole.id] = v.friendRole; + v.badges[v.friendRole.id] = v.friendRole; + if (v.friendRole.name !== name) { + console.log('Updating role name', v.friendRole.name, 'to', name); + await v.friendRole.setName(name); + } + const decimalColorCode = Number('0x' + color.replace('#', '')); + if (v.friendRole.color !== color && v.friendRole.color !== decimalColorCode) { + console.log('Updating role color for', v.friendRole.name, 'from', v.friendRole.color, 'to', color); + await v.friendRole.setColor(color); + } + const connect = PermissionFlagsBits.Connect; + const view = PermissionFlagsBits.ViewChannel; + if (cu.friend_voice_room_id) { + try { + v.friendRoom = await guild.channels.fetch(cu.friend_voice_room_id); + } catch (error) { + console.log('Failed to fetch friend room for', name); + console.log(error); + continue; + } + } else { + try { + v.friendRoom = await guild.channels.create({ + bitrate: 256000, + name, + permissionOverwrites: [ + { id: guild.roles.everyone, deny: [connect, view] }, + { id: v.friendRole.id, allow: [connect, view] }, + { id: RoleID.Bots, allow: [connect, view] }, + ], + type: 2, + userLimit: 99, + }); + await cu.setFriendVoiceRoomId(v.friendRoom.id); + } catch (error) { + console.log('Failed to create friend room for', name); + console.log(error); + continue; + } + } + if (!v.friendRoom) { + console.log('No valid friend room or failed to create.'); continue; } + // Hide room from most members while empty. + if (!(v.friendRoom.id in channelPermsModifiedRecently)) { + if (v.friendRoom.members.size === 0) { + if (v.friendRoom.permissionOverwrites.cache.has(RoleID.Grunt)) { + console.log('Hide room', v.friendRoom.name); + channelPermsModifiedRecently[v.friendRoom.id] = true; + await v.friendRoom.permissionOverwrites.set([ + { id: guild.roles.everyone, deny: [connect, view] }, + { id: v.friendRole.id, allow: [connect, view] }, + { id: RoleID.Bots, allow: [connect, view] }, + ]); + } + } else { + if (!v.friendRoom.permissionOverwrites.cache.has(RoleID.Grunt)) { + console.log('Reveal room', v.friendRoom.name); + channelPermsModifiedRecently[v.friendRoom.id] = true; + await v.friendRoom.permissionOverwrites.set([ + { id: guild.roles.everyone, deny: [connect, view] }, + { id: RoleID.Grunt, allow: [view] }, + { id: RoleID.Officer, allow: [view] }, + { id: RoleID.General, allow: [view] }, + { id: RoleID.Commander, allow: [view] }, + { id: v.friendRole.id, allow: [connect, view] }, + { id: RoleID.Bots, allow: [connect, view] }, + ]); + } + } + } + } + // Decide which people are friends with which others. + console.log('Traversing edges to detect friends'); + let edgeCount = 0; + let friendCount = 0; + for (const i in edges) { + for (const j in edges[i]) { + edgeCount++; + const e = edges[i][j]; + const d = e.discord_coplay_time || 0; + const r = e.rust_coplay_time || 0; + const t = 0.02 * d + r; + if (t > 300) { + friendCount++; + const a = vertices[i]; + const b = vertices[j]; + if (b.friendRole) { + a.badges[b.friendRole.id] = b.friendRole; + } + if (a.friendRole) { + b.badges[a.friendRole.id] = a.friendRole; + } + } + } + } + console.log(edgeCount, 'edges traversed'); + console.log(friendCount, 'friends detected'); + // Add and remove friend badges. + console.log('Adding and removing friend badges'); + for (const v of verticesSortedByScore) { const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); if (!cu) { continue; } - if (!cu.friend_role_id) { - const name = cu.getNicknameOrTitleWithInsignia(); - const rankData = RankMetadata[cu.rank]; - const color = rankData.color; + if (!cu.discord_id || !cu.citizen || !cu.good_standing) { + continue; + } + const discordMember = await guild.members.fetch(cu.discord_id); + const currentRoles = await discordMember.roles.cache; + const rolesToRemove = {}; + const rolesBefore = {}; + for (const [roleId, role] of currentRoles) { + rolesBefore[roleId] = role; + if ((roleId in allFriendRoles) && !(roleId in v.badges)) { + rolesToRemove[roleId] = role; + } + } + for (const roleId in rolesToRemove) { + const badge = rolesToRemove[roleId]; + console.log('Remove role', badge.name, 'from', discordMember.nickname); + await discordMember.roles.remove(badge); + } + for (const roleId in v.badges) { + if (roleId in rolesBefore) { + continue; + } + const badge = v.badges[roleId]; + console.log('Add role', badge.name, 'to', discordMember.nickname); + await discordMember.roles.add(badge); + } + } + // Clean up & destroy any friend roles & rooms of downranked leaders. + console.log('Clean up disused friend roles and rooms'); + for (const v of verticesSortedByScore) { + const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); + if (!cu) { + continue; + } + if (cu.rank <= 15) { + // Skip Generals. + continue; + } + if (cu.friend_role_id) { try { - const newFriendRole = await guild.roles.create({ name, color }); - await cu.setFriendRoleId(newFriendRole.id); + const friendRole = await guild.roles.fetch(cu.friend_role_id); + await friendRole.delete(); + await cu.setFriendRoleId(null); } catch (error) { - console.log('Failed to create a friend role for', name); + console.log('Failed to delete friend role for', name); console.log(error); + continue; } } - if (!cu.friend_voice_room_id) { - + if (cu.friend_voice_room_id) { + try { + const friendRoom = await guild.channels.fetch(cu.friend_voice_room_id); + await friendRoom.delete(); + await cu.setFriendVoiceRoomId(null); + } catch (error) { + console.log('Failed to delete friend room for', name); + console.log(error); + continue; + } } } // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive diff --git a/commissar-user.js b/commissar-user.js index 183de07..5a56d4e 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -347,10 +347,8 @@ class CommissarUser { } getInsignia() { - if (!this.citizen) { - return '⦁'; - } - const rank = this.getRank(); + const defaultRank = RankMetadata.length - 1; + const rank = this.getRank() || defaultRank; const rankData = RankMetadata[rank]; return rankData.insignia; } diff --git a/huddles.js b/huddles.js index c942b92..3d43eee 100644 --- a/huddles.js +++ b/huddles.js @@ -18,7 +18,7 @@ const huddles = [ { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, { name: 'Quad', userLimit: 4, position: 4000 }, - { name: 'Six Pack', userLimit: 6, position: 6000 }, + //{ name: 'Six Pack', userLimit: 6, position: 6000 }, { name: 'Squad', userLimit: 8, position: 7000 }, ]; const mainRoomControlledByProximity = false; @@ -883,7 +883,7 @@ async function HuddlesUpdate() { const roomsInOrder = await MoveOneRoomIfNeeded(guild); isUpdateNeeded = !roomsInOrder; } - setTimeout(HuddlesUpdate, 1000); + setTimeout(HuddlesUpdate, 100); } function ScheduleUpdate() { diff --git a/rank-definitions.js b/rank-definitions.js index 0ac2735..86b31db 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -158,7 +158,7 @@ module.exports = [ count: 15, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], - minScore: 210, + minScore: 250, title: 'Colonel', }, { @@ -166,7 +166,7 @@ module.exports = [ count: 15, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], - minScore: 120, + minScore: 160, title: 'Major', }, { @@ -174,7 +174,7 @@ module.exports = [ count: 15, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], - minScore: 50, + minScore: 110, title: 'Captain', }, { @@ -182,7 +182,7 @@ module.exports = [ count: 15, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], - minScore: 8, + minScore: 60, title: 'Lieutenant', }, { @@ -190,7 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 4, + minScore: 8, title: 'Staff Sergeant', }, { @@ -206,7 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 1, + minScore: 0.1, title: 'Corporal', }, { From 1f50202329c7cf71d05838706ad987d4e4046ec2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 21 Apr 2024 19:48:40 +0000 Subject: [PATCH 056/101] Update room name when name or rank changes. --- chain-of-command.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/chain-of-command.js b/chain-of-command.js index c377827..ecfd56d 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -508,6 +508,10 @@ async function CalculateChainOfCommand() { console.log('No valid friend room or failed to create.'); continue; } + if (v.friendRoom.name !== name) { + console.log('Updating room name', v.friendRoom.name, 'to', name); + await v.friendRoom.setName(name); + } // Hide room from most members while empty. if (!(v.friendRoom.id in channelPermsModifiedRecently)) { if (v.friendRoom.members.size === 0) { From f8ccf24ee231f928255db616e5ae6735fd56e6f3 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 23 Apr 2024 19:57:03 +0000 Subject: [PATCH 057/101] Add a new character to the weird character list. --- filter-username.js | 1 + 1 file changed, 1 insertion(+) diff --git a/filter-username.js b/filter-username.js index 12862d1..013ccc5 100644 --- a/filter-username.js +++ b/filter-username.js @@ -33,6 +33,7 @@ function FilterUsername(username) { 'î': 'i', 'ł': 'l', 'ø': 'o', + 'Ł': 'L', }; for (const [before, after] of Object.entries(substitutions)) { username = username.split(before).join(after); From eb02530c40803fef0a9aaf57667737c1a0535707 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 23 Apr 2024 21:36:07 +0000 Subject: [PATCH 058/101] Add descendants to microcommunities. --- chain-of-command.js | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/chain-of-command.js b/chain-of-command.js index ecfd56d..06ae3a6 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -551,7 +551,7 @@ async function CalculateChainOfCommand() { const e = edges[i][j]; const d = e.discord_coplay_time || 0; const r = e.rust_coplay_time || 0; - const t = 0.02 * d + r; + const t = 0.04 * d + r; if (t > 300) { friendCount++; const a = vertices[i]; @@ -567,6 +567,24 @@ async function CalculateChainOfCommand() { } console.log(edgeCount, 'edges traversed'); console.log(friendCount, 'friends detected'); + // Give friend badge to all descendants of eligible leaders. + let totalDescendantBadgeCount = 0; + for (const v of verticesSortedByScore) { + // For every vertex, iterate up the chain of command to find all + // this player's bosses up to but not including the very top leader. + let b = v; + while (b.boss) { + if (b.friendRole) { + // Players get badges from their bosses of high enough + // rank going all the way up the chain of command but for + // the very top leader. + v.badges[b.friendRole.id] = b.friendRole; + totalDescendantBadgeCount++; + } + b = vertices[b.boss]; + } + } + console.log(totalDescendantBadgeCount, 'total descendant badges issued'); // Add and remove friend badges. console.log('Adding and removing friend badges'); for (const v of verticesSortedByScore) { From 7ec5e4f5a9c3c79e0874cf17265d92f4bb2bd4b1 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 29 Apr 2024 16:13:16 +0000 Subject: [PATCH 059/101] Various small updates --- bot-commands.js | 8 ++++---- chain-of-command.js | 39 ++++++++++++++++++++++++++++++++++----- commissar-user.js | 1 + huddles.js | 26 +++++++++++++++----------- rank-definitions.js | 14 +++++++------- server.js | 7 +++++++ user-cache.js | 5 ++++- yen.js | 4 ++-- 8 files changed, 74 insertions(+), 30 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 9b527de..1319ade 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -111,14 +111,14 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in April 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in May 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(15); const candidateNames = []; for (const user of generalRankUsers) { - //if (user.commissar_id === 7) { - // continue; - //} + if (user.commissar_id === 7) { + continue; + } const name = user.getNicknameOrTitleWithInsignia(); candidateNames.push(name); } diff --git a/chain-of-command.js b/chain-of-command.js index 06ae3a6..008b348 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -365,6 +365,29 @@ async function CalculateChainOfCommand() { } // Print out the king's score. console.log('Top leadership score:', Math.round(king.leadershipScore)); + // Calculate 2nd in command's score as a percentage of the king's score. + // This measures the stability of the tree. How close is the top leader to changing? + const second = verticesSortedByScore[n - 2]; + const overthrowProgress = second.leadershipScore / (king.leadershipScore - second.leadershipScore); + const regimeStability = 1 - overthrowProgress; + const overthrowP = Math.round(100 * overthrowProgress); + const regimeP = Math.round(100 * regimeStability); + let stabilityMessage; + if (regimeStability > 0.7) { + stabilityMessage = 'Rock Solid'; + } else if (regimeStability > 0.5) { + stabilityMessage = 'Highly Stable'; + } else if (regimeStability > 0.3) { + stabilityMessage = 'Stable'; + } else if (regimeStability > 0.1) { + stabilityMessage = 'Not Stable'; + } else { + stabilityMessage = 'Regime Change Imminent'; + } + const kingName = UserCache.TryToFindUserGivenAnyKnownId(king.vertex_id).getRankNameAndInsignia(); + const secondName = UserCache.TryToFindUserGivenAnyKnownId(second.vertex_id).getRankNameAndInsignia(); + console.log(secondName, 'progress towards overthrowing', kingName, overthrowP, '%'); + console.log('Regime stability', regimeP, '%', stabilityMessage); // Sort each node's subordinates. for (const v of verticesSortedByScore) { v.subordinates.sort((a, b) => { @@ -476,6 +499,7 @@ async function CalculateChainOfCommand() { } const connect = PermissionFlagsBits.Connect; const view = PermissionFlagsBits.ViewChannel; + const send = PermissionFlagsBits.SendMessages; if (cu.friend_voice_room_id) { try { v.friendRoom = await guild.channels.fetch(cu.friend_voice_room_id); @@ -487,12 +511,12 @@ async function CalculateChainOfCommand() { } else { try { v.friendRoom = await guild.channels.create({ - bitrate: 256000, + bitrate: 384000, name, permissionOverwrites: [ - { id: guild.roles.everyone, deny: [connect, view] }, - { id: v.friendRole.id, allow: [connect, view] }, - { id: RoleID.Bots, allow: [connect, view] }, + { id: guild.roles.everyone, deny: [connect, send, view] }, + { id: v.friendRole.id, allow: [connect, send, view] }, + { id: RoleID.Bots, allow: [connect, send, view] }, ], type: 2, userLimit: 99, @@ -850,7 +874,12 @@ async function CalculateChainOfCommand() { name: 'chain-of-command-officers.png' }], }); - await channel.send(`Updates every 60 seconds. Your rank score = your activity in Rust + your activity in Discord + all your followers activity in Rust + all your followers activity in Discord. The structure comes from your relationships. Who you most often base with, roam with, raid with, and spend time with in Discord. To climb the ranks, be a leader. Build a base and bag people in. Lead raids. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`); + await channel.send( + `**Political Stability**\n` + + `${secondName} is ${overthrowP}% of the way to overthrowing ${kingName}. ` + + `The current regime is ${regimeP}% stable (${stabilityMessage}).` + ); + await channel.send(`**The Algorithm**\nUpdates every 60 seconds. Your rank score = your activity in Discord + your activity in Rust + all your followers activity in Discord + all your followers activity in Rust. The structure comes from your relationships. Who you usually base with, roam with, raid with, and chill with in Discord. To climb the ranks, be a leader. Build a base and bag people in. Lead raids. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`); } // A temporary in-memory cache of the highest rank seen per user. diff --git a/commissar-user.js b/commissar-user.js index 5a56d4e..eafc4a2 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -284,6 +284,7 @@ class CommissarUser { } this.steam_name = steam_name; await this.updateFieldInDatabase('steam_name', this.steam_name); + await this.setSteamNameUpdatedNow(); } async setSteamNameUpdatedNow() { diff --git a/huddles.js b/huddles.js index 3d43eee..3cc140f 100644 --- a/huddles.js +++ b/huddles.js @@ -17,9 +17,9 @@ const UserCache = require('./user-cache'); const huddles = [ { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - { name: 'Quad', userLimit: 4, position: 4000 }, + //{ name: 'Quad', userLimit: 4, position: 4000 }, //{ name: 'Six Pack', userLimit: 6, position: 6000 }, - { name: 'Squad', userLimit: 8, position: 7000 }, + //{ name: 'Squad', userLimit: 8, position: 7000 }, ]; const mainRoomControlledByProximity = false; if (!mainRoomControlledByProximity) { @@ -60,18 +60,16 @@ async function CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate) { } async function CreateNewVoiceChannel(guild, huddle) { - const level3Bitrate = 256000; - const level2Bitrate = 128000; - try { - return await CreateNewVoiceChannelWithBitrate(guild, huddle, level3Bitrate); - } catch (err) { + const bitratesToTry = [384000, 256000, 128000]; + for (const bitrate of bitratesToTry) { try { - return await CreateNewVoiceChannelWithBitrate(guild, huddle, level2Bitrate); + return await CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate); } catch (err) { - console.log('Failed to create voice channel.'); - return null; + console.log('Failed to create channel with bitrate', bitrate); } } + console.log('Failed to create channel with any bitrate'); + return null; } function GetMostRecentlyCreatedVoiceChannel(channels) { @@ -533,6 +531,9 @@ async function UpdateProximityChat() { //console.log(linkedAccounts); for (const account of linkedAccounts) { if (account && account.discordId) { + //if (account.discordId === '619279800783339530') { + // console.log('McLovin found:', account); + //} if (account.steamId) { const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (cu) { @@ -853,7 +854,10 @@ async function UpdateSteamAccountInfo() { if (!account.steamId) { return; } - //console.log(account); + //if (account.discordId === '619279800783339530') { + // console.log('McLovin found:', account); + //} + console.log(account); const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (!cu) { return; diff --git a/rank-definitions.js b/rank-definitions.js index 86b31db..ceafd8c 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -158,7 +158,7 @@ module.exports = [ count: 15, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], - minScore: 250, + minScore: 260, title: 'Colonel', }, { @@ -166,7 +166,7 @@ module.exports = [ count: 15, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], - minScore: 160, + minScore: 180, title: 'Major', }, { @@ -174,7 +174,7 @@ module.exports = [ count: 15, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], - minScore: 110, + minScore: 100, title: 'Captain', }, { @@ -182,7 +182,7 @@ module.exports = [ count: 15, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], - minScore: 60, + minScore: 50, title: 'Lieutenant', }, { @@ -190,7 +190,7 @@ module.exports = [ count: 30, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 8, + minScore: 5, title: 'Staff Sergeant', }, { @@ -198,7 +198,7 @@ module.exports = [ count: 50, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - minScore: 2, + minScore: 0.5, title: 'Sergeant', }, { @@ -206,7 +206,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 0.1, + minScore: 0.03, title: 'Corporal', }, { diff --git a/server.js b/server.js index 4ca10fc..7ea3ccf 100644 --- a/server.js +++ b/server.js @@ -348,6 +348,13 @@ async function RoutineUpdate() { setTimeout(RoutineUpdate, 60 * 1000); } +// Temporarily crawl steam names quickly to clear out the backlog. +//setTimeout(async () => { +// setInterval(async () => { +// await UpdateSomeSteamNames(); +// }, 500); +//}, 9000); + // Waits for the database and bot to both be connected, then finishes booting the bot. async function Start() { console.log('Waiting for Discord bot to connect.'); diff --git a/user-cache.js b/user-cache.js index 21ea283..04d5b43 100644 --- a/user-cache.js +++ b/user-cache.js @@ -313,7 +313,10 @@ function CountPresidentialElectionVotes() { function GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName() { let chosenUser = null; let oldestUpdateTime; - for (const i in commissarUserCache) { + const keys = Object.keys(commissarUserCache); + // Crawl users in reverse order to update most recent joiners by default. + keys.reverse(); + for (const i of keys) { const u = commissarUserCache[i]; if (!u.steam_id) { continue; diff --git a/yen.js b/yen.js index a97531e..862b470 100644 --- a/yen.js +++ b/yen.js @@ -460,13 +460,13 @@ async function UpdateYenChannel() { const channel = await guild.channels.resolve(yenChannelId); await channel.bulkDelete(99); await DiscordUtil.SendLongList(lines, channel); - const jeffSteamInventoryValue = 121462; + const jeffSteamInventoryValue = 254406; const reserveRatio = jeffSteamInventoryValue / totalYen; const formattedReserveRatio = parseInt(reserveRatio * 100); const formattedActiveYenPercent = parseInt(100 * activeYen / totalYen); let message = ''; message += `Total yen in circulation: ¥ ${totalYen}\n`; - message += `Liquidation value of Jeff's Rust skins (Nov 2023): ¥ ${jeffSteamInventoryValue}\n`; + message += `Liquidation value of Jeff's Rust skins (April 2024): ¥ ${jeffSteamInventoryValue}\n`; message += `Reserve ratio: ${formattedReserveRatio}%\n`; message += `All recently active members (90d): ¥ ${activeYen} (${formattedActiveYenPercent}%)\n`; message += `Inactive members: ¥ ${inactiveYen}\n`; From 27c1594ebbfc5d183543d7b0d4c300c1f276f3da Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 29 Apr 2024 16:19:05 +0000 Subject: [PATCH 060/101] Removed synonyms for ban command to avoid confusion with new kick and exile commands. --- bot-commands.js | 8 -------- 1 file changed, 8 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 1319ade..52e9e3e 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -754,8 +754,6 @@ async function Dispatch(discordMessage) { const handlers = { '!afk': HandleAfkCommand, '!amnesty': HandleAmnestyCommand, - '!apprehend': Ban.HandleBanCommand, - '!arrest': Ban.HandleBanCommand, '!art': Artillery, '!artillery': Artillery, '!badge': HandleBadgeCommand, @@ -767,16 +765,12 @@ async function Dispatch(discordMessage) { '!committee': HandleCommitteeCommand, '!convert': yen.HandleConvertCommand, '!convict': Ban.HandleConvictCommand, - '!detain': Ban.HandleBanCommand, - '!fuck': Ban.HandleBanCommand, '!gender': HandleGenderCommand, - '!goodbye': Ban.HandleBanCommand, '!howhigh': Artillery, '!hype': HandleHypeCommand, '!impeach': HandleImpeachCommand, '!prez': HandlePrezCommand, '!veep': HandleVeepCommand, - '!indict': Ban.HandleBanCommand, '!lottery': yen.DoLottery, '!money': yen.HandleYenCommand, '!nick': HandleNickCommand, @@ -793,10 +787,8 @@ async function Dispatch(discordMessage) { '!tax': yen.HandleTaxCommand, '!termlengthvote': HandleTermLengthVoteCommand, '!tip': yen.HandleTipCommand, - '!trial': Ban.HandleBanCommand, '!transcript': HandleTranscriptCommand, '!voiceactiveusers': HandleVoiceActiveUsersCommand, - '!welp': Ban.HandleBanCommand, '!yen': yen.HandleYenCommand, '!yencreate': yen.HandleYenCreateCommand, '!yendestroy': yen.HandleYenDestroyCommand, From c553711cfcfba490fa2804b59da7ebf13566b667 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 29 Apr 2024 17:13:14 +0000 Subject: [PATCH 061/101] Kick command. --- bot-commands.js | 62 +++++++++++++++++++++++++++++++++++++++++++++++++ huddles.js | 2 +- 2 files changed, 63 insertions(+), 1 deletion(-) diff --git a/bot-commands.js b/bot-commands.js index 52e9e3e..bb1e4f8 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -740,6 +740,61 @@ async function HandleAfkCommand(discordMessage) { } } +async function HandleExileCommand(discordMessage) { + +} + +async function HandleUnexileCommand(discordMessage) { + +} + +async function HandleKickCommand(discordMessage) { + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } + if (!author.friend_voice_room_id) { + // Auth: this command for leaders with their own voice room only. + await discordMessage.channel.send('!kick is for microcommunity leaders'); + return; + } + const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); + if (!mentionedMember) { + await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); + return; + } + const guild = await DiscordUtil.GetMainDiscordGuild(); + const channel = await guild.channels.fetch(author.friend_voice_room_id); + const mentionedMemberIsInChannel = channel.members.has(mentionedMember.id); + if (!mentionedMemberIsInChannel) { + await discordMessage.channel.send('!kick only works in your own microcommunity'); + return; + } + // If we get here, it means the mentioned member is eligible to be kicked + // and the author has the right to kick them. Try to move them to the + // fullest Main channel. + let fullestMainChannel; + let maxPop = -1; + for (const [id, c] of guild.channels.cache) { + if (c.type === 2 && !c.parent && c.name === 'Main') { + if (c.members.size > maxPop) { + maxPop = c.members.size; + fullestMainChannel = c; + } + } + } + if (fullestMainChannel) { + // Move to fullest Main channel. + await mentionedMember.voice.setChannel(fullestMainChannel); + } else { + // In case no Main channels are found or other strange circumstance + // kick the member from the channel without moving them elsewhere. + await mentionedMember.voice.disconnect(); + } + const mcName = author.getNicknameOrTitleWithInsignia(); + await discordMessage.channel.send(`${mentionedMember.nickname} is kicked out of microcommunity ${mcName} for 60 seconds`); +} + // Handle any unrecognized commands, possibly replying with an error message. async function HandleUnknownCommand(discordMessage) { // TODO: add permission checks. Only high enough ranks should get a error @@ -765,10 +820,12 @@ async function Dispatch(discordMessage) { '!committee': HandleCommitteeCommand, '!convert': yen.HandleConvertCommand, '!convict': Ban.HandleConvictCommand, + '!exile': HandleExileCommand, '!gender': HandleGenderCommand, '!howhigh': Artillery, '!hype': HandleHypeCommand, '!impeach': HandleImpeachCommand, + '!kick': HandleKickCommand, '!prez': HandlePrezCommand, '!veep': HandleVeepCommand, '!lottery': yen.DoLottery, @@ -788,12 +845,16 @@ async function Dispatch(discordMessage) { '!termlengthvote': HandleTermLengthVoteCommand, '!tip': yen.HandleTipCommand, '!transcript': HandleTranscriptCommand, + '!unexile': HandleUnexileCommand, '!voiceactiveusers': HandleVoiceActiveUsersCommand, '!yen': yen.HandleYenCommand, '!yencreate': yen.HandleYenCreateCommand, '!yendestroy': yen.HandleYenDestroyCommand, '!yenfaq': yen.HandleYenFaqCommand, }; + if (discordMessage.author.bot) { + return; + } if (!discordMessage.content || discordMessage.content.length === 0) { return; } @@ -805,6 +866,7 @@ async function Dispatch(discordMessage) { return; } const command = tokens[0].toLowerCase(); + console.log('Dispatching command:', command); if (command in handlers) { const handler = handlers[command]; await handler(discordMessage); diff --git a/huddles.js b/huddles.js index 3cc140f..640a47a 100644 --- a/huddles.js +++ b/huddles.js @@ -857,7 +857,7 @@ async function UpdateSteamAccountInfo() { //if (account.discordId === '619279800783339530') { // console.log('McLovin found:', account); //} - console.log(account); + //console.log(account); const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (!cu) { return; From 44e87538048b1854926ddadcdec7e05d4f6093b0 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 30 Apr 2024 19:07:02 +0000 Subject: [PATCH 062/101] Clean up some roles. --- commissar-user.js | 3 +-- huddles.js | 3 --- rank-definitions.js | 40 ++++++++++++++++------------------------ role-id.js | 4 ---- 4 files changed, 17 insertions(+), 33 deletions(-) diff --git a/commissar-user.js b/commissar-user.js index eafc4a2..f2a3a0c 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -348,8 +348,7 @@ class CommissarUser { } getInsignia() { - const defaultRank = RankMetadata.length - 1; - const rank = this.getRank() || defaultRank; + const rank = this.getRank(); const rankData = RankMetadata[rank]; return rankData.insignia; } diff --git a/huddles.js b/huddles.js index 640a47a..9d91d74 100644 --- a/huddles.js +++ b/huddles.js @@ -265,7 +265,6 @@ async function SetOpenPerms(channel) { { id: RoleID.Grunt, allow: [connect, view] }, { id: RoleID.Officer, allow: [connect, view] }, { id: RoleID.General, allow: [connect, view] }, - { id: RoleID.Marshal, allow: [connect, view] }, { id: RoleID.Bots, allow: [view, connect] }, ]; // Do not await. Fire and forget with rate limit. @@ -279,7 +278,6 @@ function CalculatePermsByRank(channel, rankLimit) { const perms = [ { id: channel.guild.roles.everyone.id, allow: [view], deny: [connect] }, { id: RoleID.Bots, allow: [view, connect] }, - { id: RoleID.Marshal, allow: [view, connect] }, ]; let rankIndex = 0; for (const rank of RankMetadata) { @@ -753,7 +751,6 @@ async function UpdateProximityChat() { { id: RoleID.Grunt, allow: [view] }, { id: RoleID.Officer, allow: [view] }, { id: RoleID.General, allow: [view] }, - { id: RoleID.Marshal, allow: [view] }, { id: RoleID.Bots, allow: [view, connect] }, ]; const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; diff --git a/rank-definitions.js b/rank-definitions.js index ceafd8c..6ee1c24 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -6,11 +6,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '⚑', - roles: [ - RoleID.Marshal, - RoleID.MrPresident, - RoleID.Admin, - ], + roles: [RoleID.Commander], title: 'President', titleOverride: true, }, @@ -19,11 +15,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '⚑', - roles: [ - RoleID.Marshal, - RoleID.MrVicePresident, - RoleID.Admin, - ], + roles: [RoleID.Commander], title: 'Vice President', titleOverride: true, }, @@ -32,7 +24,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 5000000, title: 'Commander', }, @@ -41,7 +33,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 2000000, title: 'Commander', }, @@ -50,7 +42,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 1000000, title: 'Commander', }, @@ -59,7 +51,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 500000, title: 'Commander', }, @@ -68,7 +60,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 240000, title: 'Commander', }, @@ -77,7 +69,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 120000, title: 'Commander', }, @@ -86,7 +78,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 60000, title: 'Commander', }, @@ -95,7 +87,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 30000, title: 'Commander', }, @@ -104,7 +96,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 15000, title: 'Commander', }, @@ -113,7 +105,7 @@ module.exports = [ color: '#189b17', count: 0, insignia: '♦', - roles: [RoleID.Commander, RoleID.Admin], + roles: [RoleID.Commander], minScore: 7000, title: 'Commander', }, @@ -122,7 +114,7 @@ module.exports = [ color: '#F4B400', count: 1, insignia: '★★★★', - roles: [RoleID.General, RoleID.Admin], + roles: [RoleID.General], minScore: 3500, title: 'General', }, @@ -131,7 +123,7 @@ module.exports = [ color: '#F4B400', count: 3, insignia: '★★★', - roles: [RoleID.General, RoleID.Admin], + roles: [RoleID.General], minScore: 1750, title: 'General', }, @@ -140,7 +132,7 @@ module.exports = [ color: '#F4B400', count: 5, insignia: '★★', - roles: [RoleID.General, RoleID.Admin], + roles: [RoleID.General], minScore: 875, title: 'General', }, @@ -149,7 +141,7 @@ module.exports = [ color: '#F4B400', count: 7, insignia: '★', - roles: [RoleID.General, RoleID.Admin], + roles: [RoleID.General], minScore: 420, title: 'General', }, diff --git a/role-id.js b/role-id.js index a344327..ba83631 100644 --- a/role-id.js +++ b/role-id.js @@ -1,5 +1,4 @@ module.exports = { - Admin: '828361832716042270', BanPower: '828361965557907517', Bots: '319352533422309376', Captain: '825491798654582804', @@ -12,9 +11,6 @@ module.exports = { IdBadge: '947942301039231016', Lieutenant: '825491800478449705', Major: '825490288218734674', - Marshal: '829432221042999356', - MrPresident: '825494427468431370', - MrVicePresident: '825494735154184212', Officer: '319300874470162434', Recruit: '825491806929027173', Sergeant: '825491803071184926', From 8cedc170a55b5e1b5d9c152f79b8e748096e0123 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 30 Apr 2024 20:50:52 +0000 Subject: [PATCH 063/101] More corporals and no badges for recruits. --- chain-of-command.js | 7 +++++++ rank-definitions.js | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/chain-of-command.js b/chain-of-command.js index 008b348..f7aa2a8 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -609,6 +609,13 @@ async function CalculateChainOfCommand() { } } console.log(totalDescendantBadgeCount, 'total descendant badges issued'); + // Remove friend badges from any bottom-ranked members. + for (const v of verticesSortedByScore) { + const recruit = RankMetadata.length - 1; + if (v.rank >= recruit) { + v.badges = {}; + } + } // Add and remove friend badges. console.log('Adding and removing friend badges'); for (const v of verticesSortedByScore) { diff --git a/rank-definitions.js b/rank-definitions.js index 6ee1c24..8d3799d 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -198,7 +198,7 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 0.03, + minScore: 0.01, title: 'Corporal', }, { From eb21cca982aa132d20a68ae4d4e81b97dfabf5b3 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 30 Apr 2024 21:54:36 +0000 Subject: [PATCH 064/101] Exile and unexile commands. --- bot-commands.js | 95 ++++++++++++++++++++++++++++++++++++++++++++- chain-of-command.js | 19 +++++++++ commissar-user.js | 4 ++ server.js | 3 ++ setup-database.sql | 7 ++++ 5 files changed, 126 insertions(+), 2 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index bb1e4f8..47199b8 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -4,6 +4,7 @@ const Ban = require('./ban'); const diff = require('diff'); const discordTranscripts = require('discord-html-transcripts'); const DiscordUtil = require('./discord-util'); +const exile = require('./exile-cache'); const FilterUsername = require('./filter-username'); const huddles = require('./huddles'); const RandomPin = require('./random-pin'); @@ -741,11 +742,101 @@ async function HandleAfkCommand(discordMessage) { } async function HandleExileCommand(discordMessage) { - + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } + if (!author.friend_voice_room_id) { + // Auth: this command for leaders with their own voice room only. + await discordMessage.channel.send('!exile is for microcommunity leaders'); + return; + } + const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); + if (!mentionedMember) { + await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); + return; + } + const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); + if (!mentionedUser) { + await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); + return; + } + const exiler = author.commissar_id; + const exilee = mentionedUser.commissar_id; + const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); + if (exile.IsExiled(exiler, exilee)) { + await discordMessage.channel.send(`${exileeName} is already exiled from microcommunity ${mcName}`); + return; + } + // Add exile record to the database to make it persistent. + await exile.AddExile(exiler, exilee); + // Revoke the microcommunity badge at once to avoid waiting for the next rank cycle. + if (mentionedMember.roles.cache.has(author.friend_role_id)) { + await mentionedMember.roles.remove(author.friend_role_id); + } + const mcName = author.getNicknameOrTitleWithInsignia(); + await discordMessage.channel.send(`${exileeName} has been exiled from microcommunity ${mcName}`); + const guild = await DiscordUtil.GetMainDiscordGuild(); + const channel = await guild.channels.fetch(author.friend_voice_room_id); + const mentionedMemberIsInChannel = channel.members.has(mentionedMember.id); + if (!mentionedMemberIsInChannel) { + // No need to remove member from channel. All set. + return; + } + // If we get here, it means the mentioned member is eligible to be kicked + // and the author has the right to kick them. Try to move them to the + // fullest Main channel. + let fullestMainChannel; + let maxPop = -1; + for (const [id, c] of guild.channels.cache) { + if (c.type === 2 && !c.parent && c.name === 'Main') { + if (c.members.size > maxPop) { + maxPop = c.members.size; + fullestMainChannel = c; + } + } + } + if (fullestMainChannel) { + // Move to fullest Main channel. + await mentionedMember.voice.setChannel(fullestMainChannel); + } else { + // In case no Main channels are found or other strange circumstance + // kick the member from the channel without moving them elsewhere. + await mentionedMember.voice.disconnect(); + } } async function HandleUnexileCommand(discordMessage) { - + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } + if (!author.friend_voice_room_id) { + // Auth: this command for leaders with their own voice room only. + await discordMessage.channel.send('!unexile is for microcommunity leaders'); + return; + } + const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); + if (!mentionedMember) { + await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); + return; + } + const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); + if (!mentionedUser) { + await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); + return; + } + const exiler = author.commissar_id; + const exilee = mentionedUser.commissar_id; + const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); + if (!exile.IsExiled(exiler, exilee)) { + await discordMessage.channel.send(`${exileeName} is not exiled from microcommunity ${mcName}`); + return; + } + // Delete exile record from the database to make it persistent. + await exile.Unexile(exiler, exilee); + const mcName = author.getNicknameOrTitleWithInsignia(); + await discordMessage.channel.send(`${exileeName} has been unexiled from microcommunity ${mcName}`); } async function HandleKickCommand(discordMessage) { diff --git a/chain-of-command.js b/chain-of-command.js index f7aa2a8..4d087cd 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -2,6 +2,7 @@ const { createCanvas } = require('canvas'); const db = require('./database'); const { PermissionFlagsBits } = require('discord.js'); const DiscordUtil = require('./discord-util'); +const exile = require('./exile-cache'); const fs = require('fs'); const kruskal = require('kruskal-mst'); const moment = require('moment'); @@ -48,6 +49,7 @@ async function CalculateChainOfCommand() { const i = v.steam_id || v.discord_id || v.commissar_id; const hc = v.citizen ? v.harmonic_centrality : 0; vertices[i] = { + commissar_id: v.commissar_id, discord_id: v.discord_id, harmonic_centrality: v.harmonic_centrality, steam_id: v.steam_id, @@ -616,6 +618,23 @@ async function CalculateChainOfCommand() { v.badges = {}; } } + // Enforce exiles by taking away exiled badges. + const exiles = exile.GetAllExilesAsList(); + for (const ex of exiles) { + const exiler = UserCache.GetCachedUserByCommissarId(ex.exiler); + if (!exiler) { + continue; + } + const exilee = UserCache.GetCachedUserByCommissarId(ex.exilee); + if (!exilee) { + continue; + } + const exileeVertexId = exilee.getSocialGraphVertexId(); + const exileeVertex = vertices[exileeVertexId]; + if (exiler.friend_role_id in exileeVertex.badges) { + delete exileeVertex.badges[exiler.friend_role_id]; + } + } // Add and remove friend badges. console.log('Adding and removing friend badges'); for (const v of verticesSortedByScore) { diff --git a/commissar-user.js b/commissar-user.js index f2a3a0c..5993644 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -381,6 +381,10 @@ class CommissarUser { return 'their'; } } + + getSocialGraphVertexId() { + return this.steam_id || this.discord_id || this.commissar_id; + } } module.exports = CommissarUser; diff --git a/server.js b/server.js index 7ea3ccf..2271889 100644 --- a/server.js +++ b/server.js @@ -8,6 +8,7 @@ const DB = require('./database'); const deepEqual = require('deep-equal'); const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('discord.js'); const DiscordUtil = require('./discord-util'); +const exile = require('./exile-cache'); const fetch = require('./fetch'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); @@ -367,6 +368,8 @@ async function Start() { console.log('Loading ban votes from database.'); await BanVoteCache.LoadVotesFromDatabase(); console.log('Ban votes loaded into cache.'); + await exile.LoadExilesFromDatabase(); + console.log('Exiles loaded into cache'); // This Discord event fires when someone joins a Discord guild that the bot is a member of. discordClient.on('guildMemberAdd', async (member) => { diff --git a/setup-database.sql b/setup-database.sql index 77ad1e5..d4bd912 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -55,6 +55,13 @@ CREATE TABLE time_together INDEX user_index (lo_user_id, hi_user_id) ); +CREATE TABLE exiles +( + exiler INT NOT NULL, + exilee INT NOT NULL, + PRIMARY KEY(exiler, exilee) +); + CREATE TABLE ban_votes ( defendant_id INT NOT NULL, From 98b5245b433d589864bb08aba3b9dbab5127c581 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 6 May 2024 13:09:51 +0000 Subject: [PATCH 065/101] Monthly orders update. --- bot-commands.js | 23 ++++++++++++----------- chain-of-command.js | 11 ++++++++++- huddles.js | 2 +- 3 files changed, 23 insertions(+), 13 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 47199b8..16c3ac9 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -299,8 +299,8 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of April 2024. Report to Rustafied.com - US Long III\n`; - content += '```client.connect uslong3.rustafied.com```\n'; // Only one newline after triple backticks. + content += `Here are your secret orders for the month of May 2024. Report to Rusty Moose |US Monthly|\n`; + content += '```client.connect monthly.us.moose.gg:28010```\n'; // Only one newline after triple backticks. if (user.rank <= 15) { content += `Generals Code 1111\n`; } @@ -311,9 +311,10 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } - content += `Run straight to E24. Don't say the location in voice chat, please. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; + content += `Run straight to E15. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around your bases even if you never have the app open.\n\n`; - content += `Check out the new https://discord.com/channels/305840605328703500/711850971072036946. We are addressing the most longstanding problem in gov: having to put up with people you don't like to hang out with the ones you do like. Pair with rustcult.com for the best possible experience.\n\n`; + content += `Check out the new https://discord.com/channels/305840605328703500/711850971072036946. Every General has their own AI powered comms. Pair with rustcult.com to avoid being left out.\n\n`; + content += `The Government is doing something special this month to show off our new tech. We are invading Rusty Moose with several large groups. Each group takes a different sector of the map and uses rustcult.com to avoid raiding each other. The main gov village will be the largest group and work the same as always. The effect of all these groups working together could be dramatic around mid-wipe. There is a chance we could make Rusty Moose a gov-only server. Dozens of leaders from past eras of the gov have reactivated this month because they don't want to miss this event. Come slap down a base on Moose and bag in a friend or 2. This is going to be a wipe weekend to remember.\n\n`; content += `Yours truly,\n`; content += `The Government <3`; console.log('Content length', content.length, 'characters.'); @@ -321,8 +322,8 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMember.send({ content, files: [{ - attachment: 'nov-2023-village-heatmap.png', - name: 'nov-2023-village-heatmap.png' + attachment: 'chain-of-command-generals.png', + name: 'chain-of-command-generals.png' }] }); } catch (error) { @@ -364,12 +365,12 @@ async function SendOrdersToOneCommissarUser(user, discordMessage) { if (discordMember.user.bot) { return; } - const hasWipeBadge = await DiscordUtil.GuildMemberHasRole(discordMember, RoleID.WipeBadge); - if (hasWipeBadge) { + //const hasWipeBadge = await DiscordUtil.GuildMemberHasRole(discordMember, RoleID.WipeBadge); + //if (hasWipeBadge) { await SendWipeBadgeOrders(user, discordMessage, discordMember); - } else { - await SendNonWipeBadgeOrders(user, discordMessage, discordMember); - } + //} else { + //await SendNonWipeBadgeOrders(user, discordMessage, discordMember); + //} } async function SendOrdersToTheseCommissarUsers(users, discordMessage) { diff --git a/chain-of-command.js b/chain-of-command.js index 4d087cd..b37cd1a 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -631,6 +631,9 @@ async function CalculateChainOfCommand() { } const exileeVertexId = exilee.getSocialGraphVertexId(); const exileeVertex = vertices[exileeVertexId]; + if (!exileeVertex) { + continue; + } if (exiler.friend_role_id in exileeVertex.badges) { delete exileeVertex.badges[exiler.friend_role_id]; } @@ -645,7 +648,13 @@ async function CalculateChainOfCommand() { if (!cu.discord_id || !cu.citizen || !cu.good_standing) { continue; } - const discordMember = await guild.members.fetch(cu.discord_id); + let discordMember; + try { + discordMember = await guild.members.fetch(cu.discord_id); + } catch (error) { + // Discord member probably left the discord. Ignore. + continue; + } const currentRoles = await discordMember.roles.cache; const rolesToRemove = {}; const rolesBefore = {}; diff --git a/huddles.js b/huddles.js index 9d91d74..ac51cfb 100644 --- a/huddles.js +++ b/huddles.js @@ -884,7 +884,7 @@ async function HuddlesUpdate() { const roomsInOrder = await MoveOneRoomIfNeeded(guild); isUpdateNeeded = !roomsInOrder; } - setTimeout(HuddlesUpdate, 100); + setTimeout(HuddlesUpdate, 1000); } function ScheduleUpdate() { From 74bc8ef3f4244ad58ca3acd4148f02a34984e0dd Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 14 May 2024 18:14:19 +0000 Subject: [PATCH 066/101] Gave officers ban power. --- ban.js | 7 +++++-- chain-of-command.js | 2 +- huddles.js | 1 - rank-definitions.js | 4 ++++ role-id.js | 2 +- 5 files changed, 11 insertions(+), 5 deletions(-) diff --git a/ban.js b/ban.js index 2a56ce1..ec183f1 100644 --- a/ban.js +++ b/ban.js @@ -8,8 +8,11 @@ const VoteDuration = require('./vote-duration'); const threeTicks = '```'; -const banCommandRank = 15; // General 1 -const banVoteRank = 19; // Lieutenant +// General 1 = 15 +// Major = 17 +// Lieutenant = 19 +const banCommandRank = 17; +const banVoteRank = 19; function SentenceLengthAsString(years) { if (years <= 0) { diff --git a/chain-of-command.js b/chain-of-command.js index b37cd1a..10502f5 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -513,7 +513,7 @@ async function CalculateChainOfCommand() { } else { try { v.friendRoom = await guild.channels.create({ - bitrate: 384000, + bitrate: 256000, name, permissionOverwrites: [ { id: guild.roles.everyone, deny: [connect, send, view] }, diff --git a/huddles.js b/huddles.js index ac51cfb..5816d9c 100644 --- a/huddles.js +++ b/huddles.js @@ -44,7 +44,6 @@ async function CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate) { bitrate, permissionOverwrites: [ { id: guild.roles.everyone, deny: perms }, - { id: RoleID.Admin, allow: perms }, { id: RoleID.Commander, allow: perms }, { id: RoleID.General, allow: perms }, { id: RoleID.Officer, allow: perms }, diff --git a/rank-definitions.js b/rank-definitions.js index 8d3799d..9dc4cf8 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -146,6 +146,7 @@ module.exports = [ title: 'General', }, { + banPower: true, color: '#DB4437', count: 15, insignia: '❱❱❱❱', @@ -154,6 +155,7 @@ module.exports = [ title: 'Colonel', }, { + banPower: true, color: '#DB4437', count: 15, insignia: '❱❱❱', @@ -162,6 +164,7 @@ module.exports = [ title: 'Major', }, { + banPower: true, color: '#DB4437', count: 15, insignia: '❱❱', @@ -170,6 +173,7 @@ module.exports = [ title: 'Captain', }, { + banPower: true, color: '#DB4437', count: 15, insignia: '❱', diff --git a/role-id.js b/role-id.js index ba83631..59f9e7b 100644 --- a/role-id.js +++ b/role-id.js @@ -1,5 +1,5 @@ module.exports = { - BanPower: '828361965557907517', + BanPower: '1237029248452132934', Bots: '319352533422309376', Captain: '825491798654582804', Colonel: '825489993132671006', From 9eeb8e9c9d579e73e4c1d21bd9aa06124d6e5a18 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 14 May 2024 22:15:25 +0000 Subject: [PATCH 067/101] Back to seniority based ranks. --- chain-of-command.js | 529 ++++---------------------------------------- rank-definitions.js | 42 +--- 2 files changed, 48 insertions(+), 523 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 10502f5..e54e7ab 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -209,9 +209,9 @@ async function CalculateChainOfCommand() { function CalculateNewGuyDemotion(distinctDateCount, distinctMonthCount) { const d = distinctDateCount || 1; const m = distinctMonthCount || 1; - const newGuyDays = 45; + const newGuyDays = 60; const newGuyMonths = 6; - const intercept = 0.2; + const intercept = 0.3; const slope = 1 - intercept; const dayDemotion = Math.min(d / newGuyDays, 1) * slope + intercept; const monthDemotion = Math.min(m / newGuyMonths, 1) * slope + intercept; @@ -224,225 +224,55 @@ async function CalculateChainOfCommand() { const v = vertices[i]; const hc = v.harmonic_centrality || 0; const iga = v.in_game_activity || 0; + v.cross_platform_activity = 0.8 * hc + 0.2 * iga; const newGuyDemotion = CalculateNewGuyDemotion(v.distinct_date_count, v.distinct_month_count); - v.cross_platform_activity = newGuyDemotion * (0.8 * hc + 0.2 * iga) / 3600; + v.rank_score = newGuyDemotion * v.cross_platform_activity; } - // Calculate final edge weights as a weighted combination of - // edge features from multiple sources. - const relationshipsToPrint = { - '76561198294876014': 'BBQ', - '76561198355439651': 'KEY', - '76561198956010410': 'JIB', - }; - const edgesFormattedForKruskal = []; - for (const i in edges) { - for (const j in edges[i]) { - const e = edges[i][j]; - const d = e.discord_coplay_time || 0; - const r = e.rust_coplay_time || 0; - const a = vertices[i]; - const b = vertices[j]; - const iDemotion = CalculateNewGuyDemotion(a.distinct_date_count, a.distinct_month_count); - const jDemotion = CalculateNewGuyDemotion(b.distinct_date_count, b.distinct_month_count); - const edgeDemotion = iDemotion * jDemotion; - const t = edgeDemotion * (0.2 * d + r) / 3600; - if ((i in relationshipsToPrint) && (j in relationshipsToPrint)) { - const iName = relationshipsToPrint[i]; - const jName = relationshipsToPrint[j]; - console.log(iName, jName, t); - } - e.cross_platform_relationship_strength = t; - if (t > 0) { - e.cross_platform_relationship_distance = 1 / t; - edgesFormattedForKruskal.push({ - from: i, - to: j, - weight: e.cross_platform_relationship_distance, - }); - } - } - } - // Calculate Minimum Spanning Tree (MST) of the relationship graph. - console.log('Calculating MST'); - const forest = kruskal.kruskal(edgesFormattedForKruskal); - console.log('MST forest has', forest.length, 'edges'); - // Index the edges of the MST by vertex for efficiency. - for (const edge of forest) { - const from = vertices[edge.from]; - if (!from.mstEdges) { - from.mstEdges = []; - } - from.mstEdges.push(edge.to); - const to = vertices[edge.to]; - if (!to.mstEdges) { - to.mstEdges = []; - } - to.mstEdges.push(edge.from); - } - // Roll up the points starting from edges of the graph. + // Sort the vertices by score. const verticesSortedByScore = []; - while (true) { - // Each iteration of the loop the first thing to do is find the next vertex to score. - let next; - let minScore; - let remainingVertices = 0; - for (const i in vertices) { - const v = vertices[i]; - if (v.leadershipScore || v.leadershipScore === 0) { - continue; - } - remainingVertices++; - const mstEdges = v.mstEdges || []; - let scoredNeighbors = 0; - let scoreSum = v.cross_platform_activity || 0; - for (const j of mstEdges) { - const u = vertices[j]; - const hasScore = u.leadershipScore || u.leadershipScore === 0; - if (hasScore) { - scoredNeighbors++; - scoreSum += u.leadershipScore; - } - } - const degree = mstEdges.length; - const unscoredNeighbors = degree - scoredNeighbors; - if (unscoredNeighbors < 2) { - if (!next || scoreSum < minScore) { - next = v; - minScore = scoreSum; - } - } - } - if (!next) { - // No more nodes left unscored. Terminate the loop. - break; - } - // If we get here, then a new vertex has been chosen to score next. Calculate each vertex's - // boss and subordinates, turning the otherwise directionless graph into a top-down tree. - next.leadershipScore = minScore; - const displayName = GetDisplayName(next.vertex_id); - const formattedScore = Math.round(minScore).toString(); // To put commas in formatted score .replace(/\B(?=(\d{3})+(?!\d))/g, ","); - let boss; - const subordinates = []; - next.subordinates = []; - const mstEdges = next.mstEdges || []; - for (const i of mstEdges) { - const v = vertices[i]; - if (!v.leadershipScore && v.leadershipScore !== 0) { - boss = v; - } else { - subordinates.push(v); - next.subordinates.push(i); - } - } - let bossName = 'NONE'; - if (boss) { - bossName = GetDisplayName(boss.vertex_id); - next.boss = boss.vertex_id; - } - subordinates.sort((a, b) => b.leadershipScore - a.leadershipScore); - const subNames = []; - for (const sub of subordinates) { - const subName = GetDisplayName(sub.vertex_id); - subNames.push(subName); - } - const allSubs = subNames.join(' '); - //console.log('(', remainingVertices, ')', formattedScore, displayName, '( boss:', bossName, ') +', allSubs); - //console.log(formattedScore + ',' + displayName); - verticesSortedByScore.push(next); + for (const i in vertices) { + const v = vertices[i]; + verticesSortedByScore.push(v); } - // Find any isolated kings and plug them directly into the king of kings. This unites all - // the disconnected components of the graph into one. - const n = verticesSortedByScore.length; - const king = verticesSortedByScore[n - 1]; - for (const v of verticesSortedByScore) { - if (v.boss) { - continue; - } - if (v === king) { - continue; + verticesSortedByScore.sort((a, b) => { + if (!a.rank_score && !b.rank_score) { + return 0; } - v.boss = king.vertex_id; - king.subordinates.push(v.vertex_id); - king.leadershipScore += v.leadershipScore; - } - // Print out the king's score. - console.log('Top leadership score:', Math.round(king.leadershipScore)); - // Calculate 2nd in command's score as a percentage of the king's score. - // This measures the stability of the tree. How close is the top leader to changing? - const second = verticesSortedByScore[n - 2]; - const overthrowProgress = second.leadershipScore / (king.leadershipScore - second.leadershipScore); - const regimeStability = 1 - overthrowProgress; - const overthrowP = Math.round(100 * overthrowProgress); - const regimeP = Math.round(100 * regimeStability); - let stabilityMessage; - if (regimeStability > 0.7) { - stabilityMessage = 'Rock Solid'; - } else if (regimeStability > 0.5) { - stabilityMessage = 'Highly Stable'; - } else if (regimeStability > 0.3) { - stabilityMessage = 'Stable'; - } else if (regimeStability > 0.1) { - stabilityMessage = 'Not Stable'; - } else { - stabilityMessage = 'Regime Change Imminent'; - } - const kingName = UserCache.TryToFindUserGivenAnyKnownId(king.vertex_id).getRankNameAndInsignia(); - const secondName = UserCache.TryToFindUserGivenAnyKnownId(second.vertex_id).getRankNameAndInsignia(); - console.log(secondName, 'progress towards overthrowing', kingName, overthrowP, '%'); - console.log('Regime stability', regimeP, '%', stabilityMessage); - // Sort each node's subordinates. - for (const v of verticesSortedByScore) { - v.subordinates.sort((a, b) => { - const aScore = vertices[a].leadershipScore; - const bScore = vertices[b].leadershipScore; - return bScore - aScore; - }); - } - // Calculate the descendants of each node. - for (const v of verticesSortedByScore) { - v.descendants = [v.vertex_id]; - for (const subId of v.subordinates) { - const sub = vertices[subId]; - v.descendants = v.descendants.concat(sub.descendants); + if (!a.rank_score) { + return 1; } - } - // Helper function to look up what rank someone should be by their score. - function ScoreToRank(score) { - for (let i = 0; i < RankMetadata.length; i++) { - const r = RankMetadata[i]; - if (!r.minScore) { - continue; - } - if (score > r.minScore) { - return i; - } + if (!b.rank_score) { + return -1; } - // Default to the most junior rank just to be safe. - return RankMetadata.length - 1; - } + return b.rank_score - a.rank_score; + }); + console.log('Top ranked vertex:', verticesSortedByScore[0]); // Assign discrete ranks to each player. + let rank = 0; + let usersAtRank = 0; + const recruitRank = RankMetadata.length - 1; for (const v of verticesSortedByScore) { - v.rank = ScoreToRank(v.leadershipScore); const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - if (cu) { - // Do not await the promotion announcement. Fire and forget. - await AnnounceIfPromotion(cu, cu.rank, v.rank); - await cu.setRank(v.rank); - } - } - // Assign the bottom rank to any known users that do not appear in the tree. - await UserCache.ForEach(async (user) => { - if (user.steam_id in vertices) { - return; + if (!cu) { + continue; } - if (user.discord_id in vertices) { - return; + if (!cu.citizen) { + await cu.setRank(recruitRank); + continue; } - if (user.commissar_id in vertices) { - return; + while (usersAtRank >= RankMetadata[rank].count) { + rank++; + usersAtRank = 0; } - await user.setRank(RankMetadata.length - 1); - }); + // When we run out of ranks, this line defaults to the last/least rank. + rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); + // Write the rank to the vertex record. + v.rank = rank; + // Do not await the promotion announcement. Fire and forget. + //AnnounceIfPromotion(cu, cu.rank, rank); + await cu.setRank(rank); + usersAtRank++; + } // Initialize each vertex's friend badges to empty. for (const v of verticesSortedByScore) { v.badges = {}; @@ -450,8 +280,6 @@ async function CalculateChainOfCommand() { // Make sure the top leaders all have their own leader role and VC. If any // are missing, create them. console.log('Create and update friend role and rooms for top leaders'); - const numTopLeadersToMaintainVoiceRoomsFor = 17; - const k = numTopLeadersToMaintainVoiceRoomsFor; const guild = await DiscordUtil.GetMainDiscordGuild(); const allFriendRoles = {}; for (const v of verticesSortedByScore) { @@ -593,31 +421,6 @@ async function CalculateChainOfCommand() { } console.log(edgeCount, 'edges traversed'); console.log(friendCount, 'friends detected'); - // Give friend badge to all descendants of eligible leaders. - let totalDescendantBadgeCount = 0; - for (const v of verticesSortedByScore) { - // For every vertex, iterate up the chain of command to find all - // this player's bosses up to but not including the very top leader. - let b = v; - while (b.boss) { - if (b.friendRole) { - // Players get badges from their bosses of high enough - // rank going all the way up the chain of command but for - // the very top leader. - v.badges[b.friendRole.id] = b.friendRole; - totalDescendantBadgeCount++; - } - b = vertices[b.boss]; - } - } - console.log(totalDescendantBadgeCount, 'total descendant badges issued'); - // Remove friend badges from any bottom-ranked members. - for (const v of verticesSortedByScore) { - const recruit = RankMetadata.length - 1; - if (v.rank >= recruit) { - v.badges = {}; - } - } // Enforce exiles by taking away exiled badges. const exiles = exile.GetAllExilesAsList(); for (const ex of exiles) { @@ -712,209 +515,6 @@ async function CalculateChainOfCommand() { } } } - // Calculate abbreviated summary tree. Kind of like a compressed version of the real massive - // tree that is more compact to render and easier to read. - async function RenderSummaryTree(howManyTopLeadersToExpand, pixelHeight, outputImageFilename) { - for (let i = n - howManyTopLeadersToExpand; i < n; i++) { - const v = verticesSortedByScore[i]; - if (v.subordinates.length > 0) { - v.expand = true; - } - } - for (const v of verticesSortedByScore) { - const expandedChildren = []; - let nonExpandedChildren = []; - for (const subId of v.subordinates) { - const sub = vertices[subId]; - if (sub.expand) { - expandedChildren.push(sub.summaryTree); - } else { - // If this node is not expanded then neither are its children. - nonExpandedChildren = nonExpandedChildren.concat(sub.descendants); - } - } - // Sort the non-expanded children to properly interleave members from different branches in rank order. - nonExpandedChildren.sort((a, b) => { - const aScore = vertices[a].leadershipScore; - const bScore = vertices[b].leadershipScore; - return bScore - aScore; - }); - if (expandedChildren.length === 0) { - if (nonExpandedChildren.length === 0) { - v.summaryTree = { - members: [v.vertex_id], - }; - } else { - v.summaryTree = { - members: [v.vertex_id], - children: [{ - members: nonExpandedChildren, - }], - }; - } - } else { - if (nonExpandedChildren.length > 0) { - expandedChildren.push({ - members: nonExpandedChildren, - }); - } - v.summaryTree = { - children: expandedChildren, - members: [v.vertex_id], - }; - } - } - const wholeSummaryTree = king.summaryTree; - const serializedSummaryTree = JSON.stringify(wholeSummaryTree, null, 2); - console.log('Summary tree (', serializedSummaryTree.length, 'chars )'); - //console.log(serializedSummaryTree); - console.log('CountNodesOfTree', CountNodesOfTree(wholeSummaryTree)); - console.log('CountMembersInTree', CountMembersInTree(wholeSummaryTree)); - const leafNodeCount = CountLeafNodesOfTree(wholeSummaryTree); - console.log('CountLeafNodesOfTree', leafNodeCount); - const maxDepth = MaxDepthOfTree(wholeSummaryTree); - console.log('MaxDepthOfTree', MaxDepthOfTree(wholeSummaryTree)); - const fontSize = 18; - const rowHeight = Math.floor(fontSize * 3 / 2); - const horizontalMargin = 8; - const horizontalPixelsPerLeafNode = 140; - const pixelWidth = leafNodeCount * horizontalPixelsPerLeafNode; - const canvas = createCanvas(pixelWidth, pixelHeight); - const ctx = canvas.getContext('2d'); - ctx.fillStyle = '#313338'; - ctx.fillRect(0, 0, canvas.width, canvas.height); - function DrawTree(tree, leftX, rightX, topY) { - const leafNodes = CountLeafNodesOfTree(tree); - // Draw members. - const centerX = Math.floor((leftX + rightX) / 2) + 0.5; - let bottomY = topY; - for (let i = 0; i < tree.members.length; i++) { - const vertexId = tree.members[i]; - const v = vertices[vertexId]; - const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - const rank = v.rank || (RankMetadata.length - 1); - const rankData = RankMetadata[rank]; - let color = rankData.color || '#4285F4'; - let insignia = rankData.insignia || '•'; - if (cu) { - if (cu.office) { - color = '#189b17'; - insignia = '⚑'; - } - } - insignia = insignia.replaceAll('⦁', '•').replaceAll('❱', '›') - const nameY = topY + (i * rowHeight) + rowHeight / 2; - const maxColumnWidth = rightX - leftX - horizontalMargin; - let displayName = GetDisplayName(vertexId).replace(/(\r\n|\n|\r)/gm, '');; - // Try removing characters from end of the display name to make it fit. - while (true) { - const nameAndInsignia = displayName + ' ' + insignia; - const textWidth = ctx.measureText(nameAndInsignia).width; - if (textWidth < maxColumnWidth) { - break; - } else { - displayName = displayName.substring(0, displayName.length - 1); - } - } - if (displayName.length === 0) { - displayName = 'John Doe'; - } - displayName = displayName.trim() + ' ' + insignia; - bottomY += rowHeight; - if (bottomY > canvas.height - rowHeight) { - const numHidden = tree.members.length - i; - if (numHidden > 1) { - displayName = `+${numHidden} more`; - } - } - ctx.fillStyle = color; - ctx.textAlign = 'center'; - ctx.textBaseline = 'middle'; - ctx.font = `${fontSize}px Uni Sans Heavy`; - ctx.fillText(displayName, centerX, nameY, maxColumnWidth); - if (bottomY > canvas.height - rowHeight) { - // Reached bottom of the page. Stop drawing names. - break; - } - } - // Draw children recursively. - let leafNodesDrawn = 0; - let leftBracketX = rightX; - let rightBracketX = leftX; - const children = tree.children || []; - let childTopY = bottomY; - if (children.length > 1) { - childTopY += 2 * rowHeight; - } - const bracketY = Math.floor((bottomY + childTopY) / 2) + 0.5; - ctx.strokeStyle = '#D2D5DA'; - for (const child of children) { - const childLeafNodes = CountLeafNodesOfTree(child); - const childLeftX = leftX + leafNodesDrawn * horizontalPixelsPerLeafNode; - const childRightX = childLeftX + childLeafNodes * horizontalPixelsPerLeafNode; - const childCenterX = Math.floor((childLeftX + childRightX) / 2) + 0.5; - leftBracketX = Math.min(leftBracketX, childCenterX); - rightBracketX = Math.max(rightBracketX, childCenterX); - // Draw the child sub-trees recursively. - DrawTree(child, childLeftX, childRightX, childTopY); - leafNodesDrawn += childLeafNodes; - // Draw the vertical white line that points down towards the child. - if (children.length > 1) { - ctx.beginPath(); - ctx.moveTo(childCenterX, bracketY); - ctx.lineTo(childCenterX, bracketY + 8 + 0.5); - ctx.stroke(); - } - } - if (children.length > 1) { - // Vertical line pointing up at the parent. - ctx.beginPath(); - ctx.moveTo(centerX, bracketY - 8 + 0.5); - ctx.lineTo(centerX, bracketY); - ctx.stroke(); - // Horizontal line. Bracket that joins siblings. - ctx.beginPath(); - ctx.moveTo(Math.floor(leftBracketX) + 0.5, bracketY); - ctx.lineTo(Math.floor(rightBracketX) + 0.5, bracketY); - ctx.stroke(); - } - } - DrawTree(wholeSummaryTree, 0, canvas.width, rowHeight); - const out = fs.createWriteStream(__dirname + '/' + outputImageFilename); - const stream = canvas.createPNGStream(); - stream.pipe(out); - // Wait for the image file to finish writing to disk. - return new Promise((resolve, reject) => { - out.on('finish', () => { - console.log('Wrote', outputImageFilename); - resolve(); - }); - }); - } - await RenderSummaryTree(20, 800, 'chain-of-command-generals.png'); - await RenderSummaryTree(70, 800, 'chain-of-command-officers.png'); - const channel = await guild.channels.fetch('711850971072036946'); - await channel.bulkDelete(99); - await channel.send({ - content: `**The Government Chain of Command**`, - files: [{ - attachment: 'chain-of-command-generals.png', - name: 'chain-of-command-generals.png' - }], - }); - await channel.send({ - content: `**More Detailed View**`, - files: [{ - attachment: 'chain-of-command-officers.png', - name: 'chain-of-command-officers.png' - }], - }); - await channel.send( - `**Political Stability**\n` + - `${secondName} is ${overthrowP}% of the way to overthrowing ${kingName}. ` + - `The current regime is ${regimeP}% stable (${stabilityMessage}).` - ); - await channel.send(`**The Algorithm**\nUpdates every 60 seconds. Your rank score = your activity in Discord + your activity in Rust + all your followers activity in Discord + all your followers activity in Rust. The structure comes from your relationships. Who you usually base with, roam with, raid with, and chill with in Discord. To climb the ranks, be a leader. Build a base and bag people in. Lead raids. Pair with https://rustcult.com every month to avoid missing out on your next promotion.`); } // A temporary in-memory cache of the highest rank seen per user. @@ -1005,59 +605,6 @@ function ReadSteamAccountsFromFile(filename) { } } -function GetDisplayName(vertexId) { - let displayName = UserCache.TryToFindDisplayNameForUserGivenAnyKnownId(vertexId); - if (displayName) { - // This user is known to commissar. Use their known name. - return displayName; - } else { - // This user is unknown to commissar. They are a rustcult.com user only. - // Import their name from outside commissar. - return recentlyActiveSteamIds[vertexId] || 'John Doe'; - } -} - -function CountNodesOfTree(t) { - let nodeCount = 1; - const children = t.children || []; - for (const child of children) { - nodeCount += CountNodesOfTree(child); - } - return nodeCount; -} - -function CountMembersInTree(t) { - let memberCount = t.members.length; - const children = t.children || []; - for (const child of children) { - memberCount += CountMembersInTree(child); - } - return memberCount; -} - -function CountLeafNodesOfTree(t) { - if (!t.children) { - return 1; - } - let leafCount = 0; - for (const child of t.children) { - leafCount += CountLeafNodesOfTree(child); - } - return leafCount; -} - -function MaxDepthOfTree(t) { - if (!t.children) { - return 1; - } - let maxDepth = -1; - for (const child of t.children) { - const d = MaxDepthOfTree(child); - maxDepth = Math.max(d + 1, maxDepth); - } - return maxDepth; -} - module.exports = { CalculateChainOfCommand, }; diff --git a/rank-definitions.js b/rank-definitions.js index 9dc4cf8..ccd8eb0 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -25,7 +25,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 5000000, title: 'Commander', }, { @@ -34,7 +33,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 2000000, title: 'Commander', }, { @@ -43,7 +41,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 1000000, title: 'Commander', }, { @@ -52,7 +49,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 500000, title: 'Commander', }, { @@ -61,7 +57,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 240000, title: 'Commander', }, { @@ -70,7 +65,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦♦', roles: [RoleID.Commander], - minScore: 120000, title: 'Commander', }, { @@ -79,7 +73,6 @@ module.exports = [ count: 0, insignia: '♦♦♦♦', roles: [RoleID.Commander], - minScore: 60000, title: 'Commander', }, { @@ -88,7 +81,6 @@ module.exports = [ count: 0, insignia: '♦♦♦', roles: [RoleID.Commander], - minScore: 30000, title: 'Commander', }, { @@ -97,7 +89,6 @@ module.exports = [ count: 0, insignia: '♦♦', roles: [RoleID.Commander], - minScore: 15000, title: 'Commander', }, { @@ -106,95 +97,84 @@ module.exports = [ count: 0, insignia: '♦', roles: [RoleID.Commander], - minScore: 7000, title: 'Commander', }, { banPower: true, color: '#F4B400', - count: 1, + count: 0, insignia: '★★★★', roles: [RoleID.General], - minScore: 3500, title: 'General', }, { banPower: true, color: '#F4B400', - count: 3, + count: 0, insignia: '★★★', roles: [RoleID.General], - minScore: 1750, title: 'General', }, { banPower: true, color: '#F4B400', - count: 5, + count: 0, insignia: '★★', roles: [RoleID.General], - minScore: 875, title: 'General', }, { banPower: true, color: '#F4B400', - count: 7, + count: 19, insignia: '★', roles: [RoleID.General], - minScore: 420, title: 'General', }, { banPower: true, color: '#DB4437', - count: 15, + count: 10, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], - minScore: 260, title: 'Colonel', }, { banPower: true, color: '#DB4437', - count: 15, + count: 10, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], - minScore: 180, title: 'Major', }, { banPower: true, color: '#DB4437', - count: 15, + count: 10, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], - minScore: 100, title: 'Captain', }, { banPower: true, color: '#DB4437', - count: 15, + count: 10, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], - minScore: 50, title: 'Lieutenant', }, { color: '#4285F4', - count: 30, + count: 40, insignia: '⦁⦁⦁⦁', roles: [RoleID.StaffSergeant, RoleID.Grunt], - minScore: 5, title: 'Staff Sergeant', }, { color: '#4285F4', - count: 50, + count: 100, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - minScore: 0.5, title: 'Sergeant', }, { @@ -202,7 +182,6 @@ module.exports = [ count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - minScore: 0.01, title: 'Corporal', }, { @@ -210,7 +189,6 @@ module.exports = [ count: 1000 * 1000, insignia: '⦁', roles: [RoleID.Recruit, RoleID.Grunt], - minScore: 0, title: 'Recruit', }, ]; From a84cbb2cc8fbbb371a5371bdad85f8becce5abde Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 14 May 2024 23:23:16 +0000 Subject: [PATCH 068/101] Add rank index to names. --- chain-of-command.js | 6 ++++++ commissar-user.js | 38 +++++++++++++++++++++++++++++++++++++- setup-database.sql | 2 ++ user-cache.js | 6 +++++- 4 files changed, 50 insertions(+), 2 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index e54e7ab..03fa384 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -250,6 +250,7 @@ async function CalculateChainOfCommand() { // Assign discrete ranks to each player. let rank = 0; let usersAtRank = 0; + let rankIndex = 1; const recruitRank = RankMetadata.length - 1; for (const v of verticesSortedByScore) { const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); @@ -258,6 +259,8 @@ async function CalculateChainOfCommand() { } if (!cu.citizen) { await cu.setRank(recruitRank); + await cu.setRankScore(0); + await cu.setRankIndex(9999999); continue; } while (usersAtRank >= RankMetadata[rank].count) { @@ -271,6 +274,9 @@ async function CalculateChainOfCommand() { // Do not await the promotion announcement. Fire and forget. //AnnounceIfPromotion(cu, cu.rank, rank); await cu.setRank(rank); + await cu.setRankScore(v.rank_score); + await cu.setRankIndex(rankIndex); + rankIndex++; usersAtRank++; } // Initialize each vertex's friend badges to empty. diff --git a/commissar-user.js b/commissar-user.js index 5993644..3c8e67c 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -11,6 +11,8 @@ class CommissarUser { nickname, nick, rank, + rank_score, + rank_index, last_seen, office, harmonic_centrality, @@ -38,6 +40,8 @@ class CommissarUser { this.nickname = nickname; this.nick = nick; this.rank = rank; + this.rank_score = rank_score; + this.rank_index = rank_index; this.last_seen = last_seen; this.office = office; this.harmonic_centrality = harmonic_centrality; @@ -98,6 +102,22 @@ class CommissarUser { await this.setPeakRank(this.rank); } + async setRankScore(rank_score) { + if (rank_score === this.rank_score) { + return; + } + this.rank_score = rank_score; + await this.updateFieldInDatabase('rank_score', this.rank_score); + } + + async setRankIndex(rank_index) { + if (rank_index === this.rank_index) { + return; + } + this.rank_index = rank_index; + await this.updateFieldInDatabase('rank_index', this.rank_index); + } + async seenNow() { this.last_seen = moment().format(); await this.updateFieldInDatabase('last_seen', this.last_seen); @@ -353,6 +373,21 @@ class CommissarUser { return rankData.insignia; } + getFormattedRankIndex() { + const i = this.rank_index; + if (!i) { + return '999'; + } + if (i > 999 || i < 1) { + return '999'; + } + let s = Math.round(i).toString(); + while (s.length < 3) { + s = '0' + s; + } + return s; + } + getNicknameWithInsignia() { const name = this.steam_name || this.nick || this.nickname || 'John Doe'; const insignia = this.getInsignia(); @@ -362,7 +397,8 @@ class CommissarUser { getNicknameOrTitleWithInsignia() { const name = this.getNicknameOrTitle(); const insignia = this.getInsignia(); - return `${name} ${insignia}`; + const formattedRankIndex = this.getFormattedRankIndex(); + return `${formattedRankIndex} ${name} ${insignia}`; } getRankNameAndInsignia() { diff --git a/setup-database.sql b/setup-database.sql index d4bd912..1b6dd12 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -14,6 +14,8 @@ CREATE TABLE users nickname VARCHAR(32), -- Last known nickname. nick VARCHAR(32), -- A user-supplied preferred nickname. rank INT NOT NULL DEFAULT 22, -- Rank. 0 = President, 1 = VP, 2 = 4-star General, etc. + rank_score FLOAT NOT NULL DEFAULT 0, + rank_index INT NOT NULL DEFAULT 9999999, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Last time active in voice chat. office VARCHAR(32), -- Which office (executive title) the user occupies, if any. harmonic_centrality FLOAT DEFAULT 0, -- A measure of this user's social influence. diff --git a/user-cache.js b/user-cache.js index 04d5b43..40959f6 100644 --- a/user-cache.js +++ b/user-cache.js @@ -19,6 +19,8 @@ async function LoadAllUsersFromDatabase() { row.nickname, row.nick, row.rank, + row.rank_score, + row.rank_index, row.last_seen, row.office, row.harmonic_centrality, @@ -136,6 +138,8 @@ async function CreateNewDatabaseUser(discordMember) { const nickname = FilterUsername(discordMember.user.username); console.log(`Create a new DB user for ${nickname}`); const rank = RankMetadata.length - 1; + const rank_score = 0; + const rank_index = 9999999; const last_seen = null; const office = null; const fields = {discord_id, nickname, rank, last_seen}; @@ -146,7 +150,7 @@ async function CreateNewDatabaseUser(discordMember) { discord_id, nickname, null, - rank, + rank, rank_score, rank_index, last_seen, office, 0, rank, null, From bd93d0dc1ee6b50cc34084fdd9e7414d5c463174 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 15 May 2024 02:29:21 +0000 Subject: [PATCH 069/101] Vote command update and bug fix. --- ban.js | 2 +- bot-commands.js | 15 +++++++++------ chain-of-command.js | 10 +--------- 3 files changed, 11 insertions(+), 16 deletions(-) diff --git a/ban.js b/ban.js index ec183f1..72588b1 100644 --- a/ban.js +++ b/ban.js @@ -157,7 +157,7 @@ async function UpdateTrial(cu) { const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); - const totalVoters = 100; + const totalVoters = 59; let baselineVoteDurationDays; let nextStateChangeMessage; if (guilty) { diff --git a/bot-commands.js b/bot-commands.js index 16c3ac9..097f112 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -175,13 +175,15 @@ async function HandlePrivateRoomVoteCommand(discordMessage) { return; } const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.create('private-comms-for-generals'); + const channel = await guild.channels.create({ name: 'vote-on-gov-future' }); const message = await channel.send( - `__**Should Generals Have Private Comms?**__\n` + - `Recently we have tried an experiment where all 15 Generals have their own private comms. Access is controlled by the Badge system.\n\n` + - `Vote YES to continue the experiment.\n\n` + - `Vote NO to delete all private comms except the Raid channel.\n\n` + - `The Generals decide. A simple majority is needed for this motion to pass. The vote ends March 2, 2023.`); + `__**Vote on the Future of the Government Community**__\n` + + `Should we keep the microcommunities, the 3 digit barcodes, and the New Guy Demotion that slows down new recruits from ranking up too quickly?\n\n` + + `The hierarchical ranks are obsolete, but they were not in vain. We needed to explore that direction to discover the concept of microcommunities that we are voting on now.\n\n` + + `The ranks we have now are almost identical to the original ones that reigned from March 2021 to March 2024. This vote is not primarily about the ranks after all. This vote is about whether to keep the microcommunities, the 3 digit barcodes, and the New Guy Demotion that slows down new recruits from ranking up too quickly.\n\n` + + `Vote YES to keep things how they are now\n\n` + + `Vote NO to go back to how everything was in March\n\n` + + `The vote ends May 30, 2024. All Generals past & present can vote.`); await message.react('✅'); await message.react('❌'); const voteSectionId = '1043778293612163133'; @@ -609,6 +611,7 @@ async function HandleBoopCommand(discordMessage) { } const cu = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); if (cu) { + await cu.setCitizen(true); await discordMessage.channel.send(`Member already exists.`); } else { // We have no record of this Discord user. Create a new record in the cache. diff --git a/chain-of-command.js b/chain-of-command.js index 03fa384..45604d5 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -37,15 +37,6 @@ async function CalculateChainOfCommand() { // Exclude banned members from the ranks until they do their time. continue; } - const activeInGame = v.steam_id in recentlyActiveSteamIds; - if (!v.last_seen && !activeInGame) { - continue; - } - const lastSeen = moment(v.last_seen); - const limit = moment().subtract(90, 'days'); - if (lastSeen.isBefore(limit) && !activeInGame) { - continue; - } const i = v.steam_id || v.discord_id || v.commissar_id; const hc = v.citizen ? v.harmonic_centrality : 0; vertices[i] = { @@ -252,6 +243,7 @@ async function CalculateChainOfCommand() { let usersAtRank = 0; let rankIndex = 1; const recruitRank = RankMetadata.length - 1; + console.log('verticesSortedByScore.length', verticesSortedByScore.length); for (const v of verticesSortedByScore) { const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); if (!cu) { From c6f43ff305fd5eace52f8c2c1e7cc60c182cae24 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 15 May 2024 06:15:19 +0000 Subject: [PATCH 070/101] Added a new rank below the old lowest rank. --- bot-commands.js | 4 ++-- chain-of-command.js | 6 ++++-- commissar-user.js | 3 +++ huddles.js | 1 + rank-definitions.js | 13 ++++++++++--- role-id.js | 3 ++- server.js | 22 +++++++++------------- setup-database.sql | 4 ++-- 8 files changed, 33 insertions(+), 23 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 097f112..208ff97 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -191,7 +191,7 @@ async function HandlePrivateRoomVoteCommand(discordMessage) { } function GenerateAkaStringForUser(cu) { - const peakRank = cu.peak_rank || 23; + const peakRank = cu.peak_rank || 24; const peakRankInsignia = RankMetadata[peakRank].insignia; const names = [ cu.steam_name, @@ -309,7 +309,7 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { if (user.rank <= 19) { content += `Officers Code 1111\n`; } - if (user.rank <= 23) { + if (user.rank <= 24) { content += `Grunt Code 1111\n`; content += `Gate Code 1111\n\n`; } diff --git a/chain-of-command.js b/chain-of-command.js index 45604d5..753b6d6 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -217,7 +217,9 @@ async function CalculateChainOfCommand() { const iga = v.in_game_activity || 0; v.cross_platform_activity = 0.8 * hc + 0.2 * iga; const newGuyDemotion = CalculateNewGuyDemotion(v.distinct_date_count, v.distinct_month_count); - v.rank_score = newGuyDemotion * v.cross_platform_activity; + const cid = v.commissar_id || 9999999; + const joinOrderBonus = 3600 / cid; + v.rank_score = newGuyDemotion * v.cross_platform_activity + joinOrderBonus; } // Sort the vertices by score. const verticesSortedByScore = []; @@ -264,7 +266,7 @@ async function CalculateChainOfCommand() { // Write the rank to the vertex record. v.rank = rank; // Do not await the promotion announcement. Fire and forget. - //AnnounceIfPromotion(cu, cu.rank, rank); + AnnounceIfPromotion(cu, cu.rank, rank); await cu.setRank(rank); await cu.setRankScore(v.rank_score); await cu.setRankIndex(rankIndex); diff --git a/commissar-user.js b/commissar-user.js index 3c8e67c..4bcc78f 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -397,6 +397,9 @@ class CommissarUser { getNicknameOrTitleWithInsignia() { const name = this.getNicknameOrTitle(); const insignia = this.getInsignia(); + if (!insignia) { + return '999 Recruit'; + } const formattedRankIndex = this.getFormattedRankIndex(); return `${formattedRankIndex} ${name} ${insignia}`; } diff --git a/huddles.js b/huddles.js index 5816d9c..bef9052 100644 --- a/huddles.js +++ b/huddles.js @@ -48,6 +48,7 @@ async function CreateNewVoiceChannelWithBitrate(guild, huddle, bitrate) { { id: RoleID.General, allow: perms }, { id: RoleID.Officer, allow: perms }, { id: RoleID.Grunt, allow: perms }, + { id: RoleID.Recruit, allow: perms }, { id: RoleID.Bots, allow: perms }, ], name: huddle.name, diff --git a/rank-definitions.js b/rank-definitions.js index ccd8eb0..cd16b51 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -179,16 +179,23 @@ module.exports = [ }, { color: '#4285F4', - count: 200, + count: 100, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], title: 'Corporal', }, { color: '#4285F4', - count: 1000 * 1000, + count: 700, insignia: '⦁', - roles: [RoleID.Recruit, RoleID.Grunt], + roles: [RoleID.Private, RoleID.Grunt], + title: 'Private', + }, + { + color: '#189b17', + count: 1000 * 1000, + insignia: null, + roles: [RoleID.Recruit], title: 'Recruit', }, ]; diff --git a/role-id.js b/role-id.js index 59f9e7b..7e75e7d 100644 --- a/role-id.js +++ b/role-id.js @@ -12,7 +12,8 @@ module.exports = { Lieutenant: '825491800478449705', Major: '825490288218734674', Officer: '319300874470162434', - Recruit: '825491806929027173', + Private: '825491806929027173', + Recruit: '1240130799743930439', Sergeant: '825491803071184926', StaffSergeant: '918218594091937792', WipeBadge: '1067177647857209436', diff --git a/server.js b/server.js index 2271889..3920ed4 100644 --- a/server.js +++ b/server.js @@ -207,13 +207,13 @@ async function UpdateAllCitizens() { } }); console.log(`${activeUsers.length} active users ${inactiveUsers.length} inactive users`); - //const activeSample = RandomSample(activeUsers, 100); - //const inactiveSample = RandomSample(inactiveUsers, 100); - //const selectedUsers = activeSample.concat(inactiveSample); - const selectedUsers = activeUsers.concat(inactiveUsers); + const activeSample = RandomSample(activeUsers, 100); + const inactiveSample = RandomSample(inactiveUsers, 100); + const selectedUsers = activeSample.concat(inactiveSample); + //const selectedUsers = activeUsers.concat(inactiveUsers); console.log(`Updating ${selectedUsers.length} users`); const guild = await DiscordUtil.GetMainDiscordGuild(); - const maxLoopDuration = 90 * 1000; + const maxLoopDuration = 60 * 1000; const startTime = Date.now(); let howManyUsersGotUpdatedCounter = 0; for (const cu of selectedUsers) { @@ -226,6 +226,8 @@ async function UpdateAllCitizens() { } if (howManyUsersGotUpdatedCounter < selectedUsers.length) { console.log(`Update cycle timed out after updating ${howManyUsersGotUpdatedCounter} users`); + } else { + console.log(`Updated all discord members successfully`); } } @@ -346,16 +348,10 @@ async function RoutineUpdate() { const endTime = new Date().getTime(); const elapsed = endTime - startTime; console.log(`Update Time: ${elapsed} ms`); - setTimeout(RoutineUpdate, 60 * 1000); + const sleepTime = Math.max(9000, 60000 - elapsed); + setTimeout(RoutineUpdate, sleepTime); } -// Temporarily crawl steam names quickly to clear out the backlog. -//setTimeout(async () => { -// setInterval(async () => { -// await UpdateSomeSteamNames(); -// }, 500); -//}, 9000); - // Waits for the database and bot to both be connected, then finishes booting the bot. async function Start() { console.log('Waiting for Discord bot to connect.'); diff --git a/setup-database.sql b/setup-database.sql index 1b6dd12..5f67550 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -13,13 +13,13 @@ CREATE TABLE users battlemetrics_id VARCHAR(32), -- User ID on Battlemetrics.com. nickname VARCHAR(32), -- Last known nickname. nick VARCHAR(32), -- A user-supplied preferred nickname. - rank INT NOT NULL DEFAULT 22, -- Rank. 0 = President, 1 = VP, 2 = 4-star General, etc. + rank INT NOT NULL DEFAULT 24, -- Rank. 0 = President, 1 = VP, 2 = 4-star General, etc. rank_score FLOAT NOT NULL DEFAULT 0, rank_index INT NOT NULL DEFAULT 9999999, last_seen TIMESTAMP DEFAULT CURRENT_TIMESTAMP, -- Last time active in voice chat. office VARCHAR(32), -- Which office (executive title) the user occupies, if any. harmonic_centrality FLOAT DEFAULT 0, -- A measure of this user's social influence. - peak_rank INT DEFAULT 12, -- The most senior rank (lowest rank number) ever achieved by this user. + peak_rank INT DEFAULT 24, -- The most senior rank (lowest rank number) ever achieved by this user. gender VARCHAR(1), -- M, F, NULL, or any other single alphabetic letter. -- L, G, B, T, Q... whatever letter people want to identify as! -- Must be a single alphabetic character from the ASCII range. From f500ab37a817a7df58606797f35b5a0524589ec5 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Fri, 17 May 2024 08:00:45 +0000 Subject: [PATCH 071/101] Make server do smaller chunks of works. --- server.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/server.js b/server.js index 3920ed4..3b4f50c 100644 --- a/server.js +++ b/server.js @@ -207,13 +207,13 @@ async function UpdateAllCitizens() { } }); console.log(`${activeUsers.length} active users ${inactiveUsers.length} inactive users`); - const activeSample = RandomSample(activeUsers, 100); - const inactiveSample = RandomSample(inactiveUsers, 100); + const activeSample = RandomSample(activeUsers, 1000); + const inactiveSample = RandomSample(inactiveUsers, 1000); const selectedUsers = activeSample.concat(inactiveSample); //const selectedUsers = activeUsers.concat(inactiveUsers); console.log(`Updating ${selectedUsers.length} users`); const guild = await DiscordUtil.GetMainDiscordGuild(); - const maxLoopDuration = 60 * 1000; + const maxLoopDuration = 3 * 60 * 1000; const startTime = Date.now(); let howManyUsersGotUpdatedCounter = 0; for (const cu of selectedUsers) { From be3c1ce2c370d87d5e74c3ae68be995130bd8918 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 5 Jun 2024 18:06:19 +0000 Subject: [PATCH 072/101] Added friend and unfriend commands that work by bot DM for discretion. --- bot-commands.js | 55 +++++++++++++++++++++++++++++++++++++++++----- discord-util.js | 4 ++++ huddles.js | 5 +++-- setup-database.sql | 1 + 4 files changed, 57 insertions(+), 8 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 208ff97..74150aa 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -79,7 +79,7 @@ async function HandleServerVoteCommand(discordMessage) { } const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); - const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for May 2024.'); + const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for June 2024.'); await message.react('❤️'); await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 11); @@ -112,9 +112,9 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in May 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in June 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); await message.react('❤️'); - const generalRankUsers = await UserCache.GetMostCentralUsers(15); + const generalRankUsers = await UserCache.GetMostCentralUsers(159); const candidateNames = []; for (const user of generalRankUsers) { if (user.commissar_id === 7) { @@ -745,6 +745,47 @@ async function HandleAfkCommand(discordMessage) { } } +async function HandleFriendCommand(discordMessage) { + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } + if (!author.friend_voice_room_id) { + // Auth: this command for leaders with their own voice room only. + await discordMessage.channel.send('!friend is for microcommunity leaders'); + return; + } + const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); + if (!mentionedMember) { + await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); + return; + } + const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); + if (!mentionedUser) { + await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); + return; + } + const exiler = author.commissar_id; + const exilee = mentionedUser.commissar_id; + const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); + const mcName = author.getNicknameOrTitleWithInsignia(); + if (exile.IsFriend(exiler, exilee)) { + await discordMessage.channel.send(`${exileeName} is already a friend of ${mcName}`); + return; + } + // Add friend record to the database to make it persistent. + await exile.SetIsFriend(exiler, exilee, true); + // Give the microcommunity badge at once to avoid waiting for the next rank cycle. + if (!mentionedMember.roles.cache.has(author.friend_role_id)) { + await mentionedMember.roles.add(author.friend_role_id); + } + await discordMessage.channel.send(`${exileeName} is invited to microcommunity ${mcName}`); +} + +async function HandleUnfriendCommand(discordMessage) { + await HandleExileCommand(discordMessage); +} + async function HandleExileCommand(discordMessage) { const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author) { @@ -768,17 +809,17 @@ async function HandleExileCommand(discordMessage) { const exiler = author.commissar_id; const exilee = mentionedUser.commissar_id; const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); + const mcName = author.getNicknameOrTitleWithInsignia(); if (exile.IsExiled(exiler, exilee)) { await discordMessage.channel.send(`${exileeName} is already exiled from microcommunity ${mcName}`); return; } // Add exile record to the database to make it persistent. - await exile.AddExile(exiler, exilee); + await exile.SetIsFriend(exiler, exilee, false); // Revoke the microcommunity badge at once to avoid waiting for the next rank cycle. if (mentionedMember.roles.cache.has(author.friend_role_id)) { await mentionedMember.roles.remove(author.friend_role_id); } - const mcName = author.getNicknameOrTitleWithInsignia(); await discordMessage.channel.send(`${exileeName} has been exiled from microcommunity ${mcName}`); const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.fetch(author.friend_voice_room_id); @@ -833,13 +874,13 @@ async function HandleUnexileCommand(discordMessage) { const exiler = author.commissar_id; const exilee = mentionedUser.commissar_id; const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); + const mcName = author.getNicknameOrTitleWithInsignia(); if (!exile.IsExiled(exiler, exilee)) { await discordMessage.channel.send(`${exileeName} is not exiled from microcommunity ${mcName}`); return; } // Delete exile record from the database to make it persistent. await exile.Unexile(exiler, exilee); - const mcName = author.getNicknameOrTitleWithInsignia(); await discordMessage.channel.send(`${exileeName} has been unexiled from microcommunity ${mcName}`); } @@ -916,6 +957,7 @@ async function Dispatch(discordMessage) { '!convert': yen.HandleConvertCommand, '!convict': Ban.HandleConvictCommand, '!exile': HandleExileCommand, + '!friend': HandleFriendCommand, '!gender': HandleGenderCommand, '!howhigh': Artillery, '!hype': HandleHypeCommand, @@ -941,6 +983,7 @@ async function Dispatch(discordMessage) { '!tip': yen.HandleTipCommand, '!transcript': HandleTranscriptCommand, '!unexile': HandleUnexileCommand, + '!unfriend': HandleUnfriendCommand, '!voiceactiveusers': HandleVoiceActiveUsersCommand, '!yen': yen.HandleYenCommand, '!yencreate': yen.HandleYenCreateCommand, diff --git a/discord-util.js b/discord-util.js index 8dfe681..da99b96 100644 --- a/discord-util.js +++ b/discord-util.js @@ -19,6 +19,10 @@ const client = new Discord.Client({ Discord.GatewayIntentBits.GuildVoiceStates, Discord.GatewayIntentBits.MessageContent, ], + partials: [ + Discord.Partials.Channel, + Discord.Partials.Message, + ] }); // Set to true once the guild roles have been cached once. diff --git a/huddles.js b/huddles.js index bef9052..f4e2b16 100644 --- a/huddles.js +++ b/huddles.js @@ -173,12 +173,13 @@ function CompareRooms(a, b) { // Rules from here on down are mainly intended for sorting the empty // VC rooms at the bottom amongst themselves. const roomOrder = ['Main', 'Duo', 'Trio', 'Quad', 'Squad']; + roomOrder.reverse(); for (const roomName of roomOrder) { if (a.name.startsWith(roomName) && !b.name.startsWith(roomName)) { - return -1; + return 1; } if (!a.name.startsWith(roomName) && b.name.startsWith(roomName)) { - return 1; + return -1; } } // Rooms with lower capacity sort up. diff --git a/setup-database.sql b/setup-database.sql index 5f67550..8d70d16 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -61,6 +61,7 @@ CREATE TABLE exiles ( exiler INT NOT NULL, exilee INT NOT NULL, + is_friend BOOL NOT NULL DEFAULT FALSE, PRIMARY KEY(exiler, exilee) ); From 481c56b43c50d1afa6255a498c805bb8075d01a6 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 5 Jun 2024 18:26:24 +0000 Subject: [PATCH 073/101] Bug fix. --- chain-of-command.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index 753b6d6..4785325 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -428,6 +428,8 @@ async function CalculateChainOfCommand() { if (!exiler) { continue; } + const exilerVertexId = exiler.getSocialGraphVertexId(); + const exilerVertex = vertices[exilerVertexId]; const exilee = UserCache.GetCachedUserByCommissarId(ex.exilee); if (!exilee) { continue; @@ -437,8 +439,12 @@ async function CalculateChainOfCommand() { if (!exileeVertex) { continue; } - if (exiler.friend_role_id in exileeVertex.badges) { - delete exileeVertex.badges[exiler.friend_role_id]; + if (ex.is_friend) { + exileeVertex.badges[exiler.friend_role_id] = exilerVertex.friendRole; + } else { + if (exiler.friend_role_id in exileeVertex.badges) { + delete exileeVertex.badges[exiler.friend_role_id]; + } } } // Add and remove friend badges. From 2bb8a5e718dcdc557f2aa83728bad3dec0638f87 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 7 Jul 2024 01:14:30 +0000 Subject: [PATCH 074/101] Various changes. --- ban.js | 18 +-- bot-commands.js | 29 +--- chain-of-command.js | 13 +- commissar-user.js | 6 - huddles.js | 379 ++------------------------------------------ rank-definitions.js | 1 - 6 files changed, 38 insertions(+), 408 deletions(-) diff --git a/ban.js b/ban.js index 72588b1..809ed3a 100644 --- a/ban.js +++ b/ban.js @@ -11,7 +11,7 @@ const threeTicks = '```'; // General 1 = 15 // Major = 17 // Lieutenant = 19 -const banCommandRank = 17; +const banCommandRank = 19; const banVoteRank = 19; function SentenceLengthAsString(years) { @@ -143,17 +143,13 @@ async function UpdateTrial(cu) { // How guilty the defendant is based on the vote margin. Ranges between 0 and 1. // One extra "mercy vote" is added to the denominator. This creates a bias in the // system towards more lenient sentences that wears off as more voters weigh in. - // The mercy vote also prevents infinite sentences by avoiding the asymptote in - // the tan() function. const howGuilty = (yesVoteCount - noVoteCount) / (yesVoteCount + noVoteCount + 1); - const degrees90 = 0.5 * Math.PI; - const radians = howGuilty * degrees90; - const sentenceYears = Math.tan(radians); + const sentenceYears = howGuilty; const banLengthInSeconds = Math.round(sentenceYears * 365.25 * 86400); banPardonTime = moment().add(banLengthInSeconds, 'seconds').format(); outcomeString = 'banned for ' + SentenceLengthAsString(sentenceYears); } - const caseTitle = `THE GOVERNMENT v ${cu.getNicknameWithInsignia()}`; + const caseTitle = `THE GOVERNMENT v ${cu.getNicknameOrTitleWithInsignia()}`; const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); @@ -220,7 +216,7 @@ async function UpdateTrial(cu) { `${underline}\n` + `Voting YES to ban: ${yesVoteCount}\n` + `Voting NO against the ban:${noVoteCount}\n\n` + - `${cu.getNicknameWithInsignia()} is ${outcomeString}` + + `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}` + `${threeTicks}` ); await message.edit(trialSummary); @@ -255,7 +251,7 @@ async function UpdateTrial(cu) { `${underline}\n` + `Voting YES to ban: ${yesVoteCount}\n` + `Voting NO against the ban: ${noVoteCount}\n\n` + - `${cu.getNicknameWithInsignia()} is ${outcomeString}. ` + + `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}. ` + `The vote ends ${timeRemaining}.` + `${threeTicks}` ); @@ -391,7 +387,7 @@ async function HandlePardonCommand(discordMessage) { await mentionedUser.setGoodStanding(true); await BanVoteCache.DeleteVotesForDefendant(mentionedUser.commissar_id); try { - await discordMessage.channel.send(`Programmer pardon ${mentionedUser.getNicknameWithInsignia()}!`); + await discordMessage.channel.send(`Programmer pardon ${mentionedUser.getNicknameOrTitleWithInsignia()}!`); } catch (error) { // In case the command was issued inside the courtroom, which no longer exists. } @@ -435,7 +431,7 @@ async function HandleConvictCommand(discordMessage) { await defendantUser.setGoodStanding(false); await BanVoteCache.DeleteVotesForDefendant(defendantUser.commissar_id); try { - await discordMessage.channel.send(`Convicted ${defendantUser.getNicknameWithInsignia()}!`); + await discordMessage.channel.send(`Convicted ${defendantUser.getNicknameOrTitleWithInsignia()}!`); } catch (error) { // In case the command was issued inside the courtroom, which no longer exists. } diff --git a/bot-commands.js b/bot-commands.js index 74150aa..543afc7 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -112,7 +112,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in June 2024. Mr. or Madam President has the power to choose where The Government builds on wipe day. If they fail to make a clear choice 20 minutes into the wipe, then it falls to the runner-up, Mr. or Madam Vice President. The community base will be there and most players will build nearby. Nobody is forced - if you want to build elsewhere then you can. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in July 2024. This vote ends .'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(159); const candidateNames = []; @@ -301,31 +301,18 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for the month of May 2024. Report to Rusty Moose |US Monthly|\n`; - content += '```client.connect monthly.us.moose.gg:28010```\n'; // Only one newline after triple backticks. - if (user.rank <= 15) { - content += `Generals Code 1111\n`; - } - if (user.rank <= 19) { - content += `Officers Code 1111\n`; - } - if (user.rank <= 24) { - content += `Grunt Code 1111\n`; - content += `Gate Code 1111\n\n`; - } - content += `Run straight to E15. Help build the community base and get a common Tier 3, then build your own small base.\n\n`; - content += `Pair with https://rustcult.com/ to get your base protected. The gov is too big to track everyone's base by word of mouth. We use a map app to avoid raiding ourselves by accident. It's easy. You don't have to input your base location. Once you are paired it somehow just knows. A force field goes up around your bases even if you never have the app open.\n\n`; - content += `Check out the new https://discord.com/channels/305840605328703500/711850971072036946. Every General has their own AI powered comms. Pair with rustcult.com to avoid being left out.\n\n`; - content += `The Government is doing something special this month to show off our new tech. We are invading Rusty Moose with several large groups. Each group takes a different sector of the map and uses rustcult.com to avoid raiding each other. The main gov village will be the largest group and work the same as always. The effect of all these groups working together could be dramatic around mid-wipe. There is a chance we could make Rusty Moose a gov-only server. Dozens of leaders from past eras of the gov have reactivated this month because they don't want to miss this event. Come slap down a base on Moose and bag in a friend or 2. This is going to be a wipe weekend to remember.\n\n`; - content += `Yours truly,\n`; - content += `The Government <3`; + content += `It's a special wipe day. RustGalaxy is 3 servers hosted in USA, Europe, and Australia. Use the telephone at Outpost to travel between islands - with your loot.\n`; + content += '```client.connect usa.rustgalaxy.com\nclient.connect europe.rustgalaxy.com\nclient.connect australia.rustgalaxy.com```\n'; // Only one newline after triple backticks. + content += `RustGalaxy was created by gov members. The admins and moderators are all gov members. We have beat Facepunch to the Nexus system, live right now in the Community tab. Being the first to unlock a multi-island universe gives our community a massive first-mover advantage. It is already gaining pop and it seems inevitable that we will give the biggest servers a run for their money. The existing tech can support over 100,000 concurrent players _in one connected world_ putting all the competition to shame. This moment is a big big deal. Come slap down a base and be part of the hype. Most gov members are building solo or duo or trio, and the usual gov rules about raiding don't apply.\n\n`; + content += `RustGalaxy never wipes BPs. Your blueprints are synced across all the islands.\n\n`; + content += `See you in there! <3\n`; console.log('Content length', content.length, 'characters.'); try { await discordMember.send({ content, files: [{ - attachment: 'chain-of-command-generals.png', - name: 'chain-of-command-generals.png' + attachment: 'galaxy-map-3.png', + name: 'galaxy-map-3.png' }] }); } catch (error) { diff --git a/chain-of-command.js b/chain-of-command.js index 4785325..eef7a81 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -429,7 +429,13 @@ async function CalculateChainOfCommand() { continue; } const exilerVertexId = exiler.getSocialGraphVertexId(); + if (!exilerVertexId) { + continue; + } const exilerVertex = vertices[exilerVertexId]; + if (!exilerVertex) { + continue; + } const exilee = UserCache.GetCachedUserByCommissarId(ex.exilee); if (!exilee) { continue; @@ -479,11 +485,16 @@ async function CalculateChainOfCommand() { await discordMember.roles.remove(badge); } for (const roleId in v.badges) { + if (!roleId) { + continue; + } if (roleId in rolesBefore) { continue; } const badge = v.badges[roleId]; - console.log('Add role', badge.name, 'to', discordMember.nickname); + if (!badge) { + continue; + } await discordMember.roles.add(badge); } } diff --git a/commissar-user.js b/commissar-user.js index 4bcc78f..2b2f2e3 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -388,12 +388,6 @@ class CommissarUser { return s; } - getNicknameWithInsignia() { - const name = this.steam_name || this.nick || this.nickname || 'John Doe'; - const insignia = this.getInsignia(); - return `${name} ${insignia}`; - } - getNicknameOrTitleWithInsignia() { const name = this.getNicknameOrTitle(); const insignia = this.getInsignia(); diff --git a/huddles.js b/huddles.js index f4e2b16..ec08e7a 100644 --- a/huddles.js +++ b/huddles.js @@ -173,13 +173,13 @@ function CompareRooms(a, b) { // Rules from here on down are mainly intended for sorting the empty // VC rooms at the bottom amongst themselves. const roomOrder = ['Main', 'Duo', 'Trio', 'Quad', 'Squad']; - roomOrder.reverse(); + //roomOrder.reverse(); // Makes Main sort to bottom. for (const roomName of roomOrder) { if (a.name.startsWith(roomName) && !b.name.startsWith(roomName)) { - return 1; + return -1; } if (!a.name.startsWith(roomName) && b.name.startsWith(roomName)) { - return -1; + return 1; } } // Rooms with lower capacity sort up. @@ -456,7 +456,8 @@ async function GetAllDiscordAccountsFromRustCultApi() { console.log('Cannot update prox because no api token.'); return null; } - const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken; + const randomNonce = Math.random().toString(); + const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken + '&nonce=' + randomNonce; let response; try { response = await fetch(url); @@ -475,365 +476,6 @@ async function GetAllDiscordAccountsFromRustCultApi() { return response; } -// Details of last seen in-game movement, keyed by discord ID. -const lastSeenCache = {}; - -async function UpdateProximityChat() { - // Get all Proximity VC rooms & members in them. - const lobbyName = mainRoomControlledByProximity ? 'Main' : 'Proximity'; - const proxRoomNames = { - Proximity: true, - Roaming: true, - Village: true, - }; - const guild = await DiscordUtil.GetMainDiscordGuild(); - const allChannels = await guild.channels.fetch(); - let lobbyChannel; - const proxChannels = {}; - const proxMembers = {}; - for (const [channelId, channel] of allChannels) { - if (channel.type !== 2) { - continue; - } - if (channel.name === lobbyName) { - lobbyChannel = channel; - for (const [memberId, member] of channel.members) { - proxMembers[memberId] = member; - } - } else if (channel.name in proxRoomNames) { - proxChannels[channelId] = channel; - for (const [memberId, member] of channel.members) { - proxMembers[memberId] = member; - } - } - } - if (!lobbyChannel) { - console.log('No prox lobby channel found. Bailing.'); - return; - } - console.log('Found', lobbyChannel ? 1 : 0, 'lobby channels'); - console.log('Found', Object.keys(proxChannels).length, 'prox channels'); - console.log('Found', Object.keys(proxMembers).length, 'prox members'); - // Ideally we want to bail early if there's no work to do, but there are some things - // like a chatroom's name switching after minutes of being rate limited that - // still need to happen in weird corner cases even with no people in the VC rooms. - //if (Object.keys(proxChannels).length === 1 && Object.keys(proxMembers).length === 0) { - // return; - //} - const draggableDiscordIds = {}; - const villageDiscordIds = {}; - // Hit the rustcult.com API to get updated player positions. - const response = await GetAllDiscordAccountsFromRustCultApi(); - if (response) { - const linkedAccounts = JSON.parse(response); - console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.com API.'); - //console.log(linkedAccounts); - for (const account of linkedAccounts) { - if (account && account.discordId) { - //if (account.discordId === '619279800783339530') { - // console.log('McLovin found:', account); - //} - if (account.steamId) { - const cu = UserCache.GetCachedUserByDiscordId(account.discordId); - if (cu) { - await cu.setSteamId(account.steamId); - await cu.setSteamName(account.steamName); - } - } - if (account.server && account.x && account.y) { - lastSeenCache[account.discordId] = account; - } - const sslm = account.secondsSinceLastMovement; - const ssbc = account.secondsSinceBreadcrumb; - if ((sslm || sslm === 0) && (ssbc || ssbc === 0)) { - if (sslm < 10 && ssbc < 30) { - draggableDiscordIds[account.discordId] = true; - } - } - if (account.howManyBasesNearby && account.howManyBasesNearby >= 10) { - villageDiscordIds[account.discordId] = true; - } - } - } - } - console.log(Object.keys(lastSeenCache).length, 'cached member locations.'); - // Make distance matrix. - function Distance(a, b) { - if (!a || !b || !a.server || !b.server || a.server !== b.server) { - // Different server or missing server = infinite distance. - return 999999; - } - const dx = a.x - b.x; - const dy = a.y - b.y; - const distance = Math.sqrt(dx * dx + dy * dy); - return distance; - } - const distanceMatrix = {}; - for (const i in proxMembers) { - const a = lastSeenCache[i]; - distanceMatrix[i] = {}; - for (const j in proxMembers) { - const b = lastSeenCache[j]; - distanceMatrix[i][j] = Distance(a, b); - } - } - console.log('distanceMatrix', distanceMatrix); - // Distance between clusters of Discord IDs. - function ClusterDistance(a, b) { - let minDist = null; - for (const i of a) { - for (const j of b) { - const d = distanceMatrix[i][j]; - if (minDist === null || d < minDist) { - minDist = d; - } - } - } - return minDist; - } - // Cluster diameter. ie: max distance between two points. - function ClusterDiameter(c) { - let maxDist = null; - const n = c.length; - for (let i = 0; i < n; i++) { - for (let j = i + 1; j < n; j++) { - const ci = c[i]; - const cj = c[j]; - const d = distanceMatrix[ci][cj]; - if (maxDist === null || d > maxDist) { - maxDist = d; - } - } - } - return maxDist; - } - // Diameter of 2 clusters combined. - function TwoClusterDiameter(a, b) { - const c = a.concat(b); - return ClusterDiameter(c); - } - // Initialize clusters. Start with n clusters: one per member in proximity VC. - const clusters = []; - for (const discordId in proxMembers) { - clusters.push([discordId]); - } - // Merge clusters until no longer possible. - while (true) { - const n = clusters.length; - let bestI; - let bestJ; - let bestDistance = null; - for (let i = 0; i < n; i++) { - for (let j = i + 1; j < n; j++) { - const a = clusters[i]; - const b = clusters[j]; - const distance = ClusterDistance(a, b); - if (distance < 438) { - const diameter = TwoClusterDiameter(a, b); - if (diameter < 730) { - if (bestDistance === null || distance < bestDistance) { - bestDistance = distance; - bestI = i; - bestJ = j; - } - } - } - } - } - if (bestDistance === null) { - break; - } - // Combine two closest clusters. - const a = clusters[bestI]; - const b = clusters[bestJ]; - const newCluster = a.concat(b); - clusters.splice(bestJ, 1); - clusters.splice(bestI, 1); - clusters.push(newCluster); - } - // Put solos and randos together into one lobby. - const clustersWithLobby = [[]]; - for (const cluster of clusters) { - if (cluster.length > 1) { - // Cluster with 2 or more members. - clustersWithLobby.push(cluster); - } else { - // Solo cluster. Isolated player detected. Add to lobby. - const solo = cluster[0]; - clustersWithLobby[0].push(solo); - } - } - console.log('Prox clusters', clustersWithLobby); - // Create new channel(s) if needed. - // Don't delete extra channels here. Do that at the end. - while (Object.keys(proxChannels).length < clustersWithLobby.length - 1) { - const newChannel = await CreateNewVoiceChannel(guild, { name: lobbyName, userLimit: 99 }); - proxChannels[newChannel.id] = newChannel; - } - // Helper functions for generating permutations of the channel list. - function ForAllPermutationsRecursive(permuted, remaining, callback) { - const n = remaining.length; - if (n === 0) { - callback(permuted); - } - for (let i = 0; i < n; i++) { - const newPermuted = permuted.slice(); - newPermuted.push(remaining[i]); - const newRemaining = remaining.slice(); - newRemaining.splice(i, 1); - ForAllPermutationsRecursive(newPermuted, newRemaining, callback); - } - } - function ForAllPermutations(arr, callback) { - ForAllPermutationsRecursive([], arr, callback); - } - // Permute clusters to minimize number of drags. - const proxChannelsAsList = Object.values(proxChannels); - let bestPermutation; - let minFails; - let minDrags; - let bestPlan; - ForAllPermutations(proxChannelsAsList, (perm) => { - console.log('Imagining permutation'); - const plan = {}; - let failCount = 0; - for (let i = 0; i < perm.length; i++) { - const channel = perm[i]; - const discordIdsInChannel = {}; - for (const [memberId, member] of channel.members) { - discordIdsInChannel[memberId] = true; - } - const cluster = i < (clustersWithLobby.length - 1) ? clustersWithLobby[i + 1] : []; - for (const discordId of cluster) { - if (!(discordId in discordIdsInChannel)) { - if (discordId in draggableDiscordIds) { - plan[discordId] = channel.id; - } else { - failCount++; - } - } - } - } - // Do lobby calculation. - const discordIdsInLobby = {}; - for (const [memberId, member] of lobbyChannel.members) { - discordIdsInLobby[memberId] = true; - } - const lobbyCluster = clustersWithLobby[0]; - for (const discordId of lobbyCluster) { - if (!(discordId in discordIdsInLobby)) { - if (discordId in draggableDiscordIds) { - plan[discordId] = lobbyChannel.id; - } else { - failCount++; - } - } - } - const dragCount = Object.keys(plan).length; - if (!bestPermutation || - failCount < minFails || - (failCount === minFails && dragCount < minDrags)) { - minFails = failCount; - minDrags = dragCount; - bestPlan = plan; - bestPermutation = perm; - } - }); - console.log('Calculated enforcement plan requires', minDrags, 'drags and has', minFails, 'fails.'); - // Open perms for the lobby (ie: channel zero). - await SetOpenPerms(lobbyChannel); - await DiscordUtil.TryToSetChannelNameWithRateLimit(lobbyChannel, lobbyName); - // Private perms for the rest of the prox channels that are not the lobby. - for (let i = 1; i < clustersWithLobby.length; i++) { - const connect = PermissionFlagsBits.Connect; - const view = PermissionFlagsBits.ViewChannel; - const perms = [ - { id: guild.roles.everyone.id, deny: [connect, view] }, - { id: RoleID.Grunt, allow: [view] }, - { id: RoleID.Officer, allow: [view] }, - { id: RoleID.General, allow: [view] }, - { id: RoleID.Bots, allow: [view, connect] }, - ]; - const cluster = i < clustersWithLobby.length ? clustersWithLobby[i] : []; - let villagePeopleDetected = false; - for (const discordId of cluster) { - perms.push({ id: discordId, allow: [view, connect] }); - if (discordId in villageDiscordIds) { - villagePeopleDetected = true; - } - } - // Add perms for users who are not in proximity VC but who are geographically - // nearby in-game to let them know which prox VC room they can join. - for (const discordId in lastSeenCache) { - // Don't make duplicate perms for users already in prox VC rooms. - if (discordId in proxMembers) { - continue; - } - const a = lastSeenCache[discordId]; - if (!a.server || !a.x || !a.y) { - continue; - } - // Get min distance to a cluster member. - let minDist = null; - for (const c of cluster) { - const b = lastSeenCache[c]; - const d = Distance(a, b); - if (minDist === null || d < minDist) { - minDist = d; - } - } - if (!guild.members.cache.has(discordId)) { - continue; - } - // Give perms if close enough. - if (minDist !== null && minDist < 400) { - perms.push({ id: discordId, allow: [view, connect] }); - } - } - // Send the accumulated perms to the discord channel. - const channel = bestPermutation[i - 1]; - console.log('Setting perms', perms); - try { - console.log('BEGIN SET PERMS'); - // Do not await. This is rate limited so we just move on. - DiscordUtil.TryToSetChannelPermsWithRateLimit(channel, perms); - //await channel.permissionOverwrites.set(perms); - console.log('END SET PERMS'); - } catch (error) { - console.log('Error while setting perms on prox channel.'); - // Do nothing. - } - // Set the channel name. Village or Roaming. - const newChannelName = villagePeopleDetected ? 'Village' : 'Roaming'; - await DiscordUtil.TryToSetChannelNameWithRateLimit(channel, newChannelName); - } - console.log('Done setting perms'); - // Drag people who need to be dragged. - for (const discordId in bestPlan) { - const member = proxMembers[discordId]; - if (!member) { - continue; - } - const channelId = bestPlan[discordId]; - if (!channelId) { - continue; - } - const channel = proxChannels[channelId]; - if (!channel) { - continue; - } - console.log('Dragging a member'); - await member.voice.setChannel(channel); - } - // Delete an extra channel if there are any. - console.log('Thinking about deleting prox channel.', Object.keys(proxChannels).length, clustersWithLobby.length); - if (Object.keys(proxChannels).length > clustersWithLobby.length) { - console.log('Deleting leftover Prox channel.'); - const channelToDelete = bestPermutation[bestPermutation.length - 1]; - await channelToDelete.delete(); - } -} - async function UpdateSteamAccountInfo() { // Hit the rustcult.com API to get updated player positions. const response = await GetAllDiscordAccountsFromRustCultApi(); @@ -852,16 +494,17 @@ async function UpdateSteamAccountInfo() { if (!account.steamId) { return; } - //if (account.discordId === '619279800783339530') { - // console.log('McLovin found:', account); - //} - //console.log(account); + if (account.discordId === '294544723518029824') { + console.log('crudeoil found!!!'); + } const cu = UserCache.GetCachedUserByDiscordId(account.discordId); if (!cu) { - return; + console.log('Discord ID not found ' + account.discordId); + continue; } await cu.setSteamId(account.steamId); await cu.setSteamName(account.steamName); + console.log('Linked account ' + cu.getNicknameOrTitleWithInsignia() + ' ' + account.discordId + ' ' + account.steamId); } } diff --git a/rank-definitions.js b/rank-definitions.js index cd16b51..2877324 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -194,7 +194,6 @@ module.exports = [ { color: '#189b17', count: 1000 * 1000, - insignia: null, roles: [RoleID.Recruit], title: 'Recruit', }, From c6d9032db7cd7dd863e4fcdaad6a75249578c7d8 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 28 Jul 2024 15:19:33 +0000 Subject: [PATCH 075/101] Various maintenance changes. --- bot-commands.js | 20 ++++++++++++++++++-- commissar-user.js | 22 +++++++++++++++++++++- setup-database.sql | 2 ++ user-cache.js | 3 +++ 4 files changed, 44 insertions(+), 3 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 543afc7..5c16471 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -79,7 +79,7 @@ async function HandleServerVoteCommand(discordMessage) { } const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); - const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for June 2024.'); + const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for August 2024.'); await message.react('❤️'); await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 11); @@ -112,7 +112,7 @@ async function HandlePresidentVoteCommand(discordMessage) { name: 'presidential-election', type: 0, }); - const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in July 2024. This vote ends .'); + const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in August 2024.'); await message.react('❤️'); const generalRankUsers = await UserCache.GetMostCentralUsers(159); const candidateNames = []; @@ -918,6 +918,20 @@ async function HandleKickCommand(discordMessage) { await discordMessage.channel.send(`${mentionedMember.nickname} is kicked out of microcommunity ${mcName} for 60 seconds`); } +async function HandleBuyCommand(discordMessage) { + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } +} + +async function HandleSellCommand(discordMessage) { + const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); + if (!author) { + return; + } +} + // Handle any unrecognized commands, possibly replying with an error message. async function HandleUnknownCommand(discordMessage) { // TODO: add permission checks. Only high enough ranks should get a error @@ -939,6 +953,7 @@ async function Dispatch(discordMessage) { '!balance': yen.HandleYenCommand, '!ban': Ban.HandleBanCommand, '!boop': HandleBoopCommand, + '!buy': HandleBuyCommand, '!code': HandleCodeCommand, '!committee': HandleCommitteeCommand, '!convert': yen.HandleConvertCommand, @@ -965,6 +980,7 @@ async function Dispatch(discordMessage) { '!presidentvote': HandlePresidentVoteCommand, '!presidentvotefix': HandlePresidentVoteFixCommand, '!privateroomvote': HandlePrivateRoomVoteCommand, + '!sell': HandleSellCommand, '!tax': yen.HandleTaxCommand, '!termlengthvote': HandleTermLengthVoteCommand, '!tip': yen.HandleTipCommand, diff --git a/commissar-user.js b/commissar-user.js index 2b2f2e3..c841027 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -34,7 +34,9 @@ class CommissarUser { presidential_election_vote, presidential_election_message_id, steam_id, - steam_name) { + steam_name, + trump_cards, + cost_basis) { this.commissar_id = commissar_id; this.discord_id = discord_id; this.nickname = nickname; @@ -65,6 +67,8 @@ class CommissarUser { this.steam_id = steam_id; this.steam_name = steam_name; this.steam_name_update_time = null; + this.trump_cards = trump_cards; + this.cost_basis = cost_basis; } async setDiscordId(discord_id) { @@ -313,6 +317,22 @@ class CommissarUser { await this.updateFieldInDatabase('steam_name_update_time', t); } + async setTrumpCards(trump_cards) { + if (trump_cards === this.trump_cards) { + return; + } + this.trump_cards = trump_cards; + await this.updateFieldInDatabase('trump_cards', this.trump_cards); + } + + async setCostBasis(cost_basis) { + if (cost_basis === this.cost_basis) { + return; + } + this.cost_basis = cost_basis; + await this.updateFieldInDatabase('cost_basis', this.cost_basis); + } + async updateFieldInDatabase(fieldName, fieldValue) { //console.log(`DB update ${fieldName} = ${fieldValue} for ${this.nickname} (ID:${this.commissar_id}).`); const sql = `UPDATE users SET ${fieldName} = ? WHERE commissar_id = ?`; diff --git a/setup-database.sql b/setup-database.sql index 8d70d16..7db56f8 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -39,6 +39,8 @@ CREATE TABLE users ban_pardon_time TIMESTAMP, -- When this user was convicted & banned in ban court. presidential_election_vote INT, -- The commissar_id that this user is voting for in the presidential election. NULL if has not voted. presidential_election_message_id VARCHAR(32), -- ID of the discord message used to display this user on the presidential election ballot. + trump_cards INT NOT NULL DEFAULT 0, -- How many Trump Cards owned by this member. Can be negative. + cost_basis INT NOT NULL DEFAULT 0, -- How many yen spend on Trump Cards. Used to calculate profit/loss. PRIMARY KEY (commissar_id), INDEX discord_index (discord_id) ); diff --git a/user-cache.js b/user-cache.js index 40959f6..e3b521a 100644 --- a/user-cache.js +++ b/user-cache.js @@ -44,6 +44,8 @@ async function LoadAllUsersFromDatabase() { row.steam_id, row.steam_name, row.steam_name_update_time, + row.trump_cards, + row.cost_basis, ); newCache[row.commissar_id] = newUser; }); @@ -160,6 +162,7 @@ async function CreateNewDatabaseUser(discordMember) { 0, null, null, null, null, null, null, null, null, + 0, 0, ); commissarUserCache[commissar_id] = newUser; return newUser; From 95d15c4fac29cb1b0bbab84a71185bd056157fe2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 28 Jul 2024 16:22:22 +0000 Subject: [PATCH 076/101] Simplify ranks down to the old HC formula. --- chain-of-command.js | 558 +++----------------------------------------- 1 file changed, 26 insertions(+), 532 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index eef7a81..a7ad1f9 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -1,260 +1,47 @@ -const { createCanvas } = require('canvas'); const db = require('./database'); const { PermissionFlagsBits } = require('discord.js'); const DiscordUtil = require('./discord-util'); -const exile = require('./exile-cache'); const fs = require('fs'); -const kruskal = require('kruskal-mst'); const moment = require('moment'); const RankMetadata = require('./rank-definitions'); const RoleID = require('./role-id'); const Sleep = require('./sleep'); const UserCache = require('./user-cache'); -const recentlyActiveSteamIds = {}; -let channelPermsModifiedRecently = {}; - -setInterval(() => { - // Clear the perms modified flags every few minutes, enabling them - // to be modified again. - channelPermsModifiedRecently = {}; -}, 9 * 60 * 1000); - async function CalculateChainOfCommand() { console.log('Chain of command'); - // Load recently active steam IDs from a file. - ReadSteamAccountsFromFile('recently-active-steam-ids-march-2024.csv'); - // Initialize the social graph made up up vertices (people) and edges (relationships). - const vertices = {}; - const edges = {}; // Populate vertex data from discord. - const discordVertices = UserCache.GetAllUsersAsFlatList(); - let minHC = null; - let maxHC = null; - let sumHC = 0; - for (const v of discordVertices) { - if (v.ban_conviction_time && v.ban_pardon_time) { - // Exclude banned members from the ranks until they do their time. - continue; - } - const i = v.steam_id || v.discord_id || v.commissar_id; - const hc = v.citizen ? v.harmonic_centrality : 0; - vertices[i] = { - commissar_id: v.commissar_id, - discord_id: v.discord_id, - harmonic_centrality: v.harmonic_centrality, - steam_id: v.steam_id, - vertex_id: i, - }; - if (minHC === null || v.harmonic_centrality < minHC) { - minHC = v.harmonic_centrality; - } - if (maxHC === null || v.harmonic_centrality > maxHC) { - maxHC = v.harmonic_centrality; - } - sumHC += v.harmonic_centrality; - } - console.log('Harmonic centrality summary stats'); - console.log('#', discordVertices.length); - console.log('min', minHC); - console.log('max', maxHC); - console.log('sum', sumHC); - console.log('max%', 100 * maxHC / sumHC, '%'); - console.log('mean', sumHC / discordVertices.length); - console.log(Object.keys(vertices).length, 'combined vertices'); - // Populate edge data from discord. - const discordEdges = await db.GetTimeMatrix(); - let minDiscord = null; - let maxDiscord = null; - let sumDiscord = 0; - for (const e of discordEdges) { - const loUser = UserCache.GetCachedUserByCommissarId(e.lo_user_id); - const hiUser = UserCache.GetCachedUserByCommissarId(e.hi_user_id); - const loid = loUser.steam_id || loUser.discord_id || loUser.commissar_id; - const hiid = hiUser.steam_id || hiUser.discord_id || hiUser.commissar_id; - const a = loid < hiid ? loid : hiid; - const b = loid < hiid ? hiid : loid; - if (!(a in vertices)) { - continue; - } - if (!(b in vertices)) { - continue; - } - if (!(a in edges)) { - edges[a] = {}; - } - edges[a][b] = { - discord_coplay_time: e.discounted_diluted_seconds, - }; - if (minDiscord === null || e.discounted_diluted_seconds < minDiscord) { - minDiscord = e.discounted_diluted_seconds; - } - if (maxDiscord === null || e.discounted_diluted_seconds > maxDiscord) { - maxDiscord = e.discounted_diluted_seconds; - } - sumDiscord += e.discounted_diluted_seconds; - } - console.log('Discord coplay time summary stats'); - console.log('#', discordEdges.length); - console.log('min', minDiscord); - console.log('max', maxDiscord); - console.log('sum', sumDiscord); - console.log('max%', 100 * maxDiscord / sumDiscord, '%'); - console.log('mean', sumDiscord / discordEdges.length); - console.log(Object.keys(vertices).length, 'combined vertices'); - // Populate vertex data from Rust. - let minActivity = null; - let maxActivity = null; - let sumActivity = 0; - const rustVertexLines = ReadLinesFromCsvFile('in-game-activity-points-march-2024.csv'); - for (const line of rustVertexLines) { - if (line.length !== 4) { - continue; - } - const i = line[0]; - if (!(i in recentlyActiveSteamIds)) { - continue; - } - const cu = UserCache.GetCachedUserBySteamId(i); - if (cu) { - if (cu.ban_conviction_time && cu.ban_pardon_time) { - // Exclude known banned users from the graph until they serve their time. - // This will still let through the steam accounts of non-linked users - // banned from discord but nothing much can be done about that automatically. - // The solution in that case is to manually link the person's account for them. - continue; - } - } - const activity = parseFloat(line[1]); - if (!(i in vertices)) { - vertices[i] = {}; - } - vertices[i].in_game_activity = activity; - vertices[i].steam_id = i; - vertices[i].vertex_id = i; - vertices[i].distinct_date_count = parseInt(line[2]); - vertices[i].distinct_month_count = parseInt(line[3]); - if (minActivity === null || activity < minActivity) { - minActivity = activity; - } - if (maxActivity === null || activity > maxActivity) { - maxActivity = activity; - } - sumActivity += activity; - } - console.log('Rust in-game activity summary stats'); - console.log('#', rustVertexLines.length); - console.log('min', minActivity); - console.log('max', maxActivity); - console.log('sum', sumActivity); - console.log('max%', 100 * maxActivity / sumActivity, '%'); - console.log('mean', sumActivity / rustVertexLines.length); - console.log(Object.keys(vertices).length, 'combined vertices'); - // Populate edge data from Rust. - let minRust = null; - let maxRust = null; - let sumRust = 0; - const rustEdgeLines = ReadLinesFromCsvFile('in-game-relationships-march-2024.csv'); - for (const line of rustEdgeLines) { - if (line.length !== 3) { - continue; - } - const i = line[0]; - const j = line[1]; - const t = parseFloat(line[2]); - const a = i < j ? i : j; - const b = i < j ? j : i; - if (!(a in vertices)) { - continue; - } - if (!(b in vertices)) { - continue; + const members = UserCache.GetAllUsersAsFlatList(); + members.sort((a, b) => { + let aScore = a.harmonic_centrality || 0; + let bScore = b.harmonic_centrality || 0; + if (a.ban_conviction_time && a.ban_pardon_time) { + aScore = 0; } - if (!(a in edges)) { - edges[a] = {}; - } - if (!(b in edges[a])) { - edges[a][b] = {}; - } - edges[a][b].rust_coplay_time = t; - if (minRust === null || t < minRust) { - minRust = t; - } - if (maxRust === null || t > maxRust) { - maxRust = t; - } - sumRust += t; - } - console.log('Rust in-game relationships summary stats'); - console.log('#', rustEdgeLines.length); - console.log('min', minRust); - console.log('max', maxRust); - console.log('sum', sumRust); - console.log('max%', 100 * maxRust / sumRust, '%'); - console.log('mean', sumRust / rustEdgeLines.length); - console.log(Object.keys(vertices).length, 'combined vertices'); - console.log(Object.keys(edges).length, 'edge buckets'); - // Helper function that calculates the "new guy" demotion. This - // stops brand new members from power-leveling too quickly no - // matter their relationships and activity level. - function CalculateNewGuyDemotion(distinctDateCount, distinctMonthCount) { - const d = distinctDateCount || 1; - const m = distinctMonthCount || 1; - const newGuyDays = 60; - const newGuyMonths = 6; - const intercept = 0.3; - const slope = 1 - intercept; - const dayDemotion = Math.min(d / newGuyDays, 1) * slope + intercept; - const monthDemotion = Math.min(m / newGuyMonths, 1) * slope + intercept; - const totalDemotion = dayDemotion * monthDemotion; - return totalDemotion; - } - // Calculate final vertex weights as a weighted combination of - // vertex features from multiple sources. - for (const i in vertices) { - const v = vertices[i]; - const hc = v.harmonic_centrality || 0; - const iga = v.in_game_activity || 0; - v.cross_platform_activity = 0.8 * hc + 0.2 * iga; - const newGuyDemotion = CalculateNewGuyDemotion(v.distinct_date_count, v.distinct_month_count); - const cid = v.commissar_id || 9999999; - const joinOrderBonus = 3600 / cid; - v.rank_score = newGuyDemotion * v.cross_platform_activity + joinOrderBonus; - } - // Sort the vertices by score. - const verticesSortedByScore = []; - for (const i in vertices) { - const v = vertices[i]; - verticesSortedByScore.push(v); - } - verticesSortedByScore.sort((a, b) => { - if (!a.rank_score && !b.rank_score) { - return 0; + if (b.ban_conviction_time && b.ban_pardon_time) { + bScore = 0; } - if (!a.rank_score) { - return 1; + if (!a.citizen) { + aScore = 0; } - if (!b.rank_score) { - return -1; + if (!b.citizen) { + bScore = 0; } - return b.rank_score - a.rank_score; + return bScore - aScore; }); - console.log('Top ranked vertex:', verticesSortedByScore[0]); + // TODO: bring back the new guy demotion with daily and monthly activity counters + // linked to discord activity instead of Rust+ activity. // Assign discrete ranks to each player. let rank = 0; let usersAtRank = 0; let rankIndex = 1; const recruitRank = RankMetadata.length - 1; - console.log('verticesSortedByScore.length', verticesSortedByScore.length); - for (const v of verticesSortedByScore) { - const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - if (!cu) { - continue; - } - if (!cu.citizen) { - await cu.setRank(recruitRank); - await cu.setRankScore(0); - await cu.setRankIndex(9999999); + console.log('members.length', members.length); + for (const m of members) { + if (!m.citizen) { + await m.setRank(recruitRank); + await m.setRankScore(0); + await m.setRankIndex(999); continue; } while (usersAtRank >= RankMetadata[rank].count) { @@ -264,274 +51,15 @@ async function CalculateChainOfCommand() { // When we run out of ranks, this line defaults to the last/least rank. rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); // Write the rank to the vertex record. - v.rank = rank; + m.rank = rank; // Do not await the promotion announcement. Fire and forget. - AnnounceIfPromotion(cu, cu.rank, rank); - await cu.setRank(rank); - await cu.setRankScore(v.rank_score); - await cu.setRankIndex(rankIndex); + AnnounceIfPromotion(m, m.rank, rank); + await m.setRank(rank); + await m.setRankScore(m.harmonic_centrality); + await m.setRankIndex(rankIndex); rankIndex++; usersAtRank++; } - // Initialize each vertex's friend badges to empty. - for (const v of verticesSortedByScore) { - v.badges = {}; - } - // Make sure the top leaders all have their own leader role and VC. If any - // are missing, create them. - console.log('Create and update friend role and rooms for top leaders'); - const guild = await DiscordUtil.GetMainDiscordGuild(); - const allFriendRoles = {}; - for (const v of verticesSortedByScore) { - const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - if (!cu) { - continue; - } - if (cu.rank > 15) { - // Higher rank index means a lower rank. 15 is General 1. - continue; - } - const name = cu.getNicknameOrTitleWithInsignia(); - const rankData = RankMetadata[cu.rank]; - const color = rankData.color; - if (cu.friend_role_id) { - try { - v.friendRole = await guild.roles.fetch(cu.friend_role_id); - } catch (error) { - console.log('Failed to fetch friend role for', name); - console.log(error); - continue; - } - } else { - try { - v.friendRole = await guild.roles.create({ name, color }); - await cu.setFriendRoleId(v.friendRole.id); - } catch (error) { - console.log('Failed to create a friend role for', name); - console.log(error); - continue; - } - } - if (!v.friendRole) { - console.log('No valid friend role or failed to create.'); - continue; - } - allFriendRoles[v.friendRole.id] = v.friendRole; - v.badges[v.friendRole.id] = v.friendRole; - if (v.friendRole.name !== name) { - console.log('Updating role name', v.friendRole.name, 'to', name); - await v.friendRole.setName(name); - } - const decimalColorCode = Number('0x' + color.replace('#', '')); - if (v.friendRole.color !== color && v.friendRole.color !== decimalColorCode) { - console.log('Updating role color for', v.friendRole.name, 'from', v.friendRole.color, 'to', color); - await v.friendRole.setColor(color); - } - const connect = PermissionFlagsBits.Connect; - const view = PermissionFlagsBits.ViewChannel; - const send = PermissionFlagsBits.SendMessages; - if (cu.friend_voice_room_id) { - try { - v.friendRoom = await guild.channels.fetch(cu.friend_voice_room_id); - } catch (error) { - console.log('Failed to fetch friend room for', name); - console.log(error); - continue; - } - } else { - try { - v.friendRoom = await guild.channels.create({ - bitrate: 256000, - name, - permissionOverwrites: [ - { id: guild.roles.everyone, deny: [connect, send, view] }, - { id: v.friendRole.id, allow: [connect, send, view] }, - { id: RoleID.Bots, allow: [connect, send, view] }, - ], - type: 2, - userLimit: 99, - }); - await cu.setFriendVoiceRoomId(v.friendRoom.id); - } catch (error) { - console.log('Failed to create friend room for', name); - console.log(error); - continue; - } - } - if (!v.friendRoom) { - console.log('No valid friend room or failed to create.'); - continue; - } - if (v.friendRoom.name !== name) { - console.log('Updating room name', v.friendRoom.name, 'to', name); - await v.friendRoom.setName(name); - } - // Hide room from most members while empty. - if (!(v.friendRoom.id in channelPermsModifiedRecently)) { - if (v.friendRoom.members.size === 0) { - if (v.friendRoom.permissionOverwrites.cache.has(RoleID.Grunt)) { - console.log('Hide room', v.friendRoom.name); - channelPermsModifiedRecently[v.friendRoom.id] = true; - await v.friendRoom.permissionOverwrites.set([ - { id: guild.roles.everyone, deny: [connect, view] }, - { id: v.friendRole.id, allow: [connect, view] }, - { id: RoleID.Bots, allow: [connect, view] }, - ]); - } - } else { - if (!v.friendRoom.permissionOverwrites.cache.has(RoleID.Grunt)) { - console.log('Reveal room', v.friendRoom.name); - channelPermsModifiedRecently[v.friendRoom.id] = true; - await v.friendRoom.permissionOverwrites.set([ - { id: guild.roles.everyone, deny: [connect, view] }, - { id: RoleID.Grunt, allow: [view] }, - { id: RoleID.Officer, allow: [view] }, - { id: RoleID.General, allow: [view] }, - { id: RoleID.Commander, allow: [view] }, - { id: v.friendRole.id, allow: [connect, view] }, - { id: RoleID.Bots, allow: [connect, view] }, - ]); - } - } - } - } - // Decide which people are friends with which others. - console.log('Traversing edges to detect friends'); - let edgeCount = 0; - let friendCount = 0; - for (const i in edges) { - for (const j in edges[i]) { - edgeCount++; - const e = edges[i][j]; - const d = e.discord_coplay_time || 0; - const r = e.rust_coplay_time || 0; - const t = 0.04 * d + r; - if (t > 300) { - friendCount++; - const a = vertices[i]; - const b = vertices[j]; - if (b.friendRole) { - a.badges[b.friendRole.id] = b.friendRole; - } - if (a.friendRole) { - b.badges[a.friendRole.id] = a.friendRole; - } - } - } - } - console.log(edgeCount, 'edges traversed'); - console.log(friendCount, 'friends detected'); - // Enforce exiles by taking away exiled badges. - const exiles = exile.GetAllExilesAsList(); - for (const ex of exiles) { - const exiler = UserCache.GetCachedUserByCommissarId(ex.exiler); - if (!exiler) { - continue; - } - const exilerVertexId = exiler.getSocialGraphVertexId(); - if (!exilerVertexId) { - continue; - } - const exilerVertex = vertices[exilerVertexId]; - if (!exilerVertex) { - continue; - } - const exilee = UserCache.GetCachedUserByCommissarId(ex.exilee); - if (!exilee) { - continue; - } - const exileeVertexId = exilee.getSocialGraphVertexId(); - const exileeVertex = vertices[exileeVertexId]; - if (!exileeVertex) { - continue; - } - if (ex.is_friend) { - exileeVertex.badges[exiler.friend_role_id] = exilerVertex.friendRole; - } else { - if (exiler.friend_role_id in exileeVertex.badges) { - delete exileeVertex.badges[exiler.friend_role_id]; - } - } - } - // Add and remove friend badges. - console.log('Adding and removing friend badges'); - for (const v of verticesSortedByScore) { - const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - if (!cu) { - continue; - } - if (!cu.discord_id || !cu.citizen || !cu.good_standing) { - continue; - } - let discordMember; - try { - discordMember = await guild.members.fetch(cu.discord_id); - } catch (error) { - // Discord member probably left the discord. Ignore. - continue; - } - const currentRoles = await discordMember.roles.cache; - const rolesToRemove = {}; - const rolesBefore = {}; - for (const [roleId, role] of currentRoles) { - rolesBefore[roleId] = role; - if ((roleId in allFriendRoles) && !(roleId in v.badges)) { - rolesToRemove[roleId] = role; - } - } - for (const roleId in rolesToRemove) { - const badge = rolesToRemove[roleId]; - console.log('Remove role', badge.name, 'from', discordMember.nickname); - await discordMember.roles.remove(badge); - } - for (const roleId in v.badges) { - if (!roleId) { - continue; - } - if (roleId in rolesBefore) { - continue; - } - const badge = v.badges[roleId]; - if (!badge) { - continue; - } - await discordMember.roles.add(badge); - } - } - // Clean up & destroy any friend roles & rooms of downranked leaders. - console.log('Clean up disused friend roles and rooms'); - for (const v of verticesSortedByScore) { - const cu = UserCache.TryToFindUserGivenAnyKnownId(v.vertex_id); - if (!cu) { - continue; - } - if (cu.rank <= 15) { - // Skip Generals. - continue; - } - if (cu.friend_role_id) { - try { - const friendRole = await guild.roles.fetch(cu.friend_role_id); - await friendRole.delete(); - await cu.setFriendRoleId(null); - } catch (error) { - console.log('Failed to delete friend role for', name); - console.log(error); - continue; - } - } - if (cu.friend_voice_room_id) { - try { - const friendRoom = await guild.channels.fetch(cu.friend_voice_room_id); - await friendRoom.delete(); - await cu.setFriendVoiceRoomId(null); - } catch (error) { - console.log('Failed to delete friend room for', name); - console.log(error); - continue; - } - } - } } // A temporary in-memory cache of the highest rank seen per user. @@ -588,40 +116,6 @@ async function AnnounceIfPromotion(user, oldRank, newRank) { await DiscordUtil.MessagePublicChatChannel(message); } -// Helper function that reads and parses a CSV file into memory. -// Only use for small files. This function is memory inefficient. -// Returns an array of arrays. -function ReadLinesFromCsvFile(filename) { - const fileContents = fs.readFileSync(filename).toString(); - const lines = fileContents.split('\n'); - const tokenizedLines = []; - for (const line of lines) { - const tokens = line.split(','); - tokenizedLines.push(tokens); - } - return tokenizedLines; -} - -// Helper function that reads and parses a CSV file into memory. -// This is only for a particular file that contains steam IDs and -// steam names. The reason why the regular CSV parser is no good -// for this situation is because sometimes steam names contain commas. -// This parser is specialized for the special case of 2 columns with -// ids and names so it is not fooled by commas in steam names. -function ReadSteamAccountsFromFile(filename) { - const fileContents = fs.readFileSync(filename).toString(); - const lines = fileContents.split('\n'); - for (const line of lines) { - const commaIndex = line.indexOf(','); - if (commaIndex < 0) { - continue; - } - const steamId = line.substring(0, commaIndex); - const steamName = line.substring(commaIndex + 1); - recentlyActiveSteamIds[steamId] = steamName; - } -} - module.exports = { CalculateChainOfCommand, }; From 1a42cdf1c55ea470f948dc9c70948393b29a1c6a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 28 Jul 2024 16:22:40 +0000 Subject: [PATCH 077/101] Delete friend related commands and old unused commands. --- bot-commands.js | 242 ------------------------------------------------ 1 file changed, 242 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 5c16471..e0e48cf 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -22,14 +22,6 @@ async function HandlePingCommand(discordMessage) { await discordMessage.channel.send('Pong!'); } -// A cheap live test harness to test the code that finds the main chat channel. -// This lets me test it anytime I'm worried it's broken. -async function HandlePingPublicChatCommand(discordMessage) { - // TODO: add permissions so only high ranking people can use - // this command. - await DiscordUtil.MessagePublicChatChannel('Pong!'); -} - // A message that starts with !code. async function HandleCodeCommand(discordMessage) { const discordId = discordMessage.author.id; @@ -128,22 +120,6 @@ async function HandlePresidentVoteCommand(discordMessage) { } } -async function HandlePresidentVoteFixCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author || author.commissar_id !== 7) { - // Auth: this command for developer use only. - return; - } - const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.resolve('1100498416162844772'); - const candidates = [ - 'EviL', - ]; - for (const candidate of candidates) { - await MakeOnePresidentVoteOption(channel, candidate); - } -} - async function HandleHypeCommand(discordMessage) { const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author || author.commissar_id !== 7) { @@ -259,27 +235,6 @@ async function HandleAmnestyCommand(discordMessage) { await discordMessage.channel.send(`These ${total} bans have been pardoned by order of the Generals`); } -async function HandleTermLengthVoteCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author || author.commissar_id !== 7) { - // Auth: this command for developer use only. - return; - } - const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.create('president-term'); - const message = await channel.send( - '__**Presidential Term of Service**__\n' + - 'Vote YES to turn Mr. President back into a General at the end of each wipe day. The main reason for Mr. President to exist is to pick the build spot in case the map is a surprise on wipe day. With their job done, the first rain (2 AM on wipe night) will cleanse Mr. President of their title.\n\n' + - 'Vote NO to keep Mr. President for the full month, until the beginning of the next presidential election.\n\n' + - 'The convention that the Generals choose now will be the one used going forward. It will apply to next month and the one after that, not only this month. We will not have this vote again next month.\n\n' + - 'This vote will end with the first rain of the new wipe (2 AM Eastern after wipe day). It requires a simple majority to pass (50% + 1).' - ); - await message.react('✅'); - await message.react('❌'); - const voteSectionId = '1043778293612163133'; - await channel.setParent(voteSectionId); -} - async function HandleVoiceActiveUsersCommand(discordMessage) { const tokens = discordMessage.content.split(' '); if (tokens.length != 2) { @@ -732,192 +687,6 @@ async function HandleAfkCommand(discordMessage) { } } -async function HandleFriendCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author) { - return; - } - if (!author.friend_voice_room_id) { - // Auth: this command for leaders with their own voice room only. - await discordMessage.channel.send('!friend is for microcommunity leaders'); - return; - } - const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); - if (!mentionedMember) { - await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); - return; - } - const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); - if (!mentionedUser) { - await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); - return; - } - const exiler = author.commissar_id; - const exilee = mentionedUser.commissar_id; - const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); - const mcName = author.getNicknameOrTitleWithInsignia(); - if (exile.IsFriend(exiler, exilee)) { - await discordMessage.channel.send(`${exileeName} is already a friend of ${mcName}`); - return; - } - // Add friend record to the database to make it persistent. - await exile.SetIsFriend(exiler, exilee, true); - // Give the microcommunity badge at once to avoid waiting for the next rank cycle. - if (!mentionedMember.roles.cache.has(author.friend_role_id)) { - await mentionedMember.roles.add(author.friend_role_id); - } - await discordMessage.channel.send(`${exileeName} is invited to microcommunity ${mcName}`); -} - -async function HandleUnfriendCommand(discordMessage) { - await HandleExileCommand(discordMessage); -} - -async function HandleExileCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author) { - return; - } - if (!author.friend_voice_room_id) { - // Auth: this command for leaders with their own voice room only. - await discordMessage.channel.send('!exile is for microcommunity leaders'); - return; - } - const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); - if (!mentionedMember) { - await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); - return; - } - const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); - if (!mentionedUser) { - await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); - return; - } - const exiler = author.commissar_id; - const exilee = mentionedUser.commissar_id; - const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); - const mcName = author.getNicknameOrTitleWithInsignia(); - if (exile.IsExiled(exiler, exilee)) { - await discordMessage.channel.send(`${exileeName} is already exiled from microcommunity ${mcName}`); - return; - } - // Add exile record to the database to make it persistent. - await exile.SetIsFriend(exiler, exilee, false); - // Revoke the microcommunity badge at once to avoid waiting for the next rank cycle. - if (mentionedMember.roles.cache.has(author.friend_role_id)) { - await mentionedMember.roles.remove(author.friend_role_id); - } - await discordMessage.channel.send(`${exileeName} has been exiled from microcommunity ${mcName}`); - const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.fetch(author.friend_voice_room_id); - const mentionedMemberIsInChannel = channel.members.has(mentionedMember.id); - if (!mentionedMemberIsInChannel) { - // No need to remove member from channel. All set. - return; - } - // If we get here, it means the mentioned member is eligible to be kicked - // and the author has the right to kick them. Try to move them to the - // fullest Main channel. - let fullestMainChannel; - let maxPop = -1; - for (const [id, c] of guild.channels.cache) { - if (c.type === 2 && !c.parent && c.name === 'Main') { - if (c.members.size > maxPop) { - maxPop = c.members.size; - fullestMainChannel = c; - } - } - } - if (fullestMainChannel) { - // Move to fullest Main channel. - await mentionedMember.voice.setChannel(fullestMainChannel); - } else { - // In case no Main channels are found or other strange circumstance - // kick the member from the channel without moving them elsewhere. - await mentionedMember.voice.disconnect(); - } -} - -async function HandleUnexileCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author) { - return; - } - if (!author.friend_voice_room_id) { - // Auth: this command for leaders with their own voice room only. - await discordMessage.channel.send('!unexile is for microcommunity leaders'); - return; - } - const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); - if (!mentionedMember) { - await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); - return; - } - const mentionedUser = await UserCache.GetCachedUserByDiscordId(mentionedMember.id); - if (!mentionedUser) { - await discordMessage.channel.send('Not sure who you mean. Try again in a few minutes.'); - return; - } - const exiler = author.commissar_id; - const exilee = mentionedUser.commissar_id; - const exileeName = mentionedUser.getNicknameOrTitleWithInsignia(); - const mcName = author.getNicknameOrTitleWithInsignia(); - if (!exile.IsExiled(exiler, exilee)) { - await discordMessage.channel.send(`${exileeName} is not exiled from microcommunity ${mcName}`); - return; - } - // Delete exile record from the database to make it persistent. - await exile.Unexile(exiler, exilee); - await discordMessage.channel.send(`${exileeName} has been unexiled from microcommunity ${mcName}`); -} - -async function HandleKickCommand(discordMessage) { - const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); - if (!author) { - return; - } - if (!author.friend_voice_room_id) { - // Auth: this command for leaders with their own voice room only. - await discordMessage.channel.send('!kick is for microcommunity leaders'); - return; - } - const mentionedMember = await DiscordUtil.ParseExactlyOneMentionedDiscordMember(discordMessage); - if (!mentionedMember) { - await discordMessage.channel.send('Not sure who you mean. Try again without any extra spaces.'); - return; - } - const guild = await DiscordUtil.GetMainDiscordGuild(); - const channel = await guild.channels.fetch(author.friend_voice_room_id); - const mentionedMemberIsInChannel = channel.members.has(mentionedMember.id); - if (!mentionedMemberIsInChannel) { - await discordMessage.channel.send('!kick only works in your own microcommunity'); - return; - } - // If we get here, it means the mentioned member is eligible to be kicked - // and the author has the right to kick them. Try to move them to the - // fullest Main channel. - let fullestMainChannel; - let maxPop = -1; - for (const [id, c] of guild.channels.cache) { - if (c.type === 2 && !c.parent && c.name === 'Main') { - if (c.members.size > maxPop) { - maxPop = c.members.size; - fullestMainChannel = c; - } - } - } - if (fullestMainChannel) { - // Move to fullest Main channel. - await mentionedMember.voice.setChannel(fullestMainChannel); - } else { - // In case no Main channels are found or other strange circumstance - // kick the member from the channel without moving them elsewhere. - await mentionedMember.voice.disconnect(); - } - const mcName = author.getNicknameOrTitleWithInsignia(); - await discordMessage.channel.send(`${mentionedMember.nickname} is kicked out of microcommunity ${mcName} for 60 seconds`); -} - async function HandleBuyCommand(discordMessage) { const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author) { @@ -946,7 +715,6 @@ async function Dispatch(discordMessage) { const handlers = { '!afk': HandleAfkCommand, '!amnesty': HandleAmnestyCommand, - '!art': Artillery, '!artillery': Artillery, '!badge': HandleBadgeCommand, '!bal': yen.HandleYenCommand, @@ -956,15 +724,10 @@ async function Dispatch(discordMessage) { '!buy': HandleBuyCommand, '!code': HandleCodeCommand, '!committee': HandleCommitteeCommand, - '!convert': yen.HandleConvertCommand, '!convict': Ban.HandleConvictCommand, - '!exile': HandleExileCommand, - '!friend': HandleFriendCommand, '!gender': HandleGenderCommand, - '!howhigh': Artillery, '!hype': HandleHypeCommand, '!impeach': HandleImpeachCommand, - '!kick': HandleKickCommand, '!prez': HandlePrezCommand, '!veep': HandleVeepCommand, '!lottery': yen.DoLottery, @@ -975,18 +738,13 @@ async function Dispatch(discordMessage) { '!pardon': Ban.HandlePardonCommand, '!pay': yen.HandlePayCommand, '!ping': HandlePingCommand, - '!pingpublic': HandlePingPublicChatCommand, '!servervote': HandleServerVoteCommand, '!presidentvote': HandlePresidentVoteCommand, - '!presidentvotefix': HandlePresidentVoteFixCommand, '!privateroomvote': HandlePrivateRoomVoteCommand, '!sell': HandleSellCommand, '!tax': yen.HandleTaxCommand, - '!termlengthvote': HandleTermLengthVoteCommand, '!tip': yen.HandleTipCommand, '!transcript': HandleTranscriptCommand, - '!unexile': HandleUnexileCommand, - '!unfriend': HandleUnfriendCommand, '!voiceactiveusers': HandleVoiceActiveUsersCommand, '!yen': yen.HandleYenCommand, '!yencreate': yen.HandleYenCreateCommand, From fb11285a097aa5665ac505e2888e915df0c63ddc Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 28 Jul 2024 16:53:56 +0000 Subject: [PATCH 078/101] Removed all connections with Steam and rustcult to simplify the code. --- bot-commands.js | 17 ++- commissar-user.js | 8 +- huddles.js | 287 +--------------------------------------------- role-id.js | 1 - server.js | 78 ------------- 5 files changed, 19 insertions(+), 372 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index e0e48cf..6b0398d 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -535,7 +535,22 @@ async function HandleCommitteeCommand(discordMessage) { } async function HandleNickCommand(discordMessage) { - await discordMessage.channel.send(`To set your nickname, link your Steam account https://rustcult.com\n\nYour discord name is your Steam name. It can take up to 12 hours to update.`); + const tokens = discordMessage.content.split(' '); + if (tokens.length < 2) { + await discordMessage.channel.send(`ERROR: wrong number of arguments. USAGE: !nick NewNicknam3`); + return; + } + const raw = discordMessage.content.substring(6); + const filtered = FilterUsername(raw); + if (filtered.length === 0) { + await discordMessage.channel.send(`ERROR: no weird nicknames.`); + return; + } + const discordId = discordMessage.author.id; + const cu = await UserCache.GetCachedUserByDiscordId(discordId); + await cu.setNick(filtered); + const newName = cu.getNicknameOrTitleWithInsignia(); + await discordMessage.channel.send(`Changed name to ${newName}`); } // Do as if the user just joined the discord. For manually resolving people who diff --git a/commissar-user.js b/commissar-user.js index c841027..387026f 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -366,7 +366,7 @@ class CommissarUser { getNicknameOrTitle() { const rank = this.getRank(); if (!rank && rank !== 0) { - return this.steam_name || this.nick || this.nickname; + return this.nick || this.nickname; } const job = RankMetadata[rank]; if (job.titleOverride) { @@ -374,7 +374,7 @@ class CommissarUser { return `${prefix} ${job.title}`; } else { // User-supplied nickname overrides their Discord-wide nickname. - return this.steam_name || this.nick || this.nickname; + return this.nick || this.nickname; } } @@ -434,10 +434,6 @@ class CommissarUser { return 'their'; } } - - getSocialGraphVertexId() { - return this.steam_id || this.discord_id || this.commissar_id; - } } module.exports = CommissarUser; diff --git a/huddles.js b/huddles.js index ec08e7a..6b01b21 100644 --- a/huddles.js +++ b/huddles.js @@ -17,7 +17,7 @@ const UserCache = require('./user-cache'); const huddles = [ { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - //{ name: 'Quad', userLimit: 4, position: 4000 }, + { name: 'Quad', userLimit: 4, position: 4000 }, //{ name: 'Six Pack', userLimit: 6, position: 6000 }, //{ name: 'Squad', userLimit: 8, position: 7000 }, ]; @@ -227,291 +227,6 @@ async function MoveOneRoomIfNeeded(guild) { return true; } -function CompareMembersByHarmonicCentrality(a, b) { - const au = UserCache.GetCachedUserByDiscordId(a.id); - const bu = UserCache.GetCachedUserByDiscordId(b.id); - const aScore = au ? (au.harmonic_centrality || 0) : 0; - const bScore = bu ? (bu.harmonic_centrality || 0) : 0; - if (aScore < bScore) { - return 1; - } - if (bScore < aScore) { - return -1; - } - return 0; -} - -function GetLowestRankingMembersFromVoiceChannel(channel, n) { - const sortableMembers = []; - for (const [id, member] of channel.members) { - sortableMembers.push(member); - } - sortableMembers.sort(CompareMembersByHarmonicCentrality); - if (sortableMembers.length <= n) { - return sortableMembers; - } - return sortableMembers.slice(-n); -} - -// Sets a channel to be accessible to everyone. -async function SetOpenPerms(channel) { - if (!channel) { - return; - } - const guild = await DiscordUtil.GetMainDiscordGuild(); - const connect = PermissionFlagsBits.Connect; - const view = PermissionFlagsBits.ViewChannel; - const perms = [ - { id: guild.roles.everyone.id, deny: [connect, view] }, - { id: RoleID.Grunt, allow: [connect, view] }, - { id: RoleID.Officer, allow: [connect, view] }, - { id: RoleID.General, allow: [connect, view] }, - { id: RoleID.Bots, allow: [view, connect] }, - ]; - // Do not await. Fire and forget with rate limit. - DiscordUtil.TryToSetChannelPermsWithRateLimit(channel, perms); -} - -// Calculates the rank-level perms to use for rank-limiting a voice channel. -function CalculatePermsByRank(channel, rankLimit) { - const connect = PermissionFlagsBits.Connect; - const view = PermissionFlagsBits.ViewChannel; - const perms = [ - { id: channel.guild.roles.everyone.id, allow: [view], deny: [connect] }, - { id: RoleID.Bots, allow: [view, connect] }, - ]; - let rankIndex = 0; - for (const rank of RankMetadata) { - if (rank.count) { - const mainRole = rank.roles[0]; - // Watch out for which way this is ordered. Lower ranks have higher indices. - if (rankIndex < rankLimit) { - perms.push({ id: mainRole, allow: [view, connect] }); - } else { - perms.push({ id: mainRole, allow: [view], deny: [connect] }); - } - } - ++rankIndex; - } - return perms; -} - -// Calculates the individual member perms to use for rank-limiting a voice channel. -async function CalculateIndividualPerms(rankLimit, scoreThreshold) { - const eligibleUsers = UserCache.GetUsersWithRankAndScoreHigherThan(rankLimit, scoreThreshold); - eligibleUsers.sort((a, b) => { - if (a.last_seen < b.last_seen) { - return 1; - } - if (a.last_seen > b.last_seen) { - return -1; - } - return 0; - }); - const howManyTop = 20; - const mostRecentUsers = eligibleUsers.length < howManyTop ? eligibleUsers : eligibleUsers.slice(0, howManyTop); - const connect = PermissionFlagsBits.Connect; - const view = PermissionFlagsBits.ViewChannel; - const perms = []; - const guild = await DiscordUtil.GetMainDiscordGuild(); - for (const user of mostRecentUsers) { - const member = await guild.members.fetch(user.discord_id); - if (member) { - perms.push({ id: member.id, allow: [connect, view] }); - } - } - return perms; -} - -// Sets perms to rank-limit a voice chat room. -// Uses a combination of rank-level perms and individual perms to efficiently -// impose a rank limit on a channel with a resolution down to the individual. -async function SetRankLimit(channel, rankLimit, scoreThreshold) { - const rankPerms = CalculatePermsByRank(channel, rankLimit); - const individualPerms = await CalculateIndividualPerms(rankLimit, scoreThreshold); - const perms = rankPerms.concat(individualPerms); - await channel.permissionOverwrites.set(perms); -} - -// Enforces a soft population cap on all voice chat rooms by moving low-ranking -// members around. Returns true if it had to move anyone, and false if no moves -// are needed. -async function Overflow(guild) { - console.log(`Overflow`); - const mainChannels = []; - const voiceChannels = []; - for (const [id, channel] of guild.channels.cache) { - if (channel.type === 2) { - voiceChannels.push(channel); - } - if (channel.type === 2 && !channel.parent && channel.name === 'Main') { - mainChannels.push(channel); - } - } - console.log(`${voiceChannels.length} voice channels detected.`); - const overflowMembers = []; - for (const channel of voiceChannels) { - const pop = channel.members.size; - const overflowLimit = channel.userLimit - 2; - console.log('Voice room with pop', pop, 'limit', overflowLimit); - if (pop < overflowLimit) { - await SetOpenPerms(channel); - } else { - const howManyExtra = pop - overflowLimit; - const lowest = GetLowestRankingMembersFromVoiceChannel(channel, howManyExtra + 1); - const extra = lowest.slice(1); - for (const member of extra) { - overflowMembers.push(member); - } - const pivotMember = lowest[0]; - const pivotUser = UserCache.GetCachedUserByDiscordId(pivotMember.id); - if (pivotUser) { - await SetRankLimit(channel, pivotUser.rank, pivotUser.harmonic_centrality); - } else { - await SetOpenPerms(channel); - } - } - } - console.log(`${overflowMembers.length} overflow members detected.`); - if (overflowMembers.length === 0) { - console.log(`No overflow. Bailing.`); - return false; - } - // If we get here then there are overflow members. Get only the - // highest ranking one and try to move them. - overflowMembers.sort(CompareMembersByHarmonicCentrality); - const memberToMove = overflowMembers[0]; - const cu = UserCache.GetCachedUserByDiscordId(memberToMove.id); - const name = cu.getNicknameOrTitleWithInsignia(); - // Now identify which is the best other voice room to move them to. - // For now choose the fullest other voice room the member is - // allowed to join. In the future personalize this so it uses the - // coplay time to choose the most familiar group to place the member with. - console.log(`Looking for destination for ${name}`); - let bestDestination; - let bestDestinationPop = 0; - for (const channel of voiceChannels) { - const overflowLimit = channel.userLimit - 2; - let superiorCount = 0; - for (const [id, member] of channel.members) { - const voiceUser = UserCache.GetCachedUserByDiscordId(member.id); - if (voiceUser.harmonic_centrality > cu.harmonic_centrality) { - superiorCount++; - } - } - console.log(`superiorCount ${superiorCount}`); - if (superiorCount >= overflowLimit) { - continue; - } - const pop = channel.members.size; - if (pop > bestDestinationPop) { - bestDestination = channel; - bestDestinationPop = pop; - } - } - // If a good destination was found then move the member. - if (bestDestination) { - console.log(`Best destination found. Moving member.`); - await memberToMove.voice.setChannel(bestDestination); - return true; - } - console.log('No suitable destination found for member.'); - // Buddy rule. Never move a member into a room where they are alone. - if (overflowMembers.length < 2) { - console.log('Bailing due to buddy rule.'); - return false; - } - // If we end up with at least 2 overflow members and nowhere to put them, - // then move them to an empty Main room together. - console.log('Trying to find an empty v channel to populate.'); - let emptyMainChannel; - for (const channel of mainChannels) { - if (channel.members.size === 0) { - emptyMainChannel = channel; - break; - } - } - if (!emptyMainChannel) { - // No empty Main channel. This is usually temporary. - // Return true to try again in a short time and hopefully - // there will be an empty Main channel by then. - console.log(`Failed to find an empty Main channel. Bailing.`); - return true; - } - console.log(`Overflow moving 2 members into an empty Main channel together.`); - const [dumb, dumber] = overflowMembers.slice(-2); - if (dumb) { - console.log('Moving 1st member to empty Main channel.'); - await dumb.voice.setChannel(emptyMainChannel); - } - if (dumber) { - console.log('Moving 2nd member to empty Main channel.'); - await dumber.voice.setChannel(emptyMainChannel); - } - return true; -} - -async function GetAllDiscordAccountsFromRustCultApi() { - if (!config.rustCultApiToken) { - console.log('Cannot update prox because no api token.'); - return null; - } - const randomNonce = Math.random().toString(); - const url = 'https://rustcult.com/getalldiscordaccounts?token=' + config.rustCultApiToken + '&nonce=' + randomNonce; - let response; - try { - response = await fetch(url); - } catch (error) { - console.log('Cannot update prox because error while querying rustcult API.'); - return null; - } - if (!response) { - console.log('Cannot update prox because no response received.'); - return null; - } - if (typeof response !== 'string') { - console.log('Cannot update prox because response is not a string.'); - return null; - } - return response; -} - -async function UpdateSteamAccountInfo() { - // Hit the rustcult.com API to get updated player positions. - const response = await GetAllDiscordAccountsFromRustCultApi(); - if (!response) { - return; - } - const linkedAccounts = JSON.parse(response); - console.log(linkedAccounts.length, 'linked accounts downloaded from rustcult.com API.'); - for (const account of linkedAccounts) { - if (!account) { - return; - } - if (!account.discordId) { - return; - } - if (!account.steamId) { - return; - } - if (account.discordId === '294544723518029824') { - console.log('crudeoil found!!!'); - } - const cu = UserCache.GetCachedUserByDiscordId(account.discordId); - if (!cu) { - console.log('Discord ID not found ' + account.discordId); - continue; - } - await cu.setSteamId(account.steamId); - await cu.setSteamName(account.steamName); - console.log('Linked account ' + cu.getNicknameOrTitleWithInsignia() + ' ' + account.discordId + ' ' + account.steamId); - } -} - -// Update steam account info once after startup, then hourly after that. -setTimeout(UpdateSteamAccountInfo, 15 * 1000); -setInterval(UpdateSteamAccountInfo, 60 * 60 * 1000); - // To avoid race conditions on the cheap, use a system of routine updates. // To schedule an update, a boolean flag is flipped. That way, the next time // the cycle goes around, it knows that an update is needed. Redundant or diff --git a/role-id.js b/role-id.js index 7e75e7d..6b2e1c0 100644 --- a/role-id.js +++ b/role-id.js @@ -8,7 +8,6 @@ module.exports = { Defendant: '918232560813871114', General: '318985002266263552', Grunt: '319302277837881344', - IdBadge: '947942301039231016', Lieutenant: '825491800478449705', Major: '825490288218734674', Officer: '319300874470162434', diff --git a/server.js b/server.js index 3b4f50c..34c5684 100644 --- a/server.js +++ b/server.js @@ -90,10 +90,6 @@ async function UpdateMemberAppearance(member) { } else { await DiscordUtil.RemoveRole(member, RoleID.RetiredGeneral); } - // ID Badge for all members with a linked steam account. - if (cu.steam_id) { - await DiscordUtil.AddRole(member, RoleID.IdBadge); - } } const afkLoungeId = '703716669452714054'; @@ -254,79 +250,6 @@ async function FilterTimeTogetherRecordsToEnforceTimeCap(timeTogetherRecords) { return matchingRecords; } -// Crawl and update the steam names of some steam-connected users. -// This routine happens often so not every user has to get updated -// every cycle. -async function UpdateSomeSteamNames() { - const u = UserCache.GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName(); - if (!u) { - return; - } - console.log('UpdateSomeSteamNames', u.steam_name, u.steam_id); - const steamWebApiKey = '22A69A4E939F0D8EC4689D6CAA5D79EE'; - const url = `http://api.steampowered.com/ISteamUser/GetPlayerSummaries/v0002/?key=${steamWebApiKey}&steamids=${u.steam_id}`; - let response; - try { - response = await fetch(url); - } catch (error) { - console.log(error); - } - if (!response) { - console.log('No response'); - return; - } - let json; - try { - json = JSON.parse(response); - } catch (error) { - console.log(error); - } - if (!json) { - console.log('Failed to parse json'); - return; - } - if (!json.response) { - console.log('Json has wrong format'); - return; - } - if (!json.response.players) { - console.log('Could not get list of players'); - return; - } - const players = json.response.players; - if (!players.length || players.length === 0) { - console.log('No valid player records received'); - return; - } - const p = players[0]; - if (p.steamid !== u.steam_id) { - console.log('Steam ID does not match response'); - return; - } - const n = p.personaname; - if (!n) { - console.log('No personaname (steam name) in response'); - return; - } - if (typeof n !== 'string') { - console.log('Steam name is not a string'); - return; - } - if (n.length === 0) { - console.log('Steam name has zero length'); - return; - } - // At this point we crawled the user's steam name successfully so we mark them - // as updated to send them to the back of the crawling queue. - await u.setSteamNameUpdatedNow(); - if (u.steam_name === n) { - console.log('Steam name already up to date'); - return; - } - console.log('Updating steam name of', u.steam_id, 'from', u.steam_name, 'to', n); - await u.setSteamName(n); -} - // Routine update event. Take care of book-keeping that need attention once every few minutes. async function RoutineUpdate() { console.log('Routine update'); @@ -339,7 +262,6 @@ async function RoutineUpdate() { await DB.ConsolidateTimeMatrix(); await UpdateHarmonicCentrality(); await com.CalculateChainOfCommand(); - await UpdateSomeSteamNames(); await UpdateAllCitizens(); await yen.DoLottery(); await recruiting.ScanInvitesForChanges(); From 7cbdf8316f6183fdf72071102eac6a344d817861 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 29 Jul 2024 15:53:19 +0000 Subject: [PATCH 079/101] Fixed a stupid bug in the pay command and some work on trump NFT. --- bot-commands.js | 47 +++++++++++++++++++++++++++++++++++++++++++++++ huddles.js | 5 +---- yen.js | 2 +- 3 files changed, 49 insertions(+), 5 deletions(-) diff --git a/bot-commands.js b/bot-commands.js index 6b0398d..985f62f 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -702,11 +702,58 @@ async function HandleAfkCommand(discordMessage) { } } +function ChooseRandomTrumpCard() { + const n = 32; + const r = Math.floor(n * Math.random()); + return `trump-cards/${r}.png`; +} + +let trumpNftPrice = 50; + async function HandleBuyCommand(discordMessage) { const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author) { return; } + const oldYen = author.yen || 0; + const oldCards = author.trump_cards || 0; + const tradePrice = trumpNftPrice; + let actionMessage = `purchased this Donald Trump NFT for ${tradePrice} yen`; + let newYen = oldYen - tradePrice; + if (authorCards < 0) { + newYen += 100; + actionMessage += ' then destroyed it, gaining 100 yen'; + } + const newCards = oldCards + 1; + if (newYen >= 0) { + trumpNftPrice++; + await author.setYen(newYen); + await author.setTrumpCards(newCards); + } else { + await discordMessage.channel.send('Not enough yen. Use !yen to check how much money you have.'); + return; + } + const name = author.getNicknameOrTitleWithInsignia(); + const prefixes = [ + 'The probability that Donald Trump will win the 2024 US presidential election is', + 'The odds that Trump will win are', + 'The probability of a Trump win is', + ]; + const r = Math.floor(prefixes.length * Math.random()); + const randomPrefix = prefixes[r]; + let content = `${name} purchased this Donald Trump NFT for ${tradePrice} yen. ${randomPrefix} ${tradePrice}%`; + const trumpCard = ChooseRandomTrumpCard(); + try { + await discordMessage.channel.send({ + content, + files: [{ + attachment: trumpCard, + name: trumpCard, + }] + }); + } catch (error) { + console.log('Failed to send orders to', name); + } } async function HandleSellCommand(discordMessage) { diff --git a/huddles.js b/huddles.js index 6b01b21..ceaafd0 100644 --- a/huddles.js +++ b/huddles.js @@ -15,16 +15,13 @@ const RoleID = require('./role-id'); const UserCache = require('./user-cache'); const huddles = [ + { name: 'Main', userLimit: 99, position: 1000 }, { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, { name: 'Quad', userLimit: 4, position: 4000 }, //{ name: 'Six Pack', userLimit: 6, position: 6000 }, //{ name: 'Squad', userLimit: 8, position: 7000 }, ]; -const mainRoomControlledByProximity = false; -if (!mainRoomControlledByProximity) { - huddles.push({ name: 'Main', userLimit: 99, position: 1000 }); -} function GetAllMatchingVoiceChannels(guild, huddle) { const matchingChannels = []; diff --git a/yen.js b/yen.js index 862b470..323615d 100644 --- a/yen.js +++ b/yen.js @@ -529,7 +529,7 @@ async function HandlePayCommandWithAmount(discordMessage, amount) { } async function HandlePayCommand(discordMessage) { - const tokens = discordMessage.content.split(' '); + const tokens = discordMessage.content.split(' ').filter(n => n); if (tokens.length !== 3) { await discordMessage.channel.send('Error. Wrong number of parameters. Example: `!pay @Jeff 42`'); return; From 34ee33399e03b81d0b8811eaeea33ccdf1ab9879 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 13 Aug 2024 16:14:29 +0000 Subject: [PATCH 080/101] Donald Trump NFT joke + rank rebalance --- ban.js | 11 ++- bot-commands.js | 209 ++++++++++++++++++++++++++++++++++++++++---- commissar-user.js | 3 +- rank-definitions.js | 8 +- role-id.js | 2 +- yen.js | 1 + 6 files changed, 212 insertions(+), 22 deletions(-) diff --git a/ban.js b/ban.js index 809ed3a..8abd55a 100644 --- a/ban.js +++ b/ban.js @@ -71,13 +71,22 @@ async function UpdateTrial(cu) { // Update or create the ban vote message itself. The votes are reactions to this message. let message; if (cu.ban_vote_message) { - message = await channel.messages.fetch(cu.ban_vote_message); + try { + message = await channel.messages.fetch(cu.ban_vote_message); + } catch (error) { + console.log('Failed to find or create ban court voting message', cu.ban_vote_message); + message = null; + } } else { message = await channel.send('Welcome to the Ban Court'); await message.react('✅'); await message.react('❌'); await message.pin(); } + if (!message) { + console.log('Failed to find or create ban court voting message', cu.ban_vote_message); + return; + } await cu.setBanVoteMessage(message.id); // Count up all the votes. Remove any unauthorized votes. for (const [reactionId, reaction] of message.reactions.cache) { diff --git a/bot-commands.js b/bot-commands.js index 985f62f..4206efb 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -6,6 +6,7 @@ const discordTranscripts = require('discord-html-transcripts'); const DiscordUtil = require('./discord-util'); const exile = require('./exile-cache'); const FilterUsername = require('./filter-username'); +const fs = require('fs'); const huddles = require('./huddles'); const RandomPin = require('./random-pin'); const RankMetadata = require('./rank-definitions'); @@ -256,18 +257,16 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `It's a special wipe day. RustGalaxy is 3 servers hosted in USA, Europe, and Australia. Use the telephone at Outpost to travel between islands - with your loot.\n`; - content += '```client.connect usa.rustgalaxy.com\nclient.connect europe.rustgalaxy.com\nclient.connect australia.rustgalaxy.com```\n'; // Only one newline after triple backticks. - content += `RustGalaxy was created by gov members. The admins and moderators are all gov members. We have beat Facepunch to the Nexus system, live right now in the Community tab. Being the first to unlock a multi-island universe gives our community a massive first-mover advantage. It is already gaining pop and it seems inevitable that we will give the biggest servers a run for their money. The existing tech can support over 100,000 concurrent players _in one connected world_ putting all the competition to shame. This moment is a big big deal. Come slap down a base and be part of the hype. Most gov members are building solo or duo or trio, and the usual gov rules about raiding don't apply.\n\n`; - content += `RustGalaxy never wipes BPs. Your blueprints are synced across all the islands.\n\n`; - content += `See you in there! <3\n`; + content += `Here are your secret orders for August 2024. The gov is taking it easy and zerging on Rusty Moose US Monthly.\n`; + content += '```client.connect monthly.us.moose.gg:28010```\n'; // Only one newline after triple backticks. + content += `We have a sweet plan involving a cave and a big monument. You are invited.\n\n`; console.log('Content length', content.length, 'characters.'); try { await discordMember.send({ content, files: [{ - attachment: 'galaxy-map-3.png', - name: 'galaxy-map-3.png' + attachment: 'nov-2023-village-heatmap.png', + name: 'nov-2023-village-heatmap.png' }] }); } catch (error) { @@ -708,32 +707,72 @@ function ChooseRandomTrumpCard() { return `trump-cards/${r}.png`; } -let trumpNftPrice = 50; +let trumpNftPrice = null; +const buyQueue = []; +const sellQueue = []; + +function LoadPrice() { + if (trumpNftPrice === null) { + const trumpPriceAsString = fs.readFileSync('trump-price.csv'); + trumpNftPrice = parseInt(trumpPriceAsString); + } +} + +function SavePrice() { + LoadPrice(); + fs.writeFileSync('trump-price.csv', trumpNftPrice.toString()); +} + +function IncreasePrice() { + LoadPrice(); + if (trumpNftPrice < 99) { + trumpNftPrice++; + } + SavePrice(); +} + +function DecreasePrice() { + LoadPrice(); + if (trumpNftPrice > 1) { + trumpNftPrice--; + } + SavePrice(); +} async function HandleBuyCommand(discordMessage) { + buyQueue.push(discordMessage); +} + +async function FulfillBuyOrder(discordMessage) { + LoadPrice(); + if (trumpNftPrice > 99) { + await discordMessage.channel.send('Cannot buy when the price is maxed out to 100. Wait for someone to sell then try again.'); + return; + } const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author) { return; } const oldYen = author.yen || 0; const oldCards = author.trump_cards || 0; + console.log('oldCards', oldCards); const tradePrice = trumpNftPrice; - let actionMessage = `purchased this Donald Trump NFT for ${tradePrice} yen`; + const name = author.getNicknameOrTitleWithInsignia(); + let actionMessage = `${name} purchased this one-of-a-kind Donald Trump NFT for ${tradePrice} yen`; let newYen = oldYen - tradePrice; - if (authorCards < 0) { + if (oldCards < 0) { newYen += 100; - actionMessage += ' then destroyed it, gaining 100 yen'; + actionMessage += ' and got back their 100 yen deposit'; } const newCards = oldCards + 1; if (newYen >= 0) { - trumpNftPrice++; + IncreasePrice(); await author.setYen(newYen); await author.setTrumpCards(newCards); } else { await discordMessage.channel.send('Not enough yen. Use !yen to check how much money you have.'); return; } - const name = author.getNicknameOrTitleWithInsignia(); const prefixes = [ 'The probability that Donald Trump will win the 2024 US presidential election is', 'The odds that Trump will win are', @@ -741,7 +780,19 @@ async function HandleBuyCommand(discordMessage) { ]; const r = Math.floor(prefixes.length * Math.random()); const randomPrefix = prefixes[r]; - let content = `${name} purchased this Donald Trump NFT for ${tradePrice} yen. ${randomPrefix} ${tradePrice}%`; + const probabilityAnnouncement = `${randomPrefix} ${trumpNftPrice}%`; + let positionStatement; + const plural = Math.abs(newCards) !== 1 ? 's' : ''; + if (newCards > 0) { + const positivePayout = newCards * 100; + positionStatement = `They have ${newCards} NFT${plural} that will pay out ${positivePayout} yen if Trump wins`; + } else if (newCards < 0) { + const negativePayout = -newCards * 100; + positionStatement = `They are short ${-newCards} more NFT${plural} that will pay out ${negativePayout} yen if Trump does not win`; + } else { + positionStatement = 'They have no NFTs left'; + } + let content = '```' + `${actionMessage}. ${positionStatement}. ${probabilityAnnouncement}` + '```'; const trumpCard = ChooseRandomTrumpCard(); try { await discordMessage.channel.send({ @@ -752,16 +803,144 @@ async function HandleBuyCommand(discordMessage) { }] }); } catch (error) { - console.log('Failed to send orders to', name); + console.log(error); } + const guild = await DiscordUtil.GetMainDiscordGuild(); + const channel = await guild.channels.fetch('1267202328243732480'); + let message = '```' + probabilityAnnouncement + '\n\n'; + if (trumpNftPrice < 100) { + message += `!buy a Donald Trump NFT for ${trumpNftPrice} yen\n`; + } + if (trumpNftPrice > 1) { + const sellPrice = trumpNftPrice - 1; + message += `!sell a Donald Trump NFT for ${sellPrice} yen\n`; + } + message += '\n'; + message += 'Every Donald Trump NFT pays out 100 yen if Donald Trump is declared the winner of the 2024 US presidential election by the Associated Press. Short-selling is allowed. If you believe that Trump will win then !buy. If you believe that he will not win then !sell.\n\n'; + message += '!buy low !sell high. It\'s just business. Personal politics aside, if the price does not reflect the true probability, then consider a !buy or !sell. See a news event that is not priced in yet? Quickly !buy or !sell to profit from being the first to bring new information to market.\n\n'; + message += 'Do you believe that one or both sides suffer from bias? Here is your chance to fine a random idiot from the internet for disagreeing with you. Make them pay!'; + message += '```'; + console.log('message:', message); + await channel.bulkDelete(99); + await channel.send({ + content: message, + files: [{ + attachment: trumpCard, + name: trumpCard, + }] + }); + await yen.UpdateYenChannel(); } async function HandleSellCommand(discordMessage) { + sellQueue.push(discordMessage); +} + +async function FulfillSellOrder(discordMessage) { + LoadPrice(); + if (trumpNftPrice < 2) { + await discordMessage.channel.send('Cannot sell when the price is bottomed out to 1. Wait for someone to buy then try again.'); + return; + } const author = await UserCache.GetCachedUserByDiscordId(discordMessage.author.id); if (!author) { return; } + const oldYen = author.yen || 0; + const oldCards = author.trump_cards || 0; + const tradePrice = trumpNftPrice - 1; + const name = author.getNicknameOrTitleWithInsignia(); + let actionMessage = `${name} sold this Donald Trump NFT for ${tradePrice} yen`; + let newYen = oldYen + tradePrice; + const newCards = oldCards - 1; + if (newCards < 0) { + newYen -= 100; + actionMessage = `${name} deposited 100 yen to mint a new Donald Trump NFT then sold it for ${tradePrice} yen`; + } + if (newYen >= 0) { + DecreasePrice(); + await author.setYen(newYen); + await author.setTrumpCards(newCards); + } else { + await discordMessage.channel.send('Not enough yen to short-sell. Use !yen to check how much money you have.'); + return; + } + const prefixes = [ + 'The probability that Donald Trump will win the 2024 US presidential election is', + 'The odds that Trump will win are', + 'The probability of a Trump win is', + ]; + const r = Math.floor(prefixes.length * Math.random()); + const randomPrefix = prefixes[r]; + const probabilityAnnouncement = `${randomPrefix} ${trumpNftPrice}%`; + let positionStatement; + const plural = Math.abs(newCards) !== 1 ? 's' : ''; + if (newCards > 0) { + const positivePayout = newCards * 100; + positionStatement = `They have ${newCards} NFT${plural} left that will pay out ${positivePayout} yen if Trump wins`; + } else if (newCards < 0) { + const negativePayout = -newCards * 100; + positionStatement = `They are short ${-newCards} NFT${plural} that will pay out ${negativePayout} yen if Trump does not win`; + } else { + positionStatement = 'They have no NFTs left'; + } + let content = '```' + `${actionMessage}. ${positionStatement}. ${probabilityAnnouncement}` + '```'; + const trumpCard = ChooseRandomTrumpCard(); + try { + await discordMessage.channel.send({ + content, + files: [{ + attachment: trumpCard, + name: trumpCard, + }] + }); + } catch (error) { + console.log(error); + } + const guild = await DiscordUtil.GetMainDiscordGuild(); + const channel = await guild.channels.fetch('1267202328243732480'); + let message = '```' + probabilityAnnouncement + '\n\n'; + if (trumpNftPrice < 100) { + message += `!buy a Donald Trump NFT for ${trumpNftPrice} yen\n`; + } + if (trumpNftPrice > 1) { + const sellPrice = trumpNftPrice - 1; + message += `!sell a Donald Trump NFT for ${sellPrice} yen\n`; + } + message += '\n'; + message += 'Every Donald Trump NFT pays out 100 yen if Donald Trump is declared the winner of the 2024 US presidential election by the Associated Press. Short-selling is allowed. If you believe that Trump will win then !buy. If you believe that he will not win then !sell.\n\n'; + message += '!buy low !sell high. It\'s just business. Personal politics aside, if the price does not reflect the true probability, then consider a !buy or !sell. See a news event that is not priced in yet? Quickly !buy or !sell to profit from being the first to bring new information to market.\n\n'; + message += 'Do you believe that one or both sides suffer from bias? Here is your chance to fine a random idiot from the internet for disagreeing with you. Make them pay!'; + message += '```'; + console.log('message:', message); + await channel.bulkDelete(99); + await channel.send({ + content: message, + files: [{ + attachment: trumpCard, + name: trumpCard, + }] + }); + await yen.UpdateYenChannel(); +} + +async function ProcessOneBuyAndOneSellOrder() { + if (buyQueue.length === 0 && sellQueue.length === 0) { + setTimeout(ProcessOneBuyAndOneSellOrder, 1000); + return; + } + if (buyQueue.length > 0) { + const buyOrder = buyQueue.shift(); + await FulfillBuyOrder(buyOrder); + await Sleep(100); + } + if (sellQueue.length > 0) { + const sellOrder = sellQueue.shift(); + await FulfillSellOrder(sellOrder); + } + setTimeout(ProcessOneBuyAndOneSellOrder, 100); } +setTimeout(ProcessOneBuyAndOneSellOrder, 1000); // Handle any unrecognized commands, possibly replying with an error message. async function HandleUnknownCommand(discordMessage) { diff --git a/commissar-user.js b/commissar-user.js index 387026f..bb2e8ab 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -35,6 +35,7 @@ class CommissarUser { presidential_election_message_id, steam_id, steam_name, + steam_name_update_time, trump_cards, cost_basis) { this.commissar_id = commissar_id; @@ -66,7 +67,7 @@ class CommissarUser { this.presidential_election_message_id = presidential_election_message_id; this.steam_id = steam_id; this.steam_name = steam_name; - this.steam_name_update_time = null; + this.steam_name_update_time = steam_name_update_time; this.trump_cards = trump_cards; this.cost_basis = cost_basis; } diff --git a/rank-definitions.js b/rank-definitions.js index 2877324..0c0b2cf 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -167,8 +167,8 @@ module.exports = [ color: '#4285F4', count: 40, insignia: '⦁⦁⦁⦁', - roles: [RoleID.StaffSergeant, RoleID.Grunt], - title: 'Staff Sergeant', + roles: [RoleID.Ensign, RoleID.Grunt], + title: 'Ensign', }, { color: '#4285F4', @@ -179,14 +179,14 @@ module.exports = [ }, { color: '#4285F4', - count: 100, + count: 200, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], title: 'Corporal', }, { color: '#4285F4', - count: 700, + count: 600, insignia: '⦁', roles: [RoleID.Private, RoleID.Grunt], title: 'Private', diff --git a/role-id.js b/role-id.js index 6b2e1c0..daebd7a 100644 --- a/role-id.js +++ b/role-id.js @@ -6,6 +6,7 @@ module.exports = { Commander: '1228834925067894794', Corporal: '825491805117874197', Defendant: '918232560813871114', + Ensign: '918218594091937792', General: '318985002266263552', Grunt: '319302277837881344', Lieutenant: '825491800478449705', @@ -14,7 +15,6 @@ module.exports = { Private: '825491806929027173', Recruit: '1240130799743930439', Sergeant: '825491803071184926', - StaffSergeant: '918218594091937792', WipeBadge: '1067177647857209436', RetiredGeneral: '1055270328395378688', }; diff --git a/yen.js b/yen.js index 323615d..fd93764 100644 --- a/yen.js +++ b/yen.js @@ -766,4 +766,5 @@ module.exports = { HandleYenCreateCommand, HandleYenDestroyCommand, HandleYenFaqCommand, + UpdateYenChannel, }; From b8d7940a5e9a34d195879880984313d65f316258 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Tue, 13 Aug 2024 16:49:19 +0000 Subject: [PATCH 081/101] Remove Quad room for lack of usage. --- huddles.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/huddles.js b/huddles.js index ceaafd0..957622f 100644 --- a/huddles.js +++ b/huddles.js @@ -18,7 +18,7 @@ const huddles = [ { name: 'Main', userLimit: 99, position: 1000 }, { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - { name: 'Quad', userLimit: 4, position: 4000 }, + //{ name: 'Quad', userLimit: 4, position: 4000 }, //{ name: 'Six Pack', userLimit: 6, position: 6000 }, //{ name: 'Squad', userLimit: 8, position: 7000 }, ]; From f0448b1845435d3e87e951109d815f7790893a8d Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 21 Aug 2024 18:10:43 +0000 Subject: [PATCH 082/101] Ban power back to Generals only. --- ban.js | 2 +- rank-definitions.js | 4 ---- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/ban.js b/ban.js index 8abd55a..729b930 100644 --- a/ban.js +++ b/ban.js @@ -11,7 +11,7 @@ const threeTicks = '```'; // General 1 = 15 // Major = 17 // Lieutenant = 19 -const banCommandRank = 19; +const banCommandRank = 15; const banVoteRank = 19; function SentenceLengthAsString(years) { diff --git a/rank-definitions.js b/rank-definitions.js index 0c0b2cf..41e81ce 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -132,7 +132,6 @@ module.exports = [ title: 'General', }, { - banPower: true, color: '#DB4437', count: 10, insignia: '❱❱❱❱', @@ -140,7 +139,6 @@ module.exports = [ title: 'Colonel', }, { - banPower: true, color: '#DB4437', count: 10, insignia: '❱❱❱', @@ -148,7 +146,6 @@ module.exports = [ title: 'Major', }, { - banPower: true, color: '#DB4437', count: 10, insignia: '❱❱', @@ -156,7 +153,6 @@ module.exports = [ title: 'Captain', }, { - banPower: true, color: '#DB4437', count: 10, insignia: '❱', From ca2fe974d74743aa23bdbfe9b71ed0630ad1e547 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 21 Aug 2024 18:55:53 +0000 Subject: [PATCH 083/101] Migrate day and month counts from rustcult --- commissar-user.js | 44 +++++++++++++++++++++++++++++++++++++++++++- server.js | 32 ++++++++++++++++++++++++++++++++ setup-database.sql | 4 ++++ user-cache.js | 4 ++++ 4 files changed, 83 insertions(+), 1 deletion(-) diff --git a/commissar-user.js b/commissar-user.js index bb2e8ab..53fbf42 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -37,7 +37,11 @@ class CommissarUser { steam_name, steam_name_update_time, trump_cards, - cost_basis) { + cost_basis, + calendar_day_count, + last_calendar_day, + calendar_month_count, + last_calendar_month) { this.commissar_id = commissar_id; this.discord_id = discord_id; this.nickname = nickname; @@ -70,6 +74,10 @@ class CommissarUser { this.steam_name_update_time = steam_name_update_time; this.trump_cards = trump_cards; this.cost_basis = cost_basis; + this.calendar_day_count = calendar_day_count; + this.last_calendar_day = last_calendar_day; + this.calendar_month_count = calendar_month_count; + this.last_calendar_month = last_calendar_month; } async setDiscordId(discord_id) { @@ -334,6 +342,40 @@ class CommissarUser { await this.updateFieldInDatabase('cost_basis', this.cost_basis); } + async setCalendarDayCount(c) { + if (c === this.calendar_day_count) { + return; + } + this.calendar_day_count = c; + await this.updateFieldInDatabase('calendar_day_count', this.calendar_day_count); + } + + async setCalendarMonthCount(c) { + if (c === this.calendar_month_count) { + return; + } + this.calendar_month_count = c; + await this.updateFieldInDatabase('calendar_month_count', this.calendar_month_count); + } + + async updateCalendarDayCount() { + const t = moment(); + const calendarDay = t.format('YYYY-MM-DD'); + const calendarMonth = t.format('YYYY-MM'); + if (calendarDay !== this.last_calendar_day) { + this.calendar_day_count++; + this.last_calendar_day = calendarDay; + await this.updateFieldInDatabase('calendar_day_count', this.calendar_day_count); + await this.updateFieldInDatabase('last_calendar_day', this.last_calendar_day); + } + if (calendarMonth !== this.last_calendar_month) { + this.calendar_month_count++; + this.last_calendar_month = calendarMonth; + await this.updateFieldInDatabase('calendar_month_count', this.calendar_month_count); + await this.updateFieldInDatabase('last_calendar_month', this.last_calendar_month); + } + } + async updateFieldInDatabase(fieldName, fieldValue) { //console.log(`DB update ${fieldName} = ${fieldValue} for ${this.nickname} (ID:${this.commissar_id}).`); const sql = `UPDATE users SET ${fieldName} = ? WHERE commissar_id = ?`; diff --git a/server.js b/server.js index 34c5684..f358308 100644 --- a/server.js +++ b/server.js @@ -10,6 +10,7 @@ const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('d const DiscordUtil = require('./discord-util'); const exile = require('./exile-cache'); const fetch = require('./fetch'); +const fs = require('fs'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); @@ -274,6 +275,35 @@ async function RoutineUpdate() { setTimeout(RoutineUpdate, sleepTime); } +async function MigrateCalendarDayCounts() { + const fileContents = fs.readFileSync('in-game-activity-points-march-2024.csv', 'utf8'); + const lines = fileContents.split('\n'); + for (const line of lines) { + if (line.length < 10) { + continue; + } + const columns = line.trim().split(','); + if (columns.length !== 4) { + continue; + } + const steamId = columns[0]; + const dayCount = parseInt(columns[2]); + const monthCount = parseInt(columns[3]); + const cu = UserCache.GetCachedUserBySteamId(steamId); + if (!cu) { + continue; + } + if (dayCount > cu.calendar_day_count) { + await cu.setCalendarDayCount(dayCount); + console.log('day count updated', steamId, dayCount); + } + if (monthCount > cu.calendar_month_count) { + await cu.setCalendarMonthCount(monthCount); + console.log('month count updated', steamId, monthCount); + } + } +} + // Waits for the database and bot to both be connected, then finishes booting the bot. async function Start() { console.log('Waiting for Discord bot to connect.'); @@ -288,6 +318,7 @@ async function Start() { console.log('Ban votes loaded into cache.'); await exile.LoadExilesFromDatabase(); console.log('Exiles loaded into cache'); + await MigrateCalendarDayCounts(); // This Discord event fires when someone joins a Discord guild that the bot is a member of. discordClient.on('guildMemberAdd', async (member) => { @@ -363,6 +394,7 @@ async function Start() { } await cu.setCitizen(true); await cu.seenNow(); + await cu.updateCalendarDayCount(); if (cu.good_standing === false) { await newVoiceState.member.voice.kick(); } diff --git a/setup-database.sql b/setup-database.sql index 7db56f8..7ee64ff 100644 --- a/setup-database.sql +++ b/setup-database.sql @@ -41,6 +41,10 @@ CREATE TABLE users presidential_election_message_id VARCHAR(32), -- ID of the discord message used to display this user on the presidential election ballot. trump_cards INT NOT NULL DEFAULT 0, -- How many Trump Cards owned by this member. Can be negative. cost_basis INT NOT NULL DEFAULT 0, -- How many yen spend on Trump Cards. Used to calculate profit/loss. + calendar_day_count INT NOT NULL DEFAULT 0, + last_calendar_day VARCHAR(16) NOT NULL DEFAULT '2000-01-01', + calendar_month_count INT NOT NULL DEFAULT 0, + last_calendar_month VARCHAR(16) NOT NULL DEFAULT '2000-01', PRIMARY KEY (commissar_id), INDEX discord_index (discord_id) ); diff --git a/user-cache.js b/user-cache.js index e3b521a..be41306 100644 --- a/user-cache.js +++ b/user-cache.js @@ -46,6 +46,10 @@ async function LoadAllUsersFromDatabase() { row.steam_name_update_time, row.trump_cards, row.cost_basis, + row.calendar_day_count, + row.last_calendar_day, + row.calendar_month_count, + row.last_calendar_month, ); newCache[row.commissar_id] = newUser; }); From 99fa72d8f4deae98da9a0aa357853c17259d3b30 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Wed, 21 Aug 2024 19:26:35 +0000 Subject: [PATCH 084/101] Remove migration code. --- server.js | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/server.js b/server.js index f358308..8a8f6a5 100644 --- a/server.js +++ b/server.js @@ -10,7 +10,6 @@ const { ContextMenuCommandBuilder, Events, ApplicationCommandType } = require('d const DiscordUtil = require('./discord-util'); const exile = require('./exile-cache'); const fetch = require('./fetch'); -const fs = require('fs'); const HarmonicCentrality = require('./harmonic-centrality'); const huddles = require('./huddles'); const moment = require('moment'); @@ -275,35 +274,6 @@ async function RoutineUpdate() { setTimeout(RoutineUpdate, sleepTime); } -async function MigrateCalendarDayCounts() { - const fileContents = fs.readFileSync('in-game-activity-points-march-2024.csv', 'utf8'); - const lines = fileContents.split('\n'); - for (const line of lines) { - if (line.length < 10) { - continue; - } - const columns = line.trim().split(','); - if (columns.length !== 4) { - continue; - } - const steamId = columns[0]; - const dayCount = parseInt(columns[2]); - const monthCount = parseInt(columns[3]); - const cu = UserCache.GetCachedUserBySteamId(steamId); - if (!cu) { - continue; - } - if (dayCount > cu.calendar_day_count) { - await cu.setCalendarDayCount(dayCount); - console.log('day count updated', steamId, dayCount); - } - if (monthCount > cu.calendar_month_count) { - await cu.setCalendarMonthCount(monthCount); - console.log('month count updated', steamId, monthCount); - } - } -} - // Waits for the database and bot to both be connected, then finishes booting the bot. async function Start() { console.log('Waiting for Discord bot to connect.'); @@ -318,7 +288,6 @@ async function Start() { console.log('Ban votes loaded into cache.'); await exile.LoadExilesFromDatabase(); console.log('Exiles loaded into cache'); - await MigrateCalendarDayCounts(); // This Discord event fires when someone joins a Discord guild that the bot is a member of. discordClient.on('guildMemberAdd', async (member) => { From b6e64a90e0adca7cbffed668f15fc108865c570f Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 19:35:13 +0000 Subject: [PATCH 085/101] Monthly election related maintenance. --- ban.js | 15 ++++++++++----- bot-commands.js | 18 +++++++++++++----- chain-of-command.js | 45 ++++++++++++++++++--------------------------- huddles.js | 4 ++-- rank-definitions.js | 4 ++-- user-cache.js | 20 +++++++++++++++++--- 6 files changed, 62 insertions(+), 44 deletions(-) diff --git a/ban.js b/ban.js index 729b930..ace21a7 100644 --- a/ban.js +++ b/ban.js @@ -125,11 +125,16 @@ async function UpdateTrial(cu) { if (member) { const before = await channel.permissionOverwrites.resolve(member.id); if (cu.peak_rank >= 20 && voteCount >= 5 && yesPercentage >= 0.909) { - await channel.permissionOverwrites.create(member, { - Connect: true, - SendMessages: false, - ViewChannel: true, - }); + try { + await channel.permissionOverwrites.create(member, { + Connect: true, + SendMessages: false, + ViewChannel: true, + }); + } catch (error) { + console.log('Error setting defendant perms for ban court chat channel:'); + console.log(error); + } if (!before || before.allow.has('SendMessages')) { await channel.send(threeTicks + 'The defendant has been removed from the courtroom.' + threeTicks); } diff --git a/bot-commands.js b/bot-commands.js index 4206efb..ea871e3 100644 --- a/bot-commands.js +++ b/bot-commands.js @@ -72,7 +72,7 @@ async function HandleServerVoteCommand(discordMessage) { } const guild = await DiscordUtil.GetMainDiscordGuild(); const channel = await guild.channels.create({ name: 'server-vote' }); - const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for August 2024.'); + const message = await channel.send('The Government will play on whichever server gets the most votes. This will be our home Rust server for September 2024.'); await message.react('❤️'); await MakeOneServerVoteOption(channel, 'Rusty Moose |US Monthly|', 'https://www.battlemetrics.com/servers/rust/9611162', 5); await MakeOneServerVoteOption(channel, 'Rustafied.com - US Long III', 'https://www.battlemetrics.com/servers/rust/433754', 11); @@ -107,7 +107,7 @@ async function HandlePresidentVoteCommand(discordMessage) { }); const message = await channel.send('Whoever gets the most votes will be Mr. or Madam President in August 2024.'); await message.react('❤️'); - const generalRankUsers = await UserCache.GetMostCentralUsers(159); + const generalRankUsers = await UserCache.GetTopRankedUsers(19); const candidateNames = []; for (const user of generalRankUsers) { if (user.commissar_id === 7) { @@ -257,9 +257,17 @@ async function SendWipeBadgeOrders(user, discordMessage, discordMember) { await discordMessage.channel.send(`Sending orders to ${name}`); const rankNameAndInsignia = user.getRankNameAndInsignia(); let content = `${rankNameAndInsignia},\n\n`; - content += `Here are your secret orders for August 2024. The gov is taking it easy and zerging on Rusty Moose US Monthly.\n`; - content += '```client.connect monthly.us.moose.gg:28010```\n'; // Only one newline after triple backticks. - content += `We have a sweet plan involving a cave and a big monument. You are invited.\n\n`; + content += `September 2024 is shaping up to be a huge month for The Government. Lots of big groups with great leaders are committed to building nearby each other with common walls. If you miss the big old gov wipes, you need to check this out!\n\n`; + if (user.rank <= 21) { + content += `The build spot is P25. Use the wipe code to get into community base.\n\n`; + } + content += '```client.connect USLarge.Rustopia.gg```\n'; // Only one newline after triple backticks. + if (user.rank <= 15) { + content += `Generals Code 3677\n`; + } + if (user.rank <= 19) { + content += `Wipe Code 0425\n`; + } console.log('Content length', content.length, 'characters.'); try { await discordMember.send({ diff --git a/chain-of-command.js b/chain-of-command.js index a7ad1f9..96fa965 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -9,39 +9,33 @@ const Sleep = require('./sleep'); const UserCache = require('./user-cache'); async function CalculateChainOfCommand() { - console.log('Chain of command'); - // Populate vertex data from discord. + const recruitRank = RankMetadata.length - 1; const members = UserCache.GetAllUsersAsFlatList(); - members.sort((a, b) => { - let aScore = a.harmonic_centrality || 0; - let bScore = b.harmonic_centrality || 0; - if (a.ban_conviction_time && a.ban_pardon_time) { - aScore = 0; - } - if (b.ban_conviction_time && b.ban_pardon_time) { - bScore = 0; - } - if (!a.citizen) { - aScore = 0; - } - if (!b.citizen) { - bScore = 0; + for (const m of members) { + if (!m.citizen || m.ban_conviction_time || m.ban_pardon_time) { + await m.setRank(recruitRank); + await m.setRankScore(0); + await m.setRankIndex(999); + await m.setHarmonicCentrality(0); + continue; } - return bScore - aScore; + //console.log(`${m.getNicknameOrTitleWithInsignia()},${m.harmonic_centrality},${m.calendar_day_count},${m.calendar_month_count}`); + const hc = m.harmonic_centrality || 0; + const dc = m.calendar_day_count || 0; + const mc = m.calendar_month_count || 0; + const r = hc * Math.sqrt(dc * mc); + await m.setRankScore(r); + } + members.sort((a, b) => { + return b.rank_score - a.rank_score; }); - // TODO: bring back the new guy demotion with daily and monthly activity counters - // linked to discord activity instead of Rust+ activity. // Assign discrete ranks to each player. let rank = 0; let usersAtRank = 0; let rankIndex = 1; - const recruitRank = RankMetadata.length - 1; console.log('members.length', members.length); for (const m of members) { if (!m.citizen) { - await m.setRank(recruitRank); - await m.setRankScore(0); - await m.setRankIndex(999); continue; } while (usersAtRank >= RankMetadata[rank].count) { @@ -51,11 +45,8 @@ async function CalculateChainOfCommand() { // When we run out of ranks, this line defaults to the last/least rank. rank = Math.max(0, Math.min(RankMetadata.length - 1, rank)); // Write the rank to the vertex record. - m.rank = rank; - // Do not await the promotion announcement. Fire and forget. - AnnounceIfPromotion(m, m.rank, rank); + await AnnounceIfPromotion(m, m.rank, rank); await m.setRank(rank); - await m.setRankScore(m.harmonic_centrality); await m.setRankIndex(rankIndex); rankIndex++; usersAtRank++; diff --git a/huddles.js b/huddles.js index 957622f..133b5d3 100644 --- a/huddles.js +++ b/huddles.js @@ -18,9 +18,9 @@ const huddles = [ { name: 'Main', userLimit: 99, position: 1000 }, { name: 'Duo', userLimit: 2, position: 2000 }, { name: 'Trio', userLimit: 3, position: 3000 }, - //{ name: 'Quad', userLimit: 4, position: 4000 }, + { name: 'Quad', userLimit: 4, position: 4000 }, //{ name: 'Six Pack', userLimit: 6, position: 6000 }, - //{ name: 'Squad', userLimit: 8, position: 7000 }, + { name: 'Squad', userLimit: 8, position: 7000 }, ]; function GetAllMatchingVoiceChannels(guild, huddle) { diff --git a/rank-definitions.js b/rank-definitions.js index 41e81ce..903ad70 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -175,14 +175,14 @@ module.exports = [ }, { color: '#4285F4', - count: 200, + count: 300, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], title: 'Corporal', }, { color: '#4285F4', - count: 600, + count: 500, insignia: '⦁', roles: [RoleID.Private, RoleID.Grunt], title: 'Private', diff --git a/user-cache.js b/user-cache.js index be41306..65ee4de 100644 --- a/user-cache.js +++ b/user-cache.js @@ -185,9 +185,6 @@ async function GetAllNicknames() { // // topN - the number of top most central users to return. Omit this // to return all citizens sorted by centrality. -// -// Returns a list of pairs: -// [(commissar_id, centrality), ...] function GetMostCentralUsers(topN) { const flat = []; for (const [commissarId, user] of Object.entries(commissarUserCache)) { @@ -201,6 +198,22 @@ function GetMostCentralUsers(topN) { return flat.slice(0, topN); } +// Get the N top users by rank score. +// +// topN - the number of top users to return. +function GetTopRankedUsers(topN) { + const flat = []; + for (const [commissarId, user] of Object.entries(commissarUserCache)) { + if (user.citizen) { + flat.push(user); + } + } + flat.sort((a, b) => { + return b.rank_score - a.rank_score; + }); + return flat.slice(0, topN); +} + function GetAllUsersAsFlatList() { const flat = []; for (const i in commissarUserCache) { @@ -360,6 +373,7 @@ module.exports = { GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName, GetMostCentralUsers, GetOrCreateUserByDiscordId, + GetTopRankedUsers, GetUsersSortedByLastSeen, GetUsersWithRankAndScoreHigherThan, LoadAllUsersFromDatabase, From f418e1f8a3807a69bcbd829db8ee6c14b6fc1bd2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 21:42:13 +0000 Subject: [PATCH 086/101] Automate unbans. --- ban.js | 74 +++++++++++++++++++++++++++++++++++++++++++++++++++ server.js | 1 + user-cache.js | 12 +++++++++ 3 files changed, 87 insertions(+) diff --git a/ban.js b/ban.js index ace21a7..f95497f 100644 --- a/ban.js +++ b/ban.js @@ -499,11 +499,85 @@ async function RateLimitBanCourtMessage(discordMessage) { } } +// Discord IDs with known issues to avoid wasting the bot's time and rate limit. +const temporarilyIgnoreTheseDiscordIdsFromUnbanning = {}; + +async function UnbanEligibleUsers() { + const guild = await DiscordUtil.GetMainDiscordGuild(); + const currentTime = moment(); + const bannedUsers = UserCache.GetAllBannedUsers(); + for (const u of bannedUsers) { + if (u.discord_id in temporarilyIgnoreTheseDiscordIdsFromUnbanning) { + continue; + } + if (!u.ban_conviction_time) { + continue; + } + const convictionTime = moment(u.ban_conviction_time); + const defaultPardonTime = convictionTime.clone().add(365, 'days'); + let pardonTime; + if (u.ban_pardon_time) { + pardonTime = moment(u.ban_pardon_time); + } else { + pardonTime = defaultPardonTime; + } + if (pardonTime.year() === 0) { + pardonTime = defaultPardonTime; + } + const sentenceLengthInDays = pardonTime.diff(convictionTime, 'days'); + if (sentenceLengthInDays < 0 || sentenceLengthInDays > 9000) { + console.log('Weird sentence length', sentenceLengthInDays, 'for user', u.discord_id, u.nickname, u.nick); + console.log(pardonTime.format(), convictionTime.format()); + temporarilyIgnoreTheseDiscordIdsFromUnbanning[u.discord_id] = true; + continue; + } + if (currentTime.isAfter(pardonTime)) { + try { + console.log('Trying to unban user', u.discord_id, u.commissar_id, u.nickname, u.nick); + let banRecord = null; + try { + banRecord = await guild.bans.fetch(u.discord_id); + } catch (innerError) { + banRecord = null; + } + if (!banRecord) { + console.log('WARNING: Could not locate discord ban record for', u.discord_id); + temporarilyIgnoreTheseDiscordIdsFromUnbanning[u.discord_id] = true; + continue; + } + const unbannedDiscordUser = await guild.bans.remove(u.discord_id); + if (!unbannedDiscordUser) { + console.log('User banned in database but failed to unban from discord'); + temporarilyIgnoreTheseDiscordIdsFromUnbanning[u.discord_id] = true; + continue; + } + await u.setGoodStanding(true); + await u.setBanConvictionTime(null); + await u.setBanPardonTime(null); + await u.setBanVoteStartTime(null); + await u.setBanVoteChatroom(null); + await u.setBanVoteMessage(null); + const name = u.nick || u.nickname || 'John Doe'; + const message = '```' + `${name} is unbanned after ${sentenceLengthInDays} days in the hole. They have not been notified. ID ${u.discord_id}` + '```'; + await DiscordUtil.MessagePublicChatChannel(message); + console.log(message); + console.log('Successfully unbanned user', u.discord_id, u.commissar_id, u.nickname, u.nick); + // Bail on successful unban so that we only unban one person at a time. + break; + } catch (error) { + console.log('Failed to unban user', u.discord_id, u.commissar_id, u.nickname, u.nick); + console.log(error); + } + } + } +} + module.exports = { HandleBanCommand, HandleConvictCommand, HandlePardonCommand, HandlePossibleReaction, RateLimitBanCourtMessage, + UnbanEligibleUsers, UpdateTrial, }; diff --git a/server.js b/server.js index 8a8f6a5..a398a1d 100644 --- a/server.js +++ b/server.js @@ -266,6 +266,7 @@ async function RoutineUpdate() { await yen.DoLottery(); await recruiting.ScanInvitesForChanges(); await BanVoteCache.ExpungeVotesWithNoOngoingTrial(); + await Ban.UnbanEligibleUsers(); await AutoUpdate(); const endTime = new Date().getTime(); const elapsed = endTime - startTime; diff --git a/user-cache.js b/user-cache.js index 65ee4de..5685601 100644 --- a/user-cache.js +++ b/user-cache.js @@ -357,11 +357,23 @@ function GetOneSteamConnectedUserWithLeastRecentlyUpdatedSteamName() { return chosenUser; } +function GetAllBannedUsers() { + const flat = []; + for (const i in commissarUserCache) { + const u = commissarUserCache[i]; + if (u.ban_conviction_time && u.ban_pardon_time) { + flat.push(u); + } + } + return flat; +} + module.exports = { BulkCentralityUpdate, CountVoiceActiveUsers, CreateNewDatabaseUser, ForEach, + GetAllBannedUsers, GetAllCitizenCommissarIds, GetAllNicknames, GetAllUsersAsFlatList, From ddb7323b6b1dfedd7783afb7315bd4c8630668e7 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 21:42:49 +0000 Subject: [PATCH 087/101] Adding a file I must have forgot to add. --- exile-cache.js | 100 +++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 100 insertions(+) create mode 100644 exile-cache.js diff --git a/exile-cache.js b/exile-cache.js new file mode 100644 index 0000000..a55b247 --- /dev/null +++ b/exile-cache.js @@ -0,0 +1,100 @@ +const db = require('./database'); + +let exileCache = []; + +async function LoadExilesFromDatabase() { + let newCache = []; + const results = await db.Query('SELECT * FROM exiles'); + for (const row of results) { + newCache.push({ + exiler: row.exiler, + exilee: row.exilee, + is_friend: row.is_friend, + }); + } + exileCache = newCache; +} + +async function AddExile(exiler, exilee) { + if (IsExiled(exiler, exilee)) { + return; + } + exileCache.push({ exiler, exilee, is_friend: false }); + await db.Query('INSERT INTO exiles (exiler, exilee, is_friend) VALUES (?, ?, FALSE)', [exiler, exilee]); +} + +async function AddFriend(exiler, exilee) { + if (IsFriend(exiler, exilee)) { + return; + } + exileCache.push({ exiler, exilee, is_friend: true }); + await db.Query('INSERT INTO exiles (exiler, exilee, is_friend) VALUES (?, ?, TRUE)', [exiler, exilee]); +} + +function GetAllExilesAsList() { + // Return a copy of the cache to avoid any shenanigans. + const result = []; + for (const ex of exileCache) { + result.push({ + exiler: ex.exiler, + exilee: ex.exilee, + is_friend: ex.is_friend, + }); + } + return result; +} + +function IsExiled(exiler, exilee) { + for (const ex of exileCache) { + if (ex.exiler === exiler && ex.exilee === exilee && !ex.is_friend) { + return true; + } + } + return false; +} + +function IsFriend(exiler, exilee) { + for (const ex of exileCache) { + if (ex.exiler === exiler && ex.exilee === exilee && ex.is_friend) { + return true; + } + } + return false; +} + +async function Unexile(exiler, exilee) { + let newCache = []; + let found = false; + for (const ex of exileCache) { + if (ex.exiler === exiler && ex.exilee === exilee) { + found = true; + } else { + newCache.push(ex); + } + } + if (!found) { + return; + } + exileCache = newCache; + await db.Query('DELETE FROM exiles WHERE exiler = ? AND exilee = ?', [exiler, exilee]); +} + +async function SetIsFriend(exiler, exilee, is_friend) { + await Unexile(exiler, exilee); + if (is_friend) { + await AddFriend(exiler, exilee); + } else { + await AddExile(exiler, exilee); + } +} + +module.exports = { + AddExile, + AddFriend, + GetAllExilesAsList, + IsExiled, + IsFriend, + LoadExilesFromDatabase, + SetIsFriend, + Unexile, +}; From e512a6dc4f9bd380e2e8f94a2b7ee6f926b2881c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 21:57:43 +0000 Subject: [PATCH 088/101] Changed number of Generals from 19 to 20 and added a 000 rank. --- chain-of-command.js | 2 +- commissar-user.js | 3 +++ 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/chain-of-command.js b/chain-of-command.js index 96fa965..87b5935 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -32,7 +32,7 @@ async function CalculateChainOfCommand() { // Assign discrete ranks to each player. let rank = 0; let usersAtRank = 0; - let rankIndex = 1; + let rankIndex = 0; console.log('members.length', members.length); for (const m of members) { if (!m.citizen) { diff --git a/commissar-user.js b/commissar-user.js index 53fbf42..9363933 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -438,6 +438,9 @@ class CommissarUser { getFormattedRankIndex() { const i = this.rank_index; + if (i === 0) { + return '000'; + } if (!i) { return '999'; } From 3070c7b48c91ed1671c987bd4c954f948f60e957 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 22:33:42 +0000 Subject: [PATCH 089/101] Max sentence 365 days. --- ban.js | 31 ++++++++----------------------- chain-of-command.js | 1 - 2 files changed, 8 insertions(+), 24 deletions(-) diff --git a/ban.js b/ban.js index f95497f..6a7e9c5 100644 --- a/ban.js +++ b/ban.js @@ -14,22 +14,6 @@ const threeTicks = '```'; const banCommandRank = 15; const banVoteRank = 19; -function SentenceLengthAsString(years) { - if (years <= 0) { - return '0 days'; - } - const daysPerYear = 365.25; - const days = years * daysPerYear; - if (days < 50) { - return `${Math.round(days)} days`; - } - const months = 12 * days / daysPerYear; - if (months < 23) { - return `${Math.round(months)} months`; - } - return `${Math.round(years)} years`; -} - async function UpdateTrial(cu) { if (!cu.ban_vote_start_time) { // No trial to update. @@ -154,14 +138,15 @@ async function UpdateTrial(cu) { const guilty = VoteDuration.SimpleMajority(yesVoteCount, noVoteCount); let banPardonTime; if (guilty) { - // How guilty the defendant is based on the vote margin. Ranges between 0 and 1. - // One extra "mercy vote" is added to the denominator. This creates a bias in the - // system towards more lenient sentences that wears off as more voters weigh in. - const howGuilty = (yesVoteCount - noVoteCount) / (yesVoteCount + noVoteCount + 1); - const sentenceYears = howGuilty; - const banLengthInSeconds = Math.round(sentenceYears * 365.25 * 86400); + const clippedYesPercentage = Math.max(Math.min(yesPercentage, 0.67), 0.5); + const sentenceFraction = (clippedYesPercentage - 0.5) / (0.67 - 0.5); + const sentenceDays = Math.round(365 * sentenceFraction); + const banLengthInSeconds = Math.round(sentenceDays * 24 * 60 * 60); banPardonTime = moment().add(banLengthInSeconds, 'seconds').format(); - outcomeString = 'banned for ' + SentenceLengthAsString(sentenceYears); + outcomeString = `banned for ${sentenceDays} days`; + if (sentenceDays === 365) { + outcomeString += ' (life sentence)'; + } } const caseTitle = `THE GOVERNMENT v ${cu.getNicknameOrTitleWithInsignia()}`; const underline = new Array(caseTitle.length + 1).join('-'); diff --git a/chain-of-command.js b/chain-of-command.js index 87b5935..c29342e 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -33,7 +33,6 @@ async function CalculateChainOfCommand() { let rank = 0; let usersAtRank = 0; let rankIndex = 0; - console.log('members.length', members.length); for (const m of members) { if (!m.citizen) { continue; From fba4e1ad5a178a49ad76febc37b98ffd701952ff Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 23:02:08 +0000 Subject: [PATCH 090/101] Simplify vote duration based on turnout. --- ban.js | 12 +++++++----- server.js | 1 + 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/ban.js b/ban.js index 6a7e9c5..f16b3f3 100644 --- a/ban.js +++ b/ban.js @@ -135,7 +135,7 @@ async function UpdateTrial(cu) { } } let outcomeString = 'NOT GUILTY'; - const guilty = VoteDuration.SimpleMajority(yesVoteCount, noVoteCount); + const guilty = yesVoteCount > noVoteCount; let banPardonTime; if (guilty) { const clippedYesPercentage = Math.max(Math.min(yesPercentage, 0.67), 0.5); @@ -182,8 +182,8 @@ async function UpdateTrial(cu) { await cu.setGoodStanding(true); } await cu.setBanVoteStartTime(startTime.format()); - const fivePercent = 0.05; - const durationDays = VoteDuration.EstimateVoteDuration(totalVoters, yesVoteCount, noVoteCount, baselineVoteDurationDays, fivePercent, VoteDuration.SimpleMajority); + const turnout = voteCount / totalVoters; + const durationDays = baselineVoteDurationDays * (1 - turnout); const durationSeconds = durationDays * 86400; const endTime = startTime.add(durationSeconds, 'seconds'); if (currentTime.isAfter(endTime)) { @@ -214,7 +214,8 @@ async function UpdateTrial(cu) { `${caseTitle}\n` + `${underline}\n` + `Voting YES to ban: ${yesVoteCount}\n` + - `Voting NO against the ban:${noVoteCount}\n\n` + + `Voting NO against the ban: ${noVoteCount}\n` + + `Total Votes: ${voteCount}\n\n` + `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}` + `${threeTicks}` ); @@ -249,7 +250,8 @@ async function UpdateTrial(cu) { `${caseTitle}\n` + `${underline}\n` + `Voting YES to ban: ${yesVoteCount}\n` + - `Voting NO against the ban: ${noVoteCount}\n\n` + + `Voting NO against the ban: ${noVoteCount}\n` + + `Total Votes: ${voteCount}\n\n` + `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}. ` + `The vote ends ${timeRemaining}.` + `${threeTicks}` diff --git a/server.js b/server.js index a398a1d..c2cd520 100644 --- a/server.js +++ b/server.js @@ -117,6 +117,7 @@ async function UpdateVoiceActiveMembersForMainDiscordGuild() { continue; } channelActive.push(cu.commissar_id); + await cu.updateCalendarDayCount(); } if (channelActive.length >= 2) { listOfLists.push(channelActive); From 548de8278334e8119ff4c7d0361ffc2c20eb1f66 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 23:08:54 +0000 Subject: [PATCH 091/101] Fixed promotion announcements for recruits. --- chain-of-command.js | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index c29342e..765f17a 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -98,10 +98,16 @@ async function AnnounceIfPromotion(user, oldRank, newRank) { maxRankByCommissarId[user.commissar_id] = newMaxRank; // If we get past here, a promotion has been detected. // Announce it in #public chat. - const name = user.getNicknameOrTitleWithInsignia(); + const name = user.nick || user.nickname || 'John Doe'; const oldMeta = RankMetadata[oldRank]; const newMeta = RankMetadata[newRank]; - const message = `${name} is promoted from ${oldMeta.title} ${oldMeta.insignia} to ${newMeta.title} ${newMeta.insignia}`; + const oldInsignia; + if (oldMeta.insignia) { + oldInsignia = oldMeta.insignia + ' '; + } else { + oldInsignia = ''; + } + const message = `${name} is promoted from ${oldMeta.title} ${oldInsignia}to ${newMeta.title} ${newMeta.insignia}`; console.log(message); await DiscordUtil.MessagePublicChatChannel(message); } From 45bbf336da497bfca18027351d4cab7897bef30a Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sat, 14 Sep 2024 23:10:34 +0000 Subject: [PATCH 092/101] Bug fix --- chain-of-command.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/chain-of-command.js b/chain-of-command.js index 765f17a..f1831df 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -101,7 +101,7 @@ async function AnnounceIfPromotion(user, oldRank, newRank) { const name = user.nick || user.nickname || 'John Doe'; const oldMeta = RankMetadata[oldRank]; const newMeta = RankMetadata[newRank]; - const oldInsignia; + let oldInsignia; if (oldMeta.insignia) { oldInsignia = oldMeta.insignia + ' '; } else { From 96b227ac1be390897cd28e870bfdc51a6252dcf4 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 15 Sep 2024 00:39:34 +0000 Subject: [PATCH 093/101] Calendar day and month counts now default to 1 instead of 0. --- chain-of-command.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/chain-of-command.js b/chain-of-command.js index f1831df..e44fc6a 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -21,8 +21,8 @@ async function CalculateChainOfCommand() { } //console.log(`${m.getNicknameOrTitleWithInsignia()},${m.harmonic_centrality},${m.calendar_day_count},${m.calendar_month_count}`); const hc = m.harmonic_centrality || 0; - const dc = m.calendar_day_count || 0; - const mc = m.calendar_month_count || 0; + const dc = m.calendar_day_count || 1; + const mc = m.calendar_month_count || 1; const r = hc * Math.sqrt(dc * mc); await m.setRankScore(r); } From c31c5034e1224502e3552cf95e028f25f97ecfb8 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 15 Sep 2024 01:48:16 +0000 Subject: [PATCH 094/101] Stylistic cleanups. --- ban.js | 6 +++--- chain-of-command.js | 2 +- rank-definitions.js | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/ban.js b/ban.js index f16b3f3..4b31b49 100644 --- a/ban.js +++ b/ban.js @@ -148,7 +148,7 @@ async function UpdateTrial(cu) { outcomeString += ' (life sentence)'; } } - const caseTitle = `THE GOVERNMENT v ${cu.getNicknameOrTitleWithInsignia()}`; + const caseTitle = `THE GOVERNMENT v ${roomName}`; const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); @@ -216,7 +216,7 @@ async function UpdateTrial(cu) { `Voting YES to ban: ${yesVoteCount}\n` + `Voting NO against the ban: ${noVoteCount}\n` + `Total Votes: ${voteCount}\n\n` + - `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}` + + `${roomName} is ${outcomeString}` + `${threeTicks}` ); await message.edit(trialSummary); @@ -252,7 +252,7 @@ async function UpdateTrial(cu) { `Voting YES to ban: ${yesVoteCount}\n` + `Voting NO against the ban: ${noVoteCount}\n` + `Total Votes: ${voteCount}\n\n` + - `${cu.getNicknameOrTitleWithInsignia()} is ${outcomeString}. ` + + `${roomName} is ${outcomeString}. ` + `The vote ends ${timeRemaining}.` + `${threeTicks}` ); diff --git a/chain-of-command.js b/chain-of-command.js index e44fc6a..d6a0085 100644 --- a/chain-of-command.js +++ b/chain-of-command.js @@ -107,7 +107,7 @@ async function AnnounceIfPromotion(user, oldRank, newRank) { } else { oldInsignia = ''; } - const message = `${name} is promoted from ${oldMeta.title} ${oldInsignia}to ${newMeta.title} ${newMeta.insignia}`; + const message = `${name} ${newMeta.insignia} is promoted from ${oldMeta.title} ${oldInsignia}to ${newMeta.title} ${newMeta.insignia}`; console.log(message); await DiscordUtil.MessagePublicChatChannel(message); } diff --git a/rank-definitions.js b/rank-definitions.js index 903ad70..89e7748 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -126,7 +126,7 @@ module.exports = [ { banPower: true, color: '#F4B400', - count: 19, + count: 20, insignia: '★', roles: [RoleID.General], title: 'General', From 31050053e8a7e7f5217daf17832d15fc270c294c Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 15 Sep 2024 02:52:48 +0000 Subject: [PATCH 095/101] Weighted voting. --- ban-vote-cache.js | 24 +++++++++++++++++++----- ban.js | 37 ++++++++++++++++++------------------- rank-definitions.js | 9 +++++++++ 3 files changed, 46 insertions(+), 24 deletions(-) diff --git a/ban-vote-cache.js b/ban-vote-cache.js index 67568b4..d0d1192 100644 --- a/ban-vote-cache.js +++ b/ban-vote-cache.js @@ -1,4 +1,5 @@ const db = require('./database'); +const RankMetadata = require('./rank-definitions'); const UserCache = require('./user-cache'); const voteCache = {}; @@ -10,8 +11,13 @@ async function DeleteVotesForDefendant(defendantId) { await db.Query('DELETE FROM ban_votes WHERE defendant_id = ?', [defendantId]); } -function CountVotesForDefendant(defendantId) { - const totals = { +function CountTotalVotesForDefendant(defendantId) { + const votes = voteCache[defendantId] || {}; + return Object.keys(votes).length; +} + +function CountWeightedVotesForDefendant(defendantId) { + const w = { 0: 0, 1: 0, 2: 0, @@ -19,9 +25,16 @@ function CountVotesForDefendant(defendantId) { const votes = voteCache[defendantId] || {}; for (const voterId in votes) { const vote = votes[voterId]; - totals[vote]++; + const voter = UserCache.GetCachedUserByCommissarId(voterId); + const rankData = RankMetadata[voter.rank]; + const individualVoteWeight = rankData.collectiveVoteWeight / rankData.count; + w[vote] += individualVoteWeight; } - return totals; + //const m = 1 / (w[0] + w[1] + w[2]); + //w[0] *= m; + //w[1] *= m; + //w[2] *= m; + return w; } async function ExpungeVotesWithNoOngoingTrial() { @@ -71,7 +84,8 @@ async function RecordVoteIfChanged(defendantId, voterId, vote) { } module.exports = { - CountVotesForDefendant, + CountTotalVotesForDefendant, + CountWeightedVotesForDefendant, DeleteVotesForDefendant, ExpungeVotesWithNoOngoingTrial, LoadVotesFromDatabase, diff --git a/ban.js b/ban.js index 4b31b49..108f28d 100644 --- a/ban.js +++ b/ban.js @@ -101,11 +101,16 @@ async function UpdateTrial(cu) { await reaction.users.remove(juror); } } - const voteTotals = BanVoteCache.CountVotesForDefendant(cu.commissar_id); - const yesVoteCount = voteTotals[1]; - const noVoteCount = voteTotals[2]; - const voteCount = yesVoteCount + noVoteCount; - const yesPercentage = voteCount > 0 ? yesVoteCount / voteCount : 0; + const weighted = BanVoteCache.CountWeightedVotesForDefendant(cu.commissar_id); + const yesWeight = weighted[1]; + const noWeight = weighted[2]; + const combinedWeight = yesWeight + noWeight; + const availableWeight = 120; + const voteCount = BanVoteCache.CountTotalVotesForDefendant(cu.commissar_id); + const yesPercentage = voteCount > 0 ? yesWeight / combinedWeight : 0; + const noPercentage = voteCount > 0 ? noWeight / combinedWeight : 0; + const formattedYesPercentage = Math.round(yesPercentage * 100).toString() + '%'; + const formattedNoPercentage = Math.round(noPercentage * 100).toString() + '%'; if (member) { const before = await channel.permissionOverwrites.resolve(member.id); if (cu.peak_rank >= 20 && voteCount >= 5 && yesPercentage >= 0.909) { @@ -135,7 +140,7 @@ async function UpdateTrial(cu) { } } let outcomeString = 'NOT GUILTY'; - const guilty = yesVoteCount > noVoteCount; + const guilty = yesWeight > noWeight; let banPardonTime; if (guilty) { const clippedYesPercentage = Math.max(Math.min(yesPercentage, 0.67), 0.5); @@ -152,13 +157,9 @@ async function UpdateTrial(cu) { const underline = new Array(caseTitle.length + 1).join('-'); const currentTime = moment(); let startTime = moment(cu.ban_vote_start_time); - const totalVoters = 59; let baselineVoteDurationDays; - let nextStateChangeMessage; if (guilty) { baselineVoteDurationDays = 3; - const n = VoteDuration.HowManyMoreNoVotes(yesVoteCount, noVoteCount, VoteDuration.SimpleMajority); - nextStateChangeMessage = `${n} more NO votes to unban`; if (cu.good_standing) { // Vote outcome flipped. Reset the clock. startTime = currentTime; @@ -173,8 +174,6 @@ async function UpdateTrial(cu) { } } else { baselineVoteDurationDays = 1; - const n = VoteDuration.HowManyMoreYesVotes(yesVoteCount, noVoteCount, VoteDuration.SimpleMajority); - nextStateChangeMessage = `${n} more YES votes to ban`; if (!cu.good_standing) { // Vote outcome flipped. Reset the clock. startTime = currentTime; @@ -182,7 +181,7 @@ async function UpdateTrial(cu) { await cu.setGoodStanding(true); } await cu.setBanVoteStartTime(startTime.format()); - const turnout = voteCount / totalVoters; + const turnout = combinedWeight / availableWeight; const durationDays = baselineVoteDurationDays * (1 - turnout); const durationSeconds = durationDays * 86400; const endTime = startTime.add(durationSeconds, 'seconds'); @@ -213,9 +212,9 @@ async function UpdateTrial(cu) { `${threeTicks}` + `${caseTitle}\n` + `${underline}\n` + - `Voting YES to ban: ${yesVoteCount}\n` + - `Voting NO against the ban: ${noVoteCount}\n` + - `Total Votes: ${voteCount}\n\n` + + `Voting YES to ban: ${formattedYesPercentage}\n` + + `Voting NO against the ban: ${formattedNoPercentage}\n` + + `Votes: ${voteCount}\n\n` + `${roomName} is ${outcomeString}` + `${threeTicks}` ); @@ -249,9 +248,9 @@ async function UpdateTrial(cu) { `${threeTicks}` + `${caseTitle}\n` + `${underline}\n` + - `Voting YES to ban: ${yesVoteCount}\n` + - `Voting NO against the ban: ${noVoteCount}\n` + - `Total Votes: ${voteCount}\n\n` + + `Voting YES to ban: ${formattedYesPercentage}\n` + + `Voting NO against the ban: ${formattedNoPercentage}\n` + + `Votes: ${voteCount}\n\n` + `${roomName} is ${outcomeString}. ` + `The vote ends ${timeRemaining}.` + `${threeTicks}` diff --git a/rank-definitions.js b/rank-definitions.js index 89e7748..f361483 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -125,6 +125,7 @@ module.exports = [ }, { banPower: true, + collectiveVoteWeight: 40, color: '#F4B400', count: 20, insignia: '★', @@ -132,6 +133,7 @@ module.exports = [ title: 'General', }, { + collectiveVoteWeight: 10, color: '#DB4437', count: 10, insignia: '❱❱❱❱', @@ -139,6 +141,7 @@ module.exports = [ title: 'Colonel', }, { + collectiveVoteWeight: 10, color: '#DB4437', count: 10, insignia: '❱❱❱', @@ -146,6 +149,7 @@ module.exports = [ title: 'Major', }, { + collectiveVoteWeight: 10, color: '#DB4437', count: 10, insignia: '❱❱', @@ -153,6 +157,7 @@ module.exports = [ title: 'Captain', }, { + collectiveVoteWeight: 10, color: '#DB4437', count: 10, insignia: '❱', @@ -160,6 +165,7 @@ module.exports = [ title: 'Lieutenant', }, { + collectiveVoteWeight: 10, color: '#4285F4', count: 40, insignia: '⦁⦁⦁⦁', @@ -167,6 +173,7 @@ module.exports = [ title: 'Ensign', }, { + collectiveVoteWeight: 10, color: '#4285F4', count: 100, insignia: '⦁⦁⦁', @@ -174,6 +181,7 @@ module.exports = [ title: 'Sergeant', }, { + collectiveVoteWeight: 10, color: '#4285F4', count: 300, insignia: '⦁⦁', @@ -181,6 +189,7 @@ module.exports = [ title: 'Corporal', }, { + collectiveVoteWeight: 10, color: '#4285F4', count: 500, insignia: '⦁', From 55ef7df0d14088b8d8f301845290be2a4d688ecd Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 15 Sep 2024 17:43:11 +0000 Subject: [PATCH 096/101] Made dollar sign read as character s for non-sighted users. --- ban.js | 12 +++++++++--- filter-username.js | 1 + 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/ban.js b/ban.js index 108f28d..809f6be 100644 --- a/ban.js +++ b/ban.js @@ -214,7 +214,7 @@ async function UpdateTrial(cu) { `${underline}\n` + `Voting YES to ban: ${formattedYesPercentage}\n` + `Voting NO against the ban: ${formattedNoPercentage}\n` + - `Votes: ${voteCount}\n\n` + + `Voters: ${voteCount}\n\n` + `${roomName} is ${outcomeString}` + `${threeTicks}` ); @@ -250,12 +250,18 @@ async function UpdateTrial(cu) { `${underline}\n` + `Voting YES to ban: ${formattedYesPercentage}\n` + `Voting NO against the ban: ${formattedNoPercentage}\n` + - `Votes: ${voteCount}\n\n` + + `Voters: ${voteCount}\n\n` + `${roomName} is ${outcomeString}. ` + `The vote ends ${timeRemaining}.` + `${threeTicks}` ); - await message.edit(trialMessage); + await message.edit({ + content: trialMessage, + files: [{ + attachment: 'may-2023-rustopia-gov-village.png', + name: 'may-2023-rustopia-gov-village.png' + }], + }); } } diff --git a/filter-username.js b/filter-username.js index 013ccc5..f247f39 100644 --- a/filter-username.js +++ b/filter-username.js @@ -34,6 +34,7 @@ function FilterUsername(username) { 'ł': 'l', 'ø': 'o', 'Ł': 'L', + '$': 's', }; for (const [before, after] of Object.entries(substitutions)) { username = username.split(before).join(after); From bc82bf28103abf34f203fde5d3eb24d573502069 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 16 Sep 2024 03:31:57 +0000 Subject: [PATCH 097/101] Weighted voting graphic in every ban court. --- ban-vote-cache.js | 29 ++++++++++++--- ban.js | 88 +++++++++++++++++++++++++++++++++++---------- rank-definitions.js | 1 + server.js | 2 +- 4 files changed, 97 insertions(+), 23 deletions(-) diff --git a/ban-vote-cache.js b/ban-vote-cache.js index d0d1192..a889193 100644 --- a/ban-vote-cache.js +++ b/ban-vote-cache.js @@ -16,6 +16,30 @@ function CountTotalVotesForDefendant(defendantId) { return Object.keys(votes).length; } +function GetSortedVotesForDefendant(defendantId) { + const w = { + 0: [], + 1: [], + 2: [], + }; + const votes = voteCache[defendantId] || {}; + for (const voterId in votes) { + const vote = votes[voterId]; + const voter = UserCache.GetCachedUserByCommissarId(voterId); + const rankData = RankMetadata[voter.rank]; + const individualVoteWeight = rankData.collectiveVoteWeight / rankData.count; + w[vote].push({ + color: rankData.color, + weight: individualVoteWeight, + }); + } + const compareWeight = (a, b) => (b.weight - a.weight); + w[0].sort(compareWeight); + w[1].sort(compareWeight); + w[2].sort(compareWeight); + return w; +} + function CountWeightedVotesForDefendant(defendantId) { const w = { 0: 0, @@ -30,10 +54,6 @@ function CountWeightedVotesForDefendant(defendantId) { const individualVoteWeight = rankData.collectiveVoteWeight / rankData.count; w[vote] += individualVoteWeight; } - //const m = 1 / (w[0] + w[1] + w[2]); - //w[0] *= m; - //w[1] *= m; - //w[2] *= m; return w; } @@ -88,6 +108,7 @@ module.exports = { CountWeightedVotesForDefendant, DeleteVotesForDefendant, ExpungeVotesWithNoOngoingTrial, + GetSortedVotesForDefendant, LoadVotesFromDatabase, RecordVoteIfChanged, }; diff --git a/ban.js b/ban.js index 809f6be..dddd288 100644 --- a/ban.js +++ b/ban.js @@ -1,5 +1,6 @@ const BadWords = require('./bad-words'); const BanVoteCache = require('./ban-vote-cache'); +const Canvas = require('canvas'); const discordTranscripts = require('discord-html-transcripts'); const DiscordUtil = require('./discord-util'); const moment = require('moment'); @@ -11,8 +12,9 @@ const threeTicks = '```'; // General 1 = 15 // Major = 17 // Lieutenant = 19 +// Private = 23 const banCommandRank = 15; -const banVoteRank = 19; +const banVoteRank = 23; async function UpdateTrial(cu) { if (!cu.ban_vote_start_time) { @@ -143,15 +145,12 @@ async function UpdateTrial(cu) { const guilty = yesWeight > noWeight; let banPardonTime; if (guilty) { - const clippedYesPercentage = Math.max(Math.min(yesPercentage, 0.67), 0.5); - const sentenceFraction = (clippedYesPercentage - 0.5) / (0.67 - 0.5); - const sentenceDays = Math.round(365 * sentenceFraction); + const clippedYesPercentage = Math.max(Math.min(yesPercentage, 0.7), 0.5); + const sentenceFraction = (clippedYesPercentage - 0.5) / (0.7 - 0.5); + const sentenceDays = Math.max(1, Math.round(365 * sentenceFraction)); const banLengthInSeconds = Math.round(sentenceDays * 24 * 60 * 60); banPardonTime = moment().add(banLengthInSeconds, 'seconds').format(); outcomeString = `banned for ${sentenceDays} days`; - if (sentenceDays === 365) { - outcomeString += ' (life sentence)'; - } } const caseTitle = `THE GOVERNMENT v ${roomName}`; const underline = new Array(caseTitle.length + 1).join('-'); @@ -212,10 +211,10 @@ async function UpdateTrial(cu) { `${threeTicks}` + `${caseTitle}\n` + `${underline}\n` + - `Voting YES to ban: ${formattedYesPercentage}\n` + - `Voting NO against the ban: ${formattedNoPercentage}\n` + - `Voters: ${voteCount}\n\n` + - `${roomName} is ${outcomeString}` + + `${roomName} is ${outcomeString}\n` + + `${formattedYesPercentage} vote YES to ban\n` + + `${formattedNoPercentage} vote NO against the ban\n` + + `${voteCount} voters` + `${threeTicks}` ); await message.edit(trialSummary); @@ -248,18 +247,71 @@ async function UpdateTrial(cu) { `${threeTicks}` + `${caseTitle}\n` + `${underline}\n` + - `Voting YES to ban: ${formattedYesPercentage}\n` + - `Voting NO against the ban: ${formattedNoPercentage}\n` + - `Voters: ${voteCount}\n\n` + - `${roomName} is ${outcomeString}. ` + - `The vote ends ${timeRemaining}.` + + `${roomName} is ${outcomeString}\n` + + `The vote ends ${timeRemaining}\n\n` + + `${formattedYesPercentage} vote YES to ban\n` + + `${formattedNoPercentage} vote NO against the ban\n` + + `${voteCount} voters` + `${threeTicks}` ); + const canvas = new Canvas.Canvas(360, 40); + const context = canvas.getContext('2d'); + context.fillStyle = '#313338'; // Discord grey. + context.fillRect(0, 0, canvas.width, canvas.height); + const sortedVotes = BanVoteCache.GetSortedVotesForDefendant(cu.commissar_id); + console.log('sortedVotes[0]', sortedVotes[0]); + console.log('sortedVotes[1]', sortedVotes[1]); + console.log('sortedVotes[2]', sortedVotes[2]); + if (voteCount > 0) { + const gap = 2; + const widthMinusGap = yesWeight > 0 && noWeight > 0 ? canvas.width - gap : canvas.width; + const maxWeight = Math.max(yesWeight, noWeight); + const yesPixels = Math.round(yesPercentage * widthMinusGap); + const yesVotes = sortedVotes[1]; + let cumulativeYesWeight = 0; + for (const vote of yesVotes) { + const left = Math.floor(yesPixels * cumulativeYesWeight / yesWeight); + cumulativeYesWeight += vote.weight; + const right = Math.floor(yesPixels * cumulativeYesWeight / yesWeight); + let rectangleWidth = right - left - gap; + if (rectangleWidth <= 0) { + rectangleWidth = yesPixels - left - gap; + } + if (rectangleWidth <= 0) { + break; + } + context.fillStyle = vote.color; + context.fillRect(left, 0, rectangleWidth, canvas.height); + } + const noPixels = widthMinusGap - yesPixels; + const noVotes = sortedVotes[2]; + let cumulativeNoWeight = 0; + for (const vote of noVotes) { + const left = Math.floor(noPixels * cumulativeNoWeight / noWeight); + cumulativeNoWeight += vote.weight; + const right = Math.floor(noPixels * cumulativeNoWeight / noWeight); + let rectangleWidth = right - left - gap; + if (rectangleWidth <= 0) { + rectangleWidth = noPixels - left - gap; + } + if (rectangleWidth <= 0) { + break; + } + context.fillStyle = vote.color; + context.fillRect(canvas.width - left - rectangleWidth, 0, rectangleWidth, canvas.height); + } + if (yesWeight > 0 && noWeight > 0) { + context.fillStyle = '#FFFFFF'; + context.fillRect(yesPixels, 19, 2, 2); + } + } + const buffer = canvas.toBuffer(); + const imageFilename = `case-${cu.commissar_id}.png`; await message.edit({ content: trialMessage, files: [{ - attachment: 'may-2023-rustopia-gov-village.png', - name: 'may-2023-rustopia-gov-village.png' + attachment: buffer, + name: imageFilename, }], }); } diff --git a/rank-definitions.js b/rank-definitions.js index f361483..176da9b 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -197,6 +197,7 @@ module.exports = [ title: 'Private', }, { + collectiveVoteWeight: 0, color: '#189b17', count: 1000 * 1000, roles: [RoleID.Recruit], diff --git a/server.js b/server.js index c2cd520..b1f4df0 100644 --- a/server.js +++ b/server.js @@ -272,7 +272,7 @@ async function RoutineUpdate() { const endTime = new Date().getTime(); const elapsed = endTime - startTime; console.log(`Update Time: ${elapsed} ms`); - const sleepTime = Math.max(9000, 60000 - elapsed); + const sleepTime = 60000; setTimeout(RoutineUpdate, sleepTime); } From 84f229a53b3e1cbe1b0469318167d08f059e4764 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 16 Sep 2024 20:11:30 +0000 Subject: [PATCH 098/101] Experimenting with darker shades of color for no votes. --- ban-vote-cache.js | 8 +++----- commissar-user.js | 4 ++-- rank-definitions.js | 10 ++++++++++ 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/ban-vote-cache.js b/ban-vote-cache.js index a889193..6a5a43c 100644 --- a/ban-vote-cache.js +++ b/ban-vote-cache.js @@ -27,11 +27,9 @@ function GetSortedVotesForDefendant(defendantId) { const vote = votes[voterId]; const voter = UserCache.GetCachedUserByCommissarId(voterId); const rankData = RankMetadata[voter.rank]; - const individualVoteWeight = rankData.collectiveVoteWeight / rankData.count; - w[vote].push({ - color: rankData.color, - weight: individualVoteWeight, - }); + const weight = rankData.collectiveVoteWeight / rankData.count; + const color = (vote === 1) ? rankData.color : rankData.secondaryColor; + w[vote].push({ color, weight }); } const compareWeight = (a, b) => (b.weight - a.weight); w[0].sort(compareWeight); diff --git a/commissar-user.js b/commissar-user.js index 9363933..25683e3 100644 --- a/commissar-user.js +++ b/commissar-user.js @@ -363,13 +363,13 @@ class CommissarUser { const calendarDay = t.format('YYYY-MM-DD'); const calendarMonth = t.format('YYYY-MM'); if (calendarDay !== this.last_calendar_day) { - this.calendar_day_count++; + this.calendar_day_count = (this.calendar_day_count || 0) + 1; this.last_calendar_day = calendarDay; await this.updateFieldInDatabase('calendar_day_count', this.calendar_day_count); await this.updateFieldInDatabase('last_calendar_day', this.last_calendar_day); } if (calendarMonth !== this.last_calendar_month) { - this.calendar_month_count++; + this.calendar_month_count = (this.calendar_month_count || 0) + 1; this.last_calendar_month = calendarMonth; await this.updateFieldInDatabase('calendar_month_count', this.calendar_month_count); await this.updateFieldInDatabase('last_calendar_month', this.last_calendar_month); diff --git a/rank-definitions.js b/rank-definitions.js index 176da9b..3c034df 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -130,6 +130,7 @@ module.exports = [ count: 20, insignia: '★', roles: [RoleID.General], + secondaryColor: '#a37800', title: 'General', }, { @@ -138,6 +139,7 @@ module.exports = [ count: 10, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], + secondaryColor: '#922d25', title: 'Colonel', }, { @@ -146,6 +148,7 @@ module.exports = [ count: 10, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], + secondaryColor: '#922d25', title: 'Major', }, { @@ -154,6 +157,7 @@ module.exports = [ count: 10, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], + secondaryColor: '#922d25', title: 'Captain', }, { @@ -162,6 +166,7 @@ module.exports = [ count: 10, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], + secondaryColor: '#922d25', title: 'Lieutenant', }, { @@ -170,6 +175,7 @@ module.exports = [ count: 40, insignia: '⦁⦁⦁⦁', roles: [RoleID.Ensign, RoleID.Grunt], + secondaryColor: '#2c59a3', title: 'Ensign', }, { @@ -178,6 +184,7 @@ module.exports = [ count: 100, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], + secondaryColor: '#2c59a3', title: 'Sergeant', }, { @@ -186,6 +193,7 @@ module.exports = [ count: 300, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], + secondaryColor: '#2c59a3', title: 'Corporal', }, { @@ -194,6 +202,7 @@ module.exports = [ count: 500, insignia: '⦁', roles: [RoleID.Private, RoleID.Grunt], + secondaryColor: '#2c59a3', title: 'Private', }, { @@ -201,6 +210,7 @@ module.exports = [ color: '#189b17', count: 1000 * 1000, roles: [RoleID.Recruit], + secondaryColor: '#10670f', title: 'Recruit', }, ]; From 71c6450173de1a06bc2aee5a8a99bc1b7e320f93 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 16 Sep 2024 20:19:43 +0000 Subject: [PATCH 099/101] Darker colors. --- rank-definitions.js | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/rank-definitions.js b/rank-definitions.js index 3c034df..9a5d343 100644 --- a/rank-definitions.js +++ b/rank-definitions.js @@ -130,7 +130,7 @@ module.exports = [ count: 20, insignia: '★', roles: [RoleID.General], - secondaryColor: '#a37800', + secondaryColor: '#7a5900', title: 'General', }, { @@ -139,7 +139,7 @@ module.exports = [ count: 10, insignia: '❱❱❱❱', roles: [RoleID.Colonel, RoleID.Officer], - secondaryColor: '#922d25', + secondaryColor: '#6d221b', title: 'Colonel', }, { @@ -148,7 +148,7 @@ module.exports = [ count: 10, insignia: '❱❱❱', roles: [RoleID.Major, RoleID.Officer], - secondaryColor: '#922d25', + secondaryColor: '#6d221b', title: 'Major', }, { @@ -157,7 +157,7 @@ module.exports = [ count: 10, insignia: '❱❱', roles: [RoleID.Captain, RoleID.Officer], - secondaryColor: '#922d25', + secondaryColor: '#6d221b', title: 'Captain', }, { @@ -166,7 +166,7 @@ module.exports = [ count: 10, insignia: '❱', roles: [RoleID.Lieutenant, RoleID.Officer], - secondaryColor: '#922d25', + secondaryColor: '#6d221b', title: 'Lieutenant', }, { @@ -175,7 +175,7 @@ module.exports = [ count: 40, insignia: '⦁⦁⦁⦁', roles: [RoleID.Ensign, RoleID.Grunt], - secondaryColor: '#2c59a3', + secondaryColor: '#21427a', title: 'Ensign', }, { @@ -184,7 +184,7 @@ module.exports = [ count: 100, insignia: '⦁⦁⦁', roles: [RoleID.Sergeant, RoleID.Grunt], - secondaryColor: '#2c59a3', + secondaryColor: '#21427a', title: 'Sergeant', }, { @@ -193,7 +193,7 @@ module.exports = [ count: 300, insignia: '⦁⦁', roles: [RoleID.Corporal, RoleID.Grunt], - secondaryColor: '#2c59a3', + secondaryColor: '#21427a', title: 'Corporal', }, { @@ -202,7 +202,7 @@ module.exports = [ count: 500, insignia: '⦁', roles: [RoleID.Private, RoleID.Grunt], - secondaryColor: '#2c59a3', + secondaryColor: '#21427a', title: 'Private', }, { @@ -210,7 +210,7 @@ module.exports = [ color: '#189b17', count: 1000 * 1000, roles: [RoleID.Recruit], - secondaryColor: '#10670f', + secondaryColor: '#0c4d0c', title: 'Recruit', }, ]; From 97a2d2ada2860c0fa90341bed205bcda0f17cee2 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Sun, 22 Sep 2024 05:53:45 +0000 Subject: [PATCH 100/101] Tweaks to ban vote visualization --- ban.js | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/ban.js b/ban.js index dddd288..e139a80 100644 --- a/ban.js +++ b/ban.js @@ -264,9 +264,8 @@ async function UpdateTrial(cu) { console.log('sortedVotes[2]', sortedVotes[2]); if (voteCount > 0) { const gap = 2; - const widthMinusGap = yesWeight > 0 && noWeight > 0 ? canvas.width - gap : canvas.width; const maxWeight = Math.max(yesWeight, noWeight); - const yesPixels = Math.round(yesPercentage * widthMinusGap); + const yesPixels = Math.round(yesPercentage * canvas.width) + (gap / 2); const yesVotes = sortedVotes[1]; let cumulativeYesWeight = 0; for (const vote of yesVotes) { @@ -283,7 +282,7 @@ async function UpdateTrial(cu) { context.fillStyle = vote.color; context.fillRect(left, 0, rectangleWidth, canvas.height); } - const noPixels = widthMinusGap - yesPixels; + const noPixels = canvas.width - yesPixels + gap; const noVotes = sortedVotes[2]; let cumulativeNoWeight = 0; for (const vote of noVotes) { @@ -300,10 +299,6 @@ async function UpdateTrial(cu) { context.fillStyle = vote.color; context.fillRect(canvas.width - left - rectangleWidth, 0, rectangleWidth, canvas.height); } - if (yesWeight > 0 && noWeight > 0) { - context.fillStyle = '#FFFFFF'; - context.fillRect(yesPixels, 19, 2, 2); - } } const buffer = canvas.toBuffer(); const imageFilename = `case-${cu.commissar_id}.png`; From 30a9f038434cd15b63af2cddebb723a5261a4325 Mon Sep 17 00:00:00 2001 From: Ubuntu Date: Mon, 23 Sep 2024 18:01:05 +0000 Subject: [PATCH 101/101] Add a line and triangle to vote tally to make it easier to understand. --- ban.js | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/ban.js b/ban.js index e139a80..2f455b1 100644 --- a/ban.js +++ b/ban.js @@ -254,14 +254,20 @@ async function UpdateTrial(cu) { `${voteCount} voters` + `${threeTicks}` ); - const canvas = new Canvas.Canvas(360, 40); + const canvas = new Canvas.Canvas(360, 16 + 32 + 16); const context = canvas.getContext('2d'); context.fillStyle = '#313338'; // Discord grey. context.fillRect(0, 0, canvas.width, canvas.height); + context.strokeStyle = '#FFFFFF'; + context.beginPath(); + const halfX = Math.floor(canvas.width / 2) + 0.5; + context.moveTo(halfX, 8); + context.lineTo(halfX, 56); + context.stroke(); const sortedVotes = BanVoteCache.GetSortedVotesForDefendant(cu.commissar_id); - console.log('sortedVotes[0]', sortedVotes[0]); - console.log('sortedVotes[1]', sortedVotes[1]); - console.log('sortedVotes[2]', sortedVotes[2]); + //console.log('sortedVotes[0]', sortedVotes[0]); + //console.log('sortedVotes[1]', sortedVotes[1]); + //console.log('sortedVotes[2]', sortedVotes[2]); if (voteCount > 0) { const gap = 2; const maxWeight = Math.max(yesWeight, noWeight); @@ -280,7 +286,7 @@ async function UpdateTrial(cu) { break; } context.fillStyle = vote.color; - context.fillRect(left, 0, rectangleWidth, canvas.height); + context.fillRect(left, 16, rectangleWidth, 32); } const noPixels = canvas.width - yesPixels + gap; const noVotes = sortedVotes[2]; @@ -297,7 +303,15 @@ async function UpdateTrial(cu) { break; } context.fillStyle = vote.color; - context.fillRect(canvas.width - left - rectangleWidth, 0, rectangleWidth, canvas.height); + context.fillRect(canvas.width - left - rectangleWidth, 16, rectangleWidth, 32); + } + if (yesWeight > 0 && noWeight > 0) { + context.fillStyle = '#FFFFFF'; + context.beginPath(); + context.moveTo(yesPixels - 1, 14); + context.lineTo(yesPixels + 5, 8); + context.lineTo(yesPixels - 7, 8); + context.fill(); } } const buffer = canvas.toBuffer();