From 89b699cf8c63902d07be4568903b7a39ac45c8c5 Mon Sep 17 00:00:00 2001 From: _FR_Starfox64 Date: Fri, 18 Sep 2015 17:19:57 +0200 Subject: [PATCH] Playtest bug fix + cardsDB overhaul Fixed multiple bugs that were discovered after the first playtest and overhauled the way the cards database is update with a complete integration of the GitHub API. --- cah.json => cardsDB.json | 0 lua/autorun/sh_cah_init.lua | 3 + lua/cah/cl_cah.lua | 36 ++++- lua/cah/sh_cah.lua | 266 +++++++++++++++++++++++++++++------- lua/cah/sv_cah.lua | 58 ++++---- lua/cah/sv_hooks.lua | 9 +- lua/external/base64.lua | 37 +++++ 7 files changed, 325 insertions(+), 84 deletions(-) rename cah.json => cardsDB.json (100%) create mode 100644 lua/external/base64.lua diff --git a/cah.json b/cardsDB.json similarity index 100% rename from cah.json rename to cardsDB.json diff --git a/lua/autorun/sh_cah_init.lua b/lua/autorun/sh_cah_init.lua index e61d184..8f88d3b 100644 --- a/lua/autorun/sh_cah_init.lua +++ b/lua/autorun/sh_cah_init.lua @@ -13,10 +13,12 @@ if (SERVER) then AddCSLuaFile("external/von.lua") AddCSLuaFile("external/netstream.lua") AddCSLuaFile("external/htmlentities.lua") + AddCSLuaFile("external/base64.lua") include("external/von.lua") include("external/netstream.lua") include("external/htmlentities.lua") + include("external/base64.lua") include("cah/sv_hooks.lua") include("cah/sh_cah.lua") @@ -25,6 +27,7 @@ else include("external/von.lua") include("external/netstream.lua") include("external/htmlentities.lua") + include("external/base64.lua") include("cah/sh_cah.lua") include("cah/cl_cah.lua") diff --git a/lua/cah/cl_cah.lua b/lua/cah/cl_cah.lua index adaa162..a9abc8a 100644 --- a/lua/cah/cl_cah.lua +++ b/lua/cah/cl_cah.lua @@ -140,8 +140,14 @@ hook.Add("PostDrawOpaqueRenderables", "CAH_PostDrawOpaqueRenderables", function( -- Black Card -- if (cahGame:GetBlackCard() and cursor.r == 180) then - local flipped = cahGame:GetStatus() < CAH_DISCOVER + local mainX, mainY = TABLE_WIDTH - 1164 - CARD_WIDTH, TABLE_HEIGHT - 350 - CARD_HEIGHT + local flipped = cahGame:GetStatus() == CAH_DISCOVER CAH:DrawCard(cahGame:GetBlackCard(), 1164, 350, flipped) + CAH:AddClickPos(mainX, mainY, CARD_WIDTH, CARD_HEIGHT, IN_ATTACK2, "preview", {cardID = cahGame:GetBlackCard(), flipped = flipped}) + + if (ply == cahGame:GetCzar() and cahGame:GetStatus() == CAH_DISCOVER) then + CAH:AddClickPos(mainX, mainY, CARD_WIDTH, CARD_HEIGHT, IN_ATTACK, "discover") + end end cam.End3D2D() @@ -185,9 +191,13 @@ hook.Add("PostDrawOpaqueRenderables", "CAH_PostDrawOpaqueRenderables", function( -- Black Card -- if (cahGame:GetBlackCard() and cursor.r == 0) then - local flipped = cahGame:GetStatus() < CAH_DISCOVER + local flipped = cahGame:GetStatus() == CAH_DISCOVER CAH:DrawCard(cahGame:GetBlackCard(), 1164, 350, flipped) CAH:AddClickPos(1164, 350, CARD_WIDTH, CARD_HEIGHT, IN_ATTACK2, "preview", {cardID = cahGame:GetBlackCard(), flipped = flipped}) + + if (ply == cahGame:GetCzar() and cahGame:GetStatus() == CAH_DISCOVER) then + CAH:AddClickPos(1164, 350, CARD_WIDTH, CARD_HEIGHT, IN_ATTACK, "discover") + end end -- Cursor -- @@ -346,6 +356,8 @@ function CAH:CheckClickPos( cursor ) if (cursor.x >= clickPos.x and cursor.y >= clickPos.y and cursor.x <= clickPos.x + clickPos.w and cursor.y <= clickPos.y + clickPos.h) then if (clickPos.action == "draw") then netstream.Start("CAH_DrawCard", clickPos.arg) + elseif (clickPos.action == "discover") then + netstream.Start("CAH_DiscoverCard") elseif (clickPos.action == "choose") then netstream.Start("CAH_ChooseCard", clickPos.arg) elseif (clickPos.action == "quit") then @@ -458,4 +470,24 @@ end) netstream.Hook("CAH_Notification", function( data ) CAH:Notify(data.m, data.i, data.ns) +end) + +netstream.Hook("CAH_LoadCards", function( sha ) + if (file.Exists("cah/"..(sha == "default" and CAH.CurrentRelease or sha)..".txt", "DATA")) then + if (sha == "default") then + CAH:LoadCards(true) + else + CAH:LoadCards(false, sha) + end + else + sha = sha == "default" and CAH.CurrentRelease or sha + + CAH:DownloadCards(sha, function( sha, success ) + if (sha == CAH.CurrentRelease) then + CAH:LoadCards(true) + else + CAH:LoadCards(false, sha) + end + end) + end end) \ No newline at end of file diff --git a/lua/cah/sh_cah.lua b/lua/cah/sh_cah.lua index 764d086..d050557 100644 --- a/lua/cah/sh_cah.lua +++ b/lua/cah/sh_cah.lua @@ -4,8 +4,11 @@ CAH.CVAR = CAH.CVAR or {} CAH.Config = CAH.Config or {} CAH.expansions = CAH.expansions or {"Base"} CAH.Ready = CAH.Ready or false +CAH.SHA = CAH.SHA or false -CAH.CardsURL = "https://raw.githubusercontent.com/Starfox64/cards-against-humanity/master/cah.json" +CAH.CardsDownloadURL = "https://api.github.com/repos/Starfox64/cards-against-humanity/git/blobs/" +CAH.LatestCardsDB = "https://api.github.com/repos/Starfox64/cards-against-humanity/git/trees/master" +CAH.CardsBackupURL = "https://raw.githubusercontent.com/Starfox64/cards-against-humanity/{version}/cardsDB.json" CAH.LatestReleaseURL = "https://api.github.com/repos/Starfox64/cards-against-humanity/releases/latest" CAH.CurrentRelease = "0.0.0" @@ -24,41 +27,153 @@ CAH.Config.expansions = CAH.Config.expansions or {Base = true} -- CAH Utils -- -function CAH:LoadCards() - http.Fetch(CAH.CardsURL, +function CAH:LoadCards( useDefault, sha ) + local cards + + if (useDefault) then + MsgC(Color(251, 184, 41), "[CAH] Loading the default cards database...\n") + if (SERVER) then + cards = file.Read("cardsDB.json", "GAME") + else + if (file.Exists("cah/"..self.CurrentRelease..".txt")) then + cards = file.Exists("cah/"..self.CurrentRelease..".txt", "DATA") + else + MsgC(Color(200, 70, 70), "[CAH] Default cards database not found! (The download probably failed, you will not be able to play)\n") + end + end + elseif (sha or file.Exists("cah/latestDB.txt", "DATA")) then + MsgC(Color(251, 184, 41), "[CAH] Loading the latest cards database...\n") + + sha = sha or file.Read("cah/latestDB.txt", "DATA") + + if (file.Exists("cah/"..sha..".txt", "DATA")) then + cards = file.Read("cah/"..sha..".txt", "DATA") + else + MsgC(Color(200, 70, 70), "[CAH] Latest cards database not found! (Deleting latestDB.txt)\n") + + file.Delete("cah/latestDB.txt") + self:LoadCards(true) + return + end + else + self:LoadCards(true) + return + end + + cards = util.JSONToTable(cards) + + if (cards) then + self.Cards = {} + + for _, card in pairs(cards) do + if (card.numAnswers > 2) then continue end -- The game isn't compatible with 3 blanks black cards. (Soon™) + + local text = htmlentities.decode(card.text) + + self.Cards[card.id] = { + cardType = card.cardType, + text = text, + numAnswers = card.numAnswers, + expansion = card.expansion + } + setmetatable(self.Cards[card.id], self.cardMeta) + + if (not table.HasValue(self.expansions, card.expansion)) then + table.insert(self.expansions, card.expansion) + end + end + + MsgC(Color(25, 200, 25), "[CAH] Loaded "..table.Count(self.Cards).." cards.\n") + self.SHA = sha or "default" + self.Ready = true + + if (SERVER) then + netstream.Start(nil, "CAH_LoadCards", sha) + end + else + MsgC(Color(200, 70, 70), "[CAH] Failed to load cards! (JSON Parsing Error)\n") + + if (sha) then + file.Delete("cah/"..sha..".txt") + self:LoadCards(true) + end + end +end + +function CAH:DownloadCards( sha, callback ) + MsgC(Color(251, 184, 41), "[CAH] Downloading the latest cards database...\n") + + local downloadURL = string.len(sha) > 8 and self.CardsDownloadURL..sha or string.Replace(self.CardsBackupURL, "{version}", self.CurrentRelease) + + http.Fetch(downloadURL, function( body, len, headers, code ) - local cards = body - cards = util.JSONToTable(cards) + local response = util.JSONToTable(body) - if (cards) then - self.Cards = {} + if (response and response.content) then + local cardsDB = baseSixFour.decode(response.content) + file.Write("cah/"..sha..".txt", cardsDB) - for _, card in pairs(cards) do - if (card.numAnswers > 2) then continue end -- The game isn't compatible with 3 blanks black cards. (Soon™) + if (callback) then + callback(sha, true) + end - local text = htmlentities.decode(card.text) + MsgC(Color(25, 200, 25), "[CAH] Cards database downloaded. ("..sha..".txt)\n") + else + MsgC(Color(200, 70, 70), "[CAH] Failed to download the latest cards database! (Invalid Response from GitHub)\n") - self.Cards[card.id] = { - cardType = card.cardType, - text = text, - numAnswers = card.numAnswers, - expansion = card.expansion - } - setmetatable(self.Cards[card.id], self.cardMeta) + if (callback) then + callback(sha, false) + end + end + end, + function( err ) + MsgC(Color(200, 70, 70), "[CAH] Failed to download the latest cards database! (http.Fetch: "..err..")\n") + + if (callback) then + callback(sha, false) + end + end + ) +end + +function CAH:CheckDBUpdate() + http.Fetch(self.LatestCardsDB, + function( body, len, headers, code ) + local response = util.JSONToTable(body) - if (not table.HasValue(self.expansions, card.expansion)) then - table.insert(self.expansions, card.expansion) + if (response and response.tree) then + local sha + for _, object in pairs(response.tree) do + if (object.path == "cardsDB.json") then + sha = object.sha + break end end - MsgC(Color(25, 200, 25), "[CAH] Loaded "..table.Count(self.Cards).." cards.\n") - self.Ready = true + if (sha) then + if (file.Exists("cah/"..sha..".txt")) then + MsgC(Color(25, 200, 25), "[CAH] Cards database up to date. ("..sha..".txt)\n") + self:LoadCards(false, sha) + else + MsgC(Color(251, 184, 41), "[CAH] A new cards database is available.\n") + self:DownloadCards(sha, function( sha, success ) + if (success) then + file.Write("cah/latestDB.txt", sha) + self:LoadCards(false, sha) + else + self:LoadCards() + end + end) + end + else + MsgC(Color(200, 70, 70), "[CAH] Cannot check for updates, GitHub API response incorrect!\n") + end else - MsgC(Color(200, 70, 70), "[CAH] Failed to load cards! (JSON Parsing Error)\n") + MsgC(Color(200, 70, 70), "[CAH] Cannot check for updates, GitHub API response incorrect!\n") end end, function( err ) - MsgC(Color(200, 70, 70), "[CAH] Failed to load cards! (http.Fetch: "..err..")\n") + MsgC(Color(200, 70, 70), "[CAH] Cannot check for updates, GitHub API unreachable! (http.Fetch: "..err..")\n") end ) end @@ -178,7 +293,8 @@ end if (SERVER) then function CAH.gameMeta:RemovePlayer( client ) - table.RemoveByValue(self.players, client) + local seatID = table.KeyFromValue(self.players, client) + self.players[seatID] = nil client:ExitVehicle() client.CAH = { @@ -218,6 +334,12 @@ if (SERVER) then for seatID, client in pairs(self:GetPlayers()) do local missingCards = 5 - #client:GetCards() + -- Generates a new white pool if it is empty + if (#self.wPool == 0) then + local wPool, bPool = CAH:GeneratePool() + self.wPool = wPool + end + if (missingCards != 0) then for i=1, missingCards do local poolIndex = math.random(1, #self.wPool) @@ -230,18 +352,58 @@ if (SERVER) then function CAH.gameMeta:NewRound() self:SetStatus(CAH_DISCOVER) + self:SetTimeLeft(CAH.Config.chooseTime) self:GenerateCards() local newCzar = self:FindNewCzar() + if (#self.bPool == 0) then + self:SetStatus(CAH_END) + self:SetTimeLeft(5) + + local winners, maxPoints = {}, 0 + for _, client in pairs(self:GetPlayers()) do + if (client:GetCAHPoints() > maxPoints) then + winners = {client} + maxPoints = client:GetCAHPoints() + elseif (client:GetCAHPoints() == maxPoints) then + table.insert(winners, client) + end + end + + local notificationText = "You ran out of black cards! The winner"..(#winners > 1 and " is " or "s are ") + if (#winners > 1) then + for k, winner in ipairs(winners) do + if (k == 1) then + notificationText = notificationText..winner:Name().."." + elseif (k == #winners) then + notificationText = notificationText.." and "..winner:Name() + else + notificationText = notificationText..", "..winner:Name() + end + end + else + notificationText = notificationText..winner:Name().."." + end + + CAH:Notify(notificationText, "cah/win64.png", cahGame:GetPlayers()) + + self:Send(true) + return + end + local poolIndex = math.random(1, #self.bPool) self:SetBlackCard(self.bPool[poolIndex]) table.remove(self.bPool, poolIndex) self:Send(true) - for _, client in pairs(self:GetPlayer()) do - CAH:Notify("A new round is starting, the Card Czar is "..newCzar:Name(), cahGame:GetPlayers()) + for _, client in pairs(self:GetPlayers()) do + if (client != newCzar) then + CAH:Notify("A new round is starting, the Card Czar is "..newCzar:Name(), client) + end end + + CAH:Notify("You are the new Card Czar! Reveal the black card.", newCzar) end function CAH.gameMeta:SetTimeLeft( timeLeft ) @@ -253,24 +415,27 @@ if (SERVER) then end function CAH.gameMeta:FindNewCzar() - local newCzar, nextIsCzar, reProcess + local newCzar, czarID, lastID, nextIsCzar for seatID, client in pairs(self:GetPlayers()) do - if (client == self) then - nextIsCzar = true + lastID = seatID - if (seatID == 4) then - reProcess = true - end + if (self:GetCzar() == nil) then + newCzar = client + break + elseif (client == self:GetCzar()) then + czarID = seatID + nextIsCzar = true elseif (nextIsCzar) then newCzar = client break end end - if (reProcess) then + if (lastID == czarID) then for _, client in pairs(self:GetPlayers()) do newCzar = client + break end end @@ -439,6 +604,18 @@ if (SERVER) then end end + function CAH.playerMeta:Discover() + local cahGame = self:GetCAHGame() + if (self == cahGame:GetCardCzar() and cahGame:GetStatus() == CAH_DISCOVER) then + cahGame:SetStatus(CAH_ANSWER) + cahGame:SetTimeLeft(CAH.Config.chooseTime) + + for _, client in pairs(cahGame:GetPlayers()) do + CAH:Notify("The Card Czar revealed the black card.", client) + end + end + end + function CAH.playerMeta:SetSelectedCard( isSecond, cardID ) if not (self.CAH) then return end local index = isSecond and 2 or 1 @@ -461,27 +638,10 @@ end if (not CAH.Ready) then if (SERVER) then - CAH:LoadCards() + CAH:CheckDBUpdate() - -- Listen servers don't like calling http.Fetch on 2 realms at the same time so we add a delay. - timer.Simple(1, function() - CAH:CheckUpdate() - end) - else - timer.Simple(1.5, function() + timer.Simple(5, function() -- The needs to be a delay between http request otherwise they fail. CAH:CheckUpdate() end) - timer.Simple(2, function() - CAH:LoadCards() - end) - end -end - --- Card loading auto-retry -- -CAH.nextTry = 0 -hook.Add("Think", "CAH_LoadRetry", function() - if (not CAH.Ready and CurTime() > CAH.nextTry) then - CAH:LoadCards() - CAH.nextTry = CurTime() + 15 end -end) +end \ No newline at end of file diff --git a/lua/cah/sv_cah.lua b/lua/cah/sv_cah.lua index b23d994..788f7a0 100644 --- a/lua/cah/sv_cah.lua +++ b/lua/cah/sv_cah.lua @@ -4,35 +4,28 @@ resource.AddFile("materials/models/props_interiors/table_picnic.vmt") function CAH:AddTable( cahTable ) - if self.Ready then - local wPool, bPool = CAH:GeneratePool(self.expansions) - - local cahGame = { - table = cahTable:EntIndex(), - players = {}, - czar = nil, - black = nil, - wPool = wPool, - bPool = bPool, - endTime = math.huge, - status = CAH_IDLE - } - setmetatable(cahGame, CAH.gameMeta) - - self.Games[cahTable:EntIndex()] = cahGame - cahGame:Send() - else - -- Change icon - CAH:Notify("Cannot create CAH Table, cards not loaded!") - end + local cahGame = { + table = cahTable:EntIndex(), + players = {}, + czar = nil, + black = nil, + wPool = {}, + bPool = {}, + endTime = math.huge, + status = CAH_IDLE + } + setmetatable(cahGame, CAH.gameMeta) + + self.Games[cahTable:EntIndex()] = cahGame + cahGame:Send() end -function CAH:GeneratePool( expansions ) +function CAH:GeneratePool() local wPool, bPool = {}, {} - for cardID, card in pairs(CAH:GetCards()) do - for k, extension in pairs(expansions) do - if (card:IsExpansion(extension)) then + for cardID, card in pairs(self:GetCards()) do + for k, expansion in pairs(self.expansions) do + if (self.Config.expansions[expansion] and card:IsExpansion(expansion)) then if (card:IsAnswer()) then table.insert(wPool, cardID) else @@ -60,15 +53,15 @@ function CAH:SaveConfig() end local configData = von.serialize(self.Config) - file.Write("cah_config.txt", configData) + file.Write("cah/config.txt", configData) MsgC(Color(25, 200, 25), "[CAH] Server config saved.\n") end end function CAH:LoadConfig() - if (file.Exists("cah_config.txt", "DATA")) then - local success, configData = pcall(von.deserialize, file.Read("cah_config.txt", "DATA")) + if (file.Exists("cah/config.txt", "DATA")) then + local success, configData = pcall(von.deserialize, file.Read("cah/config.txt", "DATA")) if (success) then -- Adds expansions to the config table if they aren't already. @@ -107,6 +100,10 @@ netstream.Hook("CAH_Quit", function( client ) end end) +netstream.Hook("CAH_DiscoverCard", function( client ) + client:Discover() +end) + netstream.Hook("CAH_DrawCard", function( client, cardID ) client:DrawCard(cardID) end) @@ -121,6 +118,11 @@ end) netstream.Hook("CAH_SaveConfig", function( client, config ) if (client:IsSuperAdmin()) then + if (#table.KeysFromValue(config.expansions, true) == 0) then + CAH:Notify("The config you are trying to save does not have any active expansions!", client) + return + end + CAH.Config = config CAH:SaveConfig() diff --git a/lua/cah/sv_hooks.lua b/lua/cah/sv_hooks.lua index 6ca328d..76085b1 100644 --- a/lua/cah/sv_hooks.lua +++ b/lua/cah/sv_hooks.lua @@ -22,6 +22,10 @@ end) hook.Add("PlayerInitialSpawn", "CAH_PlayerInitialSpawn", function( client ) netstream.Start(client, "CAH_RefreshConfig", CAH.Config) + if (CAH.Ready) then + netstream.Start(client, "CAH_LoadCards", CAH.SHA) + end + for k, cahGame in pairs(CAH:GetGames()) do cahGame:Send(true, client) end @@ -37,7 +41,7 @@ hook.Add("Think", "CAH_Think", function() cahGame:SetStatus(CAH_IDLE) cahGame:SetTimeLeft(math.huge) - for _, client in pairs(cahGame:GetPlayer()) do + for _, client in pairs(cahGame:GetPlayers()) do client:SetCAHPoints(0) client.CAH.cards = {} client.CAH.selected = {} @@ -52,6 +56,9 @@ hook.Add("Think", "CAH_Think", function() CAH:Notify("The game will start in "..CAH.Config.startTime.." seconds.", cahGame:GetPlayers()) elseif ((status == CAH_IDLE or status == CAH_DISCOVER) and timeLeft <= 0) then -- Game starting / Czar inactive. + local wPool, bPool = CAH:GeneratePool() + cahGame.wPool, cahGame.bPool = wPool, bPool + cahGame:NewRound() elseif (status == CAH_ANSWER) then -- Players are answering to / completing the black card. local playersReady, nextPhase = 0, false diff --git a/lua/external/base64.lua b/lua/external/base64.lua new file mode 100644 index 0000000..24681e8 --- /dev/null +++ b/lua/external/base64.lua @@ -0,0 +1,37 @@ +-- Lua 5.1+ base64 v3.0 (c) 2009 by Alex Kloss +-- licensed under the terms of the LGPL2 +-- edited by Starfox64 to be compatible with cards-against-humanity + +-- character table string +local b='ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/' +baseSixFour = {} + +-- encoding +function baseSixFour.encode(data) + return ((data:gsub('.', function(x) + local r,b='',x:byte() + for i=8,1,-1 do r=r..(b%2^i-b%2^(i-1)>0 and '1' or '0') end + return r; + end)..'0000'):gsub('%d%d%d?%d?%d?%d?', function(x) + if (#x < 6) then return '' end + local c=0 + for i=1,6 do c=c+(x:sub(i,i)=='1' and 2^(6-i) or 0) end + return b:sub(c+1,c+1) + end)..({ '', '==', '=' })[#data%3+1]) +end + +-- decoding +function baseSixFour.decode(data) + data = string.gsub(data, '[^'..b..'=]', '') + return (data:gsub('.', function(x) + if (x == '=') then return '' end + local r,f='',(b:find(x)-1) + for i=6,1,-1 do r=r..(f%2^i-f%2^(i-1)>0 and '1' or '0') end + return r; + end):gsub('%d%d%d?%d?%d?%d?%d?%d?', function(x) + if (#x ~= 8) then return '' end + local c=0 + for i=1,8 do c=c+(x:sub(i,i)=='1' and 2^(8-i) or 0) end + return string.char(c) + end)) +end \ No newline at end of file