From 07e3c61e982ec2ed61d84c1407df362956be0a6e Mon Sep 17 00:00:00 2001 From: RaidMax Date: Sat, 4 Nov 2017 18:42:31 -0500 Subject: [PATCH] Chat history stuff fixed kills not saving --- Admin/Application.csproj | 4 +- Admin/Server.cs | 7 +- Admin/lib/SharedLibrary.dll | Bin 110592 -> 111104 bytes Admin/webfront/chat.html | 32 +- Admin/webfront/players.html | 179 +- Admin/webfront/scripts/wordcloud2.js | 2369 +++++++++++++++++++ IW4MAdmin.sln | 35 +- Plugins/SimpleStats/Chat/ChatDatabase.cs | 105 +- Plugins/SimpleStats/Chat/ChatHistoryPage.cs | 15 +- Plugins/SimpleStats/Plugin.cs | 1 - Plugins/Tests/Tests.csproj | 2 +- SharedLibrary/Database.cs | 36 + SharedLibrary/SharedLibrary.csproj | 6 +- 13 files changed, 2648 insertions(+), 143 deletions(-) create mode 100644 Admin/webfront/scripts/wordcloud2.js diff --git a/Admin/Application.csproj b/Admin/Application.csproj index bc9bc9ed2..9bcdf6b0b 100644 --- a/Admin/Application.csproj +++ b/Admin/Application.csproj @@ -165,6 +165,9 @@ PreserveNewest + + PreserveNewest + PreserveNewest @@ -363,7 +366,6 @@ copy /Y "$(ProjectDir)lib\Kayak.dll" "$(SolutionDir)BUILD\lib" xcopy /Y /I /E "$(ProjectDir)webfront\*" "$(SolutionDir)BUILD\Webfront" - if $(ConfigurationName) == Release-Nightly powershell.exe -file "$(SolutionDir)DEPLOY\publish_nightly.ps1" 1.4 if $(ConfigurationName) == Release-Stable powershell.exe -file "$(SolutionDir)DEPLOY\publish_stable.ps1" 1.4 diff --git a/Admin/Server.cs b/Admin/Server.cs index 94bf29560..f87809dac 100644 --- a/Admin/Server.cs +++ b/Admin/Server.cs @@ -362,7 +362,7 @@ namespace IW4MAdmin if ((lastCount - playerCountStart).TotalMinutes >= SharedLibrary.Helpers.PlayerHistory.UpdateInterval) { - while (PlayerHistory.Count > ((60 / SharedLibrary.Helpers.PlayerHistory.UpdateInterval) * 12 )) // 12 times a hour for 12 hours + while (PlayerHistory.Count > ((60 / SharedLibrary.Helpers.PlayerHistory.UpdateInterval) * 12)) // 12 times a hour for 12 hours PlayerHistory.Dequeue(); PlayerHistory.Enqueue(new SharedLibrary.Helpers.PlayerHistory(ClientNum)); playerCountStart = DateTime.Now; @@ -593,8 +593,9 @@ namespace IW4MAdmin else // Not a command { E.Data = E.Data.StripColors().CleanChars(); - if (E.Data.Length > 50) - E.Data = E.Data.Substring(0, 50) + "..."; + // this should not be done for all messages. + //if (E.Data.Length > 50) + // E.Data = E.Data.Substring(0, 50) + "..."; ChatHistory.Add(new Chat(E.Origin.Name, E.Data, DateTime.Now)); } diff --git a/Admin/lib/SharedLibrary.dll b/Admin/lib/SharedLibrary.dll index df9a7c6887ceed7784ac28412c6055aa3b524971..55a33d81fccfb4563f139ed613dc2ab07be26b09 100644 GIT binary patch delta 38844 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 U4S8aPeyV&|dipADQz`2I1FfxFEC2ui delta 38595 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

HV5Laek?1 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 +

+ + \ No newline at end of file diff --git a/Admin/webfront/players.html b/Admin/webfront/players.html index 70eb83117..461cf0abf 100644 --- a/Admin/webfront/players.html +++ b/Admin/webfront/players.html @@ -1,134 +1,113 @@
- - + +
-
Name
-
Aliases
-
IP
-
Level
-
Connections
-
V2
-
Last Seen
+
Name
+
Aliases
+
IP
+
Level
+
Connections
+
Chat
+
Last Seen
-
+
diff --git a/Admin/webfront/scripts/wordcloud2.js b/Admin/webfront/scripts/wordcloud2.js new file mode 100644 index 000000000..35fd5d6e2 --- /dev/null +++ b/Admin/webfront/scripts/wordcloud2.js @@ -0,0 +1,2369 @@ +'use strict'; + + + +// setImmediate + +if (!window.setImmediate) { + + window.setImmediate = (function setupSetImmediate() { + + return window.msSetImmediate || + + window.webkitSetImmediate || + + window.mozSetImmediate || + + window.oSetImmediate || + + (function setupSetZeroTimeout() { + + if (!window.postMessage || !window.addEventListener) { + + return null; + + } + + + + var callbacks = [undefined]; + + var message = 'zero-timeout-message'; + + + + // Like setTimeout, but only takes a function argument. There's + + // no time argument (always zero) and no arguments (you have to + + // use a closure). + + var setZeroTimeout = function setZeroTimeout(callback) { + + var id = callbacks.length; + + callbacks.push(callback); + + window.postMessage(message + id.toString(36), '*'); + + + + return id; + + }; + + + + window.addEventListener('message', function setZeroTimeoutMessage(evt) { + + // Skipping checking event source, retarded IE confused this window + + // object with another in the presence of iframe + + if (typeof evt.data !== 'string' || + + evt.data.substr(0, message.length) !== message/* || + + evt.source !== window */) { + + return; + + } + + + + evt.stopImmediatePropagation(); + + + + var id = parseInt(evt.data.substr(message.length), 36); + + if (!callbacks[id]) { + + return; + + } + + + + callbacks[id](); + + callbacks[id] = undefined; + + }, true); + + + + /* specify clearImmediate() here since we need the scope */ + + window.clearImmediate = function clearZeroTimeout(id) { + + if (!callbacks[id]) { + + return; + + } + + + + callbacks[id] = undefined; + + }; + + + + return setZeroTimeout; + + })() || + + // fallback + + function setImmediateFallback(fn) { + + window.setTimeout(fn, 0); + + }; + + })(); + +} + + + +if (!window.clearImmediate) { + + window.clearImmediate = (function setupClearImmediate() { + + return window.msClearImmediate || + + window.webkitClearImmediate || + + window.mozClearImmediate || + + window.oClearImmediate || + + // "clearZeroTimeout" is implement on the previous block || + + // fallback + + function clearImmediateFallback(timer) { + + window.clearTimeout(timer); + + }; + + })(); + +} + + + +(function(global) { + + + + // Check if WordCloud can run on this browser + + var isSupported = (function isSupported() { + + var canvas = document.createElement('canvas'); + + if (!canvas || !canvas.getContext) { + + return false; + + } + + + + var ctx = canvas.getContext('2d'); + + if (!ctx) { + + return false; + + } + + if (!ctx.getImageData) { + + return false; + + } + + if (!ctx.fillText) { + + return false; + + } + + + + if (!Array.prototype.some) { + + return false; + + } + + if (!Array.prototype.push) { + + return false; + + } + + + + return true; + + }()); + + + + // Find out if the browser impose minium font size by + + // drawing small texts on a canvas and measure it's width. + + var minFontSize = (function getMinFontSize() { + + if (!isSupported) { + + return; + + } + + + + var ctx = document.createElement('canvas').getContext('2d'); + + + + // start from 20 + + var size = 20; + + + + // two sizes to measure + + var hanWidth, mWidth; + + + + while (size) { + + ctx.font = size.toString(10) + 'px sans-serif'; + + if ((ctx.measureText('\uFF37').width === hanWidth) && + + (ctx.measureText('m').width) === mWidth) { + + return (size + 1); + + } + + + + hanWidth = ctx.measureText('\uFF37').width; + + mWidth = ctx.measureText('m').width; + + + + size--; + + } + + + + return 0; + + })(); + + + + // Based on http://jsfromhell.com/array/shuffle + + var shuffleArray = function shuffleArray(arr) { + + for (var j, x, i = arr.length; i; + + j = Math.floor(Math.random() * i), + + x = arr[--i], arr[i] = arr[j], + + arr[j] = x) {} + + return arr; + + }; + + + + var WordCloud = function WordCloud(elements, options) { + + if (!isSupported) { + + return; + + } + + + + if (!Array.isArray(elements)) { + + elements = [elements]; + + } + + + + elements.forEach(function(el, i) { + + if (typeof el === 'string') { + + elements[i] = document.getElementById(el); + + if (!elements[i]) { + + throw 'The element id specified is not found.'; + + } + + } else if (!el.tagName && !el.appendChild) { + + throw 'You must pass valid HTML elements, or ID of the element.'; + + } + + }); + + + + /* Default values to be overwritten by options object */ + + var settings = { + + list: [], + + fontFamily: '"Trebuchet MS", "Heiti TC", "?????", ' + + + '"Arial Unicode MS", "Droid Fallback Sans", sans-serif', + + fontWeight: 'normal', + + color: 'random-dark', + + minSize: 0, // 0 to disable + + weightFactor: 1, + + clearCanvas: true, + + backgroundColor: '#fff', // opaque white = rgba(255, 255, 255, 1) + + + + gridSize: 8, + + drawOutOfBound: false, + + origin: null, + + + + drawMask: false, + + maskColor: 'rgba(255,0,0,0.3)', + + maskGapWidth: 0.3, + + + + wait: 0, + + abortThreshold: 0, // disabled + + abort: function noop() {}, + + + + minRotation: - Math.PI / 2, + + maxRotation: Math.PI / 2, + + rotationSteps: 0, + + + + shuffle: true, + + rotateRatio: 0.1, + + + + shape: 'circle', + + ellipticity: 0.65, + + + + classes: null, + + + + hover: null, + + click: null + + }; + + + + if (options) { + + for (var key in options) { + + if (key in settings) { + + settings[key] = options[key]; + + } + + } + + } + + + + /* Convert weightFactor into a function */ + + if (typeof settings.weightFactor !== 'function') { + + var factor = settings.weightFactor; + + settings.weightFactor = function weightFactor(pt) { + + return pt * factor; //in px + + }; + + } + + + + /* Convert shape into a function */ + + if (typeof settings.shape !== 'function') { + + switch (settings.shape) { + + case 'circle': + + /* falls through */ + + default: + + // 'circle' is the default and a shortcut in the code loop. + + settings.shape = 'circle'; + + break; + + + + case 'cardioid': + + settings.shape = function shapeCardioid(theta) { + + return 1 - Math.sin(theta); + + }; + + break; + + + + /* + + + + To work out an X-gon, one has to calculate "m", + + where 1/(cos(2*PI/X)+m*sin(2*PI/X)) = 1/(cos(0)+m*sin(0)) + + http://www.wolframalpha.com/input/?i=1%2F%28cos%282*PI%2FX%29%2Bm*sin%28 + + 2*PI%2FX%29%29+%3D+1%2F%28cos%280%29%2Bm*sin%280%29%29 + + + + Copy the solution into polar equation r = 1/(cos(t') + m*sin(t')) + + where t' equals to mod(t, 2PI/X); + + + + */ + + + + case 'diamond': + + // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ + + // %28t%2C+PI%2F2%29%29%2Bsin%28mod+%28t%2C+PI%2F2%29%29%29%2C+t+%3D + + // +0+..+2*PI + + settings.shape = function shapeSquare(theta) { + + var thetaPrime = theta % (2 * Math.PI / 4); + + return 1 / (Math.cos(thetaPrime) + Math.sin(thetaPrime)); + + }; + + break; + + + + case 'square': + + // http://www.wolframalpha.com/input/?i=plot+r+%3D+min(1%2Fabs(cos(t + + // )),1%2Fabs(sin(t)))),+t+%3D+0+..+2*PI + + settings.shape = function shapeSquare(theta) { + + return Math.min( + + 1 / Math.abs(Math.cos(theta)), + + 1 / Math.abs(Math.sin(theta)) + + ); + + }; + + break; + + + + case 'triangle-forward': + + // http://www.wolframalpha.com/input/?i=plot+r+%3D+1%2F%28cos%28mod+ + + // %28t%2C+2*PI%2F3%29%29%2Bsqrt%283%29sin%28mod+%28t%2C+2*PI%2F3%29 + + // %29%29%2C+t+%3D+0+..+2*PI + + settings.shape = function shapeTriangle(theta) { + + var thetaPrime = theta % (2 * Math.PI / 3); + + return 1 / (Math.cos(thetaPrime) + + + Math.sqrt(3) * Math.sin(thetaPrime)); + + }; + + break; + + + + case 'triangle': + + case 'triangle-upright': + + settings.shape = function shapeTriangle(theta) { + + var thetaPrime = (theta + Math.PI * 3 / 2) % (2 * Math.PI / 3); + + return 1 / (Math.cos(thetaPrime) + + + Math.sqrt(3) * Math.sin(thetaPrime)); + + }; + + break; + + + + case 'pentagon': + + settings.shape = function shapePentagon(theta) { + + var thetaPrime = (theta + 0.955) % (2 * Math.PI / 5); + + return 1 / (Math.cos(thetaPrime) + + + 0.726543 * Math.sin(thetaPrime)); + + }; + + break; + + + + case 'star': + + settings.shape = function shapeStar(theta) { + + var thetaPrime = (theta + 0.955) % (2 * Math.PI / 10); + + if ((theta + 0.955) % (2 * Math.PI / 5) - (2 * Math.PI / 10) >= 0) { + + return 1 / (Math.cos((2 * Math.PI / 10) - thetaPrime) + + + 3.07768 * Math.sin((2 * Math.PI / 10) - thetaPrime)); + + } else { + + return 1 / (Math.cos(thetaPrime) + + + 3.07768 * Math.sin(thetaPrime)); + + } + + }; + + break; + + } + + } + + + + /* Make sure gridSize is a whole number and is not smaller than 4px */ + + settings.gridSize = Math.max(Math.floor(settings.gridSize), 4); + + + + /* shorthand */ + + var g = settings.gridSize; + + var maskRectWidth = g - settings.maskGapWidth; + + + + /* normalize rotation settings */ + + var rotationRange = Math.abs(settings.maxRotation - settings.minRotation); + + var rotationSteps = Math.abs(Math.floor(settings.rotationSteps)); + + var minRotation = Math.min(settings.maxRotation, settings.minRotation); + + + + /* information/object available to all functions, set when start() */ + + var grid, // 2d array containing filling information + + ngx, ngy, // width and height of the grid + + center, // position of the center of the cloud + + maxRadius; + + + + /* timestamp for measuring each putWord() action */ + + var escapeTime; + + + + /* function for getting the color of the text */ + + var getTextColor; + + function random_hsl_color(min, max) { + + return 'hsl(' + + + (Math.random() * 360).toFixed() + ',' + + + (Math.random() * 30 + 70).toFixed() + '%,' + + + (Math.random() * (max - min) + min).toFixed() + '%)'; + + } + + switch (settings.color) { + + case 'random-dark': + + getTextColor = function getRandomDarkColor() { + + return random_hsl_color(10, 50); + + }; + + break; + + + + case 'random-light': + + getTextColor = function getRandomLightColor() { + + return random_hsl_color(50, 90); + + }; + + break; + + + + default: + + if (typeof settings.color === 'function') { + + getTextColor = settings.color; + + } + + break; + + } + + + + /* function for getting the classes of the text */ + + var getTextClasses = null; + + if (typeof settings.classes === 'function') { + + getTextClasses = settings.classes; + + } + + + + /* Interactive */ + + var interactive = false; + + var infoGrid = []; + + var hovered; + + + + var getInfoGridFromMouseTouchEvent = + + function getInfoGridFromMouseTouchEvent(evt) { + + var canvas = evt.currentTarget; + + var rect = canvas.getBoundingClientRect(); + + var clientX; + + var clientY; + + /** Detect if touches are available */ + + if (evt.touches) { + + clientX = evt.touches[0].clientX; + + clientY = evt.touches[0].clientY; + + } else { + + clientX = evt.clientX; + + clientY = evt.clientY; + + } + + var eventX = clientX - rect.left; + + var eventY = clientY - rect.top; + + + + var x = Math.floor(eventX * ((canvas.width / rect.width) || 1) / g); + + var y = Math.floor(eventY * ((canvas.height / rect.height) || 1) / g); + + + + return infoGrid[x][y]; + + }; + + + + var wordcloudhover = function wordcloudhover(evt) { + + var info = getInfoGridFromMouseTouchEvent(evt); + + + + if (hovered === info) { + + return; + + } + + + + hovered = info; + + if (!info) { + + settings.hover(undefined, undefined, evt); + + + + return; + + } + + + + settings.hover(info.item, info.dimension, evt); + + + + }; + + + + var wordcloudclick = function wordcloudclick(evt) { + + var info = getInfoGridFromMouseTouchEvent(evt); + + if (!info) { + + return; + + } + + + + settings.click(info.item, info.dimension, evt); + + evt.preventDefault(); + + }; + + + + /* Get points on the grid for a given radius away from the center */ + + var pointsAtRadius = []; + + var getPointsAtRadius = function getPointsAtRadius(radius) { + + if (pointsAtRadius[radius]) { + + return pointsAtRadius[radius]; + + } + + + + // Look for these number of points on each radius + + var T = radius * 8; + + + + // Getting all the points at this radius + + var t = T; + + var points = []; + + + + if (radius === 0) { + + points.push([center[0], center[1], 0]); + + } + + + + while (t--) { + + // distort the radius to put the cloud in shape + + var rx = 1; + + if (settings.shape !== 'circle') { + + rx = settings.shape(t / T * 2 * Math.PI); // 0 to 1 + + } + + + + // Push [x, y, t]; t is used solely for getTextColor() + + points.push([ + + center[0] + radius * rx * Math.cos(-t / T * 2 * Math.PI), + + center[1] + radius * rx * Math.sin(-t / T * 2 * Math.PI) * + + settings.ellipticity, + + t / T * 2 * Math.PI]); + + } + + + + pointsAtRadius[radius] = points; + + return points; + + }; + + + + /* Return true if we had spent too much time */ + + var exceedTime = function exceedTime() { + + return ((settings.abortThreshold > 0) && + + ((new Date()).getTime() - escapeTime > settings.abortThreshold)); + + }; + + + + /* Get the deg of rotation according to settings, and luck. */ + + var getRotateDeg = function getRotateDeg() { + + if (settings.rotateRatio === 0) { + + return 0; + + } + + + + if (Math.random() > settings.rotateRatio) { + + return 0; + + } + + + + if (rotationRange === 0) { + + return minRotation; + + } + + + + if (rotationSteps > 0) { + + // Min rotation + zero or more steps * span of one step + + return minRotation + + + Math.floor(Math.random() * rotationSteps) * + + rotationRange / (rotationSteps - 1); + + } + + else { + + return minRotation + Math.random() * rotationRange; + + } + + }; + + + + var getTextInfo = function getTextInfo(word, weight, rotateDeg) { + + // calculate the acutal font size + + // fontSize === 0 means weightFactor function wants the text skipped, + + // and size < minSize means we cannot draw the text. + + var debug = false; + + var fontSize = settings.weightFactor(weight); + + if (fontSize <= settings.minSize) { + + return false; + + } + + + + // Scale factor here is to make sure fillText is not limited by + + // the minium font size set by browser. + + // It will always be 1 or 2n. + + var mu = 1; + + if (fontSize < minFontSize) { + + mu = (function calculateScaleFactor() { + + var mu = 2; + + while (mu * fontSize < minFontSize) { + + mu += 2; + + } + + return mu; + + })(); + + } + + + + var fcanvas = document.createElement('canvas'); + + var fctx = fcanvas.getContext('2d', { willReadFrequently: true }); + + + + fctx.font = settings.fontWeight + ' ' + + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; + + + + // Estimate the dimension of the text with measureText(). + + var fw = fctx.measureText(word).width / mu; + + var fh = Math.max(fontSize * mu, + + fctx.measureText('m').width, + + fctx.measureText('\uFF37').width) / mu; + + + + // Create a boundary box that is larger than our estimates, + + // so text don't get cut of (it sill might) + + var boxWidth = fw + fh * 2; + + var boxHeight = fh * 3; + + var fgw = Math.ceil(boxWidth / g); + + var fgh = Math.ceil(boxHeight / g); + + boxWidth = fgw * g; + + boxHeight = fgh * g; + + + + // Calculate the proper offsets to make the text centered at + + // the preferred position. + + + + // This is simply half of the width. + + var fillTextOffsetX = - fw / 2; + + // Instead of moving the box to the exact middle of the preferred + + // position, for Y-offset we move 0.4 instead, so Latin alphabets look + + // vertical centered. + + var fillTextOffsetY = - fh * 0.4; + + + + // Calculate the actual dimension of the canvas, considering the rotation. + + var cgh = Math.ceil((boxWidth * Math.abs(Math.sin(rotateDeg)) + + + boxHeight * Math.abs(Math.cos(rotateDeg))) / g); + + var cgw = Math.ceil((boxWidth * Math.abs(Math.cos(rotateDeg)) + + + boxHeight * Math.abs(Math.sin(rotateDeg))) / g); + + var width = cgw * g; + + var height = cgh * g; + + + + fcanvas.setAttribute('width', width); + + fcanvas.setAttribute('height', height); + + + + if (debug) { + + // Attach fcanvas to the DOM + + document.body.appendChild(fcanvas); + + // Save it's state so that we could restore and draw the grid correctly. + + fctx.save(); + + } + + + + // Scale the canvas with |mu|. + + fctx.scale(1 / mu, 1 / mu); + + fctx.translate(width * mu / 2, height * mu / 2); + + fctx.rotate(- rotateDeg); + + + + // Once the width/height is set, ctx info will be reset. + + // Set it again here. + + fctx.font = settings.fontWeight + ' ' + + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; + + + + // Fill the text into the fcanvas. + + // XXX: We cannot because textBaseline = 'top' here because + + // Firefox and Chrome uses different default line-height for canvas. + + // Please read https://bugzil.la/737852#c6. + + // Here, we use textBaseline = 'middle' and draw the text at exactly + + // 0.5 * fontSize lower. + + fctx.fillStyle = '#000'; + + fctx.textBaseline = 'middle'; + + fctx.fillText(word, fillTextOffsetX * mu, + + (fillTextOffsetY + fontSize * 0.5) * mu); + + + + // Get the pixels of the text + + var imageData = fctx.getImageData(0, 0, width, height).data; + + + + if (exceedTime()) { + + return false; + + } + + + + if (debug) { + + // Draw the box of the original estimation + + fctx.strokeRect(fillTextOffsetX * mu, + + fillTextOffsetY, fw * mu, fh * mu); + + fctx.restore(); + + } + + + + // Read the pixels and save the information to the occupied array + + var occupied = []; + + var gx = cgw, gy, x, y; + + var bounds = [cgh / 2, cgw / 2, cgh / 2, cgw / 2]; + + while (gx--) { + + gy = cgh; + + while (gy--) { + + y = g; + + singleGridLoop: { + + while (y--) { + + x = g; + + while (x--) { + + if (imageData[((gy * g + y) * width + + + (gx * g + x)) * 4 + 3]) { + + occupied.push([gx, gy]); + + + + if (gx < bounds[3]) { + + bounds[3] = gx; + + } + + if (gx > bounds[1]) { + + bounds[1] = gx; + + } + + if (gy < bounds[0]) { + + bounds[0] = gy; + + } + + if (gy > bounds[2]) { + + bounds[2] = gy; + + } + + + + if (debug) { + + fctx.fillStyle = 'rgba(255, 0, 0, 0.5)'; + + fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); + + } + + break singleGridLoop; + + } + + } + + } + + if (debug) { + + fctx.fillStyle = 'rgba(0, 0, 255, 0.5)'; + + fctx.fillRect(gx * g, gy * g, g - 0.5, g - 0.5); + + } + + } + + } + + } + + + + if (debug) { + + fctx.fillStyle = 'rgba(0, 255, 0, 0.5)'; + + fctx.fillRect(bounds[3] * g, + + bounds[0] * g, + + (bounds[1] - bounds[3] + 1) * g, + + (bounds[2] - bounds[0] + 1) * g); + + } + + + + // Return information needed to create the text on the real canvas + + return { + + mu: mu, + + occupied: occupied, + + bounds: bounds, + + gw: cgw, + + gh: cgh, + + fillTextOffsetX: fillTextOffsetX, + + fillTextOffsetY: fillTextOffsetY, + + fillTextWidth: fw, + + fillTextHeight: fh, + + fontSize: fontSize + + }; + + }; + + + + /* Determine if there is room available in the given dimension */ + + var canFitText = function canFitText(gx, gy, gw, gh, occupied) { + + // Go through the occupied points, + + // return false if the space is not available. + + var i = occupied.length; + + while (i--) { + + var px = gx + occupied[i][0]; + + var py = gy + occupied[i][1]; + + + + if (px >= ngx || py >= ngy || px < 0 || py < 0) { + + if (!settings.drawOutOfBound) { + + return false; + + } + + continue; + + } + + + + if (!grid[px][py]) { + + return false; + + } + + } + + return true; + + }; + + + + /* Actually draw the text on the grid */ + + var drawText = function drawText(gx, gy, info, word, weight, + + distance, theta, rotateDeg, attributes) { + + + + var fontSize = info.fontSize; + + var color; + + if (getTextColor) { + + color = getTextColor(word, weight, fontSize, distance, theta); + + } else { + + color = settings.color; + + } + + + + var classes; + + if (getTextClasses) { + + classes = getTextClasses(word, weight, fontSize, distance, theta); + + } else { + + classes = settings.classes; + + } + + + + var dimension; + + var bounds = info.bounds; + + dimension = { + + x: (gx + bounds[3]) * g, + + y: (gy + bounds[0]) * g, + + w: (bounds[1] - bounds[3] + 1) * g, + + h: (bounds[2] - bounds[0] + 1) * g + + }; + + + + elements.forEach(function(el) { + + if (el.getContext) { + + var ctx = el.getContext('2d'); + + var mu = info.mu; + + + + // Save the current state before messing it + + ctx.save(); + + ctx.scale(1 / mu, 1 / mu); + + + + ctx.font = settings.fontWeight + ' ' + + + (fontSize * mu).toString(10) + 'px ' + settings.fontFamily; + + ctx.fillStyle = color; + + + + // Translate the canvas position to the origin coordinate of where + + // the text should be put. + + ctx.translate((gx + info.gw / 2) * g * mu, + + (gy + info.gh / 2) * g * mu); + + + + if (rotateDeg !== 0) { + + ctx.rotate(- rotateDeg); + + } + + + + // Finally, fill the text. + + + + // XXX: We cannot because textBaseline = 'top' here because + + // Firefox and Chrome uses different default line-height for canvas. + + // Please read https://bugzil.la/737852#c6. + + // Here, we use textBaseline = 'middle' and draw the text at exactly + + // 0.5 * fontSize lower. + + ctx.textBaseline = 'middle'; + + ctx.fillText(word, info.fillTextOffsetX * mu, + + (info.fillTextOffsetY + fontSize * 0.5) * mu); + + + + // The below box is always matches how s are positioned + + /* ctx.strokeRect(info.fillTextOffsetX, info.fillTextOffsetY, + + info.fillTextWidth, info.fillTextHeight); */ + + + + // Restore the state. + + ctx.restore(); + + } else { + + // drawText on DIV element + + var span = document.createElement('span'); + + var transformRule = ''; + + transformRule = 'rotate(' + (- rotateDeg / Math.PI * 180) + 'deg) '; + + if (info.mu !== 1) { + + transformRule += + + 'translateX(-' + (info.fillTextWidth / 4) + 'px) ' + + + 'scale(' + (1 / info.mu) + ')'; + + } + + var styleRules = { + + 'position': 'absolute', + + 'display': 'block', + + 'font': settings.fontWeight + ' ' + + + (fontSize * info.mu) + 'px ' + settings.fontFamily, + + 'left': ((gx + info.gw / 2) * g + info.fillTextOffsetX) + 'px', + + 'top': ((gy + info.gh / 2) * g + info.fillTextOffsetY) + 'px', + + 'width': info.fillTextWidth + 'px', + + 'height': info.fillTextHeight + 'px', + + 'lineHeight': fontSize + 'px', + + 'whiteSpace': 'nowrap', + + 'transform': transformRule, + + 'webkitTransform': transformRule, + + 'msTransform': transformRule, + + 'transformOrigin': '50% 40%', + + 'webkitTransformOrigin': '50% 40%', + + 'msTransformOrigin': '50% 40%' + + }; + + if (color) { + + styleRules.color = color; + + } + + span.textContent = word; + + for (var cssProp in styleRules) { + + span.style[cssProp] = styleRules[cssProp]; + + } + + if (attributes) { + + for (var attribute in attributes) { + + span.setAttribute(attribute, attributes[attribute]); + + } + + } + + if (classes) { + + span.className += classes; + + } + + el.appendChild(span); + + } + + }); + + }; + + + + /* Help function to updateGrid */ + + var fillGridAt = function fillGridAt(x, y, drawMask, dimension, item) { + + if (x >= ngx || y >= ngy || x < 0 || y < 0) { + + return; + + } + + + + grid[x][y] = false; + + + + if (drawMask) { + + var ctx = elements[0].getContext('2d'); + + ctx.fillRect(x * g, y * g, maskRectWidth, maskRectWidth); + + } + + + + if (interactive) { + + infoGrid[x][y] = { item: item, dimension: dimension }; + + } + + }; + + + + /* Update the filling information of the given space with occupied points. + + Draw the mask on the canvas if necessary. */ + + var updateGrid = function updateGrid(gx, gy, gw, gh, info, item) { + + var occupied = info.occupied; + + var drawMask = settings.drawMask; + + var ctx; + + if (drawMask) { + + ctx = elements[0].getContext('2d'); + + ctx.save(); + + ctx.fillStyle = settings.maskColor; + + } + + + + var dimension; + + if (interactive) { + + var bounds = info.bounds; + + dimension = { + + x: (gx + bounds[3]) * g, + + y: (gy + bounds[0]) * g, + + w: (bounds[1] - bounds[3] + 1) * g, + + h: (bounds[2] - bounds[0] + 1) * g + + }; + + } + + + + var i = occupied.length; + + while (i--) { + + var px = gx + occupied[i][0]; + + var py = gy + occupied[i][1]; + + + + if (px >= ngx || py >= ngy || px < 0 || py < 0) { + + continue; + + } + + + + fillGridAt(px, py, drawMask, dimension, item); + + } + + + + if (drawMask) { + + ctx.restore(); + + } + + }; + + + + /* putWord() processes each item on the list, + + calculate it's size and determine it's position, and actually + + put it on the canvas. */ + + var putWord = function putWord(item) { + + var word, weight, attributes; + + if (Array.isArray(item)) { + + word = item[0]; + + weight = item[1]; + + } else { + + word = item.word; + + weight = item.weight; + + attributes = item.attributes; + + } + + var rotateDeg = getRotateDeg(); + + + + // get info needed to put the text onto the canvas + + var info = getTextInfo(word, weight, rotateDeg); + + + + // not getting the info means we shouldn't be drawing this one. + + if (!info) { + + return false; + + } + + + + if (exceedTime()) { + + return false; + + } + + + + // If drawOutOfBound is set to false, + + // skip the loop if we have already know the bounding box of + + // word is larger than the canvas. + + if (!settings.drawOutOfBound) { + + var bounds = info.bounds; + + if ((bounds[1] - bounds[3] + 1) > ngx || + + (bounds[2] - bounds[0] + 1) > ngy) { + + return false; + + } + + } + + + + // Determine the position to put the text by + + // start looking for the nearest points + + var r = maxRadius + 1; + + + + var tryToPutWordAtPoint = function(gxy) { + + var gx = Math.floor(gxy[0] - info.gw / 2); + + var gy = Math.floor(gxy[1] - info.gh / 2); + + var gw = info.gw; + + var gh = info.gh; + + + + // If we cannot fit the text at this position, return false + + // and go to the next position. + + if (!canFitText(gx, gy, gw, gh, info.occupied)) { + + return false; + + } + + + + // Actually put the text on the canvas + + drawText(gx, gy, info, word, weight, + + (maxRadius - r), gxy[2], rotateDeg, attributes); + + + + // Mark the spaces on the grid as filled + + updateGrid(gx, gy, gw, gh, info, item); + + + + // Return true so some() will stop and also return true. + + return true; + + }; + + + + while (r--) { + + var points = getPointsAtRadius(maxRadius - r); + + + + if (settings.shuffle) { + + points = [].concat(points); + + shuffleArray(points); + + } + + + + // Try to fit the words by looking at each point. + + // array.some() will stop and return true + + // when putWordAtPoint() returns true. + + // If all the points returns false, array.some() returns false. + + var drawn = points.some(tryToPutWordAtPoint); + + + + if (drawn) { + + // leave putWord() and return true + + return true; + + } + + } + + // we tried all distances but text won't fit, return false + + return false; + + }; + + + + /* Send DOM event to all elements. Will stop sending event and return + + if the previous one is canceled (for cancelable events). */ + + var sendEvent = function sendEvent(type, cancelable, detail) { + + if (cancelable) { + + return !elements.some(function(el) { + + var evt = document.createEvent('CustomEvent'); + + evt.initCustomEvent(type, true, cancelable, detail || {}); + + return !el.dispatchEvent(evt); + + }, this); + + } else { + + elements.forEach(function(el) { + + var evt = document.createEvent('CustomEvent'); + + evt.initCustomEvent(type, true, cancelable, detail || {}); + + el.dispatchEvent(evt); + + }, this); + + } + + }; + + + + /* Start drawing on a canvas */ + + var start = function start() { + + // For dimensions, clearCanvas etc., + + // we only care about the first element. + + var canvas = elements[0]; + + + + if (canvas.getContext) { + + ngx = Math.ceil(canvas.width / g); + + ngy = Math.ceil(canvas.height / g); + + } else { + + var rect = canvas.getBoundingClientRect(); + + ngx = Math.ceil(rect.width / g); + + ngy = Math.ceil(rect.height / g); + + } + + + + // Sending a wordcloudstart event which cause the previous loop to stop. + + // Do nothing if the event is canceled. + + if (!sendEvent('wordcloudstart', true)) { + + return; + + } + + + + // Determine the center of the word cloud + + center = (settings.origin) ? + + [settings.origin[0]/g, settings.origin[1]/g] : + + [ngx / 2, ngy / 2]; + + + + // Maxium radius to look for space + + maxRadius = Math.floor(Math.sqrt(ngx * ngx + ngy * ngy)); + + + + /* Clear the canvas only if the clearCanvas is set, + + if not, update the grid to the current canvas state */ + + grid = []; + + + + var gx, gy, i; + + if (!canvas.getContext || settings.clearCanvas) { + + elements.forEach(function(el) { + + if (el.getContext) { + + var ctx = el.getContext('2d'); + + ctx.fillStyle = settings.backgroundColor; + + ctx.clearRect(0, 0, ngx * (g + 1), ngy * (g + 1)); + + ctx.fillRect(0, 0, ngx * (g + 1), ngy * (g + 1)); + + } else { + + el.textContent = ''; + + el.style.backgroundColor = settings.backgroundColor; + + el.style.position = 'relative'; + + } + + }); + + + + /* fill the grid with empty state */ + + gx = ngx; + + while (gx--) { + + grid[gx] = []; + + gy = ngy; + + while (gy--) { + + grid[gx][gy] = true; + + } + + } + + } else { + + /* Determine bgPixel by creating + + another canvas and fill the specified background color. */ + + var bctx = document.createElement('canvas').getContext('2d'); + + + + bctx.fillStyle = settings.backgroundColor; + + bctx.fillRect(0, 0, 1, 1); + + var bgPixel = bctx.getImageData(0, 0, 1, 1).data; + + + + /* Read back the pixels of the canvas we got to tell which part of the + + canvas is empty. + + (no clearCanvas only works with a canvas, not divs) */ + + var imageData = + + canvas.getContext('2d').getImageData(0, 0, ngx * g, ngy * g).data; + + + + gx = ngx; + + var x, y; + + while (gx--) { + + grid[gx] = []; + + gy = ngy; + + while (gy--) { + + y = g; + + singleGridLoop: while (y--) { + + x = g; + + while (x--) { + + i = 4; + + while (i--) { + + if (imageData[((gy * g + y) * ngx * g + + + (gx * g + x)) * 4 + i] !== bgPixel[i]) { + + grid[gx][gy] = false; + + break singleGridLoop; + + } + + } + + } + + } + + if (grid[gx][gy] !== false) { + + grid[gx][gy] = true; + + } + + } + + } + + + + imageData = bctx = bgPixel = undefined; + + } + + + + // fill the infoGrid with empty state if we need it + + if (settings.hover || settings.click) { + + + + interactive = true; + + + + /* fill the grid with empty state */ + + gx = ngx + 1; + + while (gx--) { + + infoGrid[gx] = []; + + } + + + + if (settings.hover) { + + canvas.addEventListener('mousemove', wordcloudhover); + + } + + + + var touchend = function (e) { + + e.preventDefault(); + + }; + + + + if (settings.click) { + + canvas.addEventListener('click', wordcloudclick); + + canvas.addEventListener('touchstart', wordcloudclick); + + canvas.addEventListener('touchend', touchend); + + canvas.style.webkitTapHighlightColor = 'rgba(0, 0, 0, 0)'; + + } + + + + canvas.addEventListener('wordcloudstart', function stopInteraction() { + + canvas.removeEventListener('wordcloudstart', stopInteraction); + + + + canvas.removeEventListener('mousemove', wordcloudhover); + + canvas.removeEventListener('click', wordcloudclick); + + canvas.removeEventListener('touchstart', wordcloudclick); + + canvas.removeEventListener('touchend', touchend); + + hovered = undefined; + + }); + + } + + + + i = 0; + + var loopingFunction, stoppingFunction; + + if (settings.wait !== 0) { + + loopingFunction = window.setTimeout; + + stoppingFunction = window.clearTimeout; + + } else { + + loopingFunction = window.setImmediate; + + stoppingFunction = window.clearImmediate; + + } + + + + var addEventListener = function addEventListener(type, listener) { + + elements.forEach(function(el) { + + el.addEventListener(type, listener); + + }, this); + + }; + + + + var removeEventListener = function removeEventListener(type, listener) { + + elements.forEach(function(el) { + + el.removeEventListener(type, listener); + + }, this); + + }; + + + + var anotherWordCloudStart = function anotherWordCloudStart() { + + removeEventListener('wordcloudstart', anotherWordCloudStart); + + stoppingFunction(timer); + + }; + + + + addEventListener('wordcloudstart', anotherWordCloudStart); + + + + var timer = loopingFunction(function loop() { + + if (i >= settings.list.length) { + + stoppingFunction(timer); + + sendEvent('wordcloudstop', false); + + removeEventListener('wordcloudstart', anotherWordCloudStart); + + + + return; + + } + + escapeTime = (new Date()).getTime(); + + var drawn = putWord(settings.list[i]); + + var canceled = !sendEvent('wordclouddrawn', true, { + + item: settings.list[i], drawn: drawn }); + + if (exceedTime() || canceled) { + + stoppingFunction(timer); + + settings.abort(); + + sendEvent('wordcloudabort', false); + + sendEvent('wordcloudstop', false); + + removeEventListener('wordcloudstart', anotherWordCloudStart); + + return; + + } + + i++; + + timer = loopingFunction(loop, settings.wait); + + }, settings.wait); + + }; + + + + // All set, start the drawing + + start(); + + }; + + + + WordCloud.isSupported = isSupported; + + WordCloud.minFontSize = minFontSize; + + + + // Expose the library as an AMD module + + if (typeof define === 'function' && define.amd) { + + global.WordCloud = WordCloud; + + define('wordcloud', [], function() { return WordCloud; }); + + } else if (typeof module !== 'undefined' && module.exports) { + + module.exports = WordCloud; + + } else { + + global.WordCloud = WordCloud; + + } + + + +})(this); //jshint ignore:line \ No newline at end of file diff --git a/IW4MAdmin.sln b/IW4MAdmin.sln index e6bc44a46..b5be328f8 100644 --- a/IW4MAdmin.sln +++ b/IW4MAdmin.sln @@ -75,8 +75,8 @@ Global {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU + {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {DD5DCDA2-51DB-4B1A-922F-5705546E6115}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -99,8 +99,8 @@ Global {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU + {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {4785AB75-66F3-4391-985D-63A5A049A0FA}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -123,8 +123,8 @@ Global {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU + {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {D51EECEB-438A-47DA-870F-7D7B41BC24D6}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -147,8 +147,8 @@ Global {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU + {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {AF097E6B-48D5-4452-9CCF-0A81A21F341D}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -164,14 +164,13 @@ Global {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Debug|x86.ActiveCfg = Debug|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Debug|x86.Build.0 = Debug|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|Any CPU.ActiveCfg = Release-Nightly|Any CPU - {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|Any CPU.Build.0 = Release-Nightly|Any CPU {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|Mixed Platforms.ActiveCfg = Release-Nightly|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|Mixed Platforms.Build.0 = Release-Nightly|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|x64.ActiveCfg = Release-Nightly|Any CPU {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU + {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {428D8EB9-ECA3-4A66-AA59-3A944378C33F}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -184,13 +183,12 @@ Global {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Debug|x64.ActiveCfg = Release-Stable|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Debug|x86.ActiveCfg = Debug|x86 {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Debug|x86.Build.0 = Debug|x86 - {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|Any CPU.ActiveCfg = Release-Stable|Any CPU - {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|Any CPU.Build.0 = Release-Stable|Any CPU + {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|Any CPU.ActiveCfg = Release-Nightly|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|Mixed Platforms.ActiveCfg = Release-Stable|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|x64.ActiveCfg = Release-Stable|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU + {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU {E46C85BD-A99C-484E-BCCE-0F1831C5925E}.Release-Stable|x86.ActiveCfg = Release-Stable|Any CPU @@ -211,8 +209,8 @@ Global {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU + {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Any CPU.Build.0 = Debug|Any CPU {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {C9E821BF-23AD-4CB5-B7F9-B3B99B606650}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -228,14 +226,13 @@ Global {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Debug|x86.ActiveCfg = Debug|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Debug|x86.Build.0 = Debug|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|Any CPU.ActiveCfg = Release-Nightly|Any CPU - {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|Any CPU.Build.0 = Release-Nightly|Any CPU {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|Mixed Platforms.ActiveCfg = Release-Nightly|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|Mixed Platforms.Build.0 = Release-Nightly|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|x64.ActiveCfg = Release-Nightly|Any CPU {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU + {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {1479DE87-ACB5-4046-81C8-A0BA5041227D}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU @@ -251,15 +248,13 @@ Global {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Debug|x86.ActiveCfg = Debug|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Debug|x86.Build.0 = Debug|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|Any CPU.ActiveCfg = Release-Nightly|Any CPU - {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|Any CPU.Build.0 = Release-Nightly|Any CPU {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|Mixed Platforms.ActiveCfg = Release-Nightly|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|Mixed Platforms.Build.0 = Release-Nightly|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|x64.ActiveCfg = Release-Nightly|Any CPU {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|x64.Build.0 = Release-Nightly|Any CPU {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|x86.ActiveCfg = Release-Nightly|Any CPU {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Nightly|x86.Build.0 = Release-Nightly|Any CPU - {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|Any CPU.ActiveCfg = Release-Stable|Any CPU - {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|Any CPU.Build.0 = Release-Stable|Any CPU + {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|Any CPU.ActiveCfg = Debug|Any CPU {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|Mixed Platforms.ActiveCfg = Release-Stable|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|Mixed Platforms.Build.0 = Release-Stable|x86 {B8C2A759-8663-4F6F-9BA4-19595F5E12C1}.Release-Stable|x64.ActiveCfg = Release-Stable|Any CPU diff --git a/Plugins/SimpleStats/Chat/ChatDatabase.cs b/Plugins/SimpleStats/Chat/ChatDatabase.cs index 4da3f03f6..2ef091476 100644 --- a/Plugins/SimpleStats/Chat/ChatDatabase.cs +++ b/Plugins/SimpleStats/Chat/ChatDatabase.cs @@ -12,6 +12,86 @@ namespace StatsPlugin { public class ChatDatabase : Database { + private string[] CommonWords = new string[] { "for", +"with", +"from", +"about", +"into", +"over", +"after", +"that", +"not", +"you", +"this", +"but", +"his", +"they", +"her", +"she", +"will", +"one", +"all", +"would", +"there", +"their", +"have", +"say", +"get", +"make", +"know", +"take", +"see", +"come", +"think", +"look", +"want", +"give", +"use", +"find", +"tell", +"ask", +"work", +"seem", +"feel", +"try", +"leave", +"call", +"good", +"new", +"first", +"last", +"long", +"great", +"little", +"own", +"other", +"old", +"right", +"big", +"high", +"small", +"large", +"next", +"early", +"young", +"important", +"few", +"public", +"same", +"able", +"the", +"and", +"that", +"have", +"this", +"one", +"would", + "yeah", + "yah", + "why", + "who" , + "when"}; + public ChatDatabase(string FN) : base(FN) { } @@ -65,6 +145,9 @@ namespace StatsPlugin public void AddChatHistory(int clientID, int serverID, string message) { + if (message.Length < 3) + return; + var chat = new Dictionary() { { "ClientID", clientID }, @@ -75,20 +158,22 @@ namespace StatsPlugin Insert("CHATHISTORY", chat); - message.Split(' ').Where(word => word.Length >= 3).Any(word => - { - word = word.ToLower(); - Insert("WORDSTATS", new Dictionary() { { "Word", word } }, true); - // shush :^) - ExecuteNonQuery($"UPDATE WORDSTATS SET Count = Count + 1 WHERE Word='{word.CleanChars()}'"); - return true; - } - ); + var eachWord = message.Split(new char[] { ' ' }, StringSplitOptions.RemoveEmptyEntries) + .Where (word => word.Length >= 3) + .Where(word => CommonWords.FirstOrDefault(c => c == word.ToLower()) == null) + .ToList(); + + foreach (string _word in eachWord) + { + string word = _word.ToLower(); + Insert("WORDSTATS", new Dictionary() { { "Word", word } }, true); + UpdateIncrement("WORDSTATS", "Count", new Dictionary() { { "Count", 1 } }, new KeyValuePair("Word", word)); + } } public KeyValuePair[] GetWords() { - var result = GetDataTable("SELECT * FROM WORDSTATS ORDER BY Count desc LIMIT 100"); + var result = GetDataTable("SELECT * FROM WORDSTATS ORDER BY Count desc LIMIT 200"); return result.Select().Select(w => new KeyValuePair(w["Word"].ToString(), Convert.ToInt32(w["Count"].ToString()))).ToArray(); } } diff --git a/Plugins/SimpleStats/Chat/ChatHistoryPage.cs b/Plugins/SimpleStats/Chat/ChatHistoryPage.cs index 1d44ee0c6..be0ed5e3c 100644 --- a/Plugins/SimpleStats/Chat/ChatHistoryPage.cs +++ b/Plugins/SimpleStats/Chat/ChatHistoryPage.cs @@ -11,8 +11,6 @@ namespace StatsPlugin.Chat { public class ChatPage : HTMLPage { - public ChatPage() : base(false) { } - public override string GetContent(NameValueCollection querySet, IDictionary headers) { StringBuilder S = new StringBuilder(); @@ -27,7 +25,7 @@ namespace StatsPlugin.Chat return S.ToString(); } - public override string GetName() => "Chat Stats"; + public override string GetName() => "Word Cloud"; public override string GetPath() => "/chat"; } @@ -68,11 +66,20 @@ namespace StatsPlugin.Chat public HttpResponse GetPage(NameValueCollection querySet, IDictionary headers) { + int clientID = Convert.ToInt32(querySet["clientid"]); + var client = Stats.ManagerInstance.GetClientDatabase().GetPlayer(clientID); HttpResponse resp = new HttpResponse() { contentType = GetContentType(), - content = Stats.ChatDB.GetChatForPlayer(Convert.ToInt32(querySet["clientid"])).ToArray(), + content = Stats.ChatDB.GetChatForPlayer(clientID).ToArray().Select(c => new + { + ClientID = c.ClientID, + ServerID = c.ServerID, + Message = c.Message, + TimeSent = c.TimeSent, + Client = client + }), additionalHeaders = new Dictionary() }; diff --git a/Plugins/SimpleStats/Plugin.cs b/Plugins/SimpleStats/Plugin.cs index d454ffb8b..48093dcd4 100644 --- a/Plugins/SimpleStats/Plugin.cs +++ b/Plugins/SimpleStats/Plugin.cs @@ -316,7 +316,6 @@ namespace StatsPlugin //S.Logger.WriteInfo($"{E.Origin.Name} killed {E.Target.Name} with a {killEvent.Weapon} from a distance of {Vector3.Distance(killEvent.KillOrigin, killEvent.DeathOrigin)} with {killEvent.Damage} damage, at {killEvent.HitLoc}"); var cs = statLists.Find(x => x.Port == S.GetPort()); cs.playerStats.AddKill(killEvent); - return; } Player Killer = E.Origin; diff --git a/Plugins/Tests/Tests.csproj b/Plugins/Tests/Tests.csproj index 293f6b680..d8b8646c7 100644 --- a/Plugins/Tests/Tests.csproj +++ b/Plugins/Tests/Tests.csproj @@ -91,6 +91,6 @@ - if $(ConfigurationName) == Debug copy /Y "$(TargetDir)$(TargetName).dll" "$(SolutionDir)BUILD\plugins\" + if $(ConfigurationName) == Debug (copy /Y "$(TargetDir)$(TargetName).dll" "$(SolutionDir)BUILD\plugins\") \ No newline at end of file diff --git a/SharedLibrary/Database.cs b/SharedLibrary/Database.cs index 2eca9f9b5..0f08f5391 100644 --- a/SharedLibrary/Database.cs +++ b/SharedLibrary/Database.cs @@ -64,6 +64,42 @@ namespace SharedLibrary } + protected void UpdateIncrement(String tableName, string columnName, Dictionary data, KeyValuePair where) + { + string parameters = ""; + foreach (string key in data.Keys) + { + parameters += $"{key}={key}+1,"; + } + + parameters = parameters.Substring(0, parameters.Length - 1); + var Con = GetNewConnection(); + + SQLiteCommand updatecmd = new SQLiteCommand() + { + Connection = Con, + CommandText = String.Format("UPDATE `{0}` SET {1} WHERE {2}=@{2}", tableName, parameters, where.Key) + }; + foreach (string key in data.Keys) + { + updatecmd.Parameters.AddWithValue('@' + key, data[key]); + } + + updatecmd.Parameters.AddWithValue('@' + where.Key, where.Value); + + try + { + Con.Open(); + updatecmd.ExecuteNonQuery(); + Con.Close(); + } + + catch (Exception E) + { + Console.WriteLine($"Line 96: {E.Message}"); + } + } + protected bool Update(String tableName, Dictionary data, KeyValuePair where) { string parameters = ""; diff --git a/SharedLibrary/SharedLibrary.csproj b/SharedLibrary/SharedLibrary.csproj index cac026b9b..76563899e 100644 --- a/SharedLibrary/SharedLibrary.csproj +++ b/SharedLibrary/SharedLibrary.csproj @@ -143,9 +143,11 @@ copy /Y "$(TargetDir)Newtonsoft.Json.dll" "$(SolutionDir)Admin\lib" - if not exist "$(SolutionDir)BUILD" mkdir "$(SolutionDir)BUILD" + if exist "$(SolutionDir)BUILD\Plugins" rmdir /Q /S "$(SolutionDir)BUILD\Plugins" +mkdir "$(SolutionDir)BUILD\Plugins" + +if not exist "$(SolutionDir)BUILD" mkdir "$(SolutionDir)BUILD" if not exist "$(SolutionDir)BUILD\Lib" mkdir "$(SolutionDir)BUILD\Lib" -if not exist "$(SolutionDir)BUILD\Plugins" mkdir "$(SolutionDir)BUILD\Plugins" if not exist "$(SolutionDir)BUILD\userraw\scripts" mkdir "$(SolutionDir)BUILD\userraw\scripts"