From 61b26ef38dae7a6c04230d69c5d6fee533fb3178 Mon Sep 17 00:00:00 2001 From: dapucita Date: Tue, 16 Mar 2021 18:18:46 +0900 Subject: [PATCH 1/7] start v0.5.1 --- core/package.json | 2 +- db/package.json | 2 +- package.json | 3 +-- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/core/package.json b/core/package.json index 9cb87a6..a4a85c3 100644 --- a/core/package.json +++ b/core/package.json @@ -1,6 +1,6 @@ { "name": "haxbotron-core", - "version": "0.5.0", + "version": "0.5.1", "description": "Haxbotron is a headless host server application for Haxball.", "main": "out/app.js", "scripts": { diff --git a/db/package.json b/db/package.json index 6ea6f76..8e6f4e8 100644 --- a/db/package.json +++ b/db/package.json @@ -1,6 +1,6 @@ { "name": "haxbotron-db", - "version": "0.5.0", + "version": "0.5.1", "description": "Haxbotron is a headless host server application for Haxball.", "main": "out/app.js", "author": "dapucita", diff --git a/package.json b/package.json index 3047430..df1fb9e 100644 --- a/package.json +++ b/package.json @@ -1,8 +1,7 @@ { "name": "haxbotron", - "version": "0.5.0", + "version": "0.5.1", "description": "Haxbotron is a headless host server application for Haxball.", - "main": "out/index.js", "author": "dapucita", "license": "MIT", "homepage": "https://github.com/dapucita/haxbotron", From 580681147a34946680976028a9e3aac75cb0cc59 Mon Sep 17 00:00:00 2001 From: dapucita Date: Tue, 16 Mar 2021 19:24:02 +0900 Subject: [PATCH 2/7] stats for current match --- core/game/controller/commands/stats.ts | 106 +++++++++++++++--------- core/game/resource/strings.sample.en.ts | 3 +- core/game/resource/strings.ts | 5 +- 3 files changed, 70 insertions(+), 44 deletions(-) diff --git a/core/game/controller/commands/stats.ts b/core/game/controller/commands/stats.ts index 98340d2..a8207f4 100644 --- a/core/game/controller/commands/stats.ts +++ b/core/game/controller/commands/stats.ts @@ -4,6 +4,15 @@ import * as StatCalc from "../Statistics"; import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { decideTier, getAvatarByTier, Tier } from "../../model/Statistics/Tier"; +/** + * Check if this player plays this match + * @param id Player's ID + */ +function isOnMatchNow(id: number): boolean { + if (window.gameRoom.isGamingNow && window.gameRoom.isStatRecord && window.gameRoom.playerList.get(id)?.team !== 0) return true; + else return false; +} + export function cmdStats(byPlayer: PlayerObject, message?: string): void { if (message !== undefined) { //stats for other player who are on this room @@ -12,28 +21,35 @@ export function cmdStats(byPlayer: PlayerObject, message?: string): void { if (isNaN(targetStatsID) != true && window.gameRoom.playerList.has(targetStatsID) == true) { // if the value is not NaN and there's the player let placeholder = { ticketTarget: targetStatsID - ,targetName: window.gameRoom.playerList.get(targetStatsID)!.name - ,targetAfkReason: window.gameRoom.playerList.get(targetStatsID)!.permissions.afkreason - ,targetStatsRatingAvatar: getAvatarByTier( // set avatar + , targetName: window.gameRoom.playerList.get(targetStatsID)!.name + , targetAfkReason: window.gameRoom.playerList.get(targetStatsID)!.permissions.afkreason + , targetStatsRatingAvatar: getAvatarByTier( // set avatar (window.gameRoom.playerList.get(targetStatsID)!.stats.totals < window.gameRoom.config.HElo.factor.placement_match_chances) - ? Tier.TierNew - : decideTier(window.gameRoom.playerList.get(targetStatsID)!.stats.rating)) - ,targetStatsRating: window.gameRoom.playerList.get(targetStatsID)!.stats.rating - ,targetStatsTotal: window.gameRoom.playerList.get(targetStatsID)!.stats.totals - ,targetStatsDisconns: window.gameRoom.playerList.get(targetStatsID)!.stats.disconns - ,targetStatsWins: window.gameRoom.playerList.get(targetStatsID)!.stats.wins - ,targetStatsGoals: window.gameRoom.playerList.get(targetStatsID)!.stats.goals - ,targetStatsAssists: window.gameRoom.playerList.get(targetStatsID)!.stats.assists - ,targetStatsOgs: window.gameRoom.playerList.get(targetStatsID)!.stats.ogs - ,targetStatsLosepoints: window.gameRoom.playerList.get(targetStatsID)!.stats.losePoints - ,targetStatsWinRate: StatCalc.calcWinsRate(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.wins) - ,targetStatsPassSuccess: StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(targetStatsID)!.stats.balltouch, window.gameRoom.playerList.get(targetStatsID)!.stats.passed) - ,targetStatsGoalsPerGame: StatCalc.calcGoalsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.goals) - ,targetStatsAssistsPerGame: StatCalc.calcAssistsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.assists) - ,targetStatsOgsPerGame: StatCalc.calcOGsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.ogs) - ,targetStatsLostGoalsPerGame: StatCalc.calcLoseGoalsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.losePoints) + ? Tier.TierNew + : decideTier(window.gameRoom.playerList.get(targetStatsID)!.stats.rating)) + , targetStatsRating: window.gameRoom.playerList.get(targetStatsID)!.stats.rating + , targetStatsTotal: window.gameRoom.playerList.get(targetStatsID)!.stats.totals + , targetStatsDisconns: window.gameRoom.playerList.get(targetStatsID)!.stats.disconns + , targetStatsWins: window.gameRoom.playerList.get(targetStatsID)!.stats.wins + , targetStatsGoals: window.gameRoom.playerList.get(targetStatsID)!.stats.goals + , targetStatsAssists: window.gameRoom.playerList.get(targetStatsID)!.stats.assists + , targetStatsOgs: window.gameRoom.playerList.get(targetStatsID)!.stats.ogs + , targetStatsLosepoints: window.gameRoom.playerList.get(targetStatsID)!.stats.losePoints + , targetStatsWinRate: StatCalc.calcWinsRate(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.wins) + , targetStatsPassSuccess: StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(targetStatsID)!.stats.balltouch, window.gameRoom.playerList.get(targetStatsID)!.stats.passed) + , targetStatsGoalsPerGame: StatCalc.calcGoalsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.goals) + , targetStatsAssistsPerGame: StatCalc.calcAssistsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.assists) + , targetStatsOgsPerGame: StatCalc.calcOGsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.ogs) + , targetStatsLostGoalsPerGame: StatCalc.calcLoseGoalsPerGame(window.gameRoom.playerList.get(targetStatsID)!.stats.totals, window.gameRoom.playerList.get(targetStatsID)!.stats.losePoints) + , targetStatsNowGoals: isOnMatchNow(targetStatsID) ? window.gameRoom.playerList.get(targetStatsID)!.matchRecord.goals : 0 + , targetStatsNowAssists: isOnMatchNow(targetStatsID) ? window.gameRoom.playerList.get(targetStatsID)!.matchRecord.assists : 0 + , targetStatsNowOgs: isOnMatchNow(targetStatsID) ? window.gameRoom.playerList.get(targetStatsID)!.matchRecord.ogs : 0 + , targetStatsNowPassSuccess: isOnMatchNow(targetStatsID) ? StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(targetStatsID)!.matchRecord.balltouch, window.gameRoom.playerList.get(targetStatsID)!.matchRecord.passed) : 0 } - window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.command.stats.statsMsg, placeholder), byPlayer.id, 0x479947, "normal", 1); + let resultMsg: string = (isOnMatchNow(targetStatsID)) + ? Tst.maketext(LangRes.command.stats.statsMsg + '\n' + LangRes.command.stats.matchAnalysis, placeholder) + : Tst.maketext(LangRes.command.stats.statsMsg, placeholder) + window.gameRoom._room.sendAnnouncement(resultMsg, byPlayer.id, 0x479947, "normal", 1); } else { window.gameRoom._room.sendAnnouncement(LangRes.command.stats._ErrorNoPlayer, byPlayer.id, 0xFF7777, "normal", 2); } @@ -41,30 +57,38 @@ export function cmdStats(byPlayer: PlayerObject, message?: string): void { window.gameRoom._room.sendAnnouncement(LangRes.command.stats._ErrorNoPlayer, byPlayer.id, 0xFF7777, "normal", 2); } } else { - //stats for him/herself + //stats for self let placeholder = { ticketTarget: byPlayer.id - ,targetName: window.gameRoom.playerList.get(byPlayer.id)!.name - ,targetAfkReason: window.gameRoom.playerList.get(byPlayer.id)!.permissions.afkreason - ,targetStatsRatingAvatar: getAvatarByTier( // set avatar + , targetName: window.gameRoom.playerList.get(byPlayer.id)!.name + , targetAfkReason: window.gameRoom.playerList.get(byPlayer.id)!.permissions.afkreason + , targetStatsRatingAvatar: getAvatarByTier( // set avatar (window.gameRoom.playerList.get(byPlayer.id)!.stats.totals < window.gameRoom.config.HElo.factor.placement_match_chances) - ? Tier.TierNew - : decideTier(window.gameRoom.playerList.get(byPlayer.id)!.stats.rating)) - ,targetStatsRating: window.gameRoom.playerList.get(byPlayer.id)!.stats.rating - ,targetStatsTotal: window.gameRoom.playerList.get(byPlayer.id)!.stats.totals - ,targetStatsDisconns: window.gameRoom.playerList.get(byPlayer.id)!.stats.disconns - ,targetStatsWins: window.gameRoom.playerList.get(byPlayer.id)!.stats.wins - ,targetStatsGoals: window.gameRoom.playerList.get(byPlayer.id)!.stats.goals - ,targetStatsAssists: window.gameRoom.playerList.get(byPlayer.id)!.stats.assists - ,targetStatsOgs: window.gameRoom.playerList.get(byPlayer.id)!.stats.ogs - ,targetStatsLosepoints: window.gameRoom.playerList.get(byPlayer.id)!.stats.losePoints - ,targetStatsWinRate: StatCalc.calcWinsRate(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.wins) - ,targetStatsPassSuccess: StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(byPlayer.id)!.stats.balltouch, window.gameRoom.playerList.get(byPlayer.id)!.stats.passed) - ,targetStatsGoalsPerGame: StatCalc.calcGoalsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.goals) - ,targetStatsAssistsPerGame: StatCalc.calcAssistsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.assists) - ,targetStatsOgsPerGame: StatCalc.calcOGsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.ogs) - ,targetStatsLostGoalsPerGame: StatCalc.calcLoseGoalsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.losePoints) + ? Tier.TierNew + : decideTier(window.gameRoom.playerList.get(byPlayer.id)!.stats.rating)) + , targetStatsRating: window.gameRoom.playerList.get(byPlayer.id)!.stats.rating + , targetStatsTotal: window.gameRoom.playerList.get(byPlayer.id)!.stats.totals + , targetStatsDisconns: window.gameRoom.playerList.get(byPlayer.id)!.stats.disconns + , targetStatsWins: window.gameRoom.playerList.get(byPlayer.id)!.stats.wins + , targetStatsGoals: window.gameRoom.playerList.get(byPlayer.id)!.stats.goals + , targetStatsAssists: window.gameRoom.playerList.get(byPlayer.id)!.stats.assists + , targetStatsOgs: window.gameRoom.playerList.get(byPlayer.id)!.stats.ogs + , targetStatsLosepoints: window.gameRoom.playerList.get(byPlayer.id)!.stats.losePoints + , targetStatsWinRate: StatCalc.calcWinsRate(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.wins) + , targetStatsPassSuccess: StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(byPlayer.id)!.stats.balltouch, window.gameRoom.playerList.get(byPlayer.id)!.stats.passed) + , targetStatsGoalsPerGame: StatCalc.calcGoalsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.goals) + , targetStatsAssistsPerGame: StatCalc.calcAssistsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.assists) + , targetStatsOgsPerGame: StatCalc.calcOGsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.ogs) + , targetStatsLostGoalsPerGame: StatCalc.calcLoseGoalsPerGame(window.gameRoom.playerList.get(byPlayer.id)!.stats.totals, window.gameRoom.playerList.get(byPlayer.id)!.stats.losePoints) + , targetStatsNowGoals: isOnMatchNow(byPlayer.id) ? window.gameRoom.playerList.get(byPlayer.id)!.matchRecord.goals : 0 + , targetStatsNowAssists: isOnMatchNow(byPlayer.id) ? window.gameRoom.playerList.get(byPlayer.id)!.matchRecord.assists : 0 + , targetStatsNowOgs: isOnMatchNow(byPlayer.id) ? window.gameRoom.playerList.get(byPlayer.id)!.matchRecord.ogs : 0 + , targetStatsNowPassSuccess: isOnMatchNow(byPlayer.id) ? StatCalc.calcPassSuccessRate(window.gameRoom.playerList.get(byPlayer.id)!.matchRecord.balltouch, window.gameRoom.playerList.get(byPlayer.id)!.matchRecord.passed) : 0 } - window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.command.stats.statsMsg, placeholder), byPlayer.id, 0x479947, "normal", 1); + let resultMsg: string = (isOnMatchNow(byPlayer.id)) + ? Tst.maketext(LangRes.command.stats.statsMsg + '\n' + LangRes.command.stats.matchAnalysis, placeholder) + : Tst.maketext(LangRes.command.stats.statsMsg, placeholder) + + window.gameRoom._room.sendAnnouncement(resultMsg, byPlayer.id, 0x479947, "normal", 1); } } diff --git a/core/game/resource/strings.sample.en.ts b/core/game/resource/strings.sample.en.ts index 59cc202..65ce058 100644 --- a/core/game/resource/strings.sample.en.ts +++ b/core/game/resource/strings.sample.en.ts @@ -6,7 +6,7 @@ export const scheduler = { ,shutdown: 'πŸ“’ This room will be shutdown soon. Thanks for joinning our game!' ,afkKick: 'πŸ“’ kicked: AFK' ,afkCommandTooLongKick: 'πŸ“’ AFK over 2mins' - ,afkDetect: 'πŸ“’ @{targetName} #{targetID} has been away from keyboard. Press any key, or would be kicked.' + ,afkDetect: 'πŸ“’ @{targetName}#{targetID} has been away from keyboard. Press any key, or would be kicked.' ,autoUnmute: 'πŸ”Š Player {targetName}#{targetID} is unmuted by system.' ,banVoteAutoNotify: 'πŸ—³οΈ Voting to ban is in progress (!vote #ID) : {voteList}' } @@ -77,6 +77,7 @@ export const command = { ,stats: { _ErrorNoPlayer: '❌ Wrong player ID. You can only target numeric ID.(eg: !stats #12)\nπŸ“‘ You can check IDs by command !list red,blue,spec' ,statsMsg: 'πŸ“Š {targetName}#{ticketTarget} (Rating {targetStatsRatingAvatar}{targetStatsRating}) Total {targetStatsTotal} games(winrate {targetStatsWinRate}%), Disconnected {targetStatsDisconns} games\nπŸ“Š Goal {targetStatsGoals}, Assist {targetStatsAssists}, OG {targetStatsOgs}, Lose goal {targetStatsLosepoints}, Pass Success Rate {targetStatsPassSuccess}%\nπŸ“Š and Per Game : {targetStatsGoalsPerGame}goals, {targetStatsAssistsPerGame}assists, {targetStatsOgsPerGame}ogs, {targetStatsLostGoalsPerGame}lose goals.' + ,matchAnalysis: 'πŸ“Š In this match, {targetStatsNowGoals}goals {targetStatsNowAssists}assists {targetStatsNowOgs}ogs. (Pass Success Rate {targetStatsNowPassSuccess}%)' } ,statsreset: 'πŸ“Š Reset for statistical information completed. You can\'t cancel it.' ,poss: 'πŸ“Š Ball possession : Red {possTeamRed}%, Blue {possTeamBlue}%.' diff --git a/core/game/resource/strings.ts b/core/game/resource/strings.ts index f5c8dac..c6c4c44 100644 --- a/core/game/resource/strings.ts +++ b/core/game/resource/strings.ts @@ -6,7 +6,7 @@ export const scheduler = { ,shutdown: 'πŸ“’ 방이 곧 λ‹«νž™λ‹ˆλ‹€. μ΄μš©ν•΄μ£Όμ…”μ„œ κ°μ‚¬ν•©λ‹ˆλ‹€.' ,afkKick: 'πŸ“’ 잠수둜 μΈν•œ 퇴μž₯' ,afkCommandTooLongKick: 'πŸ“’ 2λΆ„ 이상 잠수둜 퇴μž₯' - ,afkDetect: 'πŸ“’ @{targetName} #{targetID}λ‹˜μ΄ μž μˆ˜μ€‘μž…λ‹ˆλ‹€. 아무 ν‚€λ‚˜ λˆŒλŸ¬μ£Όμ„Έμš”. 계속 μž μˆ˜μ‹œ 퇴μž₯λ‹Ήν•  수 μžˆμŠ΅λ‹ˆλ‹€.' + ,afkDetect: 'πŸ“’ @{targetName}#{targetID}λ‹˜μ΄ μž μˆ˜μ€‘μž…λ‹ˆλ‹€. 아무 ν‚€λ‚˜ λˆŒλŸ¬μ£Όμ„Έμš”. 계속 μž μˆ˜μ‹œ 퇴μž₯λ‹Ήν•  수 μžˆμŠ΅λ‹ˆλ‹€.' ,autoUnmute: 'πŸ”Š {targetName}#{targetID}λ‹˜μ˜ μŒμ†Œκ±°κ°€ μžλ™μœΌλ‘œ ν•΄μ œλ˜μ—ˆμŠ΅λ‹ˆλ‹€.' ,banVoteAutoNotify: 'πŸ—³οΈ μΆ”λ°© νˆ¬ν‘œκ°€ μ§„ν–‰μ€‘μž…λ‹ˆλ‹€ (!vote #ID) : {voteList}' } @@ -76,7 +76,8 @@ export const command = { ,about: 'πŸ“„ λ°© 이름 : {RoomName} ({_LaunchTime})\nπŸ’¬ 이 방은 HaxbotronπŸ€– 봇에 μ˜ν•΄ μš΄μ˜λ©λ‹ˆλ‹€. (https://dapucita.github.io/haxbotron/)\nπŸ’¬ [λ””μŠ€μ½”λ“œ] https://discord.gg/qfg45B2 [ν›„μ›ν•˜κΈ°] https://www.patreon.com/dapucita' ,stats: { _ErrorNoPlayer: '❌ 접속쀑이지 μ•ŠμŠ΅λ‹ˆλ‹€. #μˆ«μžμ•„μ΄λ”” 의 ν˜•μ‹μœΌλ‘œ 지정해야 ν•©λ‹ˆλ‹€. (예: !stats #12)\nπŸ“‘ !list red,blue,spec λͺ…λ Ήμ–΄λ‘œ μˆ«μžμ•„μ΄λ””λ₯Ό 확인할 수 μžˆμŠ΅λ‹ˆλ‹€.' - ,statsMsg: 'πŸ“Š {targetName}#{ticketTarget}λ‹˜μ˜ 전적 (λ ˆμ΄νŒ… {targetStatsRatingAvatar}{targetStatsRating}) 총 {targetStatsTotal}판(승λ₯  {targetStatsWinRate}%), μ—°κ²°λŠκΉ€ {targetStatsDisconns}회 \nπŸ“Š (μ΄μ–΄μ„œ) 골 {targetStatsGoals}, μ–΄μ‹œ {targetStatsAssists}, μžμ±… {targetStatsOgs}, 싀점 {targetStatsLosepoints}, νŒ¨μŠ€μ„±κ³΅λ₯  {targetStatsPassSuccess}%\nπŸ“Š (μ΄μ–΄μ„œ) κ²½κΈ°λ‹Ή {targetStatsGoalsPerGame}골, {targetStatsAssistsPerGame}도움과 {targetStatsOgsPerGame}μžμ±…, {targetStatsLostGoalsPerGame}싀점을 κΈ°λ‘μ€‘μž…λ‹ˆλ‹€.' + ,statsMsg: 'πŸ“Š {targetName}#{ticketTarget}λ‹˜μ˜ 전적 (λ ˆμ΄νŒ… {targetStatsRatingAvatar}{targetStatsRating}) 총 {targetStatsTotal}판(승λ₯  {targetStatsWinRate}%), μ—°κ²°λŠκΉ€ {targetStatsDisconns}회 \nπŸ“Š (μ΄μ–΄μ„œ) 골 {targetStatsGoals}, 도움 {targetStatsAssists}, μžμ±… {targetStatsOgs}, 싀점 {targetStatsLosepoints}, νŒ¨μŠ€μ„±κ³΅λ₯  {targetStatsPassSuccess}%\nπŸ“Š (μ΄μ–΄μ„œ) κ²½κΈ°λ‹Ή {targetStatsGoalsPerGame}골, {targetStatsAssistsPerGame}도움과 {targetStatsOgsPerGame}μžμ±…, {targetStatsLostGoalsPerGame}싀점을 κΈ°λ‘μ€‘μž…λ‹ˆλ‹€.' + ,matchAnalysis: 'πŸ“Š (μ΄μ–΄μ„œ) ν˜„μž¬ κ²½κΈ°μ—μ„œ {targetStatsNowGoals}골 {targetStatsNowAssists}도움 {targetStatsNowOgs}μžμ±…μ„ κΈ°λ‘μ€‘μž…λ‹ˆλ‹€. (νŒ¨μŠ€μ„±κ³΅λ₯  {targetStatsNowPassSuccess}%)' } ,statsreset: 'πŸ“Š 전적을 μ΄ˆκΈ°ν™”ν–ˆμŠ΅λ‹ˆλ‹€. λ‹€μ‹œ 볡ꡬ할 수 μ—†μŠ΅λ‹ˆλ‹€.' ,poss: 'πŸ“Š 점유율 : Red {possTeamRed}%, Blue {possTeamBlue}%.' From af3ace5748fb1ee15304cf935a297532007d855f Mon Sep 17 00:00:00 2001 From: dapucita Date: Wed, 17 Mar 2021 14:33:44 +0900 Subject: [PATCH 3/7] small fix --- core/game/bot.ts | 22 +++++++++++----------- core/game/model/OperateHelper/Quorum.ts | 4 ++-- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/core/game/bot.ts b/core/game/bot.ts index 80ad44c..4ab2ad7 100644 --- a/core/game/bot.ts +++ b/core/game/bot.ts @@ -1,6 +1,6 @@ -// Haxbotron -// This is main part of the bot - +// Haxbotron by dapucita +// MAIN OF THE BOT +// ==================================================================================================== // import modules import * as LangRes from "./resource/strings"; import * as eventListener from "./controller/events/eventListeners"; @@ -15,7 +15,7 @@ import { TeamID } from "./model/GameObject/TeamID"; import { EmergencyTools } from "./model/ExposeLibs/EmergencyTools"; import { refreshBanVoteCache } from "./model/OperateHelper/Vote"; import { GameRoomConfig } from "./model/Configuration/GameRoomConfig"; - +// ==================================================================================================== // load initial configurations const loadedConfig: GameRoomConfig = JSON.parse(localStorage.getItem('_initConfig')!); @@ -62,9 +62,9 @@ console.log(`Haxbotron loaded bot script. (UID ${window.gameRoom.config._RUID}, window.document.title = `Haxbotron ${window.gameRoom.config._RUID}`; makeRoom(); - -// set schedulers -var advertisementTimer = setInterval(() => { +// ==================================================================================================== +// set scheduling timers +var scheduledTimer60 = setInterval(() => { window.gameRoom._room.sendAnnouncement(LangRes.scheduler.advertise, null, 0x777777, "normal", 0); // advertisement refreshBanVoteCache(); // update banvote status cache @@ -79,9 +79,9 @@ var advertisementTimer = setInterval(() => { } window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.scheduler.banVoteAutoNotify, placeholderVote), null, 0x00FF00, "normal", 0); //notify it } -}, 60000) // 1min +}, 60000); // 60secs -var scheduledTimer = setInterval(() => { +var scheduledTimer5 = setInterval(() => { const nowTimeStamp: number = getUnixTimestamp(); //get timestamp let placeholderScheduler = { @@ -131,8 +131,8 @@ var scheduledTimer = setInterval(() => { } } }); -}, 5000); // by 5seconds - +}, 5000); // 5secs +// ==================================================================================================== // declare functions function makeRoom(): void { window.gameRoom.logger.i('initialisation', `The game room is opened at ${window.gameRoom.config._LaunchDate.toLocaleString()}.`); diff --git a/core/game/model/OperateHelper/Quorum.ts b/core/game/model/OperateHelper/Quorum.ts index 4c7bceb..a371e6c 100644 --- a/core/game/model/OperateHelper/Quorum.ts +++ b/core/game/model/OperateHelper/Quorum.ts @@ -8,7 +8,7 @@ export function roomPlayersNumberCheck(): number { export function roomActivePlayersNumberCheck(): number { // return number of players actually atcivated(not afk) - return window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode !== true).length; + return window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false).length; } export function roomTeamPlayersNumberCheck(team: TeamID): number { @@ -55,4 +55,4 @@ export function putTeamNewPlayerFullify(): void { if(i < redPlayersLack) window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, TeamID.Red); if(i >= redPlayersLack && i < redPlayersLack + bluePlayersLack) window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, TeamID.Blue); } -} \ No newline at end of file +} From cfbbea9144e4bb4818676f3494b9b33331f72130 Mon Sep 17 00:00:00 2001 From: dapucita Date: Wed, 17 Mar 2021 14:33:51 +0900 Subject: [PATCH 4/7] bug fix --- core/game/controller/events/onTeamVictory.ts | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/core/game/controller/events/onTeamVictory.ts b/core/game/controller/events/onTeamVictory.ts index 14e07d3..d648ce2 100644 --- a/core/game/controller/events/onTeamVictory.ts +++ b/core/game/controller/events/onTeamVictory.ts @@ -164,9 +164,6 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise window.gameRoom.logger.i('onTeamVictory', `Whole players are shuffled. (${shuffledIDList.toString()})`); } } else { // or still under the limit, then change spec and loser team - // this count is for determine how many players will be alive in loser team - let outPlayersCount: number = window.gameRoom.config.rules.requisite.eachTeamPlayers; // init as full count. - teamPlayers .filter((player: PlayerObject) => player.team === loserTeamID) .forEach((eachPlayer: PlayerObject) => { @@ -174,8 +171,6 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise // if guarantee playing time option is enabled if ((scores.time - window.gameRoom.playerList.get(eachPlayer.id)!.entrytime.matchEntryTime) > window.gameRoom.config.settings.guaranteedPlayingTimeSeconds) { window.gameRoom._room.setPlayerTeam(eachPlayer.id, TeamID.Spec); // move losers played enough time to Spec team - } else { - outPlayersCount--; // decrease count as this player will be alive in loser team } } else { // if guarantee playing time option is disabled @@ -184,9 +179,11 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise }); // get new spec player list - let specActivePlayers: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && player.team === TeamID.Spec && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); + let newAllActivePlayers: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); + let specActivePlayers: PlayerObject[] = newAllActivePlayers.filter((player: PlayerObject) => player.team === TeamID.Spec); + let loseTeamPlayersLength: number = newAllActivePlayers.filter((player: PlayerObject) => player.team === loserTeamID).length; - for (let i: number = 0; i < outPlayersCount && i < specActivePlayers.length; i++) { + for (let i: number = 0; i < window.gameRoom.config.rules.requisite.eachTeamPlayers - loseTeamPlayersLength && i < specActivePlayers.length; i++) { window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, loserTeamID); // move Specs to challenger team } } From 74f0d721552a6766f45a30e77d861ed3686ac0e4 Mon Sep 17 00:00:00 2001 From: dapucita Date: Wed, 17 Mar 2021 19:13:03 +0900 Subject: [PATCH 5/7] notice load --- core/react/component/Admin/RoomSocial.tsx | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/core/react/component/Admin/RoomSocial.tsx b/core/react/component/Admin/RoomSocial.tsx index ab96d92..7d0f3ec 100644 --- a/core/react/component/Admin/RoomSocial.tsx +++ b/core/react/component/Admin/RoomSocial.tsx @@ -36,6 +36,7 @@ export default function RoomSocial({ styleClass }: styleClass) { const handleNoticeSet = async (event: React.FormEvent) => { event.preventDefault(); + localStorage.setItem(`_NoticeMessage`, newNoticeMessage); try { const result = await client.post(`/api/v1/room/${matchParams.ruid}/social/notice`, { message: newNoticeMessage }); if (result.status === 201) { @@ -113,6 +114,14 @@ export default function RoomSocial({ styleClass }: styleClass) { } } + const handleNoticeLoad = async (event: React.MouseEvent) => { + event.preventDefault(); + + if (localStorage.getItem(`_NoticeMessage`) !== null) { + setNewNoticeMessage(localStorage.getItem(`_NoticeMessage`)!); + } + } + useEffect(() => { getNoticeMessage(); }, []); @@ -140,6 +149,7 @@ export default function RoomSocial({ styleClass }: styleClass) { className={classes.halfInput} /> + From 2d4d97d1eec52bdfb4e74af7840973509444077d Mon Sep 17 00:00:00 2001 From: dapucita Date: Wed, 17 Mar 2021 22:35:54 +0900 Subject: [PATCH 6/7] bug fix for recurit players --- core/game/controller/events/onGameStop.ts | 2 + core/game/controller/events/onGameUnpause.ts | 4 +- core/game/controller/events/onPlayerJoin.ts | 4 +- core/game/controller/events/onPlayerLeave.ts | 5 +- core/game/controller/events/onTeamVictory.ts | 49 +++++++--------- core/game/model/OperateHelper/Quorum.ts | 60 +++++++++----------- 6 files changed, 57 insertions(+), 67 deletions(-) diff --git a/core/game/controller/events/onGameStop.ts b/core/game/controller/events/onGameStop.ts index d674e8e..f86f79d 100644 --- a/core/game/controller/events/onGameStop.ts +++ b/core/game/controller/events/onGameStop.ts @@ -1,5 +1,6 @@ import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; +import { recuritBothTeamFully } from "../../model/OperateHelper/Quorum"; import { setDefaultRoomLimitation, setDefaultStadiums } from "../RoomTools"; @@ -43,6 +44,7 @@ export function onGameStopListener(byPlayer: PlayerObject): void { // when auto emcee mode is enabled if(window.gameRoom.config.rules.autoOperating === true) { + recuritBothTeamFully(); window.gameRoom._room.startGame(); // start next new game } } diff --git a/core/game/controller/events/onGameUnpause.ts b/core/game/controller/events/onGameUnpause.ts index a94078e..b5cf254 100644 --- a/core/game/controller/events/onGameUnpause.ts +++ b/core/game/controller/events/onGameUnpause.ts @@ -1,5 +1,5 @@ import { PlayerObject } from "../../model/GameObject/PlayerObject"; -import { putTeamNewPlayerFullify } from "../../model/OperateHelper/Quorum"; +import { recuritBothTeamFully } from "../../model/OperateHelper/Quorum"; export function onGameUnpauseListener(byPlayer: PlayerObject | null): void { window.gameRoom.isGamingNow = true; // turn on @@ -7,7 +7,7 @@ export function onGameUnpauseListener(byPlayer: PlayerObject | null): void { // if auto emcee mode is enabled if(window.gameRoom.config.rules.autoOperating === true) { if(window.gameRoom.isGamingNow === true) { // when game is in match - putTeamNewPlayerFullify(); + recuritBothTeamFully(); } } } diff --git a/core/game/controller/events/onPlayerJoin.ts b/core/game/controller/events/onPlayerJoin.ts index e3f0a1e..7421e4f 100644 --- a/core/game/controller/events/onPlayerJoin.ts +++ b/core/game/controller/events/onPlayerJoin.ts @@ -6,7 +6,7 @@ import { convertToPlayerStorage, getBanlistDataFromDB, getPlayerDataFromDB, remo import { getUnixTimestamp } from "../Statistics"; import { setDefaultStadiums, updateAdmins } from "../RoomTools"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; -import { putTeamNewPlayerConditional, roomActivePlayersNumberCheck } from "../../model/OperateHelper/Quorum"; +import { recuritByOne, roomActivePlayersNumberCheck, roomTeamPlayersNumberCheck } from "../../model/OperateHelper/Quorum"; import { decideTier, getAvatarByTier, Tier } from "../../model/Statistics/Tier"; import { isExistNickname, isIncludeBannedWords } from "../TextFilter"; @@ -241,7 +241,7 @@ export async function onPlayerJoinListener(player: PlayerObject): Promise // when auto emcee mode is enabled if (window.gameRoom.config.rules.autoOperating === true) { - putTeamNewPlayerConditional(player.id); // move team + recuritByOne(); if (window.gameRoom.isGamingNow === false) { // if game is not started then start the game for active players setDefaultStadiums(); // set stadium diff --git a/core/game/controller/events/onPlayerLeave.ts b/core/game/controller/events/onPlayerLeave.ts index cbbe3c0..1b58901 100644 --- a/core/game/controller/events/onPlayerLeave.ts +++ b/core/game/controller/events/onPlayerLeave.ts @@ -4,7 +4,7 @@ import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { updateAdmins } from "../RoomTools"; import { getUnixTimestamp } from "../Statistics"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; -import { putTeamNewPlayerFullify, roomActivePlayersNumberCheck } from "../../model/OperateHelper/Quorum"; +import { recuritByOne, roomActivePlayersNumberCheck, roomTeamPlayersNumberCheck } from "../../model/OperateHelper/Quorum"; import { convertToPlayerStorage, getBanlistDataFromDB, setBanlistDataToDB, setPlayerDataToDB } from "../Storage"; export async function onPlayerLeaveListener(player: PlayerObject): Promise { @@ -48,7 +48,8 @@ export async function onPlayerLeaveListener(player: PlayerObject): Promise // when auto emcee mode is enabled if(window.gameRoom.config.rules.autoOperating === true && window.gameRoom.isGamingNow === true) { if(player.team !== TeamID.Spec) { - putTeamNewPlayerFullify(); // put new players into the team this player has left + // put new players into the team this player has left + recuritByOne(); } } } else { diff --git a/core/game/controller/events/onTeamVictory.ts b/core/game/controller/events/onTeamVictory.ts index d648ce2..2e95f66 100644 --- a/core/game/controller/events/onTeamVictory.ts +++ b/core/game/controller/events/onTeamVictory.ts @@ -4,7 +4,7 @@ import { ScoresObject } from "../../model/GameObject/ScoresObject"; import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; import { shuffleArray } from "../RoomTools"; -import { roomActivePlayersNumberCheck } from "../../model/OperateHelper/Quorum"; +import { fetchActiveSpecPlayers, roomActivePlayersNumberCheck } from "../../model/OperateHelper/Quorum"; import { HElo, MatchResult, StatsRecord } from "../../model/Statistics/HElo"; import { convertToPlayerStorage, setPlayerDataToDB } from "../Storage"; @@ -56,21 +56,21 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise if (window.gameRoom.config.rules.statsRecord == true && window.gameRoom.isStatRecord == true) { // records when game mode is for stats recording. // HElo rating part ================ // make diffs array (key: index by teamPlayers order, value: number[]) - + // make stat records - let redStatsRecords: StatsRecord[] = ratingHelper.makeStasRecord(winnerTeamID===TeamID.Red?MatchResult.Win:MatchResult.Lose, redTeamPlayers); - let blueStatsRecords: StatsRecord[] = ratingHelper.makeStasRecord(winnerTeamID===TeamID.Blue?MatchResult.Win:MatchResult.Lose, blueTeamPlayers); - + let redStatsRecords: StatsRecord[] = ratingHelper.makeStasRecord(winnerTeamID === TeamID.Red ? MatchResult.Win : MatchResult.Lose, redTeamPlayers); + let blueStatsRecords: StatsRecord[] = ratingHelper.makeStasRecord(winnerTeamID === TeamID.Blue ? MatchResult.Win : MatchResult.Lose, blueTeamPlayers); + // calc average of team ratings - let winTeamRatingsMean: number = ratingHelper.calcTeamRatingsMean(winnerTeamID===TeamID.Red?redTeamPlayers:blueTeamPlayers); - let loseTeamRatingsMean: number = ratingHelper.calcTeamRatingsMean(loserTeamID===TeamID.Red?redTeamPlayers:blueTeamPlayers); - + let winTeamRatingsMean: number = ratingHelper.calcTeamRatingsMean(winnerTeamID === TeamID.Red ? redTeamPlayers : blueTeamPlayers); + let loseTeamRatingsMean: number = ratingHelper.calcTeamRatingsMean(loserTeamID === TeamID.Red ? redTeamPlayers : blueTeamPlayers); + // get diff and update rating redStatsRecords.forEach((eachItem: StatsRecord, idx: number) => { - let diffArray: number[] = []; + let diffArray: number[] = []; let oldRating: number = window.gameRoom.playerList.get(redTeamPlayers[idx].id)!.stats.rating; - for(let i: number = 0; i < blueStatsRecords.length; i++) { + for (let i: number = 0; i < blueStatsRecords.length; i++) { diffArray.push(ratingHelper.calcBothDiff(eachItem, blueStatsRecords[i], winTeamRatingsMean, loseTeamRatingsMean, eachItem.matchKFactor)); } let newRating: number = ratingHelper.calcNewRating(eachItem.rating, diffArray); @@ -78,9 +78,9 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise window.gameRoom.logger.i('onTeamVictory', `Red Player ${redTeamPlayers[idx].name}#${redTeamPlayers[idx].id}'s rating has become ${newRating} from ${oldRating}.`); }); blueStatsRecords.forEach((eachItem: StatsRecord, idx: number) => { - let diffArray: number[] = []; + let diffArray: number[] = []; let oldRating: number = window.gameRoom.playerList.get(blueTeamPlayers[idx].id)!.stats.rating; - for(let i: number = 0; i < redStatsRecords.length; i++) { + for (let i: number = 0; i < redStatsRecords.length; i++) { diffArray.push(ratingHelper.calcBothDiff(eachItem, redStatsRecords[i], winTeamRatingsMean, loseTeamRatingsMean, eachItem.matchKFactor)); } let newRating: number = ratingHelper.calcNewRating(eachItem.rating, diffArray); @@ -164,27 +164,18 @@ export async function onTeamVictoryListener(scores: ScoresObject): Promise window.gameRoom.logger.i('onTeamVictory', `Whole players are shuffled. (${shuffledIDList.toString()})`); } } else { // or still under the limit, then change spec and loser team - teamPlayers + window.gameRoom._room.getPlayerList() .filter((player: PlayerObject) => player.team === loserTeamID) - .forEach((eachPlayer: PlayerObject) => { - if (window.gameRoom.config.settings.guaranteePlayingTime === true) { - // if guarantee playing time option is enabled - if ((scores.time - window.gameRoom.playerList.get(eachPlayer.id)!.entrytime.matchEntryTime) > window.gameRoom.config.settings.guaranteedPlayingTimeSeconds) { - window.gameRoom._room.setPlayerTeam(eachPlayer.id, TeamID.Spec); // move losers played enough time to Spec team - } - } else { - // if guarantee playing time option is disabled - window.gameRoom._room.setPlayerTeam(eachPlayer.id, TeamID.Spec); // just move all losers to Spec team + .forEach((player: PlayerObject) => { + if (window.gameRoom.config.settings.guaranteePlayingTime === false || (scores.time - window.gameRoom.playerList.get(player.id)!.entrytime.matchEntryTime) > window.gameRoom.config.settings.guaranteedPlayingTimeSeconds) { + window.gameRoom._room.setPlayerTeam(player.id, TeamID.Spec); // just move all losers to Spec team } }); - // get new spec player list - let newAllActivePlayers: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); - let specActivePlayers: PlayerObject[] = newAllActivePlayers.filter((player: PlayerObject) => player.team === TeamID.Spec); - let loseTeamPlayersLength: number = newAllActivePlayers.filter((player: PlayerObject) => player.team === loserTeamID).length; - - for (let i: number = 0; i < window.gameRoom.config.rules.requisite.eachTeamPlayers - loseTeamPlayersLength && i < specActivePlayers.length; i++) { - window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, loserTeamID); // move Specs to challenger team + const specPlayers: PlayerObject[] = fetchActiveSpecPlayers(); + const insufficiency: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.team === loserTeamID).length; + for(let i=0; i < insufficiency && i < specPlayers.length; i++) { + window.gameRoom._room.setPlayerTeam(specPlayers[i].id, loserTeamID); } } } diff --git a/core/game/model/OperateHelper/Quorum.ts b/core/game/model/OperateHelper/Quorum.ts index a371e6c..48aba6d 100644 --- a/core/game/model/OperateHelper/Quorum.ts +++ b/core/game/model/OperateHelper/Quorum.ts @@ -16,43 +16,39 @@ export function roomTeamPlayersNumberCheck(team: TeamID): number { return window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && player.team === team).length; } -export function putTeamNewPlayerConditional(playerID: number, redPlayers?: number, bluePlayers?: number): TeamID { - // check quorum of each team, and if there are any lacks then supplement automatically just one time - let newTeamID: TeamID = 0; - let teamPlayersNumber = { - red: redPlayers || roomTeamPlayersNumberCheck(TeamID.Red), - blue: bluePlayers || roomTeamPlayersNumberCheck(TeamID.Blue) +export function fetchActiveSpecPlayers(): PlayerObject[] { + return window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false && player.team === TeamID.Spec); +} + +export function recuritByOne() { + const activePlayersList: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); + const activeSpecPlayersList: PlayerObject[] = activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Spec); + + const redInsufficiency: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Red).length; + const blueInsufficiency: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Blue).length; + + if(redInsufficiency >= blueInsufficiency && redInsufficiency > 0) { + window.gameRoom._room.setPlayerTeam(activeSpecPlayersList[0].id, TeamID.Red); } - if(teamPlayersNumber.red <= teamPlayersNumber.blue) { - // if red team members are equal or less than blues, move this player to red team. - if(teamPlayersNumber.red < window.gameRoom.config.rules.requisite.eachTeamPlayers) { - // move only when team limitation is not reached. - newTeamID = TeamID.Red; - window.gameRoom._room.setPlayerTeam(playerID, TeamID.Red); - window.gameRoom.logger.i('autoOperating', `The player #${playerID} is moved to Red Team by system.`); - } - } else { - // or move to blue team. - if(teamPlayersNumber.blue < window.gameRoom.config.rules.requisite.eachTeamPlayers) { - // move only when team limitation is not reached. - newTeamID = TeamID.Blue; - window.gameRoom._room.setPlayerTeam(playerID, TeamID.Blue); - window.gameRoom.logger.i('autoOperating', `The player #${playerID} is moved to Blue Team by system.`); - } + if(redInsufficiency < blueInsufficiency && blueInsufficiency > 0) { + window.gameRoom._room.setPlayerTeam(activeSpecPlayersList[0].id, TeamID.Blue); } - return newTeamID; } -export function putTeamNewPlayerFullify(): void { - // check quorum of each team, and if there are any lacks then supplement automatically - let allActivePlayers: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); - let specActivePlayers: PlayerObject[] = allActivePlayers.filter((player: PlayerObject) => player.team === TeamID.Spec); +export function recuritBothTeamFully() { + const activePlayersList: PlayerObject[] = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false); + let activeSpecPlayersList: PlayerObject[] = activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Spec); + + const redInsufficiency: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Red).length; + const blueInsufficiency: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - activePlayersList.filter((player: PlayerObject) => player.team === TeamID.Blue).length; + + for(let i=0; i < redInsufficiency && i < activeSpecPlayersList.length; i++) { + window.gameRoom._room.setPlayerTeam(activeSpecPlayersList[i].id, TeamID.Red); + } - let redPlayersLack: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - allActivePlayers.filter((player: PlayerObject) => player.team === TeamID.Red).length; - let bluePlayersLack: number = window.gameRoom.config.rules.requisite.eachTeamPlayers - allActivePlayers.filter((player: PlayerObject) => player.team === TeamID.Blue).length; + activeSpecPlayersList = window.gameRoom._room.getPlayerList().filter((player: PlayerObject) => player.id !== 0 && window.gameRoom.playerList.get(player.id)!.permissions.afkmode === false && player.team === TeamID.Spec); - for(let i: number = 0; i < specActivePlayers.length; i++) { - if(i < redPlayersLack) window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, TeamID.Red); - if(i >= redPlayersLack && i < redPlayersLack + bluePlayersLack) window.gameRoom._room.setPlayerTeam(specActivePlayers[i].id, TeamID.Blue); + for(let i=0; i < blueInsufficiency && i < activeSpecPlayersList.length; i++) { + window.gameRoom._room.setPlayerTeam(activeSpecPlayersList[i].id, TeamID.Blue); } } From 7589bab7e586ec3737d99c71c0c2eed88243fbe0 Mon Sep 17 00:00:00 2001 From: dapucita Date: Fri, 19 Mar 2021 18:50:20 +0900 Subject: [PATCH 7/7] replay recording and uploading, discord webhook --- core/game/bot.ts | 9 + core/game/controller/events/onGameStart.ts | 4 + core/game/controller/events/onGameStop.ts | 17 ++ core/game/model/RoomObject/RoomObject.ts | 2 +- core/game/resource/strings.sample.en.ts | 4 +- core/game/resource/strings.ts | 4 +- core/lib/browser.interface.ts | 6 + core/lib/browser.ts | 63 ++++- core/package-lock.json | 78 ++++++- core/package.json | 1 + core/react/component/Admin/RoomSocial.tsx | 233 ++++++++++++++++--- core/typings/global.d.ts | 10 + core/web/controller/api/v1/room.ts | 42 ++++ core/web/router/api/v1/room.ts | 3 + core/web/schema/discordwebhook.validation.ts | 8 + 15 files changed, 439 insertions(+), 45 deletions(-) create mode 100644 core/lib/browser.interface.ts create mode 100644 core/web/schema/discordwebhook.validation.ts diff --git a/core/game/bot.ts b/core/game/bot.ts index 4ab2ad7..8f11289 100644 --- a/core/game/bot.ts +++ b/core/game/bot.ts @@ -23,6 +23,14 @@ window.gameRoom = { _room: window.HBInit(loadedConfig._config) ,config: loadedConfig ,link: '' + ,social: { + discordWebhook: { + feed: false + ,replayUpload: false + ,id: '' + ,token: '' + } + } ,stadiumData: { default: localStorage.getItem('_defaultMap')! ,training: localStorage.getItem('_readyMap')! @@ -64,6 +72,7 @@ window.document.title = `Haxbotron ${window.gameRoom.config._RUID}`; makeRoom(); // ==================================================================================================== // set scheduling timers + var scheduledTimer60 = setInterval(() => { window.gameRoom._room.sendAnnouncement(LangRes.scheduler.advertise, null, 0x777777, "normal", 0); // advertisement diff --git a/core/game/controller/events/onGameStart.ts b/core/game/controller/events/onGameStart.ts index fc7b2b6..fac7cc8 100644 --- a/core/game/controller/events/onGameStart.ts +++ b/core/game/controller/events/onGameStart.ts @@ -101,5 +101,9 @@ export function onGameStartListener(byPlayer: PlayerObject | null): void { } else { window.gameRoom._room.sendAnnouncement(Tst.maketext(LangRes.onStart.stopRecord, placeholderStart), null, 0x00FF00, "normal", 0); } + + // replay record start + window.gameRoom._room.startRecording(); + window.gameRoom.logger.i('onGameStart', msg); } diff --git a/core/game/controller/events/onGameStop.ts b/core/game/controller/events/onGameStop.ts index f86f79d..11a281b 100644 --- a/core/game/controller/events/onGameStop.ts +++ b/core/game/controller/events/onGameStop.ts @@ -1,3 +1,5 @@ +import * as Tst from "../Translator"; +import * as LangRes from "../../resource/strings"; import { PlayerObject } from "../../model/GameObject/PlayerObject"; import { convertTeamID2Name, TeamID } from "../../model/GameObject/TeamID"; import { recuritBothTeamFully } from "../../model/OperateHelper/Quorum"; @@ -42,6 +44,21 @@ export function onGameStopListener(byPlayer: PlayerObject): void { window.gameRoom.ballStack.clear(); // clear the stack. window.gameRoom.ballStack.possClear(); // clear possession count + // stop replay record and send it + const replay = window.gameRoom._room.stopRecording(); + + if(replay && window.gameRoom.social.discordWebhook.feed && window.gameRoom.social.discordWebhook.replayUpload && window.gameRoom.social.discordWebhook.id && window.gameRoom.social.discordWebhook.token) { + const placeholder = { + roomName: window.gameRoom.config._config.roomName + ,replayDate: Date().toLocaleString() + } + + window._feedSocialDiscordWebhook(window.gameRoom.social.discordWebhook.id, window.gameRoom.social.discordWebhook.token, "replay", { + message: Tst.maketext(LangRes.onStop.feedSocialDiscordWebhook.replayMessage, placeholder) + ,data: JSON.stringify(Array.from(replay)) + }); + } + // when auto emcee mode is enabled if(window.gameRoom.config.rules.autoOperating === true) { recuritBothTeamFully(); diff --git a/core/game/model/RoomObject/RoomObject.ts b/core/game/model/RoomObject/RoomObject.ts index 2b764cc..2134776 100644 --- a/core/game/model/RoomObject/RoomObject.ts +++ b/core/game/model/RoomObject/RoomObject.ts @@ -125,7 +125,7 @@ export interface Room { Stops the recording previously started with startRecording and returns the replay file contents as a Uint8Array. Returns null if recording was not started or had already been stopped. */ - stopRecording(): Uint8Array; + stopRecording(): Uint8Array | null; /* Changes the password of the room, if pass is null the password will be cleared. diff --git a/core/game/resource/strings.sample.en.ts b/core/game/resource/strings.sample.en.ts index 65ce058..20a24d8 100644 --- a/core/game/resource/strings.sample.en.ts +++ b/core/game/resource/strings.sample.en.ts @@ -204,7 +204,9 @@ export const onStart = { } export const onStop = { - + feedSocialDiscordWebhook: { + replayMessage: 'πŸ’½ Replay file from {roomName} ({replayDate})' + } } export const onVictory = { diff --git a/core/game/resource/strings.ts b/core/game/resource/strings.ts index c6c4c44..6cb31ca 100644 --- a/core/game/resource/strings.ts +++ b/core/game/resource/strings.ts @@ -204,7 +204,9 @@ export const onStart = { } export const onStop = { - + feedSocialDiscordWebhook: { + replayMessage: 'πŸ’½ {roomName}의 λ¦¬ν”Œλ ˆμ΄ 파일 ({replayDate})' + } } export const onVictory = { diff --git a/core/lib/browser.interface.ts b/core/lib/browser.interface.ts new file mode 100644 index 0000000..9e33e57 --- /dev/null +++ b/core/lib/browser.interface.ts @@ -0,0 +1,6 @@ +export type DiscordWebhookConfig = { + feed: boolean + id: string + token: string + replayUpload: boolean +} diff --git a/core/lib/browser.ts b/core/lib/browser.ts index 31aa9ea..6d12029 100644 --- a/core/lib/browser.ts +++ b/core/lib/browser.ts @@ -6,6 +6,12 @@ import * as dbUtilityInject from "./db.injection"; import { loadStadiumData } from "./stadiumLoader"; import { Server as SIOserver, Socket as SIOsocket } from "socket.io"; import { TeamID } from "../game/model/GameObject/TeamID"; +import Discord from 'discord.js'; +import { DiscordWebhookConfig } from "./browser.interface"; + +function typedArrayToBuffer(array: Uint8Array): ArrayBuffer { + return array.buffer.slice(array.byteOffset, array.byteLength + array.byteOffset) +} /** * Use this class for control Headless Browser. @@ -83,8 +89,11 @@ export class HeadlessBrowser { * Close given page. */ private async closePage(ruid: string) { - await this._PageContainer.get(ruid)?.close(); - this._PageContainer.delete(ruid); + await this._PageContainer.get(ruid)?.evaluate(() => { + window.gameRoom._room.stopRecording(); // suspend recording for prevent memory leak + }); + await this._PageContainer.get(ruid)?.close(); // close page + this._PageContainer.delete(ruid); // delete from container } /** @@ -164,19 +173,39 @@ export class HeadlessBrowser { page.addListener('_SIO.StatusChange', (event: any) => { this._SIOserver?.sockets.emit('statuschange', { ruid: ruid, playerID: event.playerID }); }); + page.addListener('_SOCIAL.DiscordWebhook', (event: any) => { + const webhookClient = new Discord.WebhookClient(event.id, event.token); + + switch (event.type as string) { + case "replay": { + const bufferData = Buffer.from(JSON.parse(event.content.data)); + const date = Date.now().toLocaleString(); + const attachment = new Discord.MessageAttachment( + bufferData + , `${date}.hbr2`); + webhookClient.send(event.content.message, { + files: [attachment], + }); + break; + } + } + }); // ================================================================================ // ================================================================================ // inject some functions ========================================================== await page.exposeFunction('_emitSIOLogEvent', (origin: string, type: string, message: string) => { page.emit('_SIO.Log', { origin: origin, type: type, message: message }); - }) + }); await page.exposeFunction('_emitSIOPlayerInOutEvent', (playerID: number) => { page.emit('_SIO.InOut', { playerID: playerID }); - }) + }); await page.exposeFunction('_emitSIOPlayerStatusChangeEvent', (playerID: number) => { page.emit('_SIO.StatusChange', { playerID: playerID }); - }) + }); + await page.exposeFunction('_feedSocialDiscordWebhook', (id: string, token: string, type: string, content: any) => { + page.emit('_SOCIAL.DiscordWebhook', { id: id, token: token, type: type, content: content }); + }); // inject functions for CRUD with DB Server ==================================== await page.exposeFunction('_createSuperadminDB', dbUtilityInject.createSuperadminDB); @@ -616,4 +645,28 @@ export class HeadlessBrowser { window.gameRoom.logger.i('system', `[TeamColour] New team colour is set for Team ${team}.`); }, team, angle, textColour, teamColour1, teamColour2, teamColour3); } + + /** + * Get discord webhook configuration + * @param ruid ruid Game room's UID + * @returns discord webhook configuration + */ + public async getDiscordWebhookConfig(ruid: string) { + return await this._PageContainer.get(ruid)!.evaluate(() => { + return window.gameRoom.social.discordWebhook as DiscordWebhookConfig; + }); + } + + /** + * Set discord webhook configuration + * @param ruid ruid Game room's UID + * @param config discord webhook configuration + */ + public async setDiscordWebhookConfig(ruid: string, config: DiscordWebhookConfig) { + await this._PageContainer.get(ruid)!.evaluate((config: DiscordWebhookConfig) => { + window.gameRoom.social.discordWebhook = config; + }, config); + } } + + diff --git a/core/package-lock.json b/core/package-lock.json index 8c04c03..f700be9 100644 --- a/core/package-lock.json +++ b/core/package-lock.json @@ -1,6 +1,6 @@ { "name": "haxbotron-core", - "version": "0.5.0", + "version": "0.5.1", "lockfileVersion": 1, "requires": true, "dependencies": { @@ -48,6 +48,21 @@ "kuler": "^2.0.0" } }, + "@discordjs/collection": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-0.1.6.tgz", + "integrity": "sha512-utRNxnd9kSS2qhyivo9lMlt5qgAUasH2gb7BEOn6p0efFh24gjGomHzWKMAPn2hEReOPQZCJaRKoURwRotKucQ==" + }, + "@discordjs/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/@discordjs/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-ZfFsbgEXW71Rw/6EtBdrP5VxBJy4dthyC0tpQKGKmYFImlmmrykO14Za+BiIVduwjte0jXEBlhSKf0MWbFp9Eg==", + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, "@emotion/hash": { "version": "0.8.0", "resolved": "https://registry.npmjs.org/@emotion/hash/-/hash-0.8.0.tgz", @@ -782,6 +797,14 @@ "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==" }, + "abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "requires": { + "event-target-shim": "^5.0.0" + } + }, "accepts": { "version": "1.3.7", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.7.tgz", @@ -903,6 +926,11 @@ "resolved": "https://registry.npmjs.org/async/-/async-3.2.0.tgz", "integrity": "sha512-TR2mEZFVOj2pLStYxLht7TyfuRzaydfpxr3k9RpHIzMgw7A64dzsdqCxH1WJyQdoe8T10nDXd9wnEigmiuHIZw==" }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha1-x57Zf380y48robyXkLzDZkdLS3k=" + }, "at-least-node": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/at-least-node/-/at-least-node-1.0.0.tgz", @@ -1193,6 +1221,14 @@ "text-hex": "1.0.x" } }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "requires": { + "delayed-stream": "~1.0.0" + } + }, "command-line-usage": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/command-line-usage/-/command-line-usage-6.1.0.tgz", @@ -1341,6 +1377,11 @@ "integrity": "sha512-FJ3UgI4gIl+PHZm53knsuSFpE+nESMr7M4v9QcgB7S63Kj/6WqMiFQJpBBYz1Pt+66bZpP3Q7Lye0Oo9MPKEdg==", "dev": true }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha1-3zrhmayt+31ECqrgsp4icrJOxhk=" + }, "delegates": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", @@ -1366,6 +1407,21 @@ "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.809251.tgz", "integrity": "sha512-pf+2OY6ghMDPjKkzSWxHMq+McD+9Ojmq5XVRYpv/kPd9sTMQxzEt21592a31API8qRjro0iYYOc3ag46qF/1FA==" }, + "discord.js": { + "version": "12.5.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-12.5.1.tgz", + "integrity": "sha512-VwZkVaUAIOB9mKdca0I5MefPMTQJTNg0qdgi1huF3iwsFwJ0L5s/Y69AQe+iPmjuV6j9rtKoG0Ta0n9vgEIL6w==", + "requires": { + "@discordjs/collection": "^0.1.6", + "@discordjs/form-data": "^3.0.1", + "abort-controller": "^3.0.0", + "node-fetch": "^2.6.1", + "prism-media": "^1.2.2", + "setimmediate": "^1.0.5", + "tweetnacl": "^1.0.3", + "ws": "^7.3.1" + } + }, "dom-helpers": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.0.tgz", @@ -1587,6 +1643,11 @@ "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", "dev": true }, + "event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==" + }, "events": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/events/-/events-3.2.0.tgz", @@ -3179,6 +3240,11 @@ "resolved": "https://registry.npmjs.org/popper.js/-/popper.js-1.16.1-lts.tgz", "integrity": "sha512-Kjw8nKRl1m+VrSFCoVGPph93W/qrSO7ZkqPpTf7F4bk/sqcfWK019dWBUpE/fBOsOQY1dks/Bmcbfn1heM/IsA==" }, + "prism-media": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/prism-media/-/prism-media-1.2.8.tgz", + "integrity": "sha512-bJ8J9PKpUdG6GmtnlaPSi2cMdGDLsS9o4iOlOncJasku73uJucgcN9Yr7/jlENqfh7hoR6LDqPr17JEzp6srjg==" + }, "process-nextick-args": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", @@ -3634,6 +3700,11 @@ "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", "integrity": "sha1-BF+XgtARrppoA93TgrJDkrPYkPc=" }, + "setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=" + }, "setprototypeof": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.1.tgz", @@ -4032,6 +4103,11 @@ "resolved": "https://registry.npmjs.org/tsscmp/-/tsscmp-1.0.6.tgz", "integrity": "sha512-LxhtAkPDTkVCMQjt2h6eBVY28KCjikZqZfMcC15YBeNjkgUpdCfBu5HoiOTDu86v6smE8yOjyEktJ8hlbANHQA==" }, + "tweetnacl": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-1.0.3.tgz", + "integrity": "sha512-6rt+RN7aOi1nGMyC4Xa5DdYiukl2UWCbcJft7YhxReBGQD7OAM8Pbxw6YMo4r2diNEA8FEmu32YOn9rhaiE5yw==" + }, "type-is": { "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", diff --git a/core/package.json b/core/package.json index a4a85c3..3da14d0 100644 --- a/core/package.json +++ b/core/package.json @@ -39,6 +39,7 @@ "axios": "^0.21.1", "bcrypt": "^5.0.0", "cookie": "^0.4.1", + "discord.js": "^12.5.1", "dotenv": "^8.2.0", "joi": "^17.3.0", "jsonwebtoken": "^8.5.1", diff --git a/core/react/component/Admin/RoomSocial.tsx b/core/react/component/Admin/RoomSocial.tsx index 7d0f3ec..51f4d35 100644 --- a/core/react/component/Admin/RoomSocial.tsx +++ b/core/react/component/Admin/RoomSocial.tsx @@ -7,10 +7,11 @@ import Paper from '@material-ui/core/Paper'; import Copyright from '../common/Footer.Copyright'; import Title from './common/Widget.Title'; import { useParams } from 'react-router-dom'; -import { Button, IconButton, Table, TableBody, TableCell, TableHead, TableRow, TextField } from '@material-ui/core'; +import { Button, Divider, FormControlLabel, IconButton, Switch, Table, TableBody, TableCell, TableHead, TableRow, TextField, Typography } from '@material-ui/core'; import client from '../../lib/client'; import Alert, { AlertColor } from '../common/Alert'; import BackspaceIcon from '@material-ui/icons/Backspace'; +import { LiveHelp } from '@material-ui/icons'; interface styleClass { styleClass: any @@ -20,6 +21,13 @@ interface matchParams { ruid: string } +type DiscordWebhookConfig = { + feed: boolean + id: string + token: string + replayUpload: boolean +} + export default function RoomSocial({ styleClass }: styleClass) { const classes = styleClass; @@ -31,6 +39,11 @@ export default function RoomSocial({ styleClass }: styleClass) { const [newNoticeMessage, setNewNoticeMessage] = useState(''); const [noticeMessage, setNoticeMessage] = useState(''); + const [newDiscordWebhookID, setNewDiscordWebhookID] = useState(''); + const [newDiscordWebhookToken, setNewDiscordWebhookToken] = useState(''); + const [newDiscordWebhookFeed, setNewDiscordWebhookFeed] = useState(false); + const [newDiscordWebhookReplayUpload, setNewDiscordWebhookReplayUpload] = useState(false); + const [flashMessage, setFlashMessage] = useState(''); const [alertStatus, setAlertStatus] = useState("success" as AlertColor); @@ -74,10 +87,69 @@ export default function RoomSocial({ styleClass }: styleClass) { } } + const handleDiscordWebhookSet = async (event: React.FormEvent) => { + event.preventDefault(); + localStorage.setItem(`_DiscordWebhookConfig`, JSON.stringify({ + feed: newDiscordWebhookFeed, id: newDiscordWebhookID, token: newDiscordWebhookToken, replayUpload: newDiscordWebhookReplayUpload + } as DiscordWebhookConfig)); + try { + const result = await client.post(`/api/v1/room/${matchParams.ruid}/social/discord/webhook`, { + feed: newDiscordWebhookFeed, + id: newDiscordWebhookID, + token: newDiscordWebhookToken, + replayUpload: newDiscordWebhookReplayUpload + }); + if (result.status === 201) { + setFlashMessage('Discord Webhook is configured.'); + setAlertStatus('success'); + getDiscordWebhookConfig(); + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } catch (error) { + setAlertStatus('error'); + switch (error.response.status) { + case 400: { + setFlashMessage('Request body for Discord webhook is unfulfilled.'); + break; + } + case 401: { + setFlashMessage('No permission.'); + break; + } + case 404: { + setFlashMessage('No exists room.'); + break; + } + default: { + setFlashMessage('Unexpected error is caused. Please try again.'); + break; + } + } + setTimeout(() => { + setFlashMessage(''); + }, 3000); + } + } + const onChangeNoticeMessage = (e: React.ChangeEvent) => { setNewNoticeMessage(e.target.value); } + const onChangeDiscordWebhookID = (e: React.ChangeEvent) => { + setNewDiscordWebhookID(e.target.value); + } + const onChangeDiscordWebhookToken = (e: React.ChangeEvent) => { + setNewDiscordWebhookToken(e.target.value); + } + const onChangeDiscordWebhookFeed = (e: React.ChangeEvent) => { + setNewDiscordWebhookFeed(e.target.checked); // switch toggle component + } + const onChangeDiscordWebhookReplayUpload = (e: React.ChangeEvent) => { + setNewDiscordWebhookReplayUpload(e.target.checked); // switch toggle component + } + const getNoticeMessage = async () => { try { const result = await client.get(`/api/v1/room/${matchParams.ruid}/social/notice`); @@ -95,6 +167,26 @@ export default function RoomSocial({ styleClass }: styleClass) { } } } + const getDiscordWebhookConfig = async () => { + try { + const result = await client.get(`/api/v1/room/${matchParams.ruid}/social/discord/webhook`); + if (result.status === 200) { + const config: DiscordWebhookConfig = result.data; + setNewDiscordWebhookID(config.id); + setNewDiscordWebhookToken(config.token); + setNewDiscordWebhookFeed(config.feed); + setNewDiscordWebhookReplayUpload(config.replayUpload); + } + } catch (error) { + setAlertStatus('error'); + if (error.response.status === 404) { + setFlashMessage('Failed to load Discord webhook configuration.'); + setNoticeMessage(''); + } else { + setFlashMessage('Unexpected error is caused. Please try again.'); + } + } + } const deleteNoticeMessage = async () => { try { @@ -122,8 +214,21 @@ export default function RoomSocial({ styleClass }: styleClass) { } } + const handleDiscordWebhookLoad = async (event: React.MouseEvent) => { + event.preventDefault(); + + if (localStorage.getItem(`_DiscordWebhookConfig`) !== null) { + const config: DiscordWebhookConfig = JSON.parse(localStorage.getItem(`_DiscordWebhookConfig`)!); + setNewDiscordWebhookID(config.id); + setNewDiscordWebhookToken(config.token); + setNewDiscordWebhookFeed(config.feed); + setNewDiscordWebhookReplayUpload(config.replayUpload); + } + } + useEffect(() => { getNoticeMessage(); + getDiscordWebhookConfig(); }, []); return ( @@ -133,44 +238,100 @@ export default function RoomSocial({ styleClass }: styleClass) { {flashMessage && {flashMessage}} + Notice
- - - + + + + + + + + + + + + + + +
+ + + Notice Message + + + + + + {noticeMessage} + + {noticeMessage && + + + + } + + + +
+ + + + + Discord Webhook + + + + {'Create a webhook in the Discord application and submit your webhook\'s ID and Token. (e.g. https://discord.com/api/webhooks/id/token)'} + window.open('https://github.com/dapucita/haxbotron/wiki/Discord-Webhook-Configuration', '_blank')} edge="start" size="medium" aria-label="get help"> + + + + + +
+ + + } + label="Enable" labelPlacement="top" + /> + + + + + + + + + + + + + + + + + } + label="Replay Upload" labelPlacement="top" + /> + +
- - - - Notice Message - - - - - - {noticeMessage} - - {noticeMessage && - - - - } - - - -
diff --git a/core/typings/global.d.ts b/core/typings/global.d.ts index a57d380..ac98a37 100644 --- a/core/typings/global.d.ts +++ b/core/typings/global.d.ts @@ -20,6 +20,15 @@ declare global { config: GameRoomConfig // bot settings collection link: string // for sharing URI link of the room + social: { + discordWebhook: { + feed: boolean + id: string + token: string + replayUpload: boolean + } + } + stadiumData: { default: string training: string @@ -89,6 +98,7 @@ declare global { _emitSIOLogEvent(origin: string, type: string, message: string): void _emitSIOPlayerInOutEvent(playerID: number): void _emitSIOPlayerStatusChangeEvent(playerID: number): void + _feedSocialDiscordWebhook(id: string, token: string, type: string, content: any): void // CRUD with DB Server via REST API async _createPlayerDB(ruid: string, player: PlayerStorage): Promise async _readPlayerDB(ruid: string, playerAuth: string): Promise diff --git a/core/web/controller/api/v1/room.ts b/core/web/controller/api/v1/room.ts index bff28cd..eb31320 100644 --- a/core/web/controller/api/v1/room.ts +++ b/core/web/controller/api/v1/room.ts @@ -3,6 +3,7 @@ import { Player } from "../../../../game/model/GameObject/Player"; import { TeamID } from "../../../../game/model/GameObject/TeamID"; import { HeadlessBrowser } from "../../../../lib/browser"; import { BrowserHostRoomInitConfig } from '../../../../lib/browser.hostconfig'; +import { discordWebhookConfigSchema } from "../../../schema/discordwebhook.validation"; import { nestedHostRoomConfigSchema } from "../../../schema/hostroomconfig.validation"; import { teamColourSchema } from "../../../schema/teamcolour.validation"; @@ -476,3 +477,44 @@ export async function setTeamColours(ctx: Context) { ctx.status = 201; } } + +/** + * Get discord webhook configuration + */ +export async function getDiscordWebhookConfig(ctx: Context) { + const { ruid } = ctx.params; + ctx.status = 404; + + if (browser.checkExistRoom(ruid)) { + const config = await browser.getDiscordWebhookConfig(ruid); + ctx.body = { + feed: config.feed + ,id: config.id + ,token: config.token + ,replayUpload: config.replayUpload + } + ctx.status = 200; + } +} + +/** + * Set discord webhook configuration + */ +export async function setDiscordWebhookConfig(ctx: Context) { + const { ruid } = ctx.params; + const { feed, id, token, replayUpload } = ctx.request.body; + ctx.status = 404; + + const validationResult = discordWebhookConfigSchema.validate(ctx.request.body); + + if (validationResult.error) { + ctx.status = 400; + ctx.body = validationResult.error; + return; + } + + if (browser.checkExistRoom(ruid)) { + browser.setDiscordWebhookConfig(ruid, { feed, id, token, replayUpload }); + ctx.status = 201; + } +} diff --git a/core/web/router/api/v1/room.ts b/core/web/router/api/v1/room.ts index a8d34ff..553657f 100644 --- a/core/web/router/api/v1/room.ts +++ b/core/web/router/api/v1/room.ts @@ -35,6 +35,9 @@ roomRouter.get('/:ruid/social/notice', checkLoginMiddleware, roomController.getN roomRouter.post('/:ruid/social/notice', checkLoginMiddleware, roomController.setNotice); // set notice message roomRouter.delete('/:ruid/social/notice', checkLoginMiddleware, roomController.deleteNotice); // delete notice message +roomRouter.get('/:ruid/social/discord/webhook', checkLoginMiddleware, roomController.getDiscordWebhookConfig); // get discord webhook configuration +roomRouter.post('/:ruid/social/discord/webhook', checkLoginMiddleware, roomController.setDiscordWebhookConfig); // set discord webhook configuration + roomRouter.get('/:ruid/filter/nickname', checkLoginMiddleware, roomController.getNicknameTextFilteringPool); // get banned words pool for chat filter roomRouter.get('/:ruid/filter/chat', checkLoginMiddleware, roomController.getChatTextFilteringPool); // get banned words pool for nickname filter roomRouter.post('/:ruid/filter/nickname', checkLoginMiddleware, roomController.setNicknameTextFilter); // set banned words pool for chat filter diff --git a/core/web/schema/discordwebhook.validation.ts b/core/web/schema/discordwebhook.validation.ts new file mode 100644 index 0000000..e7d5509 --- /dev/null +++ b/core/web/schema/discordwebhook.validation.ts @@ -0,0 +1,8 @@ +import Joi from 'joi'; + +export const discordWebhookConfigSchema = Joi.object().keys({ + feed: Joi.boolean().required() + ,id: Joi.string().optional().allow(null, '') + ,token: Joi.string().optional().allow(null, '') + ,replayUpload: Joi.boolean().required() +});