From 0f04ac49f3d997bfe4fca083143229ab6f987c27 Mon Sep 17 00:00:00 2001 From: Thomas Smyth Date: Sun, 14 Jun 2020 12:53:38 +0100 Subject: [PATCH] Add more limitations to map voting --- connectors/data/layers.json | 182 ++++++++++++++++++++++++------- connectors/squad-layer-filter.js | 125 ++++++++++++++++++++- plugins/mapvote/README.md | 15 ++- plugins/mapvote/mapvote.js | 25 ++++- 4 files changed, 303 insertions(+), 44 deletions(-) diff --git a/connectors/data/layers.json b/connectors/data/layers.json index 931cdeb..17d9eb5 100644 --- a/connectors/data/layers.json +++ b/connectors/data/layers.json @@ -209,7 +209,11 @@ }, "tanks": "x1 for INS, 30 min delay", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Al Basrah TC v2", @@ -232,7 +236,11 @@ }, "tanks": "x1 for INS, 20 min delay", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Belaya AAS v1", @@ -471,7 +479,11 @@ }, "tanks": "x1 per team, 20min delay", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Chora AAS v1", @@ -710,12 +722,16 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Fool's Road AAS v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_AAS_v1", + "layerClassname": "Fools_Road_AAS_v1", "mapSize": "2x2 km", "gamemode": "AAS", "version": "v1", @@ -742,7 +758,7 @@ { "layer": "Fool's Road AAS v2", "map": "Fool's Road", - "layerClassname": "FoolsRoad_AAS_v2", + "layerClassname": "Fools_Road_AAS_v2", "mapSize": "2x2 km", "gamemode": "AAS", "version": "v2", @@ -769,7 +785,7 @@ { "layer": "Fool's Road Destruction v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_Destruction_v1", + "layerClassname": "Fools_Road_Destruction_v1", "mapSize": "2x2 km", "gamemode": "Destruction", "version": "v1", @@ -796,7 +812,7 @@ { "layer": "Fool's Road Invasion v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_Invasion_v1", + "layerClassname": "Fools_Road_Invasion_v1", "mapSize": "2x2 km", "gamemode": "Invasion", "version": "v1", @@ -823,7 +839,7 @@ { "layer": "Fool's Road RAAS v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_RAAS_v1", + "layerClassname": "Fools_Road_RAAS_v1", "mapSize": "2x2 km", "gamemode": "RAAS", "version": "v1", @@ -850,7 +866,7 @@ { "layer": "Fool's Road RAAS v2", "map": "Fool's Road", - "layerClassname": "FoolsRoad_RAAS_v2", + "layerClassname": "Fools_Road_RAAS_v2", "mapSize": "2x2 km", "gamemode": "RAAS", "version": "v2", @@ -877,7 +893,7 @@ { "layer": "Fool's Road RAAS v3", "map": "Fool's Road", - "layerClassname": "FoolsRoad_RAAS_v3", + "layerClassname": "Fools_Road_RAAS_v3", "mapSize": "2x2 km", "gamemode": "RAAS", "version": "v3", @@ -904,7 +920,7 @@ { "layer": "Fool's Road Skirmish v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_Skirmish_v1", + "layerClassname": "Fools_Road_Skirmish_v1", "mapSize": "2x2 km", "gamemode": "Skirmish", "version": "v1", @@ -931,7 +947,7 @@ { "layer": "Fool's Road Skirmish v2", "map": "Fool's Road", - "layerClassname": "FoolsRoad_Skirmish_v2", + "layerClassname": "Fools_Road_Skirmish_v2", "mapSize": "2x2 km", "gamemode": "Skirmish", "version": "v2", @@ -958,7 +974,7 @@ { "layer": "Fool's Road TC v1", "map": "Fool's Road", - "layerClassname": "FoolsRoad_TC_v1", + "layerClassname": "Fools_Road_TC_v1", "mapSize": "2x2 km", "gamemode": "TC", "version": "v1", @@ -976,7 +992,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Gorodok AAS v1", @@ -1296,7 +1316,11 @@ }, "tanks": "N/A", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Jensen's Range Training v1", @@ -1697,7 +1721,11 @@ }, "tanks": "N/A", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Kamdesh TC v2", @@ -1720,7 +1748,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Kohat AAS v1", @@ -1986,7 +2018,11 @@ }, "tanks": "N/A", "helicopters": "x2 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Kokan AAS v1", @@ -2171,7 +2207,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Logar Valley AAS v1", @@ -2329,7 +2369,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Mestia AAS v1", @@ -2487,7 +2531,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Mutaha AAS v1", @@ -2618,7 +2666,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Mutaha TC v2", @@ -2641,7 +2693,11 @@ }, "tanks": "x1 per team, 20min delay", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Narva AAS v1", @@ -2880,7 +2936,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Narva TC v2", @@ -2903,7 +2963,11 @@ }, "tanks": "x1 per team, 30min delay", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Skorpo AAS v1", @@ -3142,7 +3206,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Skorpo TC v2", @@ -3165,7 +3233,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Skorpo TC v3", @@ -3188,7 +3260,11 @@ }, "tanks": "x1 per team, 20min delay", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "Sumari AAS v1", @@ -3373,7 +3449,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 36, + "max": 80 + } }, { "layer": "Tallil Outskirts AAS v1", @@ -3739,7 +3819,11 @@ }, "tanks": "x2 per team, 20min delay", "helicopters": "x2 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Tutorials Infantry Training Tutorial", @@ -4159,7 +4243,11 @@ }, "tanks": "x2 per team, 20min delay", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "Yehorivka TC v2", @@ -4182,7 +4270,11 @@ }, "tanks": "x1 per team", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 54, + "max": 80 + } }, { "layer": "CAF_Albasrah_Invasion_v1", @@ -4313,7 +4405,11 @@ }, "tanks": "N/A", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "CAF_Jensens_Range_v4", @@ -4417,7 +4513,11 @@ }, "tanks": "N/A", "helicopters": "N/A", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 40, + "max": 80 + } }, { "layer": "CAF_Kohat_Invasion_v1", @@ -4710,7 +4810,11 @@ }, "tanks": "N/A", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } }, { "layer": "CAF_Mestia_RAAS_v1", @@ -4949,6 +5053,10 @@ }, "tanks": "N/A", "helicopters": "x1 per team", - "newForVersion": false + "newForVersion": false, + "estimatedSuitablePlayerCount": { + "min": 45, + "max": 80 + } } ] \ No newline at end of file diff --git a/connectors/squad-layer-filter.js b/connectors/squad-layer-filter.js index f9550a7..f3dcc94 100644 --- a/connectors/squad-layer-filter.js +++ b/connectors/squad-layer-filter.js @@ -16,7 +16,20 @@ export default class SquadLayerFilter extends SquadLayersClass { // defaults as off ...activeLayerFilter.gamemodeHistoryTolerance }, + gamemodeRepetitiveTolerance: { + // defaults as off + ...activeLayerFilter.gamemodeRepetitiveTolerance + }, playerCountComplianceEnabled: true, + factionComplianceEnabled: true, + factionHistoryTolerance: { + // defaults as off + ...activeLayerFilter.factionHistoryTolerance + }, + factionRepetitiveTolerance: { + // defaults as off + ...activeLayerFilter.factionRepetitiveTolerance + }, ...activeLayerFilter }; } @@ -124,6 +137,7 @@ export default class SquadLayerFilter extends SquadLayersClass { ) { if (new Date() - server.layerHistory[i].time > this.activeLayerFilter.historyResetTime) return true; + if (server.layerHistory[i].map === layer.map) return false; } return true; @@ -144,13 +158,120 @@ export default class SquadLayerFilter extends SquadLayersClass { return true; const historyLayer = SquadLayers.getLayerByLayerName(server.layerHistory[i].layer); - if (historyLayer && historyLayer.gamemode === layer.gamemode) return false; + if (!historyLayer) continue; + + if (historyLayer.gamemode === layer.gamemode) return false; } return true; } + isGamemodeRepetitiveCompliant(server, layer) { + if (this.activeLayerFilter === null) return true; + + if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer); + + const gamemodeRepetitiveTolerance = this.activeLayerFilter.gamemodeRepetitiveTolerance[ + layer.gamemode + ]; + if (!gamemodeRepetitiveTolerance) return true; + + for (let i = 0; i < Math.min(server.layerHistory.length, gamemodeRepetitiveTolerance); i++) { + if (new Date() - server.layerHistory[i].time > this.activeLayerFilter.historyResetTime) + return true; + + const historyLayer = SquadLayers.getLayerByLayerName(server.layerHistory[i].layer); + if (!historyLayer) return true; + + if (historyLayer.gamemode !== layer.gamemode) return true; + } + return false; + } + + isFactionCompliant(server, layer) { + if ( + this.activeLayerFilter === null || + this.activeLayerFilter.factionComplianceEnabled === false + ) + return true; + if (server.layerHistory.length === 0) return true; + + if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer); + + const historyLayer = SquadLayers.getLayerByLayerName(server.layerHistory[0].layer); + + return ( + !historyLayer || + ( + historyLayer.teamOne.faction !== layer.teamTwo.faction && + historyLayer.teamTwo.faction !== layer.teamOne.faction + ) + ); + } + + isFactionHistoryCompliant(server, layer, faction = null) { + if (this.activeLayerFilter === null) return true; + + if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer); + + if (faction === null) { + return ( + this.isFactionHistoryCompliant(server, layer, layer.teamOne.faction) && + this.isFactionHistoryCompliant(server, layer, layer.teamTwo.faction) + ); + } else { + const factionThreshold = this.activeLayerFilter.factionHistoryTolerance[faction]; + if (!factionThreshold) return true; + + for (let i = 0; i < Math.min(server.layerHistory.length, factionThreshold); i++) { + if (new Date() - server.layerHistory[i].time > this.activeLayerFilter.historyResetTime) + return true; + + const historyLayer = SquadLayers.getLayerByLayerName(server.layerHistory[i].layer); + if (!historyLayer) continue; + + if (historyLayer.teamOne.faction === faction || historyLayer.teamTwo.faction === faction) + return false; + } + + return true; + } + } + + isFactionRepetitiveCompliant(server, layer, faction = null) { + if (this.activeLayerFilter === null) return true; + + if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer); + + if (faction === null) { + return ( + this.isFactionRepetitiveCompliant(server, layer, layer.teamOne.faction) && + this.isFactionRepetitiveCompliant(server, layer, layer.teamTwo.faction) + ); + } else { + const factionThreshold = this.activeLayerFilter.factionRepetitiveTolerance[faction]; + if (!factionThreshold) return true; + + for (let i = 0; i < Math.min(server.layerHistory.length, factionThreshold); i++) { + if (new Date() - server.layerHistory[i].time > this.activeLayerFilter.historyResetTime) + return true; + + const historyLayer = SquadLayers.getLayerByLayerName(server.layerHistory[i].layer); + if (!historyLayer) return true; + + if (historyLayer.teamOne.faction !== faction && historyLayer.teamTwo.faction !== faction) + return true; + } + + return false; + } + } + isPlayerCountCompliant(server, layer) { - if (this.activeLayerFilter === null || this.playerCountComplianceEnabled === false) return true; + if ( + this.activeLayerFilter === null || + this.activeLayerFilter.playerCountComplianceEnabled === false + ) + return true; if (typeof layer === 'string') layer = SquadLayers.getLayerByLayerName(layer); diff --git a/plugins/mapvote/README.md b/plugins/mapvote/README.md index 803cabb..50d1512 100644 --- a/plugins/mapvote/README.md +++ b/plugins/mapvote/README.md @@ -37,7 +37,20 @@ const activeLayerFilter = { Invasion: 4 // invasion can only be played once every x layers // if not specified they will default to off }, - playerCountComplianceEnabled: true // filter layers based on suggested player counts if true + gamemodeRepetitiveTolerance: { + Invasion: 4 // invasion can only be played up to x times in a row + // if not specified they will default to off + }, + playerCountComplianceEnabled: true, // filter layers based on suggested player counts if true + factionComplianceEnabled: true, // a team cannot play the same faction twice in a row + factionHistoryTolerance: { + RUS: 4 // rus can only be played once every x layers + // if not specified they will default to off + }, + factionRepetitiveTolerance: { + RUS: 4 // rus can only be played up to x times in a row + // if not specified they will default to off + }, }; ``` diff --git a/plugins/mapvote/mapvote.js b/plugins/mapvote/mapvote.js index 3cedef6..7e6d072 100644 --- a/plugins/mapvote/mapvote.js +++ b/plugins/mapvote/mapvote.js @@ -9,6 +9,7 @@ export default class MapVote extends EventEmitter { this.squadLayerFilter = squadLayerFilter; this.layerVotes = {}; + this.layerVoteTimes = {}; this.playerVotes = {}; this.currentWinner = null; @@ -20,6 +21,7 @@ export default class MapVote extends EventEmitter { this.layerVotes[layerName] += 1; } else { this.layerVotes[layerName] = 1; + this.layerVoteTimes[layerName] = new Date(); } this.playerVotes[identifier] = layerName; } @@ -29,8 +31,11 @@ export default class MapVote extends EventEmitter { if (this.layerVotes[this.playerVotes[identifier]]) this.layerVotes[this.playerVotes[identifier]] -= 1; - if (this.layerVotes[this.playerVotes[identifier]] === 0) + + if (this.layerVotes[this.playerVotes[identifier]] === 0) { delete this.layerVotes[this.playerVotes[identifier]]; + delete this.layerVoteTimes[this.playerVotes[identifier]]; + } delete this.playerVotes[identifier]; } @@ -49,7 +54,7 @@ export default class MapVote extends EventEmitter { .sort((a, b) => { if (a.votes > b.votes) return -1; if (a.votes < b.votes) return 1; - else return Math.random() < 0.5 ? 1 : -1; + return this.layerVoteTimes[a.layer.layer] < this.layerVoteTimes[b.layer.layer] ? -1 : 1; }); } else return []; } @@ -66,6 +71,18 @@ export default class MapVote extends EventEmitter { throw new Error(`${layer.map} was played too recently.`); if (!this.squadLayerFilter.isGamemodeHistoryCompliant(this.server, layer)) throw new Error(`${layer.gamemode} was played too recently.`); + if (!this.squadLayerFilter.isGamemodeRepetitiveCompliant(this.server, layer)) + throw new Error(`${layer.gamemode} has been played too much recently.`); + if (!this.squadLayerFilter.isFactionCompliant(this.server, layer)) + throw new Error('Cannot be played as one team will remain the same faction.'); + if (!this.squadLayerFilter.isFactionHistoryCompliant(this.server, layer)) + throw new Error( + `Cannot be played as either ${layer.teamOne.faction} or ${layer.teamTwo.faction} has been played too recently.` + ); + if (!this.squadLayerFilter.isFactionRepetitiveCompliant(this.server, layer)) + throw new Error( + `Cannot be played as either ${layer.teamOne.faction} or ${layer.teamTwo.faction} has been played too much recently.` + ); if (!this.squadLayerFilter.isPlayerCountCompliant(this.server, layer)) throw new Error( `${layer.layer} is only suitable for a player count between ${layer.estimatedSuitablePlayerCount.min} and ${layer.estimatedSuitablePlayerCount.max}.` @@ -77,10 +94,10 @@ export default class MapVote extends EventEmitter { const results = this.getResults(true); if (results.length > 0) { - if (results[0] !== this.currentWinner) { + if (results[0].layer.layer !== this.currentWinner) { await this.server.rcon.execute(`AdminSetNextMap ${results[0].layer.layer}`); this.emit('NEW_WINNER', results); - this.currentWinner = results[0]; + this.currentWinner = results[0].layer.layer; } }