working new plugin

This commit is contained in:
Fantino Davide 2022-08-13 12:05:40 +02:00
parent 42c1552f0c
commit 0721661af3
8 changed files with 196 additions and 1296 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -1,333 +0,0 @@
/*
Viz.js 2.1.2 (Graphviz 2.40.1, Expat 2.2.5, Emscripten 1.37.36)
Copyright (c) 2014-2018 Michael Daines
Licensed under MIT license
This distribution contains other software in object code form:
Graphviz
Licensed under Eclipse Public License - v 1.0
http://www.graphviz.org
Expat
Copyright (c) 1998, 1999, 2000 Thai Open Source Software Center Ltd and Clark Cooper
Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006 Expat maintainers.
Licensed under MIT license
http://www.libexpat.org
zlib
Copyright (C) 1995-2013 Jean-loup Gailly and Mark Adler
http://www.zlib.net/zlib_license.html
*/
(function (global, factory) {
typeof exports === 'object' && typeof module !== 'undefined' ? module.exports = factory() :
typeof define === 'function' && define.amd ? define(factory) :
(global.Viz = factory());
}(this, (function () { 'use strict';
var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) {
return typeof obj;
} : function (obj) {
return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj;
};
var classCallCheck = function (instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError("Cannot call a class as a function");
}
};
var createClass = function () {
function defineProperties(target, props) {
for (var i = 0; i < props.length; i++) {
var descriptor = props[i];
descriptor.enumerable = descriptor.enumerable || false;
descriptor.configurable = true;
if ("value" in descriptor) descriptor.writable = true;
Object.defineProperty(target, descriptor.key, descriptor);
}
}
return function (Constructor, protoProps, staticProps) {
if (protoProps) defineProperties(Constructor.prototype, protoProps);
if (staticProps) defineProperties(Constructor, staticProps);
return Constructor;
};
}();
var _extends = Object.assign || function (target) {
for (var i = 1; i < arguments.length; i++) {
var source = arguments[i];
for (var key in source) {
if (Object.prototype.hasOwnProperty.call(source, key)) {
target[key] = source[key];
}
}
}
return target;
};
var WorkerWrapper = function () {
function WorkerWrapper(worker) {
var _this = this;
classCallCheck(this, WorkerWrapper);
this.worker = worker;
this.listeners = [];
this.nextId = 0;
this.worker.addEventListener('message', function (event) {
var id = event.data.id;
var error = event.data.error;
var result = event.data.result;
_this.listeners[id](error, result);
delete _this.listeners[id];
});
}
createClass(WorkerWrapper, [{
key: 'render',
value: function render(src, options) {
var _this2 = this;
return new Promise(function (resolve, reject) {
var id = _this2.nextId++;
_this2.listeners[id] = function (error, result) {
if (error) {
reject(new Error(error.message, error.fileName, error.lineNumber));
return;
}
resolve(result);
};
_this2.worker.postMessage({ id: id, src: src, options: options });
});
}
}]);
return WorkerWrapper;
}();
var ModuleWrapper = function ModuleWrapper(module, render) {
classCallCheck(this, ModuleWrapper);
var instance = module();
this.render = function (src, options) {
return new Promise(function (resolve, reject) {
try {
resolve(render(instance, src, options));
} catch (error) {
reject(error);
}
});
};
};
// https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/Base64_encoding_and_decoding
function b64EncodeUnicode(str) {
return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g, function (match, p1) {
return String.fromCharCode('0x' + p1);
}));
}
function defaultScale() {
if ('devicePixelRatio' in window && window.devicePixelRatio > 1) {
return window.devicePixelRatio;
} else {
return 1;
}
}
function svgXmlToImageElement(svgXml) {
var _ref = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref$scale = _ref.scale,
scale = _ref$scale === undefined ? defaultScale() : _ref$scale,
_ref$mimeType = _ref.mimeType,
mimeType = _ref$mimeType === undefined ? "image/png" : _ref$mimeType,
_ref$quality = _ref.quality,
quality = _ref$quality === undefined ? 1 : _ref$quality;
return new Promise(function (resolve, reject) {
var svgImage = new Image();
svgImage.onload = function () {
var canvas = document.createElement('canvas');
canvas.width = svgImage.width * scale;
canvas.height = svgImage.height * scale;
var context = canvas.getContext("2d");
context.drawImage(svgImage, 0, 0, canvas.width, canvas.height);
canvas.toBlob(function (blob) {
var image = new Image();
image.src = URL.createObjectURL(blob);
image.width = svgImage.width;
image.height = svgImage.height;
resolve(image);
}, mimeType, quality);
};
svgImage.onerror = function (e) {
var error;
if ('error' in e) {
error = e.error;
} else {
error = new Error('Error loading SVG');
}
reject(error);
};
svgImage.src = 'data:image/svg+xml;base64,' + b64EncodeUnicode(svgXml);
});
}
function svgXmlToImageElementFabric(svgXml) {
var _ref2 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref2$scale = _ref2.scale,
scale = _ref2$scale === undefined ? defaultScale() : _ref2$scale,
_ref2$mimeType = _ref2.mimeType,
mimeType = _ref2$mimeType === undefined ? 'image/png' : _ref2$mimeType,
_ref2$quality = _ref2.quality,
quality = _ref2$quality === undefined ? 1 : _ref2$quality;
var multiplier = scale;
var format = void 0;
if (mimeType == 'image/jpeg') {
format = 'jpeg';
} else if (mimeType == 'image/png') {
format = 'png';
}
return new Promise(function (resolve, reject) {
fabric.loadSVGFromString(svgXml, function (objects, options) {
// If there's something wrong with the SVG, Fabric may return an empty array of objects. Graphviz appears to give us at least one <g> element back even given an empty graph, so we will assume an error in this case.
if (objects.length == 0) {
reject(new Error('Error loading SVG with Fabric'));
}
var element = document.createElement("canvas");
element.width = options.width;
element.height = options.height;
var canvas = new fabric.Canvas(element, { enableRetinaScaling: false });
var obj = fabric.util.groupSVGElements(objects, options);
canvas.add(obj).renderAll();
var image = new Image();
image.src = canvas.toDataURL({ format: format, multiplier: multiplier, quality: quality });
image.width = options.width;
image.height = options.height;
resolve(image);
});
});
}
var Viz = function () {
function Viz() {
var _ref3 = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {},
workerURL = _ref3.workerURL,
worker = _ref3.worker,
Module = _ref3.Module,
render = _ref3.render;
classCallCheck(this, Viz);
if (typeof workerURL !== 'undefined') {
this.wrapper = new WorkerWrapper(new Worker(workerURL));
} else if (typeof worker !== 'undefined') {
this.wrapper = new WorkerWrapper(worker);
} else if (typeof Module !== 'undefined' && typeof render !== 'undefined') {
this.wrapper = new ModuleWrapper(Module, render);
} else if (typeof Viz.Module !== 'undefined' && typeof Viz.render !== 'undefined') {
this.wrapper = new ModuleWrapper(Viz.Module, Viz.render);
} else {
throw new Error('Must specify workerURL or worker option, Module and render options, or include one of full.render.js or lite.render.js after viz.js.');
}
}
createClass(Viz, [{
key: 'renderString',
value: function renderString(src) {
var _ref4 = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {},
_ref4$format = _ref4.format,
format = _ref4$format === undefined ? 'svg' : _ref4$format,
_ref4$engine = _ref4.engine,
engine = _ref4$engine === undefined ? 'dot' : _ref4$engine,
_ref4$files = _ref4.files,
files = _ref4$files === undefined ? [] : _ref4$files,
_ref4$images = _ref4.images,
images = _ref4$images === undefined ? [] : _ref4$images,
_ref4$yInvert = _ref4.yInvert,
yInvert = _ref4$yInvert === undefined ? false : _ref4$yInvert,
_ref4$nop = _ref4.nop,
nop = _ref4$nop === undefined ? 0 : _ref4$nop;
for (var i = 0; i < images.length; i++) {
files.push({
path: images[i].path,
data: '<?xml version="1.0" encoding="UTF-8" standalone="no"?>\n<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd">\n<svg width="' + images[i].width + '" height="' + images[i].height + '"></svg>'
});
}
return this.wrapper.render(src, { format: format, engine: engine, files: files, images: images, yInvert: yInvert, nop: nop });
}
}, {
key: 'renderSVGElement',
value: function renderSVGElement(src) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) {
var parser = new DOMParser();
return parser.parseFromString(str, 'image/svg+xml').documentElement;
});
}
}, {
key: 'renderImageElement',
value: function renderImageElement(src) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var scale = options.scale,
mimeType = options.mimeType,
quality = options.quality;
return this.renderString(src, _extends({}, options, { format: 'svg' })).then(function (str) {
if ((typeof fabric === 'undefined' ? 'undefined' : _typeof(fabric)) === "object" && fabric.loadSVGFromString) {
return svgXmlToImageElementFabric(str, { scale: scale, mimeType: mimeType, quality: quality });
} else {
return svgXmlToImageElement(str, { scale: scale, mimeType: mimeType, quality: quality });
}
});
}
}, {
key: 'renderJSONObject',
value: function renderJSONObject(src) {
var options = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : {};
var format = options.format;
if (format !== 'json' || format !== 'json0') {
format = 'json';
}
return this.renderString(src, _extends({}, options, { format: format })).then(function (str) {
return JSON.parse(str);
});
}
}]);
return Viz;
}();
return Viz;
})));

