Graal Pastebin
New PastePasted on June 15, 2012.
View the raw file
// Simple implementation of an up-down (think Reddit) voting // system for Graal // basic configuration const VOTE_OPEN = true; // anti-cheating const VOTE_SUPPRESS_THRESHOLD_SCORE = 0.25; // 25% of votes must be upvotes const VOTE_SUPPRESS_THRESHOLD_UP = 20; // most upvotes allowed in one hour const VOTE_SUPPRESS_THRESHOLD_DOWN = 5; // most downvotes allowed in one hour const VOTE_MAXPERHOUR = 5; // max responses allowed per hour // display options const VOTE_DISPLAY_THRESHOLD = (- 5); // don't show below this score const VOTE_SHOW_SUBMITTER = true; // show submitter to everyone (or just staff) // current topic const VOTE_TOPIC = "PK Bonuses"; const VOTE_TOPIC_DESCRIPTION = "We plan to add bonuses to the PK system. " @ "Bonuses will be awarded upon PK " @ "achievements. For example, killing someone while blind might " @ "give you the bonus called \"White-Hot\" which makes you immune " @ "to concussion grenades for 5 minutes. " @ "We want you to help us come up with more bonuses. Try to keep " @ "your suggestions short and simple. Complex ones won't be " @ "used."; function onCreated() { this.join("func_sql"); this.join("func_strings"); this.createTables(); } for (temp.pl : allplayers) temp.pl.addWeapon(name); function createTables() { req2("default", "CREATE TABLE IF NOT EXISTS voting_responses ( id INTEGER PRIMARY KEY AUTOINCREMENT, account TEXT NOT NULL, title TEXT NOT NULL, description TEXT NOT NULL, vote_cache INT NOT NULL DEFAULT 0, deleted INT NOT NULL DEFAULT 0, deleted_by TEXT DEFAULT NULL, time REAL NOT NULL )", true); req2("default", "CREATE TABLE IF NOT EXISTS voting_votes ( account TEXT NOT NULL, response_id INT NOT NULL, score INT NOT NULL, time REAL NOT NULL, suppressed INT NOT NULL DEFAULT 0, suppress_reason TEXT DEFAULT NULL )", true); } function onActionServerSide(temp.cmd, temp.data) { if (player.level.name == "era_jail.nw") { return; } if (temp.cmd == "requestVotingData") { if (VOTE_OPEN || this.isAdmin(player)) { temp.responses = this.getResponses(player); triggerClient("gui", this.name, "returnVotingData", {VOTE_TOPIC, VOTE_TOPIC_DESCRIPTION, this.isAdmin(player), temp.responses}); } } else if (temp.cmd == "submit") { temp.title = temp.data[0]; temp.description = temp.data[1]; temp.failReason = null; if (temp.title.length() < 3 || temp.title.length() > 25) { temp.failReason = "Your title must be between 3 and 25 characters long."; } temp.description = replaceText(temp.description, "\\n", " "); // remove newlines if (temp.description.length() < 10 || temp.description.length() > 100) { temp.failReason = "Your description must be between 10 and 100 characters long."; } // get rid of any HTML temp.description = replaceText(temp.description, "<", "<"); temp.description = replaceText(temp.description, ">", ">"); // try to add the response if (temp.failReason == null) { temp.worked = this.addResponse(player, temp.title, temp.description); if (! temp.worked[0]) { if (temp.worked[1] == "maxperhour") { temp.failReason = "You can't submit that many ideas per hour."; } else { temp.failReason = "There was an unknown error."; } } else { triggerClient("gui", this.name, "destroySubmitWindow"); } } // was there an error? if (temp.failReason != null) { player.addMessage("There was an error submitting your idea:", "b", 3); player.addMessage(temp.failReason, "b", 3); triggerClient("gui", this.name, "enableSubmitWindow"); } } else if (temp.cmd == "delete") { temp.responseID = int(temp.data); if (this.isAdmin(player)) { this.deleteResponse(player, temp.responseID); } } else if (temp.cmd == "getResponses") { triggerClient("gui", this.name, "returnResponses", {this.isAdmin(player), this.getResponses(player)}); } else if (temp.cmd == "vote") { temp.responseID = int(temp.data[0]); temp.score = int(temp.data[1]); if (! (temp.score in {1, (- 1)})) { return echo("Hacker?: " @ player.getLogName() @ " is trying to vote too much (" @ temp.score @ ")!"); } this.addVote(player, temp.responseID, temp.score); } } // these functions do not validate input; the caller is responsible for // ensuring that all variables are of proper type and that all actions // are authorized (e.g. deleting) function getResponses(temp.pl) { temp.isAdmin = this.isAdmin(temp.pl); temp.responses = req2("default", "SELECT * FROM voting_responses WHERE vote_cache >= " @ VOTE_DISPLAY_THRESHOLD @ " AND voting_responses.deleted = 0", true).rows; temp.plResponses = null; // hide the actual votes a little bit to make gaming it harder for (temp.i = 0; temp.i < temp.responses.size(); temp.i ++) { if (abs(temp.responses[temp.i].vote_cache) > 1) { temp.responses[temp.i].vote_cache += int(random((- 3), 3)); } } temp.responses.sortByValue("vote_cache", "float", false); // organize the responses to send back to the client for (temp.response : temp.responses) { temp.response.my_score = req2("default", "SELECT score FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "' AND response_id = " @ temp.response.id, true).rows[0][0]; temp.plResponses.add({ // responseID, title, description, account (possibly null), response score, my score temp.response.id, temp.response.title, temp.response.description, (temp.isAdmin || VOTE_SHOW_SUBMITTER || temp.response.account == temp.pl.account) ? temp.response.account : null, temp.response.vote_cache, temp.response.my_score + 0 }); } return temp.plResponses; } function addResponse(temp.pl, temp.title, temp.description) { if (this.getPlayerResponsesPastHour(temp.pl) >= VOTE_MAXPERHOUR && ! this.isAdmin(temp.pl)) { return {false, "maxperhour"}; } temp.responseID = req2("default", "INSERT INTO voting_responses (account, title, description, time) VALUES ('" @ temp.pl.account.escape() @ "', '" @ temp.title.escape() @ "', '" @ temp.description.escape() @ "', " @ timevar2 @ ")", true).lastinsertid; this.addVote(temp.pl, temp.responseID, 1); return {true}; } function deleteResponse(temp.pl, temp.responseID) { req2("default", "UPDATE voting_responses SET deleted = 1, deleted_by = '" @ temp.pl.account.escape() @ "' WHERE id = " @ temp.responseID); } function addVote(temp.pl, temp.responseID, temp.score) { this.removeVote(temp.pl, temp.responseID, true); req2("default", "INSERT INTO voting_votes (account, response_id, score, time) VALUES ('" @ temp.pl.account.escape() @ "', " @ temp.responseID @ ", " @ temp.score @ ", " @ timevar2 @ ")", true); // test for suppression if (this.getPlayerUpvotesPastHour(temp.pl) > VOTE_SUPPRESS_THRESHOLD_UP) { this.suppressVote(temp.pl, temp.responseID, "threshold_up"); } else if (this.getPlayerDownvotesPastHour(temp.pl) > VOTE_SUPPRESS_THRESHOLD_DOWN) { this.suppressVote(temp.pl, temp.responseID, "threshold_down"); } else if (this.getPlayerVotingPercent(temp.pl) < VOTE_SUPPRESS_THRESHOLD_SCORE) { this.suppressVote(temp.pl, temp.responseID, "threshold_score_low"); } // update vote cache this.updateVoteCache(temp.responseID); } function suppressVote(temp.pl, temp.responseID, temp.reason) { req2("default", "UPDATE voting_votes SET suppressed = 1, suppress_reason = '" @ temp.reason.escape() @ "' WHERE account = '" @ temp.pl.account.escape() @ "' AND response_id = " @ temp.responseID); } function removeVote(temp.pl, temp.responseID, temp.dontUpdate) { req2("default", "DELETE FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "' AND response_id = " @ temp.responseID); if (! temp.dontUpdate) { this.updateVoteCache(temp.responseID); } } function updateVoteCache(temp.responseID) { temp.score = req2("default", "SELECT SUM(score) AS Score FROM voting_votes WHERE suppressed = 0 AND response_id = " @ temp.responseID, true).rows[0].Score + 0; req2("default", "UPDATE voting_responses SET vote_cache = " @ temp.score @ " WHERE id = " @ temp.responseID, true); } function getPlayerResponsesPastHour(temp.pl) { return req2("default", "SELECT count(*) AS ResponsesPastHour FROM voting_responses WHERE account = '" @ temp.pl.account.escape() @ "' AND time > " @ (timevar2 - (60 * 60)), true).rows[0].ResponsesPastHour; } function getPlayerUpvotesPastHour(temp.pl) { return req2("default", "SELECT count(*) AS UpvotesPastHour FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "' AND score = 1 AND time > " @ (timevar2 - (60 * 60)), true).rows[0].UpvotesPastHour; } function getPlayerDownvotesPastHour(temp.pl) { return req2("default", "SELECT count(*) AS DownvotesPastHour FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "' AND score = (- 1) AND time > " @ (timevar2 - (60 * 60)), true).rows[0].DownvotesPastHour; } function getPlayerVotingPercent(temp.pl) { temp.totalVotes = req2("default", "SELECT count(*) AS TotalVotes FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "'", true).rows[0].TotalVotes; temp.totalUpvotes = req2("default", "SELECT count(*) AS TotalUpvotes FROM voting_votes WHERE account = '" @ temp.pl.account.escape() @ "' AND score = 1", true).rows[0].TotalUpvotes; return (temp.totalUpvotes / temp.totalVotes); } function isAdmin(temp.pl) { return temp.pl.clientr.staff >= 2; } //#CLIENTSIDE function onCreated() { // un-comment this to open the voting window on login // this.openVotingWindow(); } public function openVotingWindow() { triggerServer("gui", this.name, "requestVotingData"); } function openWindow(temp.isAdmin, temp.topic, temp.description) { if (UpDownVote_Window != null) { UpDownVote_Window.destroy(); } new GuiWindowCtrl("UpDownVote_Window") { profile = GuiBlueWindowProfile; canClose = canMinimize = canMaximize = canResize = clientRelative = visible = true; clientWidth = 500; clientHeight = 400; x = (GraalControl.width - width) / 2; y = (GraalControl.height - height) / 2; text = temp.topic; new GuiControl("UpDownVote_Topic") { useOwnProfile = true; profile.border = 1; profile.borderColor = {0, 0, 0}; profile.opaque = true; profile.fillColor = {200, 200, 235, 180}; x = 5; y = 5; width = UpDownVote_Window.clientWidth - (x * 2); height = 100; horizSizing = "width"; new GuiMLTextCtrl("UpDownVote_Topic_Text") { profile = GuiBlueMLTextProfile; useOwnProfile = true; profile.fontColor = {0, 0, 0, 190}; x = 5; y = 5; width = UpDownVote_Topic.width - (x * 2); text = "<center><font size=26><b>" @ temp.topic @ "</b></font><br><font size=14>" @ temp.description; horizSizing = "width"; } } new GuiScrollCtrl("UpDownVote_Scroll") { profile = GuiBlueScrollProfile; useOwnProfile = true; profile.border = 1; x = UpDownVote_Topic.x; y = UpDownVote_Topic.y + UpDownVote_Topic.height + 5; width = UpDownVote_Topic.width; height = UpDownVote_Window.clientHeight - y - 38; vertSizing = "height"; horizSizing = "width"; vScrollBar = "alwaysOn"; hScrollBar = "alwaysOff"; } new GuiButtonCtrl("UpDownVote_Refresh") { profile = GuiBlueButtonProfile; height = UpDownVote_Window.clientHeight - UpDownVote_Scroll.height - UpDownVote_Scroll.y - 10; x = 5; y = UpDownVote_Window.clientHeight - height - 5; vertSizing = "top"; this.setIconSize(12, 15); this.icon.drawImage(0, 0, "guiblue_refresh.png"); width = 30; } new GuiButtonCtrl("UpDownVote_Submit") { profile = GuiBlueButtonProfile; x = UpDownVote_Refresh.x + UpDownVote_Refresh.width + 5; y = UpDownVote_Refresh.y; width = UpDownVote_Window.clientWidth - x - (UpDownVote_Refresh.x * 2); height = UpDownVote_Refresh.height; vertSizing = "top"; horizSizing = "width"; text = "Submit Idea"; } } } function UpDownVote_Submit.onAction() { this.showSubmitWindow(); } function showSubmitWindow() { if (UpDownVoteSubmit_Window.visible) { return UpDownVoteSubmit_Window.showTop(); } new GuiWindowCtrl("UpDownVoteSubmit_Window") { profile = GuiBlueWindowProfile; canClose = canMinimize = canMaximize = canResize = clientRelative = visible = true; clientWidth = 300; clientHeight = 135; x = (GraalControl.width - width) / 2; y = (GraalControl.height - height) / 2; text = "Submit Idea"; minExtent = {200, 100}; this.showTop(); new GuiControl("UpDownVoteSubmit_Back") { useOwnProfile = true; profile.opaque = true; profile.fillColor = {200, 200, 235, 120}; x = 0; y = 0; width = UpDownVoteSubmit_Window.clientWidth; height = UpDownVoteSubmit_Window.clientHeight; horizSizing = "width"; vertSizing = "height"; new GuiTextCtrl("UpDownVoteSubmit_Title_Label") { profile = GuiBlueTextProfile; useOwnProfile = true; profile.fontColor = {0, 0, 0}; profile.fontStyle = "b"; text = "Title:"; x = 5; y = 7; width = profile.getTextWidth(text); height = profile.getTextHeight() * 1.2; } new GuiTextEditCtrl("UpDownVoteSubmit_Title") { profile = GuiBlueTextEditProfile; x = UpDownVoteSubmit_Title_Label.x + UpDownVoteSubmit_Title_Label.width + 5; y = 5; width = UpDownVoteSubmit_Window.clientWidth - x - 5; height = 25; text = ""; horizSizing = "width"; } new GuiMLTextCtrl("UpDownVoteSubmit_Description_Label") { profile = GuiBlueTextProfile; useOwnProfile = true; profile.fontColor = {0, 0, 0}; profile.fontStyle = "b"; text = "<b>Description:</b> <font color=\"#444444\">(one or two lines)</font>"; x = 5; y = UpDownVoteSubmit_Title_Label.y + UpDownVoteSubmit_Title_Label.height + 10; width = profile.getTextWidth(text); } new GuiScrollCtrl("UpDownVoteSubmit_Description_Scroll") { profile = GuiBlueScrollProfile; useOwnProfile = true; profile.border = 1; x = 5; y = UpDownVoteSubmit_Description_Label.y + (UpDownVoteSubmit_Description_Label.profile.getTextHeight() * 1.2); width = UpDownVoteSubmit_Window.clientWidth - (x * 2); height = UpDownVoteSubmit_Window.clientHeight - y - 38; hScrollBar = "alwaysOff"; vScrollBar = "alwaysOn"; vertSizing = "height"; horizSizing = "width"; new GuiMLTextEditCtrl("UpDownVoteSubmit_Description") { profile = GuiBlueMLTextEditProfile; x = 0; y = 0; width = UpDownVoteSubmit_Description_Scroll.clientWidth - 22; horizSizing = "width"; } } new GuiButtonCtrl("UpDownVoteSubmit_Submit") { profile = GuiBlueButtonProfile; height = UpDownVoteSubmit_Window.clientHeight - UpDownVoteSubmit_Description_Scroll.height - UpDownVoteSubmit_Description_Scroll.y - 10; x = 5; y = UpDownVoteSubmit_Window.clientHeight - clientHeight - 5; width = UpDownVoteSubmit_Window.clientWidth - (x * 2); text = "Submit"; horizSizing = "width"; vertSizing = "top"; } } } } function onActionClientSide(temp.cmd, temp.data) { if (temp.cmd == "destroySubmitWindow") { UpDownVoteSubmit_Window.destroy(); this.refresh(); // TODO: improve this? } else if (temp.cmd == "enableSubmitWindow") { UpDownVoteSubmit_Title.active = true; UpDownVoteSubmit_Description.active = true; UpDownVoteSubmit_Submit.active = true; } else if (temp.cmd == "returnVotingData") { temp.topic = temp.data[0]; temp.description = temp.data[1]; temp.isAdmin = temp.data[2]; temp.posts = temp.data[3]; this.openWindow(temp.isAdmin, temp.topic, temp.description); this.showPosts(temp.isAdmin, temp.posts); } else if (temp.cmd == "returnResponses") { temp.isAdmin = temp.data[0]; temp.posts = temp.data[1]; this.showPosts(temp.isAdmin, temp.posts); } } function UpDownVoteSubmit_Submit.onAction() { temp.title = UpDownVoteSubmit_Title.text; temp.description = UpDownVoteSubmit_Description.text; UpDownVoteSubmit_Title.active = false; UpDownVoteSubmit_Description.active = false; UpDownVoteSubmit_Submit.active = false; triggerServer("gui", this.name, "submit", {temp.title, temp.description}); } function showPosts(temp.isAdmin, temp.posts) { UpDownVote_Refresh.active = true; UpDownVote_Scroll.clearControls(); temp.i = 0; for (temp.post : temp.posts) { // responseID, title, description, account (possibly null), response score, my score temp.responseID = temp.post[0]; temp.title = temp.post[1]; temp.description = temp.post[2]; temp.account = temp.post[3]; temp.totalScore = temp.post[4]; temp.myScore = temp.post[5]; new GuiControl("UpDownVote_Post_" @ (++ this.postGuiID)) { useOwnProfile = true; profile.border = true; profile.borderColor = {0, 0, 0}; profile.opaque = true; profile.fillColor = {255, 255, 255, 90}; x = 2; width = UpDownVote_Scroll.clientWidth - 21 - (x * 2); UpDownVote_Scroll.addControl(this); horizSizing = "width"; temp.postCtrl = this; this.responseID = temp.responseID; this.totalScore = temp.totalScore; this.myScore = temp.myScore; // draw voting box new GuiControl(name @ "_Votes") { x = 0; y = 8; width = 40; height = 48; new GuiShowImgCtrl(name @ "_Upvote") { temp.postCtrl.upvoteArrow = this; cursor = "pointer"; width = 20; height = 11; x = 10; y = 0; hint = "Vote Up"; image = "guiblue_upvote.png"; mode = 1; if (temp.myScore == 1) { alpha = 1; } else { alpha = 0.2; } thiso.catchEvent(this, "onMouseDown", "onUpvote"); } new GuiTextCtrl(name @ "_Counter") { temp.postCtrl.counter = this; useOwnProfile = true; profile.align = "center"; profile.fontColor = {0, 0, 0, 190}; profile.fontSize = 20; profile.fontStyle = "b"; profile.modal = false; x = 0; y = 11; width = 40; height = profile.getTextHeight() * 1.2; text = temp.totalScore; } new GuiShowImgCtrl(name @ "_Downvote") { temp.postCtrl.downvoteArrow = this; cursor = "pointer"; width = 20; height = 11; x = 10; y = 37; hint = "Vote Down"; image = "guiblue_downvote.png"; mode = 1; if (temp.myScore == (- 1)) { alpha = 1; } else { alpha = 0.2; } thiso.catchEvent(this, "onMouseDown", "onDownvote"); } } // delete button if (temp.isAdmin) { new GuiButtonCtrl(name @ "_Delete") { profile = GuiBlueButtonProfile; width = 45; height = 17; x = temp.postCtrl.width - width - 5; y = 5; horizSizing = "left"; text = "Delete"; thiso.catchEvent(this, "onAction", "onDeletePost"); } } // draw the details of the response new GuiMLTextCtrl(name @ "_Details") { profile = GuiBlueMLTextProfile; useOwnProfile = true; profile.fontColor = {0, 0, 0, 180}; profile.lineSpacing = 0; active = false; x = 40; y = 10; width = temp.postCtrl.width - x - 5; text = "<b>" @ temp.title @ (temp.account != null ? " (" @ temp.account @ ")" : "") @ "</b>\n" @ temp.description; thiso.catchEvent(this, "onResize", "onReflowPosts"); horizSizing = "width"; } } temp.i ++; } this.reflowPosts(); } function onDeletePost(temp.ctrl) { temp.post = temp.ctrl.parent; // change appearance a bit temp.ctrl.active = false; temp.post.alpha = 0.2; temp.post.deleted = true; // ask the server to delete it triggerServer("gui", this.name, "delete", temp.post.responseID); } function onUpvote(temp.ctrl) { this.vote(temp.ctrl, 1); } function onDownvote(temp.ctrl) { this.vote(temp.ctrl, (- 1)); } function onReflowPosts(temp.postText) { temp.postText.parent.height = max(63, (temp.postText.y * 2) + temp.postText.height); temp.postText.y = (temp.postText.parent.height - temp.postText.height) / 2; // center vertically this.cancelEvents("reflowAllPosts"); this.scheduleEvent(0.05, "reflowAllPosts", null); } function onReflowAllPosts() { this.reflowPosts(); } function vote(temp.arrowControl, temp.score) { temp.post = temp.arrowControl.parent.parent; if (temp.post.deleted) { return; } // update the arrows temp.post.upvoteArrow.alpha = temp.score == 1 ? 1 : 0.2; temp.post.downvoteArrow.alpha = temp.score == (- 1) ? 1 : 0.2; // remove my old vote, add my new vote temp.post.totalScore -= temp.post.myScore; temp.post.totalScore += temp.score; // update the counter and my current vote temp.post.counter.text = temp.post.totalScore; temp.post.myScore = temp.score; // tell the server about my vote triggerServer("gui", this.name, "vote", {temp.post.responseID, temp.score}); } function reflowPosts() { temp.totalY = 2; for (temp.ctrl : UpDownVote_Scroll.controls) { temp.ctrl.y = temp.totalY; temp.totalY += 2 + temp.ctrl.height; } } function UpDownVote_Refresh.onAction() { this.refresh(); } function refresh() { UpDownVote_Scroll.clearControls(); UpDownVote_Refresh.active = false; new GuiShowImgCtrl("UpDownVote_Loading") { width = 32; height = 32; image = "guiblue_loading.gif"; vertSizing = "center"; horizSizing = "center"; UpDownVote_Scroll.addControl(this); } triggerServer("gui", this.name, "getResponses"); } function UpDownVote_Topic_Text.onResize(temp.newWidth, temp.newHeight) { UpDownVote_Topic.height = temp.newHeight + (UpDownVote_Topic.y * 2); UpDownVote_Scroll.y = UpDownVote_Topic.height + UpDownVote_Topic.y + 5; UpDownVote_Scroll.height = UpDownVote_Window.clientHeight - UpDownVote_Scroll.y - 38; }