View File

@ -1 +0,0 @@
{"modes":["AAS","RAAS","Invasion"],"rules":{"default":[{"map":"Yehorivka","modes":["Any"],"versions":["Any"]},{"map":"Chora","modes":["Any"],"versions":["Any"]},{"map":"Narva","modes":["Any"],"versions":["Any"]}],"Albasrah":[{"map":"Mutaha","modes":["Any"],"versions":["Any"]},{"map":"Skorpo","modes":["RAAS"],"versions":["v2"]},{"map":"Narva","modes":["Any"],"versions":["Any"]}],"Anvil":[{"map":"Mutaha","modes":["Any"],"versions":["Any"]},{"map":"Logar","modes":["Any"],"versions":["Any"]},{"map":"FoolsRoad","modes":["Any"],"versions":["Any"]}],"Belaya":[{"map":"Sumari","modes":["Any"],"versions":["Any"]},{"map":"Mutaha","modes":["Any"],"versions":["Any"]},{"map":"Narva","modes":["Any"],"versions":["Any"]}],"Fallujah":[{"map":"Yehorivka","modes":["RAAS"],"versions":["v4"]},{"map":"Narva","modes":["Any"],"versions":["Any"]},{"map":"Mestia","modes":["AAS","RAAS"],"versions":["Any"]}],"Gorodok":[{"map":"Kokan","modes":["RAAS"],"versions":["Any"]},{"map":"Logar","modes":["AAS","RAAS"],"versions":["Any"]},{"map":"Mestia","modes":["Any"],"versions":["Any"]}],"Kamdesh":[{"map":"Gorodok","modes":["Any"],"versions":["Any"]},{"map":"Skorpo","modes":["Any"],"versions":["Any"]}],"Kokan":[{"map":"Fallujah","modes":["Any"],"versions":["Any"]},{"map":"Belaya","modes":["Any"],"versions":["Any"]},{"map":"Logar","modes":["Any"],"versions":["Any"]}],"LashkarValley":[{"map":"Mestia","modes":["Any"],"versions":["Any"]},{"map":"Kokan","modes":["Any"],"versions":["Any"]},{"map":"FoolsRoad","modes":["Any"],"versions":["Any"]}],"Narva":[{"map":"Kohat","modes":["Any"],"versions":["Any"]},{"map":"Tallil","modes":["Any"],"versions":["Any"]},{"map":"Albasrah","modes":["Invasion"],"versions":["v2"]}],"Skorpo":[{"map":"Chora","modes":["Any"],"versions":["Any"]},{"map":"Mutaha","modes":["Any"],"versions":["Any"]},{"map":"Kokan","modes":["AAS","RAAS"],"versions":["Any"]}],"Sumari":[{"map":"Yehorivka","modes":["Any"],"versions":["Any"]},{"map":"Belaya","modes":["Any"],"versions":["Any"]},{"map":"CAF_Manic","modes":["Any"],"versions":["Any"]}],"Tallil":[{"map":"FoolsRoad","modes":["Any"],"versions":["Any"]},{"map":"CAF_Manic","modes":["AAS","RAAS"],"versions":["Any"]},{"map":"Mestia","modes":["AAS","RAAS"],"versions":["Any"]}],"Yehorivka":[{"map":"Yehorivka","modes":["Any"],"versions":["Any"]},{"map":"Narva","modes":["AAS","RAAS"],"versions":["Any"]},{"map":"Mutaha","modes":["Any"],"versions":["Any"]}],"Chora":[{"map":"Gorodok","modes":["Any"],"versions":["Any"]},{"map":"CAF_Manic","modes":["AAS","RAAS"],"versions":["Any"]},{"map":"Anvil","modes":["RAAS","Invasion"],"versions":["Any"]}],"Kohat":[{"map":"Gorodok","modes":["Any"],"versions":["Any"]},{"map":"Yehorivka","modes":["Any"],"versions":["Any"]},{"map":"Kokan","modes":["Any"],"versions":["Any"]}],"Logar":[{"map":"Tallil","modes":["Any"],"versions":["Any"]},{"map":"CAF_GooseBay","modes":["Any"],"versions":["Any"]},{"map":"Kohat","modes":["Any"],"versions":["Any"]}]},"addon_map_layer_strings":[], "mode_repeat_blacklist":[]}

View File

@ -1,47 +0,0 @@
{
"modes":
[
"AAS",
"RAAS",
"Invasion"
],
"mode_repeat_blacklist":
[
"Invasion"
],
"rules":
{
"Gorodok":
[
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]},
{"map": "Gorodok", "modes": ["Any"], "versions": ["Any"]}
],
"Gorodok_Invasion":
[
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]}
],
"Yehorivka_RAAS_v1":
[
{"map": "Gorodok", "modes": ["AAS"], "versions": ["Any"]}
],
"default":
[
{"map": "Yehorivka", "modes": ["Any"], "versions": ["Any"]},
{"map": "Gorodok", "modes": ["Any"], "versions": ["Any"]}
]
},
"addon_map_layer_strings":
{
"example":
[
"string1",
"string2"
]
}
}

View File

@ -1,507 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Map vote plugin configuration generator</title>
<style>
*
{
background-color: lightgray;
}
div#main
{
display: flex;
justify-content: space-around;
margin: auto;
}
div.pannel
{
border: black;
border-radius: 15px;
border-style: solid;
padding: 10px;
padding-bottom: 30px;
}
li.rule
{
border: 1px solid black;
border-radius: 10px;
padding: 5px;
margin: 2px;
}
ul.rules
{
margin: 5px;
padding: 2px;
overflow: clip;
overflow-y: scroll;
width: 82ch;
height: 100ch;
border: solid black;
}
ul.nomination_list
{
list-style-type: none;
padding: 2px;
margin-top: 2px;
margin-bottom: 2px;
width: 75ch;
min-height: 5ch;
border: solid black;
}
ul.border_list
{
list-style-type: none;
margin: 5px;
padding: 2px;
overflow: clip;
overflow-y: scroll;
width: 75ch;
height: 10ch;
border: solid black;
}
li.selected
{
background-color: cadetblue;
}
div#info
{
margin: 2%;
}
li.nomination
{
border: 1px solid black;
margin: 5px;
padding: 5px;
}
input.short_input
{
width: 10ch;
}
input.rule_input
{
width: 30ch;
}
.graph_container
{
overflow: hidden;
}
</style>
</head>
<body>
<div id="main">
<div id="left" class="pannel">
<div id="controls">
<button id="save">save</button>
<input type="file" id="load" accept=".json">
</div>
<div id="rules_pannel">
<h3>vote rules:</h3>
<button id="add_rule">add rule</button>
<ul id="rules_list" class="rules"></ul>
</div>
<div id="modes">
<h3>mode strings (case sensitive):</h3>
<ul id="modes_list" class="border_list"></ul>
<input type="text" id="mode_input">
<button id="add_mode">add</button>
<button id="remove_mode">remove</button>
</div>
<div id="addons">
<h3>addon layer strings:</h3>
(not currently implemented)
<ul id="addons_list"></ul>
<input type="text">
<button>add</button>
<button>remove</button>
</div>
<div id="blacklist">
<h3>mode repeat blacklist strings (case sensitive):</h3>
<ul id="blacklist_list" class="border_list"></ul>
<input type="text" id="blacklist_input">
<button id="add_blacklist">add</button>
<button id="remove_blacklist">remove</button>
</div>
</div>
<div id="info">
<button onclick="render_graph()">render graph</button>
<div class="pannel" style="width: 95%;">
<div id="graph_container" class="graph_container"></div>
</div>
<h3>info:</h3>
tool by maskedmonkyman <br>
<h4>warning:</h4>
<p>
this tool is a thin wrapper around
the Squad JS pluggin config schemea. This tool
does <b>NO INPUT VALIDATION</b> and is just a graphical utility.
It is very possible to create broken configs using
this tool. Be sure to check your inputs.
</p>
<h4>notes:</h4>
<ol>
<li>
be sure to always have a "defualt" vote rule.
</li>
<li>
the Any option for modes, will select modes from the
mode strings section so be sure to fill it out.
</li>
<li>
The plugin looks for maps from specific to general so if the
current layer is Yehorivka_RAAS_v1 then the plugin will look for rules: <br>
Yehorivka_RAAS_v1 <br>
Yehorivka_RAAS <br>
Yehorivka <br>
defualt <br>
then load from the first one it finds
</li>
</ol>
</p>
</div>
</div>
<script src="configTool/viz.js"></script>
<script src="configTool/full.render.js"></script>
<script src="configTool/svg-pan-zoom.min.js" type="text/javascript" charset="utf-8"></script>
<script>
"use strict";
//TODO addon layers
const save_button = document.getElementById("save");
const load_button = document.getElementById("load");
//rules
const add_rule_button = document.getElementById("add_rule");
const remove_rule_button = document.getElementById("remove_rule");
const rules_list = document.getElementById("rules_list");
let selected_rule = null;
//mode
const add_mode_button = document.getElementById("add_mode");
const remove_mode_button = document.getElementById("remove_mode");
const mode_input = document.getElementById("mode_input");
const modes_list = document.getElementById("modes_list");
//blacklist
const add_blacklist_button = document.getElementById("add_blacklist");
const remove_blacklist_button = document.getElementById("remove_blacklist");
const blacklist_input = document.getElementById("blacklist_input");
const blacklist_list = document.getElementById("blacklist_list");
let selected_mode = null;
let selected_blacklist_item = null;
let svg_element = null;
function fetch_layer_strings()
{
const wiki_url = "https://raw.githubusercontent.com/Squad-Wiki-Editorial/squad-wiki-pipeline-map-data/master/completed_output/_Current%20Version/finished.json";
let layer_strings = [];
fetch(wiki_url).then(response=>response.json()).then(function(response){
for(const layer of response.Maps)
layer_strings.push(layer.rawName);
});
return layer_strings;
}
function render_graph()
{
const graph = build_graph_string(build_vote_rules());
//let layer_strings = fetch_layer_strings();
var viz = new Viz();
if (svg_element)
svg_element.parentNode.removeChild(svg_element);
viz.renderSVGElement(graph, {"engine" : "circo"})
.then(function(element) {
svg_element = element;
//set svg to the size of it's container
svg_element.setAttribute("width", "100%");
//remove height initially to auto scale
svg_element.removeAttribute("height");
document.getElementById("graph_container").appendChild(svg_element);
let panZoom = svgPanZoom(svg_element, {
zoomEnabled: true,
controlIconsEnabled: true,
fit: false,
center: true
});
//hacky work around for pan zoom so it scale properly
svg_element.setAttribute("height", `${panZoom.getSizes().height}px`)
})
.catch(error => {
// Create a new Viz instance (@see Caveats page for more info)
viz = new Viz();
// Possibly display the error
console.error(error);
});
}
function build_graph_string(vote_rules)
{
let graph = "digraph G \n";
graph += "\{";
let rules = vote_rules.rules;
for(const rule in rules)
{
const nomination = rules[rule];
for (const layer of nomination)
{
graph += `${rule} -> ${layer.map}\n`;
}
}
graph += "\}";
return graph;
}
function build_vote_rules()
{
let vote_rules = {
"modes": [],
"rules": {},
"addon_map_layer_strings": [],
"mode_repeat_blacklist": []
};
for (let mode of modes_list.children)
{
vote_rules.modes.push(mode.innerHTML.trim());
}
for (let blacklist_item of blacklist_list.children)
{
vote_rules.mode_repeat_blacklist.push(blacklist_item.innerHTML.trim());
}
for (let rule_element of rules_list.children)
{
let rule = {
"name": rule_element.rule_string_input.value,
"nominations": []
};
for (let nomination of rule_element.nomination_list.children)
{
rule.nominations.push({
"map": nomination.map_input.value.trim(),
"modes": nomination.mode_input.value.trim().split(',').map(e => e.trim()),
"versions": nomination.verison_input.value.trim().split(',').map(e => e.trim())
});
}
vote_rules.rules[rule.name] = rule.nominations;
}
return vote_rules;
}
add_blacklist_button.onclick = ()=>{
if (blacklist_input.value == "")
return;
add_blacklist_item(blacklist_input.value);
};
add_rule_button.onclick = ()=>{
let rule = build_rule_element();
rules_list.appendChild(rule);
};
add_mode_button.onclick = () => {
if (mode_input.value == "")
return;
add_mode(mode_input.value);
};
function add_mode(mode_string)
{
let mode = document.createElement("li");
mode.innerHTML = mode_string;
modes_list.appendChild(mode);
mode.onclick = () => {
if (selected_mode)
{
selected_mode.classList.remove("selected");
}
selected_mode = mode;
selected_mode.classList.add("selected");
};
}
function add_blacklist_item(blacklist_string)
{
let blacklist_item = document.createElement("li");
blacklist_item.innerHTML = blacklist_string;
blacklist_list.appendChild(blacklist_item);
blacklist_item.onclick = () => {
if (selected_blacklist_item)
{
selected_blacklist_item.classList.remove("selected");
}
selected_blacklist_item = blacklist_item;
selected_blacklist_item.classList.add("selected");
};
}
remove_blacklist_button.onclick = () => {
if (selected_blacklist_item)
{
selected_blacklist_item.parentNode.removeChild(selected_blacklist_item);
selected_blacklist_item = null;
}
};
remove_mode_button.onclick = () => {
if (selected_mode)
{
selected_mode.parentNode.removeChild(selected_mode);
selected_mode = null;
}
};
save_button.onclick = () => {
let vote_rules = build_vote_rules();
var a = document.createElement("a");
a.href = window.URL.createObjectURL(new Blob([JSON.stringify(vote_rules)], {type: "text/plain"}));
a.download = "vote_rules.json";
a.click();
};
load_button.onchange = () => {
let reader = new FileReader();
rules_list.innerHTML = "";
modes_list.innerHTML = "";
reader.addEventListener("load", (event) => {
let vote_rules = JSON.parse(event.target.result);
for (let mode of vote_rules.modes)
add_mode(mode);
for (let blacklist_item of vote_rules.mode_repeat_blacklist)
add_blacklist_item(blacklist_item);
let rules = Object.keys(vote_rules.rules).sort();
for (let rule of rules)
{
let rule_element = build_rule_element();
rule_element.rule_string_input.value = rule;
rules_list.appendChild(rule_element);
for(let nomination of vote_rules.rules[rule])
{
let nomination_element = build_nomination_element(nomination.map, nomination.modes, nomination.versions);
rule_element.nomination_list.appendChild(nomination_element);
}
}
});
reader.readAsText(load_button.files[0]);
};
function build_rule_element()
{
let rule = document.createElement("li");
rule.classList.add("rule");
let label = document.createElement("label");
label.innerHTML = "Rule string: ";
let input = document.createElement("input");
input.setAttribute("type", "text");
input.classList.add("rule_input");
label.appendChild(input);
rule.appendChild(label);
rule["rule_string_input"] = input;
let add_nomination_button = document.createElement("button");
add_nomination_button.innerHTML = "add nomination";
rule.appendChild(add_nomination_button);
let nominations_list = document.createElement("ul");
nominations_list.classList.add("nomination_list");
rule.appendChild(nominations_list);
rule["nomination_list"] = nominations_list;
let delete_button = document.createElement("button");
delete_button.innerHTML = "delete";
rule.appendChild(delete_button);
add_nomination_button.onclick = () => {
nominations_list.appendChild(build_nomination_element());
};
delete_button.onclick = () => {
rule.parentNode.removeChild(rule);
}
return rule;
}
function build_nomination_element(map_string = "", mode_string = "Any", version_string = "Any")
{
let nomination = document.createElement("li");
nomination.classList.add("nomination");
//map field
let map_label = document.createElement("label");
map_label.innerHTML = "Map:";
nomination.appendChild(map_label);
let map_input = document.createElement("input");
map_input.setAttribute("type", "text");
map_input.value = map_string;
map_label.appendChild(map_input);
nomination["map_input"] = map_input;
//mode field
let mode_label = document.createElement("label");
mode_label.innerHTML = "Mode(s):";
nomination.appendChild(mode_label);
let mode_input = document.createElement("input");
mode_input.setAttribute("type", "text");
mode_input.classList.add("short_input");
mode_input.value = mode_string;
mode_label.appendChild(mode_input);
nomination["mode_input"] = mode_input;
//version feild
let version = document.createElement("label");
version.innerHTML = "Version(s):";
nomination.appendChild(version);
let verison_input = document.createElement("input");
verison_input.setAttribute("type", "text");
verison_input.classList.add("short_input");
verison_input.value = version_string;
version.appendChild(verison_input);
nomination["verison_input"] = verison_input;
//delete button
let delete_button = document.createElement("button");
delete_button.innerHTML = "delete";
delete_button.onclick = () => {
nomination.parentNode.removeChild(nomination);
};
nomination.appendChild(delete_button);
return nomination;
}
function main()
{
console.log("hello world");
}
document.addEventListener("DOMContentLoaded", main);
</script>
</body>
</html>

View File

@ -5,35 +5,29 @@ import BasePlugin from "./base-plugin.js";
import fs from "fs";
import { Layers } from "../layers/index.js"
function randomElement(array)
{
return array[Math.floor(Math.random() * array.length)];
function randomElement(array) {
return array[ Math.floor(Math.random() * array.length) ];
}
function formatChoice(choiceIndex, mapString, currentVotes)
{
return `type !vote ${choiceIndex + 1} : ${mapString} (${currentVotes} votes)`
function formatChoice(choiceIndex, mapString, currentVotes, firstBroadcast) {
return `${choiceIndex + 1}${mapString} ` + (!firstBroadcast ? `(${currentVotes})` : "");
// return `${choiceIndex + 1}❱ ${mapString} (${currentVotes} votes)`
}
function toMils(min)
{
return min * 60 * 1000;
function toMils(min) {
return min * 60 * 1000;
}
export default class MapVote extends BasePlugin
{
static get description()
{
export default class MapVote extends BasePlugin {
static get description() {
return "Map Voting plugin";
}
static get defaultEnabled()
{
static get defaultEnabled() {
return true;
}
static get optionsSpecification()
{
static get optionsSpecification() {
return {
commandPrefix:
{
@ -47,7 +41,7 @@ export default class MapVote extends BasePlugin
description: 'the path to the layersConfig file',
default: ''
},
minPlayersForVote:
minPlayersForVote:
{
required: false,
description: 'number of players needed on the server for a vote to start',
@ -67,101 +61,96 @@ export default class MapVote extends BasePlugin
}
};
}
constructor(server, options, connectors)
{
constructor(server, options, connectors) {
super(server, options, connectors);
this.voteRules = {}; //data object holding vote configs
this.nominations = []; //layer strings for the current vote choices
this.trackedVotes = {}; //player votes, keyed by steam id
this.trackedVotes = {}; //player votes, keyed by steam id
this.tallies = []; //votes per layer, parellel with nominations
this.votingEnabled = false;
this.onConnectBound = false;
this.broadcastIntervalTask = null;
this.firstBroadcast = true;
this.onNewGame = this.onNewGame.bind(this);
this.onPlayerDisconnected = this.onPlayerDisconnected.bind(this);
this.onChatMessage = this.onChatMessage.bind(this);
this.broadcastNominations = this.broadcastNominations.bind(this);
this.broadcastNominations = this.broadcastNominations.bind(this);
this.beginVoting = this.beginVoting.bind(this);
this.msgBroadcast = (msg) => {this.server.rcon.broadcast(msg);};
this.msgDirect = (steamid, msg) => {this.server.rcon.warn(steamid, msg);};
this.msgBroadcast = (msg) => { this.server.rcon.broadcast(msg); };
this.msgDirect = (steamid, msg) => { this.server.rcon.warn(steamid, msg); };
//load voteRules with options from source file
this.loadLayersConfig();
}
async mount()
{
this.server.on('NEW_GAME', this.onNewGame);
async mount() {
this.server.on('NEW_GAME', this.onNewGame);
this.server.on('CHAT_MESSAGE', this.onChatMessage);
this.server.on('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
this.verbose(1, 'Map vote was mounted.');
this.verbose(1, 'Map vote was mounted.');
}
async unmount()
{
this.server.removeEventListener('NEW_GAME', this.onNewGame);
async unmount() {
this.server.removeEventListener('NEW_GAME', this.onNewGame);
this.server.removeEventListener('CHAT_MESSAGE', this.onChatMessage);
this.server.removeEventListener('PLAYER_DISCONNECTED', this.onPlayerDisconnected);
clearInterval(this.broadcastIntervalTask);
this.verbose(1, 'Map vote was un-mounted.');
this.verbose(1, 'Map vote was un-mounted.');
}
//loads layer configs from disk into plugin memory
loadLayersConfig()
{
loadLayersConfig() {
this.verbose(1, `Fetching Map Voting Lists...`);
let layersConfigString = '';
try
{
if (!fs.existsSync(this.options.voteRulesPath))
try {
if (!fs.existsSync(this.options.voteRulesPath))
throw new Error(`Could not find Map Vote List at ${this.options.voteRulesPath}`);
layersConfigString = fs.readFileSync(this.options.voteRulesPath, 'utf8');
}
catch (error)
{
catch (error) {
this.verbose('SquadServer', 1, `Error fetching mapvoting list: ${options.voteRulesPath}`, error);
}
this.voteRules = JSON.parse(layersConfigString);
}
async onNewGame()
{
async onNewGame() {
//wait to start voting
this.endVoting();
this.trackedVotes = {};
this.tallies = [];
this.nominations = [];
this.factionStrings = [];
setTimeout(this.beginVoting, toMils(this.options.voteWaitTimeFromMatchStart));
}
async onPlayerDisconnected()
{
}
async onPlayerDisconnected() {
if (!this.votingEnabled) return;
await this.server.updatePlayerList();
await this.server.updatePlayerList();
this.clearVote();
this.updateNextMap();
}
async onChatMessage(info)
{
const {steamID, name: playerName} = info;
async onChatMessage(info) {
const { steamID, name: playerName } = info;
const message = info.message.toLowerCase();
//check to see if this message has a command prefix
if (!message.startsWith(this.options.commandPrefix))
if (!message.startsWith(this.options.commandPrefix) && isNaN(message))
return;
const subCommand = message.substring(this.options.commandPrefix.length).trim();
if(!isNaN(subCommand)) // if this succeeds player is voting for a map
const commandSplit = (isNaN(message) ? message.substring(this.options.commandPrefix.length).trim().split(' ') : [ message ]);
let cmdLayers = commandSplit.slice(1);
for (let k in cmdLayers) cmdLayers[ k ] = cmdLayers[ k ].toLowerCase();
const subCommand = commandSplit[ 0 ];
if (!isNaN(subCommand)) // if this succeeds player is voting for a map
{
const mapNumber = parseInt(subCommand); //try to get a vote number
if (!this.votingEnabled)
{
const mapNumber = parseInt(subCommand); //try to get a vote number
if (!this.votingEnabled) {
await this.msgDirect(steamID, "There is no vote running right now");
return;
}
@ -169,94 +158,93 @@ export default class MapVote extends BasePlugin
this.updateNextMap();
return;
}
const isAdmin = info.chat === "ChatAdmin";
switch(subCommand) // select the sub command
switch (subCommand) // select the sub command
{
case "choices": //sends choices to player in the from of a warning
if (!this.votingEnabled)
{
if (!this.votingEnabled) {
await this.msgDirect(steamID, "There is no vote running right now");
return;
}
this.directMsgNominations(steamID);
return;
case "results": //sends player the results in a warning
if (!this.votingEnabled)
{
if (!this.votingEnabled) {
await this.msgDirect(steamID, "There is no vote running right now");
return;
}
this.directMsgNominations(steamID);
return;
case "restart": //starts the vote again if it was canceled
if(!isAdmin) return;
if(this.votingEnabled)
{
case "start": //starts the vote again if it was canceled
if (!isAdmin) return;
if (this.votingEnabled) {
await this.msgDirect(steamID, "Voting is already enabled");
return;
}
this.beginVoting(true);
this.beginVoting(true, steamID, cmdLayers);
return;
case "restart": //starts the vote again if it was canceled
if (!isAdmin) return;
this.endVoting();
this.beginVoting(true, steamID, cmdLayers);
return;
case "cancel": //cancels the current vote and wont set next map to current winnner
if(!isAdmin) return;
if(!this.votingEnabled)
{
await this.msgDirect(steamID, "Voting is already disabled, emotional damage!");
if (!isAdmin) return;
if (!this.votingEnabled) {
await this.msgDirect(steamID, "There is no vote running right now");
return;
}
this.endVoting();
await this.msgDirect(steamID, "ending current vote");
await this.msgDirect(steamID, "Ending current vote");
return;
case "reload": //allows for config hot reloads
if(!isAdmin) return;
this.loadLayersConfig();
await this.msgDirect(steamID, "Reloaded map vote layers configuration")
case "broadcast":
if (!this.votingEnabled) {
await this.msgDirect(steamID, "There is no vote running right now");
return;
}
this.broadcastNominations();
return;
case "help": //displays available commands
await this.msgDirect(steamID, `Map voting system built by JetDave for MAD`);
await this.msgDirect(steamID, `!vote <choices|number|results>`);
if(!isAdmin) return;
await this.msgDirect(steamID, `!vote <restart|cancel|reload> (admin only)`);
if (!isAdmin) return;
await this.msgDirect(steamID, `!vote <start|restart|cancel|broadcast> (admin only)`);
return;
default:
//give them an error
await this.msgDirect(steamID, `Unknown vote subcommand: ${subCommand}`);
return;
}
}
updateNextMap() //sets next map to current mapvote winner, if there is a tie will pick at random
{
const nextMap = randomElement(this.currentWinners);
this.server.rcon.execute(`AdminSetNextLayer ${nextMap}`);
}
matchLayers(builtString)
{
matchLayers(builtString) {
return Layers.layers.filter(element => element.layerid.includes(builtString));
}
getMode(nomination, currentMode)
{
getMode(nomination, currentMode) {
const mapName = nomination.map;
let modes = nomination.modes;
let mode = modes[0];
let mode = modes[ 0 ];
if (mode === "Any")
modes = this.voteRules.modes;
if (this.voteRules.mode_repeat_blacklist.includes(currentMode))
{
if (this.voteRules.mode_repeat_blacklist.includes(currentMode)) {
modes = modes.filter(mode => !mode.includes(currentMode));
}
while (modes.length > 0)
{
while (modes.length > 0) {
mode = randomElement(modes);
modes = modes.filter(elem => elem !== mode);
if (this.matchLayers(`${mapName}_${mode}`).length > 0)
@ -267,93 +255,80 @@ export default class MapVote extends BasePlugin
}
//TODO: right now if version is set to "Any" no caf layers will be selected
populateNominations() //gets nomination strings from layer options
populateNominations(steamid = null, cmdLayers = null, bypassRaasFilter = false) //gets nomination strings from layer options
{
//helpers
const splitName = name => name.substring(0, name.lastIndexOf("_"));
const removeCAF = name => name.replace("CAF_", "");
let layerString = "";
let currentMode = "";
if (this.server.currentLayer)
{
layerString = this.server.currentLayer.layerid
currentMode = this.server.currentLayer.gamemode
// this.nominations.push(builtLayerString);
// this.tallies.push(0);
const translations = {
'United States Army': "USA",
'United States Marine Corps': "USMC",
'Russian Ground Forces': "RUS",
'British Army': "GB",
'Canadian Army': "CAF",
'Australian Defence Force': "AUS",
'Irregular Militia Forces': "IRR",
'Middle Eastern Alliance': "MEA",
'Insurgent Forces': "INS",
}
this.nominations = [];
const rulesList = this.voteRules.rules;
let nominationsList = rulesList.default;
//chomp string until we find a match
while(layerString.length > 0)
{
if(layerString in rulesList)
{
nominationsList = rulesList[layerString];
break;
this.tallies = [];
this.factionStrings = [];
let rnd_layers = [];
// let rnd_layers = [];
if (!cmdLayers) {
const all_layers = Layers.layers.filter((l) => [ 'RAAS', 'AAS', 'INVASION' ].includes(l.gamemode.toUpperCase()));
for (let i = 0; i < 6; i++) {
// rnd_layers.push(all_layers[Math.floor(Math.random()*all_layers.length)]);
let l = all_layers[ Math.floor(Math.random() * all_layers.length) ];
rnd_layers.push(l);
this.nominations.push(l.layerid)
this.tallies.push(0);
this.factionStrings.push(getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]));
}
layerString = removeCAF(layerString);
layerString = splitName(layerString);
if (!bypassRaasFilter && rnd_layers.filter((l) => l.gamemode === 'RAAS').length < 3) this.populateNominations();
} else {
if (cmdLayers.length <= 6)
for (let cl of cmdLayers) {
const cls = cl.split('_');
const fLayers = Layers.layers.filter((l) => (l.classname.toLowerCase().startsWith(cls[ 0 ]) && (l.gamemode.toLowerCase().startsWith(cls[ 1 ]) || (!cls[ 1 ] && [ 'RAAS', 'AAS', 'INVASION' ].includes(l.gamemode.toUpperCase()))) && (!cls[ 2 ] || l.version.toLowerCase().startsWith("v" + cls[ 2 ].replace(/v/gi, '')))));
let l = fLayers[ Math.floor(Math.random() * fLayers.length) ]; rnd_layers.push(l);
this.nominations.push(l.layerid)
this.tallies.push(0);
this.factionStrings.push(getTranslation(l.teams[ 0 ]) + "-" + getTranslation(l.teams[ 1 ]));
}
else if (steamid) this.msgDirect(steamid, "You cannot start a vote with more than 6 options"); return;
}
for(const nomination of nominationsList)
{
const mapName = nomination.map;
let mode = this.getMode(nomination, currentMode);
let version = randomElement(nomination.versions);
let cafPrefix = "";
if (version.includes("CAF_"))
{
cafPrefix = "CAF_";
version = removeCAF(version);
function getTranslation(t) {
if (translations[ t.faction ]) return translations[ t.faction ]
else {
const f = t.faction.split(' ');
let fTag = "";
f.forEach((e) => { fTag += e[ 0 ] });
return fTag.toUpperCase();
}
let builtLayerString = `${cafPrefix}${mapName}_${mode}_${version}`;
if (version === "Any")
{
let maps = this.matchLayers(`${mapName}_${mode}`);
if (maps.length == 0)
{
this.verbose(1, `error: could not find layer for ${builtLayerString} from vote rule \"${layerString}\"`);
continue;
}
maps = maps.map(l => l.layerid);
builtLayerString = randomElement(maps);
}
if (!Layers.getLayerByCondition((layer) => layer.layerid === builtLayerString))
{
this.verbose(1, `error: could not find layer for ${builtLayerString} from vote rule \"${layerString}\"`);
continue;
}
this.nominations.push(builtLayerString);
this.tallies.push(0);
}
}
//checks if there are enough players to start voting, if not binds itself to player connected
//when there are enough players it clears old votes, sets up new nominations, and starts broadcast
beginVoting(force = false)
{
beginVoting(force = false, steamid = null, cmdLayers = null) {
const playerCount = this.server.players.length;
const minPlayers = this.options.minPlayersForVote;
if (this.votingEnabled) //voting has already started
return;
if (playerCount < minPlayers && !force)
{
if (this.onConnectBound == false)
{
if (playerCount < minPlayers && !force) {
if (this.onConnectBound == false) {
this.server.on("PLAYER_CONNECTED", this.beginVoting)
this.onConnectBound = true;
}
return;
}
if (this.onConnectBound)
{
if (this.onConnectBound) {
this.server.removeEventListener("PLAYER_CONNECTED", this.beginVoting);
this.onConnectBound = false;
}
@ -361,97 +336,93 @@ export default class MapVote extends BasePlugin
// these need to be reset after reenabling voting
this.trackedVotes = {};
this.tallies = [];
this.populateNominations();
this.populateNominations(steamid, cmdLayers);
this.votingEnabled = true;
this.broadcastNominations();
this.firstBroadcast = true;
this.broadcastNominations();
this.broadcastIntervalTask = setInterval(this.broadcastNominations, toMils(this.options.voteBroadcastInterval));
}
endVoting()
{
endVoting() {
this.votingEnabled = false;
clearInterval(this.broadcastIntervalTask);
this.broadcastIntervalTask = null;
}
//sends a message about nominations through a broadcast
//NOTE: max squad broadcast message length appears to be 485 characters
//NOTE: max squad broadcast message length appears to be 485 characters
//Note: broadcast strings with multi lines are very strange
async broadcastNominations()
{
await this.msgBroadcast("Type !vote <map number> in chat to cast your vote, Candidates:\n");
let nominationStrings = [];
for(let choice in this.nominations)
{
choice = Number(choice);
nominationStrings.push(formatChoice(choice, this.nominations[choice], this.tallies[choice]));
}
await this.msgBroadcast(nominationStrings.join("\n"));
async broadcastNominations() {
if(this.nominations.length>0){
await this.msgBroadcast("✯ MAPVOTE ✯ Vote for the next map by writing in chat the corresponding number!\n");
let nominationStrings = [];
for (let choice in this.nominations) {
choice = Number(choice);
nominationStrings.push(formatChoice(choice, this.nominations[ choice ].replace(/\_/gi, ' ').replace(/\sv\d{1,2}/gi, '') + ' ' + this.factionStrings[ choice ], this.tallies[ choice ], this.firstBroadcast));
}
await this.msgBroadcast(nominationStrings.join("\n"));
this.firstBroadcast = false;
}
//const winners = this.currentWinners;
//await this.msgBroadcast(`Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
}
async directMsgNominations(steamID)
{
for(let choice in this.nominations)
{
choice = Number(choice);
await this.msgDirect(steamID, formatChoice(choice, this.nominations[choice], this.tallies[choice]));
}
async directMsgNominations(steamID) {
for (let choice in this.nominations) {
choice = Number(choice);
await this.msgDirect(steamID, formatChoice(choice, this.nominations[ choice ], this.tallies[ choice ]));
}
const winners = this.currentWinners;
await this.msgDirect(steamID, `Current winner${winners.length > 1 ? "s" : ""}: ${winners.join(", ")}`);
}
//counts a vote from a player and adds it to tallies
async registerVote(steamID, nominationIndex, playerName)
{
async registerVote(steamID, nominationIndex, playerName) {
nominationIndex -= 1; // shift indices from display range
if(nominationIndex < 0 || nominationIndex > this.nominations.length)
{
if (nominationIndex < 0 || nominationIndex > this.nominations.length) {
await this.msgDirect(steamID, `[Map Vote] ${playerName}: invalid map number, typ !vote results to see map numbers`);
return;
}
const previousVote = this.trackedVotes[steamID];
this.trackedVotes[steamID] = nominationIndex;
this.tallies[nominationIndex] += 1;
if(previousVote !== undefined)
this.tallies[previousVote] -= 1;
await this.msgDirect(steamID, `you voted for ${this.nominations[nominationIndex]}`);
const previousVote = this.trackedVotes[ steamID ];
this.trackedVotes[ steamID ] = nominationIndex;
this.tallies[ nominationIndex ] += 1;
if (previousVote !== undefined)
this.tallies[ previousVote ] -= 1;
await this.msgDirect(steamID, `Registered vote: ${this.nominations[ nominationIndex ].replace(/\_/gi, ' ').replace(/\sv\d{1,2}/gi, '')} ${this.factionStrings[ nominationIndex ]} (${this.tallies[ nominationIndex ]} votes)`);
// await this.msgDirect(steamID, `Registered vote`);// ${this.nominations[ nominationIndex ]} ${this.factionStrings[ nominationIndex ]} (${this.tallies[ nominationIndex ]} votes)`);
// await this.msgDirect(steamID, `${this.nominations[ nominationIndex ]} (${this.tallies[ nominationIndex ]} votes)`);
// await this.msgDirect(steamID, `${this.factionStrings[ nominationIndex ]}`);
// await this.msgDirect(steamID, `${this.tallies[ nominationIndex ]} votes`);
}
//removes a players vote if they disconnect from the sever
clearVote()
{
clearVote() {
const currentPlayers = this.server.players.map((p) => p.steamID);
for (const steamID in this.trackedVotes)
{
if (!(currentPlayers.includes(steamID)))
{
const vote = this.trackedVotes[steamID];
this.tallies[vote] -= 1;
delete this.trackedVotes[steamID];
for (const steamID in this.trackedVotes) {
if (!(currentPlayers.includes(steamID))) {
const vote = this.trackedVotes[ steamID ];
this.tallies[ vote ] -= 1;
delete this.trackedVotes[ steamID ];
}
}
}
}
//calculates the current winner(s) of the vote and returns thier strings in an array
get currentWinners()
{
get currentWinners() {
const ties = [];
let highestScore = -Infinity;
for(let choice in this.tallies)
{
const score = this.tallies[choice];
if(score < highestScore)
for (let choice in this.tallies) {
const score = this.tallies[ choice ];
if (score < highestScore)
continue;
else if(score > highestScore)
{
else if (score > highestScore) {
highestScore = score;
ties.length = 0;
ties.push(choice);
@ -459,7 +430,7 @@ export default class MapVote extends BasePlugin
else // equal
ties.push(choice);
}
return ties.map(i => this.nominations[i]);
return ties.map(i => this.nominations[ i ]);
}
}