GameAnalyticsSDK.rbxmx 123 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712271327142715271627172718271927202721272227232724272527262727272827292730273127322733273427352736273727382739274027412742274327442745274627472748274927502751275227532754275527562757275827592760276127622763276427652766276727682769277027712772277327742775277627772778277927802781278227832784278527862787278827892790279127922793279427952796279727982799280028012802280328042805280628072808280928102811281228132814281528162817281828192820282128222823282428252826282728282829283028312832283328342835283628372838283928402841284228432844284528462847284828492850285128522853285428552856285728582859286028612862286328642865286628672868286928702871287228732874287528762877287828792880288128822883288428852886288728882889289028912892289328942895289628972898289929002901290229032904290529062907290829092910291129122913291429152916291729182919292029212922292329242925292629272928292929302931293229332934293529362937293829392940294129422943294429452946294729482949295029512952295329542955295629572958295929602961296229632964296529662967296829692970297129722973297429752976297729782979298029812982298329842985298629872988298929902991299229932994299529962997299829993000300130023003300430053006300730083009301030113012301330143015301630173018301930203021302230233024302530263027302830293030303130323033303430353036303730383039304030413042304330443045304630473048304930503051305230533054305530563057305830593060306130623063306430653066306730683069307030713072307330743075307630773078307930803081308230833084308530863087308830893090309130923093309430953096309730983099310031013102310331043105310631073108310931103111311231133114311531163117311831193120312131223123312431253126312731283129313031313132313331343135313631373138313931403141314231433144314531463147314831493150315131523153315431553156315731583159316031613162316331643165316631673168316931703171317231733174317531763177317831793180318131823183318431853186318731883189319031913192319331943195319631973198319932003201320232033204320532063207320832093210321132123213321432153216321732183219322032213222322332243225322632273228322932303231323232333234323532363237323832393240324132423243324432453246324732483249325032513252325332543255325632573258325932603261326232633264326532663267326832693270327132723273327432753276327732783279328032813282328332843285328632873288328932903291329232933294329532963297329832993300330133023303330433053306330733083309331033113312331333143315331633173318331933203321332233233324332533263327332833293330333133323333333433353336333733383339334033413342334333443345334633473348334933503351335233533354335533563357335833593360336133623363336433653366336733683369337033713372337333743375337633773378337933803381338233833384338533863387338833893390339133923393339433953396339733983399340034013402340334043405340634073408340934103411
  1. <roblox xmlns:xmime="http://www.w3.org/2005/05/xmlmime" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:noNamespaceSchemaLocation="http://www.roblox.com/roblox.xsd" version="4">
  2. <External>null</External>
  3. <External>nil</External>
  4. <Item class="ModuleScript" referent="RBXf309a3c5773d40eaae7007cb4478e182">
  5. <Properties>
  6. <Content name="LinkedSource"><null></null></Content>
  7. <string name="Name">GameAnalyticsSDK</string>
  8. <string name="ScriptGuid">{7431f9d9-7538-46e6-a8c6-45d71ee7e57a}</string>
  9. <ProtectedString name="Source"><![CDATA[local module = {}
  10. return module
  11. ]]></ProtectedString>
  12. <BinaryString name="Tags"></BinaryString>
  13. </Properties>
  14. <Item class="Script" referent="RBXa82cd927306847a28e2acd9083a050b2">
  15. <Properties>
  16. <bool name="Disabled">false</bool>
  17. <Content name="LinkedSource"><null></null></Content>
  18. <string name="Name">GameAnalyticsServer</string>
  19. <string name="ScriptGuid">{4efacb9d-efec-45dc-892a-1ddedb9a9ae3}</string>
  20. <ProtectedString name="Source"><![CDATA[--[[
  21. NOTE: This script should be in game.ServerScriptService
  22. --]]
  23. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  24. local ServerStorage = game:GetService("ServerStorage")
  25. --Validate
  26. if not script:IsDescendantOf(game:GetService("ServerScriptService")) then
  27. error("GameAnalytics: Disabled server. GameAnalyticsServer has to be located in game.ServerScriptService.")
  28. return
  29. end
  30. -- if not ReplicatedStorage:FindFirstChild("GameAnalyticsSendMessage") then
  31. -- --Create
  32. -- local f = Instance.new("RemoteEvent")
  33. -- f.Name = "GameAnalyticsSendMessage"
  34. -- f.Parent = ReplicatedStorage
  35. -- end
  36. if not ReplicatedStorage:FindFirstChild("GameAnalyticsCommandCenter") then
  37. --Create
  38. local f = Instance.new("RemoteEvent")
  39. f.Name = "GameAnalyticsCommandCenter"
  40. f.Parent = ReplicatedStorage
  41. end
  42. if not ReplicatedStorage:FindFirstChild("OnPlayerReadyEvent") then
  43. --Create
  44. local f = Instance.new("BindableEvent")
  45. f.Name = "OnPlayerReadyEvent"
  46. f.Parent = ReplicatedStorage
  47. end
  48. --Modules
  49. local GameAnalytics = require(ServerStorage.GameAnalytics)
  50. local store = require(ServerStorage.GameAnalytics.Store)
  51. local state = require(ServerStorage.GameAnalytics.State)
  52. local LS = game:GetService("LogService")
  53. local MKT = game:GetService("MarketplaceService")
  54. local Players = game:GetService("Players")
  55. local ProductCache = {}
  56. local ONE_HOUR_IN_SECONDS = 3600
  57. local MaxErrorsPerHour = 10
  58. local ErrorDS = {}
  59. local errorCountCache = {}
  60. local errorCountCacheKeys = {}
  61. spawn(function()
  62. local currentHour = math.floor(os.time()/3600)
  63. ErrorDS = store:GetErrorDataStore(currentHour)
  64. while wait(ONE_HOUR_IN_SECONDS) do
  65. currentHour = math.floor(os.time()/3600)
  66. ErrorDS = store:GetErrorDataStore(currentHour)
  67. errorCountCache = {}
  68. errorCountCacheKeys = {}
  69. end
  70. end)
  71. spawn(function()
  72. while wait(store.AutoSaveData) do
  73. for _, key in pairs(errorCountCacheKeys) do
  74. local errorCount = errorCountCache[key]
  75. local step = errorCount.currentCount - errorCount.countInDS
  76. errorCountCache[key].countInDS = store:IncrementErrorCount(ErrorDS, key, step)
  77. errorCountCache[key].currentCount = errorCountCache[key].countInDS
  78. end
  79. end
  80. end)
  81. --Error Logging
  82. LS.MessageOut:Connect(function(message, messageType)
  83. --Validate
  84. if not state.ReportErrors then
  85. return
  86. end
  87. if messageType ~= Enum.MessageType.MessageError then
  88. return
  89. end
  90. local m = message
  91. if #m > 8192 then
  92. m = string.sub(m, 1, 8192)
  93. end
  94. local key = m
  95. if #key > 50 then
  96. key = string.sub(key, 1, 50)
  97. end
  98. if errorCountCache[key] == nil then
  99. errorCountCacheKeys[#errorCountCacheKeys + 1] = key
  100. errorCountCache[key] = {}
  101. errorCountCache[key].countInDS = 0
  102. errorCountCache[key].currentCount = 0
  103. end
  104. -- don't report error if limit has been exceeded
  105. if errorCountCache[key].currentCount > MaxErrorsPerHour then
  106. return
  107. end
  108. --Report (use nil for playerId as real player id is not available)
  109. GameAnalytics:addErrorEvent(nil, {
  110. severity = GameAnalytics.EGAErrorSeverity.error,
  111. message = m
  112. })
  113. -- increment error count
  114. errorCountCache[key].currentCount = errorCountCache[key].currentCount + 1
  115. end)
  116. --Record Gamepasses. NOTE: This doesn't record gamepass purchases if a player buys it from the website
  117. MKT.PromptGamePassPurchaseFinished:Connect(function(Player, ID, Purchased)
  118. --Validate
  119. if not state.AutomaticSendBusinessEvents then
  120. return
  121. end
  122. --Validate
  123. if not Purchased then return end
  124. --Variables
  125. local GamepassInfo = ProductCache[ID]
  126. --Cache
  127. if not GamepassInfo then
  128. --Get
  129. GamepassInfo = MKT:GetProductInfo(ID, Enum.InfoType.GamePass)
  130. ProductCache[ID] = GamepassInfo
  131. end
  132. GameAnalytics:addBusinessEvent(Player.UserId, {
  133. amount = GamepassInfo.PriceInRobux,
  134. itemType = "Gamepass",
  135. itemId = GameAnalytics:filterForBusinessEvent(GamepassInfo.Name)
  136. })
  137. end)
  138. -- Fire for players already in game
  139. for _, Player in pairs(Players:GetPlayers()) do
  140. GameAnalytics:PlayerJoined(Player)
  141. end
  142. -- New Players
  143. Players.PlayerAdded:Connect(function(Player)
  144. local joinData = Player:GetJoinData()
  145. local teleportData = joinData.TeleportData
  146. local gaData = nil
  147. if teleportData then
  148. gaData = teleportData.gameanalyticsData and teleportData.gameanalyticsData[tostring(Player.UserId)]
  149. end
  150. GameAnalytics:PlayerJoined(Player, gaData)
  151. end)
  152. -- Players leaving
  153. Players.PlayerRemoving:Connect(function(Player)
  154. GameAnalytics:PlayerRemoved(Player)
  155. end)
  156. ]]></ProtectedString>
  157. <BinaryString name="Tags"></BinaryString>
  158. </Properties>
  159. </Item>
  160. <Item class="ModuleScript" referent="RBXa82cd927306847a28e2acd9083a990b2">
  161. <Properties>
  162. <bool name="Disabled">false</bool>
  163. <Content name="LinkedSource"><null></null></Content>
  164. <string name="Name">Postie</string>
  165. <string name="ScriptGuid">{4efacb9d-efec-45dc-892a-1ddedb9a9ae9}</string>
  166. <ProtectedString name="Source"><![CDATA[--[[
  167. Postie - An elegant alternative to RemoteFunctions with a timeout
  168. By Dandystan
  169. INTERFACE:
  170. Function bool, Tuple Postie.InvokeClient(string id, Instance<Player> player, number timeout, Tuple args) [server_side] [yields]
  171. Invoke player with arguments args. Invocation identified by id. Yield until timeout (given in seconds) is reached
  172. and return false, or a signal is received back from the client and return true plus any values received from the
  173. client.
  174. Function bool, Tuple Postie.InvokeServer(string id, number timeout, Tuple args) [client_side] [yields]
  175. Invoke the server with arguments args. Invocation identified by id. Yield until timeout (given in seconds) is
  176. reached and return false, or a signal is received back from the server and return true plus any values received
  177. from the server.
  178. Function void Postie.SetCallback(string id, func callback)
  179. Set the callback that is invoked when an invocation identified by id is sent. Arguments passed with the invocation
  180. are passed to the callback. If on the server, the player who invoked is implicitly received as the first argument.
  181. Function func? Postie.GetCallback(string id)
  182. Return the callback associated with id.
  183. EXAMPLE 1 - server to client:
  184. Server:
  185. local postie = require(postieObj)
  186. -- arbritary func to be called whenever
  187. local function getTrampolinesOnScreen(player)
  188. -- get objects on screen from player
  189. local isSuccessful, trampolines = postie.InvokeClient("RequestObjectsOnScreen", player, 5, "Trampolines")
  190. -- check for timeout
  191. if isSuccessful then
  192. -- validate returned data type for security purposes
  193. if typeof(trampolines) == "number" then
  194. return true, trampolines
  195. end
  196. end
  197. return false
  198. end
  199. Client:
  200. local postie = require(postieObj)
  201. postie.SetCallback("RequestObjectsOnScreen", function(objectType)
  202. return objectsOnScreen[objectType]
  203. end)
  204. EXAMPLE 2 - client to server:
  205. Server:
  206. local postie = require(postieObj)
  207. postie.SetCallback("GetCoins", function(player)
  208. return playerCoins[player]
  209. end)
  210. Client:
  211. local postie = require(postieObj)
  212. local function getCoins()
  213. return postie.InvokeServer("GetCoins", 5)
  214. end
  215. --]]
  216. -- services:
  217. local runService = game:GetService("RunService")
  218. if not script:FindFirstChild("Sent") then
  219. --Create
  220. local f = Instance.new("RemoteEvent")
  221. f.Name = "Sent"
  222. f.Parent = script
  223. end
  224. if not script:FindFirstChild("Received") then
  225. --Create
  226. local f = Instance.new("RemoteEvent")
  227. f.Name = "Received"
  228. f.Parent = script
  229. end
  230. -- variables:
  231. local sent = script.Sent
  232. local received = script.Received
  233. local isServer = runService:IsServer()
  234. local idCallbacks = {}
  235. local listeners = {}
  236. local signalVersion = 1
  237. -- Postie:
  238. local postie = {}
  239. function postie.InvokeClient(id, player, timeout, ...)
  240. assert(isServer, "Postie.InvokeClient can only be called from the server")
  241. assert(typeof(id) == "string", "bad argument #1 to Postie.InvokeClient, expects string")
  242. assert(typeof(player) == "Instance" and player:IsA("Player"), "bad argument #2 to Postie.InvokeClient, expects Instance<Player>")
  243. assert(typeof(timeout) == "number", "bad argument #3 to Postie.InvokeClient, expects number")
  244. -- define variables
  245. local thread = coroutine.running()
  246. local isResumed = false
  247. local pos = #listeners + 1
  248. -- get signal version
  249. local version = signalVersion
  250. signalVersion = signalVersion + 1
  251. -- await signal from client
  252. listeners[pos] = function(playerWhoFired, versionOfSignal, ...)
  253. if not (playerWhoFired == player and versionOfSignal == version) then return end
  254. isResumed = true
  255. table.remove(listeners, pos)
  256. coroutine.resume(thread, true, ...)
  257. return true
  258. end
  259. -- await timeout
  260. coroutine.wrap(function()
  261. wait(timeout)
  262. if isResumed then return end
  263. table.remove(listeners, pos)
  264. coroutine.resume(thread, false)
  265. end)()
  266. -- send signal
  267. sent:FireClient(player, id, version, ...)
  268. return coroutine.yield()
  269. end
  270. function postie.InvokeServer(id, timeout, ...)
  271. assert(not isServer, "Postie.InvokeServer can only be called from the client")
  272. assert(typeof(id) == "string", "bad argument #1 to Postie.InvokeServer, expects string")
  273. assert(typeof(timeout) == "number", "bad argument #2 to Postie.InvokeServer, expects number")
  274. -- define variables
  275. local thread = coroutine.running()
  276. local isResumed = false
  277. local pos = #listeners + 1
  278. -- get signal version
  279. local version = signalVersion
  280. signalVersion = signalVersion + 1
  281. -- await signal from client
  282. listeners[pos] = function(versionOfSignal, ...)
  283. if versionOfSignal ~= id then return end
  284. isResumed = true
  285. table.remove(listeners, pos)
  286. coroutine.resume(thread, true, ...)
  287. return true
  288. end
  289. -- await timeout
  290. coroutine.wrap(function()
  291. wait(timeout)
  292. if isResumed then return end
  293. table.remove(listeners, pos)
  294. coroutine.resume(thread, false)
  295. end)()
  296. -- send signal
  297. sent:FireServer(id, version, ...)
  298. return coroutine.yield()
  299. end
  300. function postie.SetCallback(id, callback)
  301. assert(typeof(id) == "string", "bad argument #1 to Postie.SetCallback, expects string")
  302. assert(typeof(callback) == "function", "bad argument #2 to Postie.SetCallback, expects func")
  303. idCallbacks[id] = callback
  304. end
  305. function postie.GetCallback(id)
  306. assert(typeof(id) == "string", "bad argument #1 to Postie.GetCallback, expects string")
  307. return idCallbacks[id]
  308. end
  309. -- main:
  310. -- handle signals
  311. if isServer then
  312. -- handle received
  313. received.OnServerEvent:Connect(function(...)
  314. for _, listener in ipairs(listeners) do
  315. if listener(...) then return end
  316. end
  317. end)
  318. -- handle sent
  319. sent.OnServerEvent:Connect(function(player, id, version, ...)
  320. local callback = idCallbacks[id]
  321. received:FireClient(player, version, callback and callback(player, ...))
  322. end)
  323. else
  324. -- handle received
  325. received.OnClientEvent:Connect(function(...)
  326. for _, listener in ipairs(listeners) do
  327. if listener(...) then return end
  328. end
  329. end)
  330. -- handle sent
  331. sent.OnClientEvent:Connect(function(id, version, ...)
  332. local callback = idCallbacks[id]
  333. received:FireServer(version, callback and callback(...))
  334. end)
  335. end
  336. return postie
  337. ]]></ProtectedString>
  338. <BinaryString name="Tags"></BinaryString>
  339. </Properties>
  340. </Item>
  341. <Item class="Script" referent="RBXa82cd927306847a28e2acd9083a150a5">
  342. <Properties>
  343. <bool name="Disabled">false</bool>
  344. <Content name="LinkedSource"><null></null></Content>
  345. <string name="Name">GameAnalyticsServerInitUsingSettings</string>
  346. <string name="ScriptGuid">{4efacb9d-efec-45dc-892a-1ddedb9a9ae3}</string>
  347. <ProtectedString name="Source"><![CDATA[--[[
  348. NOTE: This script should be in game.ServerScriptService
  349. --]]
  350. local ServerStorage = game:GetService("ServerStorage")
  351. --Validate
  352. if not script:IsDescendantOf(game:GetService("ServerScriptService")) then
  353. error("GameAnalytics: GameAnalyticsServerInitUsingSettings has to be located in game.ServerScriptService.")
  354. return
  355. end
  356. --Modules
  357. local GameAnalytics = require(ServerStorage.GameAnalytics)
  358. local Settings = require(ServerStorage.GameAnalytics.Settings)
  359. local Players = game:GetService("Players")
  360. if Settings.EnableInfoLog then
  361. GameAnalytics:setEnabledInfoLog(Settings.EnableInfoLog)
  362. end
  363. if Settings.EnableVerboseLog then
  364. GameAnalytics:setEnabledVerboseLog(Settings.EnableVerboseLog)
  365. end
  366. if #Settings.AvailableCustomDimensions01 > 0 then
  367. GameAnalytics:configureAvailableCustomDimensions01(Settings.AvailableCustomDimensions01)
  368. end
  369. if #Settings.AvailableCustomDimensions02 > 0 then
  370. GameAnalytics:configureAvailableCustomDimensions02(Settings.AvailableCustomDimensions02)
  371. end
  372. if #Settings.AvailableCustomDimensions03 > 0 then
  373. GameAnalytics:configureAvailableCustomDimensions03(Settings.AvailableCustomDimensions03)
  374. end
  375. if #Settings.AvailableResourceCurrencies > 0 then
  376. GameAnalytics:configureAvailableResourceCurrencies(Settings.AvailableResourceCurrencies)
  377. end
  378. if #Settings.AvailableResourceItemTypes > 0 then
  379. GameAnalytics:configureAvailableResourceItemTypes(Settings.AvailableResourceItemTypes)
  380. end
  381. if #Settings.Build > 0 then
  382. GameAnalytics:configureBuild(Settings.Build)
  383. end
  384. GameAnalytics:initialize({
  385. gameKey = Settings.GameKey,
  386. secretKey = Settings.SecretKey
  387. })
  388. -- Fire for players already in game
  389. for _, Player in pairs(Players:GetPlayers()) do
  390. GameAnalytics:PlayerJoined(Player)
  391. end
  392. ]]></ProtectedString>
  393. <BinaryString name="Tags"></BinaryString>
  394. </Properties>
  395. </Item>
  396. <Item class="Script" referent="RBXb7b6780e3b3b47999e926fea38fa454f">
  397. <Properties>
  398. <bool name="Disabled">false</bool>
  399. <Content name="LinkedSource"><null></null></Content>
  400. <string name="Name">INSTALL</string>
  401. <string name="ScriptGuid">{4efacb9d-efec-45dc-892a-1ddedb9a2ae1}</string>
  402. <ProtectedString name="Source"><![CDATA[--[[
  403. Thanks for using the official GameAnalytics Roblox SDK module (based on gillern's GameAnalytics module)!
  404. To start using the plugin, follow these steps:
  405. 1. Create an account on gameanalytics.com and create a game
  406. 2. Get your game key and secret key
  407. 3. Add the keys to 'GameKey' and 'SecretKey' under GameAnalytics.Settings script
  408. 4. Move the 'GameAnalytics' script (together with its children) under 'ServerStorage'
  409. 5. Move the 'GameAnalyticsServer' script under 'ServerScriptService'
  410. 6. Move the 'GameAnalyticsServerInitUsingSettings' script under 'ServerScriptService' (optional, NOT needed if you want to programmatically initialize the SDK from your own script)
  411. 7. Move the 'GameAnalyticsClient' script under 'StarterPlayer/StarerPlayerScripts'
  412. 8. Move the 'Postie' script under 'ReplicatedStorage'
  413. 9. You're ready!
  414. NOTE: The provided rojo.json in the repository is set up to perform steps 4, 5 and 6 automatically through the Rojo workflow.
  415. For details on how to send events and more with the SDK go to this page: https://gameanalytics.com/docs/item/roblox-sdk
  416. Resources:
  417. Roblox SDK Docs: https://gameanalytics.com/docs/item/roblox-sdk
  418. Github: https://github.com/GameAnalytics/GA-SDK-ROBLOX
  419. Account sign up: https://go.gameanalytics.com/signup
  420. Support: https://gameanalytics.com/contact
  421. --]]
  422. ]]></ProtectedString>
  423. <BinaryString name="Tags"></BinaryString>
  424. </Properties>
  425. </Item>
  426. <Item class="LocalScript" referent="RBX5ddcab423f9e430186a33c70b93fc965">
  427. <Properties>
  428. <bool name="Disabled">false</bool>
  429. <Content name="LinkedSource"><null></null></Content>
  430. <string name="Name">GameAnalyticsClient</string>
  431. <string name="ScriptGuid">{7cd38da4-97aa-470d-a875-cace696c90ba}</string>
  432. <ProtectedString name="Source"><![CDATA[--Variables
  433. --local GameAnalyticsSendMessage = game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  434. --Services
  435. local GS = game:GetService("GuiService")
  436. local UIS = game:GetService("UserInputService")
  437. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  438. local Postie = require(ReplicatedStorage.Postie)
  439. --Functions
  440. local function getPlatform()
  441. if (GS:IsTenFootInterface()) then
  442. return "Console"
  443. elseif (UIS.TouchEnabled and not UIS.MouseEnabled) then
  444. return "Mobile"
  445. else
  446. return "Desktop"
  447. end
  448. end
  449. --Filtering
  450. Postie.SetCallback("getPlatform", getPlatform);
  451. -- debug stuff
  452. --GameAnalyticsSendMessage.OnClientEvent:Connect(function(chatProperties)
  453. -- game:GetService("StarterGui"):SetCore("ChatMakeSystemMessage", chatProperties)
  454. --end)
  455. ]]></ProtectedString>
  456. <BinaryString name="Tags"></BinaryString>
  457. </Properties>
  458. </Item>
  459. <Item class="ModuleScript" referent="RBXf8708688395b460f9941544945f03ab9">
  460. <Properties>
  461. <Content name="LinkedSource"><null></null></Content>
  462. <string name="Name">GameAnalytics</string>
  463. <string name="ScriptGuid"></string>
  464. <ProtectedString name="Source"><![CDATA[local GAResourceFlowType = require(script.GAResourceFlowType)
  465. local GAProgressionStatus = require(script.GAProgressionStatus)
  466. local GAErrorSeverity = require(script.GAErrorSeverity)
  467. local ga = {
  468. EGAResourceFlowType = GAResourceFlowType,
  469. EGAProgressionStatus = GAProgressionStatus,
  470. EGAErrorSeverity = GAErrorSeverity
  471. }
  472. local logger = require(script.Logger)
  473. local threading = require(script.Threading)
  474. local state = require(script.State)
  475. local validation = require(script.Validation)
  476. local store = require(script.Store)
  477. local events = require(script.Events)
  478. local Players = game:GetService("Players")
  479. local MKT = game:GetService("MarketplaceService")
  480. local RunService = game:GetService("RunService")
  481. local ReplicatedStorage = game:GetService("ReplicatedStorage")
  482. local Postie = require(ReplicatedStorage.Postie)
  483. local ProductCache = {}
  484. local OnPlayerReadyEvent
  485. -- local functions
  486. local function isSdkReady(options)
  487. local playerId = options["playerId"] or nil
  488. local needsInitialized = options["needsInitialized"] or true
  489. local shouldWarn = options["shouldWarn"] or false
  490. local message = options["message"] or ""
  491. -- Is SDK initialized
  492. if needsInitialized and not state.Initialized then
  493. if shouldWarn then
  494. logger:w(message .. " SDK is not initialized")
  495. end
  496. return false
  497. end
  498. -- Is SDK enabled
  499. if needsInitialized and playerId and not state:isEnabled(playerId) then
  500. if shouldWarn then
  501. logger:w(message .. " SDK is disabled")
  502. end
  503. return false
  504. end
  505. -- Is session started
  506. if needsInitialized and playerId and not state:sessionIsStarted(playerId) then
  507. if shouldWarn then
  508. logger:w(message .. " Session has not started yet")
  509. end
  510. return false
  511. end
  512. return true
  513. end
  514. function ga:configureAvailableCustomDimensions01(customDimensions)
  515. threading:performTaskOnGAThread(function()
  516. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  517. logger:w("Available custom dimensions must be set before SDK is initialized")
  518. return
  519. end
  520. state:setAvailableCustomDimensions01(customDimensions)
  521. end)
  522. end
  523. function ga:configureAvailableCustomDimensions02(customDimensions)
  524. threading:performTaskOnGAThread(function()
  525. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  526. logger:w("Available custom dimensions must be set before SDK is initialized")
  527. return
  528. end
  529. state:setAvailableCustomDimensions02(customDimensions)
  530. end)
  531. end
  532. function ga:configureAvailableCustomDimensions03(customDimensions)
  533. threading:performTaskOnGAThread(function()
  534. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  535. logger:w("Available custom dimensions must be set before SDK is initialized")
  536. return
  537. end
  538. state:setAvailableCustomDimensions03(customDimensions)
  539. end)
  540. end
  541. function ga:configureAvailableResourceCurrencies(resourceCurrencies)
  542. threading:performTaskOnGAThread(function()
  543. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  544. logger:w("Available resource currencies must be set before SDK is initialized")
  545. return
  546. end
  547. events:setAvailableResourceCurrencies(resourceCurrencies)
  548. end)
  549. end
  550. function ga:configureAvailableResourceItemTypes(resourceItemTypes)
  551. threading:performTaskOnGAThread(function()
  552. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  553. logger:w("Available resource item types must be set before SDK is initialized")
  554. return
  555. end
  556. events:setAvailableResourceItemTypes(resourceItemTypes)
  557. end)
  558. end
  559. function ga:configureBuild(build)
  560. threading:performTaskOnGAThread(function()
  561. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  562. logger:w("Build version must be set before SDK is initialized.")
  563. return
  564. end
  565. events:setBuild(build)
  566. end)
  567. end
  568. function ga:initialize(options)
  569. threading:performTaskOnGAThread(function()
  570. if isSdkReady({needsInitialized=true, shouldWarn=false}) then
  571. logger:w("SDK already initialized. Can only be called once.")
  572. return
  573. end
  574. local gameKey = options["gameKey"]
  575. local secretKey = options["secretKey"]
  576. if not validation:validateKeys(gameKey, secretKey) then
  577. logger:w("SDK failed initialize. Game key or secret key is invalid. Can only contain characters A-z 0-9, gameKey is 32 length, secretKey is 40 length. Failed keys - gameKey: " .. gameKey .. ", secretKey: " .. secretKey)
  578. return
  579. end
  580. events.GameKey = gameKey
  581. events.SecretKey = secretKey
  582. state.Initialized = true
  583. events:processEventQueue()
  584. end)
  585. end
  586. function ga:startNewSession(player, teleportData)
  587. threading:performTaskOnGAThread(function()
  588. if not state:isEventSubmissionEnabled() then
  589. return
  590. end
  591. if not state.Initialized then
  592. logger:w("Cannot start new session. SDK is not initialized yet.")
  593. return
  594. end
  595. state:startNewSession(player, teleportData)
  596. end)
  597. end
  598. function ga:endSession(playerId)
  599. threading:performTaskOnGAThread(function()
  600. if not state:isEventSubmissionEnabled() then
  601. return
  602. end
  603. state:endSession(playerId)
  604. end)
  605. end
  606. function ga:filterForBusinessEvent(text)
  607. return string.gsub(text, "[^A-Za-z0-9%s%-_%.%(%)!%?]", "")
  608. end
  609. function ga:addBusinessEvent(playerId, options)
  610. threading:performTaskOnGAThread(function()
  611. if not state:isEventSubmissionEnabled() then
  612. return
  613. end
  614. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not add business event"}) then
  615. return
  616. end
  617. -- Send to events
  618. local amount = options["amount"] or 0
  619. local itemType = options["itemType"] or ""
  620. local itemId = options["itemId"] or ""
  621. local cartType = options["cartType"] or ""
  622. local USDSpent = math.floor((amount * 0.7) * 0.35)
  623. events:addBusinessEvent(playerId, "USD", USDSpent, itemType, itemId, cartType)
  624. end)
  625. end
  626. function ga:addResourceEvent(playerId, options)
  627. threading:performTaskOnGAThread(function()
  628. if not state:isEventSubmissionEnabled() then
  629. return
  630. end
  631. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not add resource event"}) then
  632. return
  633. end
  634. -- Send to events
  635. local flowType = options["flowType"] or 0
  636. local currency = options["currency"] or ""
  637. local amount = options["amount"] or 0
  638. local itemType = options["itemType"] or ""
  639. local itemId = options["itemId"] or ""
  640. events:addResourceEvent(playerId, flowType, currency, amount, itemType, itemId)
  641. end)
  642. end
  643. function ga:addProgressionEvent(playerId, options)
  644. threading:performTaskOnGAThread(function()
  645. if not state:isEventSubmissionEnabled() then
  646. return
  647. end
  648. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not add progression event"}) then
  649. return
  650. end
  651. -- Send to events
  652. local progressionStatus = options["progressionStatus"] or 0
  653. local progression01 = options["progression01"] or ""
  654. local progression02 = options["progression02"] or nil
  655. local progression03 = options["progression03"] or nil
  656. local score = options["score"] or nil
  657. events:addProgressionEvent(playerId, progressionStatus, progression01, progression02, progression03, score)
  658. end)
  659. end
  660. function ga:addDesignEvent(playerId, options)
  661. threading:performTaskOnGAThread(function()
  662. if not state:isEventSubmissionEnabled() then
  663. return
  664. end
  665. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not add design event"}) then
  666. return
  667. end
  668. -- Send to events
  669. local eventId = options["eventId"] or ""
  670. local value = options["value"] or nil
  671. events:addDesignEvent(playerId, eventId, value)
  672. end)
  673. end
  674. function ga:addErrorEvent(playerId, options)
  675. threading:performTaskOnGAThread(function()
  676. if not state:isEventSubmissionEnabled() then
  677. return
  678. end
  679. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not add error event"}) then
  680. return
  681. end
  682. -- Send to events
  683. local severity = options["severity"] or 0
  684. local message = options["message"] or ""
  685. events:addErrorEvent(playerId, severity, message)
  686. end)
  687. end
  688. function ga:setEnabledDebugLog(flag)
  689. threading:performTaskOnGAThread(function()
  690. if RunService:IsStudio() then
  691. if flag then
  692. logger:setDebugLog(flag)
  693. logger:i("Debug logging enabled")
  694. else
  695. logger:i("Debug logging disabled")
  696. logger:setDebugLog(flag)
  697. end
  698. else
  699. logger:i("setEnabledDebugLog can only be used in studio")
  700. end
  701. end)
  702. end
  703. function ga:setEnabledInfoLog(flag)
  704. threading:performTaskOnGAThread(function()
  705. if flag then
  706. logger:setInfoLog(flag)
  707. logger:i("Info logging enabled")
  708. else
  709. logger:i("Info logging disabled")
  710. logger:setInfoLog(flag)
  711. end
  712. end)
  713. end
  714. function ga:setEnabledVerboseLog(flag)
  715. threading:performTaskOnGAThread(function()
  716. if flag then
  717. logger:setVerboseLog(flag)
  718. logger:ii("Verbose logging enabled")
  719. else
  720. logger:ii("Verbose logging disabled")
  721. logger:setVerboseLog(flag)
  722. end
  723. end)
  724. end
  725. function ga:setEnabledEventSubmission(flag)
  726. threading:performTaskOnGAThread(function()
  727. if flag then
  728. state:setEventSubmission(flag)
  729. logger:i("Event submission enabled")
  730. else
  731. logger:i("Event submission disabled")
  732. state:setEventSubmission(flag)
  733. end
  734. end)
  735. end
  736. function ga:setCustomDimension01(playerId, dimension)
  737. threading:performTaskOnGAThread(function()
  738. if not validation:validateDimension(state._availableCustomDimensions01, dimension) then
  739. logger:w("Could not set custom01 dimension value to '" .. dimension .. "'. Value not found in available custom01 dimension values")
  740. return
  741. end
  742. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not set custom01 dimension"}) then
  743. return
  744. end
  745. state:setCustomDimension01(playerId, dimension)
  746. end)
  747. end
  748. function ga:setCustomDimension02(playerId, dimension)
  749. threading:performTaskOnGAThread(function()
  750. if not validation:validateDimension(state._availableCustomDimensions02, dimension) then
  751. logger:w("Could not set custom02 dimension value to '" .. dimension .. "'. Value not found in available custom02 dimension values")
  752. return
  753. end
  754. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not set custom02 dimension"}) then
  755. return
  756. end
  757. state:setCustomDimension02(playerId, dimension)
  758. end)
  759. end
  760. function ga:setCustomDimension03(playerId, dimension)
  761. threading:performTaskOnGAThread(function()
  762. if not validation:validateDimension(state._availableCustomDimensions03, dimension) then
  763. logger:w("Could not set custom03 dimension value to '" .. dimension .. "'. Value not found in available custom03 dimension values")
  764. return
  765. end
  766. if not isSdkReady({playerId=playerId, needsInitialized=true, shouldWarn=true, message="Could not set custom03 dimension"}) then
  767. return
  768. end
  769. state:setCustomDimension03(playerId, dimension)
  770. end)
  771. end
  772. function ga:setEnabledReportErrors(flag)
  773. threading:performTaskOnGAThread(function()
  774. state.ReportErrors = flag
  775. end)
  776. end
  777. function ga:setEnabledAutomaticSendBusinessEvents(flag)
  778. threading:performTaskOnGAThread(function()
  779. state.AutomaticSendBusinessEvents = flag
  780. end)
  781. end
  782. function ga:addGameAnalyticsTeleportData(playerIds, teleportData)
  783. local gameAnalyticsTeleportData = {}
  784. for index = 1, #playerIds do
  785. local playerId = playerIds[index]
  786. local PlayerData = store.PlayerCache[playerId]
  787. PlayerData.PlayerTeleporting = true
  788. local data = {
  789. ["SessionID"] = PlayerData.SessionID,
  790. ["Sessions"] = PlayerData.Sessions,
  791. ["SessionStart"] = PlayerData.SessionStart
  792. }
  793. gameAnalyticsTeleportData[tostring(playerId)] = data
  794. end
  795. teleportData["gameanalyticsData"] = gameAnalyticsTeleportData
  796. return teleportData
  797. end
  798. function ga:getCommandCenterValueAsString(playerId, options)
  799. local key = options["key"] or ""
  800. local defaultValue = options["defaultValue"] or nil
  801. return state:getConfigurationStringValue(playerId, key, defaultValue)
  802. end
  803. function ga:isCommandCenterReady(playerId)
  804. return state:isCommandCenterReady(playerId)
  805. end
  806. function ga:getConfigurationsContentAsString(playerId)
  807. return state:getConfigurationsContentAsString(playerId)
  808. end
  809. function ga:PlayerJoined(Player, teleportData)
  810. if store.PlayerCache[Player.UserId] then
  811. return
  812. end
  813. --Variables
  814. local PlayerData = store:GetPlayerData(Player)
  815. local PlayerPlatform = "unknown"
  816. local isSuccessful, platform = Postie.InvokeClient("getPlatform", Player, 5)
  817. if isSuccessful then
  818. PlayerPlatform = platform
  819. end
  820. --Fill Data
  821. for key, value in pairs(store.BasePlayerData) do
  822. PlayerData[key] = PlayerData[key] or value
  823. end
  824. store.PlayerCache[Player.UserId] = PlayerData
  825. PlayerData.Platform = (PlayerPlatform == "Console" and "uwp_console") or (PlayerPlatform == "Mobile" and "uwp_mobile") or (PlayerPlatform == "Desktop" and "uwp_desktop") or ("uwp_desktop")
  826. PlayerData.OS = PlayerData.Platform .. " 0.0.0"
  827. ga:startNewSession(Player, teleportData)
  828. OnPlayerReadyEvent = OnPlayerReadyEvent or game:GetService("ReplicatedStorage"):WaitForChild("OnPlayerReadyEvent")
  829. OnPlayerReadyEvent:Fire(Player)
  830. --Autosave
  831. spawn(function()
  832. --Loop
  833. while true do
  834. --Delay
  835. wait(store.AutoSaveData)
  836. --Validate
  837. if (not Player) or (Player.Parent ~= Players) then return end
  838. --Save
  839. store:SavePlayerData(Player)
  840. end
  841. end)
  842. end
  843. function ga:PlayerRemoved(Player)
  844. --Save
  845. store:SavePlayerData(Player)
  846. local PlayerData = store.PlayerCache[Player.UserId]
  847. if PlayerData and not PlayerData.PlayerTeleporting then
  848. ga:endSession(Player.UserId)
  849. end
  850. end
  851. function ga:isPlayerReady(playerId)
  852. if store.PlayerCache[playerId] then
  853. return true
  854. else
  855. return false
  856. end
  857. end
  858. function ga:ProcessReceiptCallback(Info)
  859. --Variables
  860. local ProductInfo = ProductCache[Info.ProductId]
  861. --Cache
  862. if not ProductInfo then
  863. --Get
  864. ProductInfo = MKT:GetProductInfo(Info.ProductId, Enum.InfoType.Product)
  865. ProductCache[Info.ProductId] = ProductInfo
  866. end
  867. ga:addBusinessEvent(Info.PlayerId, {
  868. amount = Info.CurrencySpent,
  869. itemType = "DeveloperProduct",
  870. itemId = ga:filterForBusinessEvent(ProductInfo.Name)
  871. })
  872. end
  873. return ga
  874. ]]></ProtectedString>
  875. <BinaryString name="Tags"></BinaryString>
  876. </Properties>
  877. <Item class="ModuleScript" referent="RBX34831ce54bf7450a8e97b4ff9458caa6">
  878. <Properties>
  879. <Content name="LinkedSource"><null></null></Content>
  880. <string name="Name">Settings</string>
  881. <string name="ScriptGuid"></string>
  882. <ProtectedString name="Source"><![CDATA[local settings = {
  883. EnableInfoLog = true,
  884. EnableVerboseLog = false,
  885. AutomaticSendBusinessEvents = true,
  886. ReportErrors = true,
  887. Build = "0.1",
  888. AvailableCustomDimensions01 = {},
  889. AvailableCustomDimensions02 = {},
  890. AvailableCustomDimensions03 = {},
  891. AvailableResourceCurrencies = {},
  892. AvailableResourceItemTypes = {},
  893. GameKey = "",
  894. SecretKey = ""
  895. }
  896. return settings
  897. ]]></ProtectedString>
  898. <BinaryString name="Tags"></BinaryString>
  899. </Properties>
  900. </Item>
  901. <Item class="ModuleScript" referent="RBXd615e5db16034cae976316f3d4fea9e0">
  902. <Properties>
  903. <Content name="LinkedSource"><null></null></Content>
  904. <string name="Name">HttpApi</string>
  905. <string name="ScriptGuid"></string>
  906. <ProtectedString name="Source"><![CDATA[local RunService = game:GetService("RunService")
  907. local validation = require(script.Parent.Validation)
  908. local version = require(script.Parent.Version)
  909. local sha256 = require(script.sha256)
  910. local hmac = require(script.hmac)
  911. local base64 = require(script.base64)
  912. local http_api = {
  913. protocol = "https",
  914. hostName = "api.gameanalytics.com",
  915. version = "v2",
  916. initializeUrlPath = "init",
  917. eventsUrlPath = "events",
  918. EGAHTTPApiResponse = {
  919. NoResponse=0,
  920. BadResponse=1,
  921. RequestTimeout=2,
  922. JsonEncodeFailed=3,
  923. JsonDecodeFailed=4,
  924. InternalServerError=5,
  925. BadRequest=6,
  926. Unauthorized=7,
  927. UnknownResponseCode=8,
  928. Ok=9
  929. }
  930. }
  931. local HTTP = game:GetService("HttpService")
  932. local logger = require(script.Parent.Logger)
  933. local baseUrl = (RunService:IsStudio() and "http" or http_api.protocol) .. "://" .. (RunService:IsStudio() and "sandbox-" or "") .. http_api.hostName .. "/" .. http_api.version
  934. local Encoding = {}
  935. local function getInitAnnotations(playerData, playerId)
  936. local initAnnotations = {
  937. ["user_id"] = tostring(playerId),
  938. ["sdk_version"] = "roblox " .. version.SdkVersion,
  939. ["os_version"] = playerData.OS,
  940. ["platform"] = playerData.Platform
  941. }
  942. return initAnnotations
  943. end
  944. local function encode(payload, secretKey)
  945. --Validate
  946. if not secretKey then logger:w("Error encoding, invalid SecretKey") return end
  947. --Encode
  948. local payloadHmac = hmac(
  949. RunService:IsStudio() and "16813a12f718bc5c620f56944e1abc3ea13ccbac" or secretKey, -- key
  950. payload, -- message
  951. sha256, -- hashing function
  952. 64, -- block size
  953. nil, -- output size
  954. true -- output as binary data
  955. )
  956. return base64.encode(payloadHmac)
  957. end
  958. local function processRequestResponse(response, requestId)
  959. local statusCode = response.StatusCode
  960. local body = response.Body
  961. if not body or #body == 0 then
  962. logger:d(requestId .. " request. failed. Might be no connection. Status code: " .. tostring(statusCode))
  963. return http_api.EGAHTTPApiResponse.NoResponse
  964. end
  965. if statusCode == 200 then
  966. return http_api.EGAHTTPApiResponse.Ok
  967. elseif statusCode == 0 or statusCode == 401 then
  968. logger:d(requestId .. " request. 401 - Unauthorized.")
  969. return http_api.EGAHTTPApiResponse.Unauthorized
  970. elseif statusCode == 400 then
  971. logger:d(requestId .. " request. 400 - Bad Request.")
  972. return http_api.EGAHTTPApiResponse.BadRequest
  973. elseif statusCode == 500 then
  974. logger:d(requestId .. " request. 500 - Internal Server Error.")
  975. return http_api.EGAHTTPApiResponse.InternalServerError
  976. else
  977. return http_api.EGAHTTPApiResponse.UnknownResponseCode
  978. end
  979. end
  980. function http_api:initRequest(gameKey, secretKey, playerData, playerId)
  981. local url = "https://rubick.gameanalytics.com/v2/command_center?game_key=" .. gameKey .. "&interval_seconds=1000000"
  982. if RunService:IsStudio() then
  983. url = baseUrl .. "/5c6bcb5402204249437fb5a7a80a4959/" .. self.initializeUrlPath
  984. end
  985. logger:d("Sending 'init' URL: " .. url)
  986. local payload = HTTP:JSONEncode(getInitAnnotations(playerData, playerId))
  987. local authorization = encode(payload, secretKey)
  988. local res
  989. local success, err = pcall(function()
  990. res = HTTP:RequestAsync({
  991. Url = url,
  992. Method = "POST",
  993. Headers = {
  994. ["Authorization"] = authorization
  995. },
  996. Body = payload
  997. })
  998. end)
  999. if not success then
  1000. logger:d("Failed Init Call. error: " .. err)
  1001. return {
  1002. statusCode = http_api.EGAHTTPApiResponse.UnknownResponseCode,
  1003. body = nil
  1004. }
  1005. end
  1006. logger:d("init request content: " .. res.Body)
  1007. local requestResponseEnum = processRequestResponse(res, "Init")
  1008. -- if not 200 result
  1009. if requestResponseEnum ~= http_api.EGAHTTPApiResponse.Ok and requestResponseEnum ~= http_api.EGAHTTPApiResponse.BadRequest then
  1010. logger:d("Failed Init Call. URL: " .. url .. ", JSONString: " .. payload .. ", Authorization: " .. authorization)
  1011. return {
  1012. statusCode = requestResponseEnum,
  1013. body = nil
  1014. }
  1015. end
  1016. --Response
  1017. local responseBody
  1018. success, _ = ypcall(function()
  1019. responseBody = HTTP:JSONDecode(res.Body)
  1020. end)
  1021. if not success then
  1022. logger:d("Failed Init Call. Json decoding failed: " .. err)
  1023. return {
  1024. statusCode = http_api.EGAHTTPApiResponse.JsonDecodeFailed,
  1025. body = nil
  1026. }
  1027. end
  1028. -- print reason if bad request
  1029. if requestResponseEnum == http_api.EGAHTTPApiResponse.BadRequest then
  1030. logger:d("Failed Init Call. Bad request. Response: " .. res.Body)
  1031. return {
  1032. statusCode = requestResponseEnum,
  1033. body = nil
  1034. }
  1035. end
  1036. -- validate Init call values
  1037. local validatedInitValues = validation:validateAndCleanInitRequestResponse(responseBody)
  1038. if not validatedInitValues then
  1039. return {
  1040. statusCode = http_api.EGAHTTPApiResponse.BadResponse,
  1041. body = nil
  1042. }
  1043. end
  1044. -- all ok
  1045. return {
  1046. statusCode = http_api.EGAHTTPApiResponse.Ok,
  1047. body = responseBody
  1048. }
  1049. end
  1050. function http_api:sendEventsInArray(gameKey, secretKey, eventArray)
  1051. if not eventArray or #eventArray == 0 then
  1052. logger:d("sendEventsInArray called with missing eventArray")
  1053. return
  1054. end
  1055. -- Generate URL
  1056. local url = baseUrl .. "/" .. gameKey .. "/" .. self.eventsUrlPath
  1057. if RunService:IsStudio() then
  1058. url = baseUrl .. "/5c6bcb5402204249437fb5a7a80a4959/" .. self.eventsUrlPath
  1059. end
  1060. logger:d("Sending 'events' URL: " .. url)
  1061. -- make JSON string from data
  1062. local payload = HTTP:JSONEncode(eventArray)
  1063. local authorization = encode(payload, secretKey)
  1064. local res
  1065. local success, err = pcall(function()
  1066. res = HTTP:RequestAsync({
  1067. Url = url,
  1068. Method = "POST",
  1069. Headers = {
  1070. ["Authorization"] = authorization
  1071. },
  1072. Body = payload
  1073. })
  1074. end)
  1075. if not success then
  1076. logger:d("Failed Events Call. error: " .. err)
  1077. return {
  1078. statusCode = http_api.EGAHTTPApiResponse.UnknownResponseCode,
  1079. body = nil
  1080. }
  1081. end
  1082. logger:d("body: " .. res.Body)
  1083. local requestResponseEnum = processRequestResponse(res, "Events")
  1084. -- if not 200 result
  1085. if requestResponseEnum ~= http_api.EGAHTTPApiResponse.Ok and requestResponseEnum ~= http_api.EGAHTTPApiResponse.BadRequest then
  1086. logger:d("Failed Events Call. URL: " .. url .. ", JSONString: " .. payload .. ", Authorization: " .. authorization)
  1087. return {
  1088. statusCode = requestResponseEnum,
  1089. body = nil
  1090. }
  1091. end
  1092. local responseBody
  1093. ypcall(function()
  1094. responseBody = HTTP:JSONDecode(res.Body)
  1095. end)
  1096. if not responseBody then
  1097. logger:d("Failed Events Call. Json decoding failed")
  1098. return {
  1099. statusCode = http_api.EGAHTTPApiResponse.JsonDecodeFailed,
  1100. body = nil
  1101. }
  1102. end
  1103. -- print reason if bad request
  1104. if requestResponseEnum == http_api.EGAHTTPApiResponse.BadRequest then
  1105. logger:d("Failed Events Call. Bad request. Response: " .. res.Body)
  1106. return {
  1107. statusCode = requestResponseEnum,
  1108. body = nil
  1109. }
  1110. end
  1111. -- all ok
  1112. return {
  1113. statusCode = http_api.EGAHTTPApiResponse.Ok,
  1114. body = responseBody
  1115. }
  1116. end
  1117. return http_api
  1118. ]]></ProtectedString>
  1119. <BinaryString name="Tags"></BinaryString>
  1120. </Properties>
  1121. <Item class="ModuleScript" referent="RBX4b14a91351d1404f89ac5d7683accce5">
  1122. <Properties>
  1123. <Content name="LinkedSource"><null></null></Content>
  1124. <string name="Name">base64</string>
  1125. <string name="ScriptGuid"></string>
  1126. <ProtectedString name="Source"><![CDATA[local ASSERTIONS_ENABLED = true -- Whether to run several checks when the module is first loaded
  1127. local CHAR_SET = { [0] =
  1128. "A", "B", "C", "D", "E", "F", "G", "H", "I", "J", "K", "L", "M",
  1129. "N", "O", "P", "Q", "R", "S", "T", "U", "V", "W", "X", "Y", "Z",
  1130. "a", "b", "c", "d", "e", "f", "g", "h", "i", "j", "k", "l", "m",
  1131. "n", "o", "p", "q", "r", "s", "t", "u", "v", "w", "x", "y", "z",
  1132. "0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "+", "/",
  1133. }
  1134. local REVERSE_CHAR_SET = {
  1135. [65] = 0, [66] = 1, [67] = 2, [68] = 3, [69] = 4, [70] = 5, [71] = 6, [72] = 7, [73] = 8, [74] = 9, [75] = 10, [76] = 11, [77] = 12, [78] = 13,
  1136. [79] = 14, [80] = 15, [81] = 16, [82] = 17, [83] = 18, [84] = 19, [85] = 20, [86] = 21, [87] = 22, [88] = 23, [89] = 24, [90] = 25, [97] = 26,
  1137. [98] = 27, [99] = 28, [100] = 29, [101] = 30, [102] = 31, [103] = 32, [104] = 33, [105] = 34, [106] = 35, [107] = 36, [108] = 37, [109] = 38, [110] = 39,
  1138. [111] = 40, [112] = 41, [113] = 42, [114] = 43, [115] = 44, [116] = 45, [117] = 46, [118] = 47, [119] = 48, [120] = 49, [121] = 50, [122] = 51, [48] = 52,
  1139. [49] = 53, [50] = 54, [51] = 55, [52] = 56, [53] = 57, [54] = 58, [55] = 59, [56] = 60, [57] = 61, [43] = 62, [47] = 63,
  1140. }
  1141. --- Packs three 8-bit integers into one unsigned 24-bit integer.
  1142. local function packUint24FromOctets(a, b, c)
  1143. return bit32.lshift(a, 16)+bit32.lshift(b, 8)+c
  1144. end
  1145. --- Packs four 6-bit integers into one unsigned 24-bit integer
  1146. local function packUint24FromSextets(a, b, c, d)
  1147. return bit32.lshift(a, 18)+bit32.lshift(b, 12)+bit32.lshift(c, 6)+d
  1148. end
  1149. --- Encodes `input` from plaintext into base64, optionally omitting padding
  1150. local function encodeBase64(input, omitPadding)
  1151. local output = {}
  1152. local padding = #input%3
  1153. local c = 1
  1154. for i = 1, #input, 3 do
  1155. local b1, b2, b3 = string.byte(input, i, i+2)
  1156. local packed = packUint24FromOctets(b1, b2 or 0, b3 or 0)
  1157. output[c] = CHAR_SET[bit32.extract(packed, 18, 6)]
  1158. output[c+1] = CHAR_SET[bit32.extract(packed, 12, 6)]
  1159. if b2 then
  1160. output[c+2] = CHAR_SET[bit32.extract(packed, 6, 6)]
  1161. if b3 then
  1162. output[c+3] = CHAR_SET[bit32.extract(packed, 0, 6)]
  1163. end
  1164. end
  1165. c = c+4
  1166. end
  1167. if not omitPadding then
  1168. if padding == 2 then
  1169. output[c-1] = "="
  1170. elseif padding == 1 then
  1171. output[c-2] = "=="
  1172. end
  1173. end
  1174. return table.concat(output)
  1175. end
  1176. --- Decodes `input` from base64 to plaintext.
  1177. local function decodeBase64(input)
  1178. assert(not (string.find(input, "[^%w+/=]")), "input contains invalid characters")
  1179. local output = {}
  1180. local c = 1
  1181. for i = 1, #input, 4 do
  1182. local b1, b2, b3, b4 = string.byte(input, i, i+3)
  1183. b1 = REVERSE_CHAR_SET[b1]
  1184. b2 = REVERSE_CHAR_SET[b2]
  1185. b3 = REVERSE_CHAR_SET[b3]
  1186. b4 = REVERSE_CHAR_SET[b4]
  1187. local packed = packUint24FromSextets(b1, b2, b3 or 0, b4 or 0)
  1188. output[c] = string.char(bit32.extract(packed, 16, 8))
  1189. if not b3 then
  1190. break
  1191. end
  1192. output[c+1] = string.char(bit32.extract(packed, 8, 8))
  1193. if not b4 then
  1194. break
  1195. end
  1196. output[c+2] = string.char(bit32.extract(packed, 0, 8))
  1197. c = c+3
  1198. end
  1199. return table.concat(output)
  1200. end
  1201. if ASSERTIONS_ENABLED then
  1202. assert(packUint24FromOctets(77, 97, 110) == 5071214, "(Base64) packUint24FromOctets check")
  1203. assert(packUint24FromSextets(19, 22, 5, 46) == 5071214, "(Base64) packUint24FromSextets check")
  1204. assert(encodeBase64("Man") == "TWFu", "(Base64) Man failed to encode into TWFu")
  1205. assert(encodeBase64("Ma") == "TWE=", "(Base64) Ma failed to encode into TWE=")
  1206. assert(encodeBase64("M") == "TQ==", "(Base64) M failed to encode into TQ==")
  1207. assert(encodeBase64("Baby shark") == "QmFieSBzaGFyaw==", "(Base64) Baby shark failed to encode into QmFieSBzaGFyaw==")
  1208. assert(encodeBase64("Almost heaven, West Virginia\nBlue Ridge Mountains, Shenandoah River\nLife is old there, older than the trees\nYounger than the mountains, blowing like a breeze\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nAll my memories gather round her\nMiner's lady, stranger to blue water\nDark and dusty, painted on the sky\nMisty taste of moonshine, teardrop in my eye\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nI hear her voice, in the morning hour she calls me\nThe radio reminds me of my home far away\nAnd driving down the road I get a feeling\nThat I should have been home yesterday, yesterday\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nTake me home, down country roads\nTake me home, down country roads") == "QWxtb3N0IGhlYXZlbiwgV2VzdCBWaXJnaW5pYQpCbHVlIFJpZGdlIE1vdW50YWlucywgU2hlbmFuZG9haCBSaXZlcgpMaWZlIGlzIG9sZCB0aGVyZSwgb2xkZXIgdGhhbiB0aGUgdHJlZXMKWW91bmdlciB0aGFuIHRoZSBtb3VudGFpbnMsIGJsb3dpbmcgbGlrZSBhIGJyZWV6ZQpDb3VudHJ5IHJvYWRzLCB0YWtlIG1lIGhvbWUKVG8gdGhlIHBsYWNlIEkgYmVsb25nCldlc3QgVmlyZ2luaWEsIG1vdW50YWluIG1hbWEKVGFrZSBtZSBob21lLCBjb3VudHJ5IHJvYWRzCkFsbCBteSBtZW1vcmllcyBnYXRoZXIgcm91bmQgaGVyCk1pbmVyJ3MgbGFkeSwgc3RyYW5nZXIgdG8gYmx1ZSB3YXRlcgpEYXJrIGFuZCBkdXN0eSwgcGFpbnRlZCBvbiB0aGUgc2t5Ck1pc3R5IHRhc3RlIG9mIG1vb25zaGluZSwgdGVhcmRyb3AgaW4gbXkgZXllCkNvdW50cnkgcm9hZHMsIHRha2UgbWUgaG9tZQpUbyB0aGUgcGxhY2UgSSBiZWxvbmcKV2VzdCBWaXJnaW5pYSwgbW91bnRhaW4gbWFtYQpUYWtlIG1lIGhvbWUsIGNvdW50cnkgcm9hZHMKSSBoZWFyIGhlciB2b2ljZSwgaW4gdGhlIG1vcm5pbmcgaG91ciBzaGUgY2FsbHMgbWUKVGhlIHJhZGlvIHJlbWluZHMgbWUgb2YgbXkgaG9tZSBmYXIgYXdheQpBbmQgZHJpdmluZyBkb3duIHRoZSByb2FkIEkgZ2V0IGEgZmVlbGluZwpUaGF0IEkgc2hvdWxkIGhhdmUgYmVlbiBob21lIHllc3RlcmRheSwgeWVzdGVyZGF5CkNvdW50cnkgcm9hZHMsIHRha2UgbWUgaG9tZQpUbyB0aGUgcGxhY2UgSSBiZWxvbmcKV2VzdCBWaXJnaW5pYSwgbW91bnRhaW4gbWFtYQpUYWtlIG1lIGhvbWUsIGNvdW50cnkgcm9hZHMKQ291bnRyeSByb2FkcywgdGFrZSBtZSBob21lClRvIHRoZSBwbGFjZSBJIGJlbG9uZwpXZXN0IFZpcmdpbmlhLCBtb3VudGFpbiBtYW1hClRha2UgbWUgaG9tZSwgY291bnRyeSByb2FkcwpUYWtlIG1lIGhvbWUsIGRvd24gY291bnRyeSByb2FkcwpUYWtlIG1lIGhvbWUsIGRvd24gY291bnRyeSByb2Fkcw==", "(Base64) Country Roads failed to encode properly")
  1209. assert(encodeBase64("Man", true) == "TWFu", "(Base64) Man with padding disabled failed to encode into TWFu")
  1210. assert(encodeBase64("Ma", true) == "TWE", "(Base64) Ma with padding disabled failed to encode into TWE")
  1211. assert(encodeBase64("M", true) == "TQ", "(Base64) M with padding disabled failed to encode into TQ")
  1212. assert(encodeBase64("Baby shark", true) == "QmFieSBzaGFyaw", "(Base64) Baby shark with padding disabled failed to encode into QmFieSBzaGFyaw")
  1213. assert(encodeBase64("") == "", "(Base64) Empty string failed to encode properly")
  1214. assert(encodeBase64("f") == "Zg==", "(Base64) f failed to encode into Zg==")
  1215. assert(encodeBase64("fo") == "Zm8=", "(Base64) fo failed to encode into Zm8=")
  1216. assert(encodeBase64("foo") == "Zm9v", "(Base64) foo failed to encode into Zm9v")
  1217. assert(encodeBase64("foob") == "Zm9vYg==", "(Base64) foob failed to encode into Zm9vYg==")
  1218. assert(encodeBase64("fooba") == "Zm9vYmE=", "(Base64) fooba failed to encode into Zm9vYmE=")
  1219. assert(encodeBase64("foobar") == "Zm9vYmFy", "(Base64) foobar failed to encode into Zm9vYmFy")
  1220. assert(encodeBase64("A\0B") == "QQBC", "(Base64) A\\0B failed to encode into QQBC")
  1221. assert(encodeBase64("A\n\t\v") == "QQoJCw==", "(Base64) A\\n\\t\\v failed to encode into QQoJCw==")
  1222. assert(encodeBase64("☺☻") == "4pi64pi7", "(Base64) ☺☻ failed to encode into 4pi64pi7")
  1223. assert(encodeBase64("テスト") == "44OG44K544OI", "(Base64) テスト failed to encode into 44OG44K544OI")
  1224. assert(decodeBase64("TWFu") == "Man", "(Base64) Man failed to decode into TWFu")
  1225. assert(decodeBase64("TWE=") == "Ma", "(Base64) Ma failed to decode into TWE=")
  1226. assert(decodeBase64("TQ==") == "M", "(Base64) M failed to decode into TQ==")
  1227. assert(decodeBase64("QmFieSBzaGFyaw==") == "Baby shark", "(Base64) Baby shark failed to decode into QmFieSBzaGFyaw==")
  1228. assert(decodeBase64("QWxtb3N0IGhlYXZlbiwgV2VzdCBWaXJnaW5pYQpCbHVlIFJpZGdlIE1vdW50YWlucywgU2hlbmFuZG9haCBSaXZlcgpMaWZlIGlzIG9sZCB0aGVyZSwgb2xkZXIgdGhhbiB0aGUgdHJlZXMKWW91bmdlciB0aGFuIHRoZSBtb3VudGFpbnMsIGJsb3dpbmcgbGlrZSBhIGJyZWV6ZQpDb3VudHJ5IHJvYWRzLCB0YWtlIG1lIGhvbWUKVG8gdGhlIHBsYWNlIEkgYmVsb25nCldlc3QgVmlyZ2luaWEsIG1vdW50YWluIG1hbWEKVGFrZSBtZSBob21lLCBjb3VudHJ5IHJvYWRzCkFsbCBteSBtZW1vcmllcyBnYXRoZXIgcm91bmQgaGVyCk1pbmVyJ3MgbGFkeSwgc3RyYW5nZXIgdG8gYmx1ZSB3YXRlcgpEYXJrIGFuZCBkdXN0eSwgcGFpbnRlZCBvbiB0aGUgc2t5Ck1pc3R5IHRhc3RlIG9mIG1vb25zaGluZSwgdGVhcmRyb3AgaW4gbXkgZXllCkNvdW50cnkgcm9hZHMsIHRha2UgbWUgaG9tZQpUbyB0aGUgcGxhY2UgSSBiZWxvbmcKV2VzdCBWaXJnaW5pYSwgbW91bnRhaW4gbWFtYQpUYWtlIG1lIGhvbWUsIGNvdW50cnkgcm9hZHMKSSBoZWFyIGhlciB2b2ljZSwgaW4gdGhlIG1vcm5pbmcgaG91ciBzaGUgY2FsbHMgbWUKVGhlIHJhZGlvIHJlbWluZHMgbWUgb2YgbXkgaG9tZSBmYXIgYXdheQpBbmQgZHJpdmluZyBkb3duIHRoZSByb2FkIEkgZ2V0IGEgZmVlbGluZwpUaGF0IEkgc2hvdWxkIGhhdmUgYmVlbiBob21lIHllc3RlcmRheSwgeWVzdGVyZGF5CkNvdW50cnkgcm9hZHMsIHRha2UgbWUgaG9tZQpUbyB0aGUgcGxhY2UgSSBiZWxvbmcKV2VzdCBWaXJnaW5pYSwgbW91bnRhaW4gbWFtYQpUYWtlIG1lIGhvbWUsIGNvdW50cnkgcm9hZHMKQ291bnRyeSByb2FkcywgdGFrZSBtZSBob21lClRvIHRoZSBwbGFjZSBJIGJlbG9uZwpXZXN0IFZpcmdpbmlhLCBtb3VudGFpbiBtYW1hClRha2UgbWUgaG9tZSwgY291bnRyeSByb2FkcwpUYWtlIG1lIGhvbWUsIGRvd24gY291bnRyeSByb2FkcwpUYWtlIG1lIGhvbWUsIGRvd24gY291bnRyeSByb2Fkcw==") == "Almost heaven, West Virginia\nBlue Ridge Mountains, Shenandoah River\nLife is old there, older than the trees\nYounger than the mountains, blowing like a breeze\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nAll my memories gather round her\nMiner's lady, stranger to blue water\nDark and dusty, painted on the sky\nMisty taste of moonshine, teardrop in my eye\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nI hear her voice, in the morning hour she calls me\nThe radio reminds me of my home far away\nAnd driving down the road I get a feeling\nThat I should have been home yesterday, yesterday\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nCountry roads, take me home\nTo the place I belong\nWest Virginia, mountain mama\nTake me home, country roads\nTake me home, down country roads\nTake me home, down country roads", "(Base64) Country Roads failed to decode properly")
  1229. assert(decodeBase64("TWE") == "Ma", "(Base64) TWE failed to decode into Ma")
  1230. assert(decodeBase64("TQ") == "M", "(Base64) TQ failed to decode into M")
  1231. assert(decodeBase64("QmFieSBzaGFyaw") == "Baby shark", "(Base64) QmFieSBzaGFyaw failed to decode into Baby shark")
  1232. assert(decodeBase64("") == "", "(Base64) Empty string failed to decode")
  1233. assert(decodeBase64("Zg==") == "f", "(Base64) Zg== failed to decode into f")
  1234. assert(decodeBase64("Zm8=") == "fo", "(Base64) Zm8= failed to decode into fo")
  1235. assert(decodeBase64("Zm9v") == "foo", "(Base64) Zm9v failed to decode into foo")
  1236. assert(decodeBase64("Zm9vYg==") == "foob", "(Base64) Zm9vYg== failed to decode into foob")
  1237. assert(decodeBase64("Zm9vYmE=") == "fooba", "(Base64) Zm9vYmE= failed to decode into fooba")
  1238. assert(decodeBase64("Zm9vYmFy") == "foobar", "(Base64) Zm9vYmFy failed to decode into foobar")
  1239. assert(decodeBase64("QQBC") == "A\0B", "(Base64) QQBC failed to decode into A\\0B")
  1240. assert(decodeBase64("QQoJCw==") == "A\n\t\v", "(Base64) QQoJCw== failed to decode into A\\n\\t\\v")
  1241. assert(decodeBase64("4pi64pi7") == "☺☻", "(Base64) 4pi64pi7 failed to decode into ☺☻")
  1242. assert(decodeBase64("44OG44K544OI") == "テスト", "(Base64) 44OG44K544OI failed to decode into テスト")
  1243. end
  1244. return {
  1245. encode = encodeBase64,
  1246. decode = decodeBase64,
  1247. }
  1248. ]]></ProtectedString>
  1249. <BinaryString name="Tags"></BinaryString>
  1250. </Properties>
  1251. </Item>
  1252. <Item class="ModuleScript" referent="RBX4b14a91351d1404f89ac5d7683addde5">
  1253. <Properties>
  1254. <Content name="LinkedSource"><null></null></Content>
  1255. <string name="Name">hmac</string>
  1256. <string name="ScriptGuid"></string>
  1257. <ProtectedString name="Source"><![CDATA[local ASSERTIONS_ENABLED = false -- Whether to run several checks when the module is first loaded
  1258. local INNER_PADDING_CHAR = string.char(0x36)
  1259. local OUTER_PADDING_CHAR = string.char(0x5C)
  1260. local binaryStringMap = {} -- For the sake of speed of converting hexes to strings, there's a map of the conversions here
  1261. for i = 0, 255 do
  1262. binaryStringMap[string.format("%02x", i)] = string.char(i)
  1263. end
  1264. --- XORs two strings together on a byte level
  1265. local function xorStrings(str1, str2)
  1266. local output = {}
  1267. for i = 1, #str1 do
  1268. output[i] = string.char(bit32.bxor(string.byte(str1, i), string.byte(str2, i)))
  1269. end
  1270. return table.concat(output)
  1271. end
  1272. --- Converts hex strings to their binary equivalent
  1273. local function hexToBinary(string)
  1274. return ( string.gsub(string, "%x%x", binaryStringMap) )
  1275. end
  1276. --- Outputs a HMAC string (in hex) given a key, message, hashing function, and block size.
  1277. --- Optionally accepts an output size to truncate the HMAC to, and a boolean to indicate whether to output the data as a binary string or not.
  1278. --- Both blockSize and outputSize are in bytes for ease of computation.
  1279. local function hmac(key, message, hash, blockSize, outputSize, asBinaryData)
  1280. local innerPadding = string.rep(INNER_PADDING_CHAR, blockSize)
  1281. local outerPadding = string.rep(OUTER_PADDING_CHAR, blockSize)
  1282. if #key > blockSize then
  1283. key = hexToBinary(hash(key))
  1284. end
  1285. if #key < blockSize then
  1286. key = key..string.rep("\0", blockSize-#key)
  1287. end
  1288. local outerKey = xorStrings(key, outerPadding)
  1289. local innerKey = xorStrings(key, innerPadding)
  1290. local mac = hash( outerKey..hexToBinary(hash(innerKey..message)) )
  1291. local output;
  1292. if outputSize then
  1293. output = string.sub(mac, 1, outputSize*2) -- Today's gross hack is brought to you by every byte being represented by two hex digits
  1294. else
  1295. output = mac
  1296. end
  1297. if asBinaryData then
  1298. return hexToBinary(output)
  1299. else
  1300. return output
  1301. end
  1302. end
  1303. if ASSERTIONS_ENABLED then
  1304. local sha256 = require(script.Parent.sha256)
  1305. -- SHA-256 tests (https://tools.ietf.org/html/rfc4231)
  1306. assert(hmac(string.rep(string.char(0x0b), 20), "Hi There", sha256, 64) == "b0344c61d8db38535ca8afceaf0bf12b881dc200c9833da726e9376c2e32cff7", "(HMAC-SHA-256) RFC test case 1 hash does not match")
  1307. assert(hmac("Jefe", "what do ya want for nothing?", sha256, 64) == "5bdcc146bf60754e6a042426089575c75a003f089d2739839dec58b964ec3843", "(HMAC-SHA-256) RFC test case 2 hash does not match")
  1308. assert(hmac(string.rep(string.char(0xaa), 20), string.rep(string.char(0xdd), 50), sha256, 64) == "773ea91e36800e46854db8ebd09181a72959098b3ef8c122d9635514ced565fe", "(HMAC-SHA-256) RFC test case 3 hash does not match")
  1309. assert(hmac(hexToBinary("0102030405060708090a0b0c0d0e0f10111213141516171819"), string.rep(string.char(0xcd), 50), sha256, 64) == "82558a389a443c0ea4cc819899f2083a85f0faa3e578f8077a2e3ff46729665b", "(HMAC-SHA-256) RFC test case 4 hash does not match")
  1310. assert(hmac(string.rep(string.char(0x0c), 20), "Test With Truncation", sha256, 64, 16) == "a3b6167473100ee06e0c796c2955552b", "(HMAC-SHA-256) RFC test case 5 hash does not match")
  1311. assert(hmac(string.rep(string.char(0xaa), 131), "Test Using Larger Than Block-Size Key - Hash Key First", sha256, 64) == "60e431591ee0b67f0d8a26aacbf5b77f8e0bc6213728c5140546040f0ee37f54", "(HMAC-SHA-256) RFC test case 6 hash does not match")
  1312. assert(hmac(string.rep(string.char(0xaa), 131), "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", sha256, 64) == "9b09ffa71b942fcb27635fbcd5b0e944bfdc63644f0713938a7f51535c3a35e2", "(HMAC-SHA-256) RFC test case 7 hash does not match")
  1313. -- Tests explicitly for binary data output
  1314. assert(hmac(string.rep(string.char(0x0b), 20), "Hi There", sha256, 64, nil, true) == "\176\52\76\97\216\219\56\83\92\168\175\206\175\11\241\43\136\29\194\0\201\131\61\167\38\233\55\108\46\50\207\247", "(HMAC-SHA-256) RFC test case 1 hash as binary does not match")
  1315. assert(hmac("Jefe", "what do ya want for nothing?", sha256, 64, nil, true) == "\91\220\193\70\191\96\117\78\106\4\36\38\8\149\117\199\90\0\63\8\157\39\57\131\157\236\88\185\100\236\56\67", "(HMAC-SHA-256) RFC test case 2 hash as binary does not match")
  1316. assert(hmac(string.rep(string.char(0xaa), 20), string.rep(string.char(0xdd), 50), sha256, 64, nil, true) == "\119\62\169\30\54\128\14\70\133\77\184\235\208\145\129\167\41\89\9\139\62\248\193\34\217\99\85\20\206\213\101\254", "(HMAC-SHA-256) RFC test case 3 hash as binary does not match")
  1317. assert(hmac(hexToBinary("0102030405060708090a0b0c0d0e0f10111213141516171819"), string.rep(string.char(0xcd), 50), sha256, 64, nil, true) == "\130\85\138\56\154\68\60\14\164\204\129\152\153\242\8\58\133\240\250\163\229\120\248\7\122\46\63\244\103\41\102\91", "(HMAC-SHA-256) RFC test case 4 hash as binary does not match")
  1318. assert(hmac(string.rep(string.char(0x0c), 20), "Test With Truncation", sha256, 64, 16, true) == "\163\182\22\116\115\16\14\224\110\12\121\108\41\85\85\43", "(HMAC-SHA-256) RFC test case 5 hash as binary does not match")
  1319. assert(hmac(string.rep(string.char(0xaa), 131), "Test Using Larger Than Block-Size Key - Hash Key First", sha256, 64, nil, true) == "\96\228\49\89\30\224\182\127\13\138\38\170\203\245\183\127\142\11\198\33\55\40\197\20\5\70\4\15\14\227\127\84", "(HMAC-SHA-256) RFC test case 6 hash as binary does not match")
  1320. assert(hmac(string.rep(string.char(0xaa), 131), "This is a test using a larger than block-size key and a larger than block-size data. The key needs to be hashed before being used by the HMAC algorithm.", sha256, 64, nil, true) == "\155\9\255\167\27\148\47\203\39\99\95\188\213\176\233\68\191\220\99\100\79\7\19\147\138\127\81\83\92\58\53\226", "(HMAC-SHA-256) RFC test case 7 hash as binary does not match")
  1321. end
  1322. return hmac]]></ProtectedString>
  1323. <BinaryString name="Tags"></BinaryString>
  1324. </Properties>
  1325. </Item>
  1326. <Item class="ModuleScript" referent="RBX4b14a91351d1404f89ac5d7683afffe5">
  1327. <Properties>
  1328. <Content name="LinkedSource"><null></null></Content>
  1329. <string name="Name">sha256</string>
  1330. <string name="ScriptGuid"></string>
  1331. <ProtectedString name="Source"><![CDATA[local ASSERTIONS_ENABLED = false -- Whether to run several checks when the module is first loaded and when a message is preprocessed.
  1332. local INIT_0_256 = 0x6a09e667
  1333. local INIT_1_256 = 0xbb67ae85
  1334. local INIT_2_256 = 0x3c6ef372
  1335. local INIT_3_256 = 0xa54ff53a
  1336. local INIT_4_256 = 0x510e527f
  1337. local INIT_5_256 = 0x9b05688c
  1338. local INIT_6_256 = 0x1f83d9ab
  1339. local INIT_7_256 = 0x5be0cd19
  1340. local APPEND_CHAR = string.char(0x80)
  1341. local INT_32_CAP = 2^32
  1342. local K = { [0] =
  1343. 0x428a2f98, 0x71374491, 0xb5c0fbcf, 0xe9b5dba5, 0x3956c25b, 0x59f111f1, 0x923f82a4, 0xab1c5ed5,
  1344. 0xd807aa98, 0x12835b01, 0x243185be, 0x550c7dc3, 0x72be5d74, 0x80deb1fe, 0x9bdc06a7, 0xc19bf174,
  1345. 0xe49b69c1, 0xefbe4786, 0x0fc19dc6, 0x240ca1cc, 0x2de92c6f, 0x4a7484aa, 0x5cb0a9dc, 0x76f988da,
  1346. 0x983e5152, 0xa831c66d, 0xb00327c8, 0xbf597fc7, 0xc6e00bf3, 0xd5a79147, 0x06ca6351, 0x14292967,
  1347. 0x27b70a85, 0x2e1b2138, 0x4d2c6dfc, 0x53380d13, 0x650a7354, 0x766a0abb, 0x81c2c92e, 0x92722c85,
  1348. 0xa2bfe8a1, 0xa81a664b, 0xc24b8b70, 0xc76c51a3, 0xd192e819, 0xd6990624, 0xf40e3585, 0x106aa070,
  1349. 0x19a4c116, 0x1e376c08, 0x2748774c, 0x34b0bcb5, 0x391c0cb3, 0x4ed8aa4a, 0x5b9cca4f, 0x682e6ff3,
  1350. 0x748f82ee, 0x78a5636f, 0x84c87814, 0x8cc70208, 0x90befffa, 0xa4506ceb, 0xbef9a3f7, 0xc67178f2,
  1351. }
  1352. ---Packs four 8-bit integers into one 32-bit integer
  1353. local function packUint32(a, b, c, d)
  1354. return bit32.lshift(a, 24)+bit32.lshift(b, 16)+bit32.lshift(c, 8)+d
  1355. end
  1356. ---Unpacks one 32-bit integer into four 8-bit integers
  1357. local function unpackUint32(int)
  1358. return bit32.extract(int, 24, 8), bit32.extract(int, 16, 8),
  1359. bit32.extract(int, 08, 8), bit32.extract(int, 00, 8)
  1360. end
  1361. local function CH(x, y, z)
  1362. -- C ~ (A & (B ~ C)) has less ops than (A & B) ^ (~A & C)
  1363. return bit32.bxor( z, bit32.band(x, bit32.bxor(y, z)) )
  1364. end
  1365. local function MAJ(x, y, z)
  1366. -- A | (B | C) | (B & C) has less ops than (A & B) ^ (A & C) ^ (B & C)
  1367. return bit32.bor( bit32.band(x, bit32.bor(y, z)), bit32.band(y, z) )
  1368. end
  1369. local function BSIG0(x)
  1370. return bit32.bxor( bit32.rrotate(x, 2), bit32.rrotate(x, 13), bit32.rrotate(x, 22) )
  1371. end
  1372. local function BSIG1(x)
  1373. return bit32.bxor( bit32.rrotate(x, 6), bit32.rrotate(x, 11), bit32.rrotate(x, 25) )
  1374. end
  1375. local function SSIG0(x)
  1376. return bit32.bxor( bit32.rrotate(x, 7), bit32.rrotate(x, 18), bit32.rshift(x, 3) )
  1377. end
  1378. local function SSIG1(x)
  1379. return bit32.bxor( bit32.rrotate(x, 17), bit32.rrotate(x, 19), bit32.rshift(x, 10) )
  1380. end
  1381. local function preprocessMessage(message)
  1382. local initMsgLen = #message*8 -- Message length in bits
  1383. local msgLen = initMsgLen+8
  1384. local nulCount = 4 -- This is equivalent to 32 bits.
  1385. -- We're packing 32 bits of size, but the SHA-256 standard calls for 64, meaning we have to add at least 32 0s
  1386. -- Unfortunately 64 bits is not possible due to Lua numbers being doubles
  1387. message = message..APPEND_CHAR
  1388. while (msgLen+64)%512 ~= 0 do
  1389. nulCount = nulCount+1
  1390. msgLen = msgLen+8
  1391. end
  1392. message = message..string.rep("\0", nulCount)
  1393. message = message..string.char(unpackUint32(initMsgLen))
  1394. if ASSERTIONS_ENABLED then
  1395. assert(msgLen%512 == 448, "message length space check")
  1396. assert(#message%64 == 0, "message length check")
  1397. end
  1398. return message
  1399. end
  1400. local function sha256(message)
  1401. local message = preprocessMessage(message)
  1402. local H0 = INIT_0_256
  1403. local H1 = INIT_1_256
  1404. local H2 = INIT_2_256
  1405. local H3 = INIT_3_256
  1406. local H4 = INIT_4_256
  1407. local H5 = INIT_5_256
  1408. local H6 = INIT_6_256
  1409. local H7 = INIT_7_256
  1410. local W = {}
  1411. for chunkStart = 1, #message, 64 do
  1412. local place = chunkStart
  1413. for t = 0, 15 do
  1414. W[t] = packUint32(string.byte(message, place, place+3))
  1415. place = place+4
  1416. end
  1417. for t = 16, 63 do
  1418. W[t] = SSIG1(W[t-2])+W[t-7]+SSIG0(W[t-15])+W[t-16]
  1419. end
  1420. local a, b, c, d, e, f, g, h = H0, H1, H2, H3, H4, H5, H6, H7
  1421. for t = 0, 63 do
  1422. T1 = h + BSIG1(e) + CH(e, f, g) + K[t] + W[t]
  1423. T2 = BSIG0(a) + MAJ(a, b, c)
  1424. h = g
  1425. g = f
  1426. f = e
  1427. e = d + T1
  1428. d = c
  1429. c = b
  1430. b = a
  1431. a = T1 + T2
  1432. end
  1433. H0 = (H0+a)%INT_32_CAP
  1434. H1 = (H1+b)%INT_32_CAP
  1435. H2 = (H2+c)%INT_32_CAP
  1436. H3 = (H3+d)%INT_32_CAP
  1437. H4 = (H4+e)%INT_32_CAP
  1438. H5 = (H5+f)%INT_32_CAP
  1439. H6 = (H6+g)%INT_32_CAP
  1440. H7 = (H7+h)%INT_32_CAP
  1441. end
  1442. return string.format("%08x%08x%08x%08x%08x%08x%08x%08x", H0, H1, H2, H3, H4, H5, H6, H7)
  1443. end
  1444. if ASSERTIONS_ENABLED then
  1445. assert(packUint32(255, 167, 125, 235) == 4289166827, "(SHA-256/224) packUint32 check 1")
  1446. assert(packUint32(255, 0, 125, 235) == 4278222315, "(SHA-256/224) packUint32 check 2")
  1447. local b0, b1, b2, b3 = unpackUint32(4278222315)
  1448. assert(b0 == 255, "(SHA-256/224) unpackUint32 check 1")
  1449. assert(b1 == 000, "(SHA-256/224) unpackUint32 check 2")
  1450. assert(b2 == 125, "(SHA-256/224) unpackUint32 check 3")
  1451. assert(b3 == 235, "(SHA-256/224) unpackUint32 check 4")
  1452. -- SHA-256 tests
  1453. assert(sha256("abc") == "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad", "(SHA-256) abc hash does not match")
  1454. assert(sha256("") == "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855", "(SHA-256) empty hash does not match")
  1455. assert(sha256("abcdbcdecdefdefgefghfghighijhijkijkljklmklmnlmnomnopnopq") == "248d6a61d20638b8e5c026930c3e6039a33ce45964ff2167f6ecedd419db06c1", "(SHA-256) 448 bit alphabet hash does not match")
  1456. assert(sha256("abcdefghbcdefghicdefghijdefghijkefghijklfghijklmghijklmnhijklmnoijklmnopjklmnopqklmnopqrlmnopqrsmnopqrstnopqrstu") == "cf5b16a778af8380036ce59e7b0492370b249b11e8f07a51afac45037afee9d1", "(SHA-256) 896 bit alphabet hash does not match")
  1457. assert(sha256("foo") == "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae", "(SHA-256) foo hash does not match")
  1458. assert(sha256("bar") == "fcde2b2edba56bf408601fb721fe9b5c338d10ee429ea04fae5511b68fbf8fb9", "(SHA-256) bar hash does not match")
  1459. assert(sha256("baz") == "baa5a0964d3320fbc0c6a922140453c8513ea24ab8fd0577034804a967248096", "(SHA-256) baz hash does not match")
  1460. if true then
  1461. assert(sha256(string.rep("e", 199999)) == "434cf81dca15a72777e811ed4ae9144f9272ca3c04ff9c2de1533bbbffed5449", "(SHA-256) e hash does not match")
  1462. assert(sha256(string.rep("a", 1e6)) == "cdc76e5c9914fb9281a1c7e284d73e67f1809a48a497200e046d39ccc7112cd0", "(SHA-256) million a hash does not match")
  1463. end
  1464. end
  1465. return sha256]]></ProtectedString>
  1466. <BinaryString name="Tags"></BinaryString>
  1467. </Properties>
  1468. </Item>
  1469. </Item>
  1470. <Item class="ModuleScript" referent="RBX4b14a91351d1404f89ac5d7683abdde5">
  1471. <Properties>
  1472. <Content name="LinkedSource"><null></null></Content>
  1473. <string name="Name">Logger</string>
  1474. <string name="ScriptGuid"></string>
  1475. <ProtectedString name="Source"><![CDATA[local RunService = game:GetService("RunService")
  1476. --local GameAnalyticsSendMessage
  1477. local logger = {
  1478. _infoLogEnabled = false,
  1479. _infoLogAdvancedEnabled = false,
  1480. _debugEnabled = RunService:IsStudio()
  1481. }
  1482. function logger:setDebugLog(enabled)
  1483. self._debugEnabled = enabled
  1484. end
  1485. function logger:setInfoLog(enabled)
  1486. self._infoLogEnabled = enabled
  1487. end
  1488. function logger:setVerboseLog(enabled)
  1489. self._infoLogAdvancedEnabled = enabled
  1490. end
  1491. function logger:i(format)
  1492. if not self._infoLogEnabled then
  1493. return
  1494. end
  1495. local m = "Info/GameAnalytics: " .. format
  1496. print(m)
  1497. -- GameAnalyticsSendMessage = GameAnalyticsSendMessage or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  1498. -- GameAnalyticsSendMessage:FireAllClients({
  1499. -- Text = m,
  1500. -- Font = Enum.Font.Arial,
  1501. -- Color = Color3.new(255, 255, 255),
  1502. -- FontSize = Enum.FontSize.Size96
  1503. -- })
  1504. end
  1505. function logger:w(format)
  1506. local m = "Warning/GameAnalytics: " .. format
  1507. warn(m)
  1508. -- GameAnalyticsSendMessage = GameAnalyticsSendMessage or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  1509. -- GameAnalyticsSendMessage:FireAllClients({
  1510. -- Text = m,
  1511. -- Font = Enum.Font.Arial,
  1512. -- Color = Color3.new(255, 255, 0),
  1513. -- FontSize = Enum.FontSize.Size96
  1514. -- })
  1515. end
  1516. function logger:e(format)
  1517. spawn(function ()
  1518. local m = "Error/GameAnalytics: " .. format
  1519. error(m, 0)
  1520. -- GameAnalyticsSendMessage = GameAnalyticsSendMessage or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  1521. -- GameAnalyticsSendMessage:FireAllClients({
  1522. -- Text = m,
  1523. -- Font = Enum.Font.Arial,
  1524. -- Color = Color3.new(255, 0, 0),
  1525. -- FontSize = Enum.FontSize.Size96
  1526. -- })
  1527. end)
  1528. end
  1529. function logger:d(format)
  1530. if not self._debugEnabled then
  1531. return
  1532. end
  1533. local m = "Debug/GameAnalytics: " .. format
  1534. print(m)
  1535. -- GameAnalyticsSendMessage = GameAnalyticsSendMessage or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  1536. -- GameAnalyticsSendMessage:FireAllClients({
  1537. -- Text = m,
  1538. -- Font = Enum.Font.Arial,
  1539. -- Color = Color3.new(255, 255, 255),
  1540. -- FontSize = Enum.FontSize.Size96
  1541. -- })
  1542. end
  1543. function logger:ii(format)
  1544. if not self._infoLogAdvancedEnabled then
  1545. return
  1546. end
  1547. local m = "Verbose/GameAnalytics: " .. format
  1548. print(m)
  1549. -- GameAnalyticsSendMessage = GameAnalyticsSendMessage or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsSendMessage")
  1550. -- GameAnalyticsSendMessage:FireAllClients({
  1551. -- Text = m,
  1552. -- Font = Enum.Font.Arial,
  1553. -- Color = Color3.new(255, 255, 255),
  1554. -- FontSize = Enum.FontSize.Size96
  1555. -- })
  1556. end
  1557. return logger
  1558. ]]></ProtectedString>
  1559. <BinaryString name="Tags"></BinaryString>
  1560. </Properties>
  1561. </Item>
  1562. <Item class="ModuleScript" referent="RBX0fd16702bea24d64882fe169a7f9157d">
  1563. <Properties>
  1564. <Content name="LinkedSource"><null></null></Content>
  1565. <string name="Name">Store</string>
  1566. <string name="ScriptGuid"></string>
  1567. <ProtectedString name="Source"><![CDATA[local DS = game:GetService("DataStoreService")
  1568. local RunService = game:GetService("RunService")
  1569. local store = {
  1570. PlayerDS = RunService:IsStudio() and {} or DS:GetDataStore("GA_PlayerDS_1.0.0"),
  1571. AutoSaveData = 180, --Set to 0 to disable
  1572. BasePlayerData = {
  1573. Sessions = 0,
  1574. Transactions = 0,
  1575. ProgressionTries = {},
  1576. CurrentCustomDimension01 = "",
  1577. CurrentCustomDimension02 = "",
  1578. CurrentCustomDimension03 = "",
  1579. InitAuthorized = false,
  1580. SdkConfig = {},
  1581. ClientServerTimeOffset = 0,
  1582. Configurations = {},
  1583. CommandCenterIsReady = false,
  1584. PlayerTeleporting = false
  1585. },
  1586. DataToSave = {
  1587. "Sessions",
  1588. "Transactions",
  1589. "ProgressionTries",
  1590. "CurrentCustomDimension01",
  1591. "CurrentCustomDimension02",
  1592. "CurrentCustomDimension03"
  1593. },
  1594. --Cache
  1595. PlayerCache = {},
  1596. EventsQueue = {}
  1597. }
  1598. function store:GetPlayerData(Player)
  1599. local PlayerData
  1600. local success, _ = pcall(function()
  1601. PlayerData = RunService:IsStudio() and {} or (store.PlayerDS:GetAsync(Player.UserId) or {})
  1602. end)
  1603. if not success then
  1604. PlayerData = {}
  1605. end
  1606. return PlayerData
  1607. end
  1608. function store:GetErrorDataStore(scope)
  1609. local ErrorDS
  1610. local success, _ = pcall(function()
  1611. ErrorDS = RunService:IsStudio() and {} or DS:GetDataStore("GA_ErrorDS_1.0.0", scope)
  1612. end)
  1613. if not success then
  1614. ErrorDS = {}
  1615. end
  1616. return ErrorDS
  1617. end
  1618. function store:SavePlayerData(Player)
  1619. --Variables
  1620. local PlayerData = store.PlayerCache[Player.UserId]
  1621. local SavePlayerData = {}
  1622. if not PlayerData then
  1623. return
  1624. end
  1625. --Fill
  1626. for _, key in pairs(store.DataToSave) do
  1627. SavePlayerData[key] = PlayerData[key]
  1628. end
  1629. --Save
  1630. if not RunService:IsStudio() then
  1631. pcall(function()
  1632. store.PlayerDS:SetAsync(Player.UserId, SavePlayerData)
  1633. end)
  1634. end
  1635. end
  1636. function store:IncrementErrorCount(ErrorDS, ErrorKey, step)
  1637. if not ErrorKey then
  1638. return
  1639. end
  1640. local count = 0
  1641. --Increment count
  1642. if not RunService:IsStudio() then
  1643. pcall(function()
  1644. count = ErrorDS:IncrementAsync(ErrorKey, step)
  1645. end)
  1646. end
  1647. return count
  1648. end
  1649. return store
  1650. ]]></ProtectedString>
  1651. <BinaryString name="Tags"></BinaryString>
  1652. </Properties>
  1653. </Item>
  1654. <Item class="ModuleScript" referent="RBXbba5a886fb3443b989529266284f613e">
  1655. <Properties>
  1656. <Content name="LinkedSource"><null></null></Content>
  1657. <string name="Name">Events</string>
  1658. <string name="ScriptGuid"></string>
  1659. <ProtectedString name="Source"><![CDATA[local events = {
  1660. ProcessEventsInterval = 8,
  1661. GameKey = "",
  1662. SecretKey = "",
  1663. _build = "",
  1664. _availableResourceCurrencies = {},
  1665. _availableResourceItemTypes = {},
  1666. }
  1667. local store = require(script.Parent.Store)
  1668. local logger = require(script.Parent.Logger)
  1669. local version = require(script.Parent.Version)
  1670. local validation = require(script.Parent.Validation)
  1671. local threading = require(script.Parent.Threading)
  1672. local http_api = require(script.Parent.HttpApi)
  1673. local utilities = require(script.Parent.Utilities)
  1674. local GAResourceFlowType = require(script.Parent.GAResourceFlowType)
  1675. local GAProgressionStatus = require(script.Parent.GAProgressionStatus)
  1676. local GAErrorSeverity = require(script.Parent.GAErrorSeverity)
  1677. local HTTP = game:GetService("HttpService")
  1678. local CategorySessionStart = "user"
  1679. local CategorySessionEnd = "session_end"
  1680. local CategoryBusiness = "business"
  1681. local CategoryResource = "resource"
  1682. local CategoryProgression = "progression"
  1683. local CategoryDesign = "design"
  1684. local CategoryError = "error"
  1685. local MAX_EVENTS_TO_SEND_IN_ONE_BATCH = 500
  1686. local MAX_AGGREGATED_EVENTS = 2000
  1687. local function addDimensionsToEvent(playerId, eventData)
  1688. if not eventData then
  1689. return
  1690. end
  1691. if not playerId then
  1692. return
  1693. end
  1694. local PlayerData = store.PlayerCache[playerId]
  1695. -- add to dict (if not nil)
  1696. if PlayerData and PlayerData.CurrentCustomDimension01 and #PlayerData.CurrentCustomDimension01 > 0 then
  1697. eventData["custom_01"] = PlayerData.CurrentCustomDimension01
  1698. end
  1699. if PlayerData and PlayerData.CurrentCustomDimension02 and #PlayerData.CurrentCustomDimension02 > 0 then
  1700. eventData["custom_02"] = PlayerData.CurrentCustomDimension02
  1701. end
  1702. if PlayerData and PlayerData.CurrentCustomDimension03 and #PlayerData.CurrentCustomDimension03 > 0 then
  1703. eventData["custom_03"] = PlayerData.CurrentCustomDimension03
  1704. end
  1705. end
  1706. local function getClientTsAdjusted(playerId)
  1707. if not playerId then
  1708. return os.time()
  1709. end
  1710. local PlayerData = store.PlayerCache[playerId]
  1711. local clientTs = os.time()
  1712. local clientTsAdjustedInteger = clientTs + PlayerData.ClientServerTimeOffset
  1713. if validation:validateClientTs(clientTsAdjustedInteger) then
  1714. return clientTsAdjustedInteger;
  1715. else
  1716. return clientTs
  1717. end
  1718. end
  1719. local DUMMY_SESSION_ID = HTTP:GenerateGUID(false):lower()
  1720. local function getEventAnnotations(playerId)
  1721. local PlayerData
  1722. local id
  1723. if playerId then
  1724. id = playerId
  1725. PlayerData = store.PlayerCache[playerId]
  1726. else
  1727. id = "DummyId"
  1728. PlayerData = {
  1729. OS = "uwp_desktop 0.0.0",
  1730. Platform = "uwp_desktop",
  1731. SessionID = DUMMY_SESSION_ID,
  1732. Sessions = 1
  1733. }
  1734. end
  1735. local annotations = {
  1736. -- ---- REQUIRED ----
  1737. -- collector event API version
  1738. ["v"] = 2,
  1739. -- User identifier
  1740. ["user_id"] = tostring(id),
  1741. -- Client Timestamp (the adjusted timestamp)
  1742. ["client_ts"] = getClientTsAdjusted(playerId),
  1743. -- SDK version
  1744. ["sdk_version"] = "roblox " .. version.SdkVersion,
  1745. -- Operation system version
  1746. ["os_version"] = PlayerData.OS,
  1747. -- Device make (hardcoded to apple)
  1748. ["manufacturer"] = "unknown",
  1749. -- Device version
  1750. ["device"] = "unknown",
  1751. -- Platform (operating system)
  1752. ["platform"] = PlayerData.Platform,
  1753. -- Session identifier
  1754. ["session_id"] = PlayerData.SessionID,
  1755. -- Session number
  1756. ["session_num"] = PlayerData.Sessions
  1757. }
  1758. if validation:validateBuild(events._build) then
  1759. annotations["build"] = events._build
  1760. end
  1761. return annotations
  1762. end
  1763. local function addEventToStore(playerId, eventData)
  1764. -- Get default annotations
  1765. local ev = getEventAnnotations(playerId)
  1766. -- Merge with eventData
  1767. for k,_ in pairs(eventData) do
  1768. ev[k] = eventData[k]
  1769. end
  1770. -- Create json string representation
  1771. local json = HTTP:JSONEncode(ev)
  1772. -- output if VERBOSE LOG enabled
  1773. logger:ii("Event added to queue: " .. json)
  1774. -- Add to store
  1775. store.EventsQueue[#store.EventsQueue + 1] = ev
  1776. end
  1777. local function dequeueMaxEvents()
  1778. if #store.EventsQueue <= MAX_EVENTS_TO_SEND_IN_ONE_BATCH then
  1779. local eventsQueue = store.EventsQueue
  1780. store.EventsQueue = {}
  1781. return eventsQueue
  1782. else
  1783. logger:w(("More than %d events queued! Sending %d."):format(MAX_EVENTS_TO_SEND_IN_ONE_BATCH, MAX_EVENTS_TO_SEND_IN_ONE_BATCH))
  1784. if #self.EventsQueue > MAX_AGGREGATED_EVENTS then
  1785. logger:w(("DROPPING EVENTS: More than %d events queued!"):format(MAX_AGGREGATED_EVENTS))
  1786. end
  1787. -- Expensive operation to get ordered events cleared out (O(n))
  1788. local eventsQueue = {}
  1789. for i=1, MAX_EVENTS_TO_SEND_IN_ONE_BATCH do
  1790. eventsQueue[i] = store.EventsQueue[i]
  1791. end
  1792. -- Shift everything down and overwrite old events
  1793. local eventCount = #self._events
  1794. for i=1, math.min(MAX_AGGREGATED_EVENTS, eventCount) do
  1795. store.EventsQueue[i] = store.EventsQueue[i + MAX_EVENTS_TO_SEND_IN_ONE_BATCH]
  1796. end
  1797. -- Clear additional events
  1798. for i=MAX_AGGREGATED_EVENTS+1, eventCount do
  1799. store.EventsQueue[i] = nil
  1800. end
  1801. return eventsQueue
  1802. end
  1803. end
  1804. local function processEvents()
  1805. local queue = dequeueMaxEvents()
  1806. if #queue == 0 then
  1807. logger:i("Event queue: No events to send")
  1808. return
  1809. end
  1810. -- Log
  1811. logger:i("Event queue: Sending " .. tostring(#queue) .. " events.")
  1812. local eventsResult = http_api:sendEventsInArray(events.GameKey, events.SecretKey, queue)
  1813. local statusCode = eventsResult.statusCode
  1814. local responseBody = eventsResult.body
  1815. if statusCode == http_api.EGAHTTPApiResponse.Ok and responseBody then
  1816. logger:i("Event queue: " .. tostring(#queue) .. " events sent.")
  1817. else
  1818. if statusCode == http_api.EGAHTTPApiResponse.NoResponse then
  1819. logger:w("Event queue: Failed to send events to collector - Retrying next time")
  1820. for _,e in pairs(queue) do
  1821. if #store.EventsQueue < MAX_AGGREGATED_EVENTS then
  1822. store.EventsQueue[#store.EventsQueue + 1] = e
  1823. else
  1824. break
  1825. end
  1826. end
  1827. else
  1828. if statusCode == http_api.EGAHTTPApiResponse.BadRequest and responseBody then
  1829. logger:w("Event queue: " .. tostring(#queue) .. " events sent. " .. tostring(#responseBody) .. " events failed GA server validation.")
  1830. else
  1831. logger:w("Event queue: Failed to send events.")
  1832. end
  1833. end
  1834. end
  1835. end
  1836. function events:processEventQueue()
  1837. processEvents()
  1838. threading:scheduleTimer(events.ProcessEventsInterval, function()
  1839. events:processEventQueue()
  1840. end)
  1841. end
  1842. function events:setBuild(build)
  1843. if not validation:validateBuild(build) then
  1844. logger:w("Validation fail - configure build: Cannot be null, empty or above 32 length. String: " .. build)
  1845. return
  1846. end
  1847. self._build = build
  1848. logger:i("Set build version: " .. build)
  1849. end
  1850. function events:setAvailableResourceCurrencies(availableResourceCurrencies)
  1851. if not validation:validateResourceCurrencies(availableResourceCurrencies) then
  1852. return
  1853. end
  1854. self._availableResourceCurrencies = availableResourceCurrencies
  1855. logger:i("Set available resource currencies: (" .. table.concat(availableResourceCurrencies, ", ") .. ")")
  1856. end
  1857. function events:setAvailableResourceItemTypes(availableResourceItemTypes)
  1858. if not validation:validateResourceCurrencies(availableResourceItemTypes) then
  1859. return
  1860. end
  1861. self._availableResourceItemTypes = availableResourceItemTypes
  1862. logger:i("Set available resource item types: (" .. table.concat(availableResourceItemTypes, ", ") .. ")")
  1863. end
  1864. function events:addSessionStartEvent(playerId, teleportData)
  1865. local PlayerData = store.PlayerCache[playerId]
  1866. if teleportData then
  1867. PlayerData.Sessions = teleportData.Sessions
  1868. else
  1869. local eventDict = {}
  1870. -- Event specific data
  1871. eventDict["category"] = CategorySessionStart
  1872. -- Increment session number and persist
  1873. PlayerData.Sessions = PlayerData.Sessions + 1
  1874. -- Add custom dimensions
  1875. addDimensionsToEvent(playerId, eventDict)
  1876. -- Add to store
  1877. addEventToStore(playerId, eventDict)
  1878. logger:i("Add SESSION START event")
  1879. processEvents()
  1880. end
  1881. end
  1882. function events:addSessionEndEvent(playerId)
  1883. local PlayerData = store.PlayerCache[playerId]
  1884. local session_start_ts = PlayerData.SessionStart
  1885. local client_ts_adjusted = getClientTsAdjusted(playerId)
  1886. local sessionLength = client_ts_adjusted - session_start_ts
  1887. if sessionLength < 0 then
  1888. -- Should never happen.
  1889. -- Could be because of edge cases regarding time altering on device.
  1890. logger:w("Session length was calculated to be less then 0. Should not be possible. Resetting to 0.")
  1891. sessionLength = 0
  1892. end
  1893. -- Event specific data
  1894. local eventDict = {}
  1895. eventDict["category"] = CategorySessionEnd
  1896. eventDict["length"] = sessionLength
  1897. -- Add custom dimensions
  1898. addDimensionsToEvent(playerId, eventDict)
  1899. -- Add to store
  1900. addEventToStore(playerId, eventDict)
  1901. logger:i("Add SESSION END event.")
  1902. processEvents()
  1903. end
  1904. function events:addBusinessEvent(playerId, currency, amount, itemType, itemId, cartType)
  1905. -- Validate event params
  1906. if not validation:validateBusinessEvent(currency, amount, cartType, itemType, itemId) then
  1907. -- TODO: add sdk error event
  1908. return
  1909. end
  1910. -- Create empty eventData
  1911. local eventDict = {}
  1912. -- Increment transaction number and persist
  1913. local PlayerData = store.PlayerCache[playerId]
  1914. PlayerData.Transactions = PlayerData.Transactions + 1
  1915. -- Required
  1916. eventDict["event_id"] = itemType .. ":" .. itemId
  1917. eventDict["category"] = CategoryBusiness
  1918. eventDict["currency"] = currency
  1919. eventDict["amount"] = amount
  1920. eventDict["transaction_num"] = PlayerData.Transactions
  1921. -- Optional
  1922. if not utilities:isStringNullOrEmpty(cartType) then
  1923. eventDict["cart_type"] = cartType
  1924. end
  1925. -- Add custom dimensions
  1926. addDimensionsToEvent(playerId, eventDict)
  1927. logger:i("Add BUSINESS event: {currency:" .. currency .. ", amount:" .. tostring(amount) .. ", itemType:" .. itemType .. ", itemId:" .. itemId .. ", cartType:" .. cartType .. "}")
  1928. -- Send to store
  1929. addEventToStore(playerId, eventDict)
  1930. end
  1931. function events:addResourceEvent(playerId, flowType, currency, amount, itemType, itemId)
  1932. -- Validate event params
  1933. if not validation:validateResourceEvent(GAResourceFlowType, flowType, currency, amount, itemType, itemId, self._availableResourceCurrencies, self._availableResourceItemTypes) then
  1934. -- TODO: add sdk error event
  1935. return
  1936. end
  1937. -- If flow type is sink reverse amount
  1938. if flowType == GAResourceFlowType.Sink then
  1939. amount = (-1 * amount)
  1940. end
  1941. -- Create empty eventData
  1942. local eventDict = {}
  1943. -- insert event specific values
  1944. local flowTypeString = GAResourceFlowType[flowType]
  1945. eventDict["event_id"] = flowTypeString .. ":" .. currency .. ":" .. itemType .. ":" .. itemId
  1946. eventDict["category"] = CategoryResource
  1947. eventDict["amount"] = amount
  1948. -- Add custom dimensions
  1949. addDimensionsToEvent(playerId, eventDict)
  1950. logger:i("Add RESOURCE event: {currency:" .. currency .. ", amount:" .. tostring(amount) .. ", itemType:" .. itemType .. ", itemId:" .. itemId .. "}")
  1951. -- Send to store
  1952. addEventToStore(playerId, eventDict)
  1953. end
  1954. function events:addProgressionEvent(playerId, progressionStatus, progression01, progression02, progression03, score)
  1955. -- Validate event params
  1956. if not validation:validateProgressionEvent(GAProgressionStatus, progressionStatus, progression01, progression02, progression03) then
  1957. -- TODO: add sdk error event
  1958. return
  1959. end
  1960. -- Create empty eventData
  1961. local eventDict = {}
  1962. -- Progression identifier
  1963. local progressionIdentifier
  1964. if utilities:isStringNullOrEmpty(progression02) then
  1965. progressionIdentifier = progression01
  1966. elseif utilities:isStringNullOrEmpty(progression03) then
  1967. progressionIdentifier = progression01 .. ":" .. progression02
  1968. else
  1969. progressionIdentifier = progression01 .. ":" .. progression02 .. ":" .. progression03
  1970. end
  1971. local statusString = GAProgressionStatus[progressionStatus]
  1972. -- Append event specifics
  1973. eventDict["category"] = CategoryProgression
  1974. eventDict["event_id"] = statusString .. ":" .. progressionIdentifier
  1975. -- Attempt
  1976. local attempt_num = 0
  1977. -- Add score if specified and status is not start
  1978. if score ~= nil and progressionStatus ~= GAProgressionStatus.Start then
  1979. eventDict["score"] = score
  1980. end
  1981. local PlayerData = store.PlayerCache[playerId]
  1982. -- Count attempts on each progression fail and persist
  1983. if progressionStatus == GAProgressionStatus.Fail then
  1984. -- Increment attempt number
  1985. local progressionTries = PlayerData.ProgressionTries[progressionIdentifier] or 0
  1986. PlayerData.ProgressionTries[progressionIdentifier] = progressionTries + 1
  1987. end
  1988. -- increment and add attempt_num on complete and delete persisted
  1989. if progressionStatus == GAProgressionStatus.Complete then
  1990. -- Increment attempt number
  1991. local progressionTries = PlayerData.ProgressionTries[progressionIdentifier] or 0
  1992. PlayerData.ProgressionTries[progressionIdentifier] = progressionTries + 1
  1993. -- Add to event
  1994. attempt_num = PlayerData.ProgressionTries[progressionIdentifier]
  1995. eventDict["attempt_num"] = attempt_num
  1996. -- Clear
  1997. PlayerData.ProgressionTries[progressionIdentifier] = 0
  1998. end
  1999. -- Add custom dimensions
  2000. addDimensionsToEvent(playerId, eventDict)
  2001. local progression02String = ""
  2002. if not utilities:isStringNullOrEmpty(progression02) then
  2003. progression02String = progression02
  2004. end
  2005. local progression03String = ""
  2006. if not utilities:isStringNullOrEmpty(progression03) then
  2007. progression03String = progression03
  2008. end
  2009. logger:i("Add PROGRESSION event: {status:" .. statusString .. ", progression01:" .. progression01 .. ", progression02:" .. progression02String .. ", progression03:" .. progression03String .. ", score:" .. tostring(score) .. ", attempt:" .. tostring(attempt_num) .. "}")
  2010. -- Send to store
  2011. addEventToStore(playerId, eventDict)
  2012. end
  2013. function events:addDesignEvent(playerId, eventId, value)
  2014. -- Validate
  2015. if not validation:validateDesignEvent(eventId) then
  2016. -- TODO: add sdk error event
  2017. return
  2018. end
  2019. -- Create empty eventData
  2020. local eventData = {}
  2021. -- Append event specifics
  2022. eventData["category"] = CategoryDesign
  2023. eventData["event_id"] = eventId
  2024. if value ~= nil then
  2025. eventData["value"] = value
  2026. end
  2027. -- Add custom dimensions
  2028. addDimensionsToEvent(playerId, eventData)
  2029. logger:i("Add DESIGN event: {eventId:" .. eventId .. ", value:" .. tostring(value) .. "}")
  2030. -- Send to store
  2031. addEventToStore(playerId, eventData)
  2032. end
  2033. function events:addErrorEvent(playerId, severity, message)
  2034. -- Validate
  2035. if not validation:validateErrorEvent(GAErrorSeverity, severity, message) then
  2036. -- TODO: add sdk error event
  2037. return
  2038. end
  2039. -- Create empty eventData
  2040. local eventData = {}
  2041. local severityString = GAErrorSeverity[severity]
  2042. eventData["category"] = CategoryError
  2043. eventData["severity"] = severityString
  2044. eventData["message"] = message
  2045. -- Add custom dimensions
  2046. addDimensionsToEvent(playerId, eventData)
  2047. local messageString = ""
  2048. if not utilities:isStringNullOrEmpty(message) then
  2049. messageString = message
  2050. end
  2051. logger:i("Add ERROR event: {severity:" .. severityString .. ", message:" .. messageString .. "}")
  2052. -- Send to store
  2053. addEventToStore(playerId, eventData)
  2054. end
  2055. return events
  2056. ]]></ProtectedString>
  2057. <BinaryString name="Tags"></BinaryString>
  2058. </Properties>
  2059. </Item>
  2060. <Item class="ModuleScript" referent="RBX5190ffbee57e4a9b94c4e0a74eb63cab">
  2061. <Properties>
  2062. <Content name="LinkedSource"><null></null></Content>
  2063. <string name="Name">Utilities</string>
  2064. <string name="ScriptGuid"></string>
  2065. <ProtectedString name="Source"><![CDATA[local utilities = {}
  2066. function utilities:isStringNullOrEmpty(s)
  2067. return (not s) or #s == 0
  2068. end
  2069. function utilities:stringArrayContainsString(array, search)
  2070. if #array == 0 then
  2071. return false
  2072. end
  2073. for _,s in pairs(array) do
  2074. if s == search then
  2075. return true
  2076. end
  2077. end
  2078. return false
  2079. end
  2080. return utilities
  2081. ]]></ProtectedString>
  2082. <BinaryString name="Tags"></BinaryString>
  2083. </Properties>
  2084. </Item>
  2085. <Item class="ModuleScript" referent="RBX02095c98e62847afaf7fdea69dab9f66">
  2086. <Properties>
  2087. <Content name="LinkedSource"><null></null></Content>
  2088. <string name="Name">Version</string>
  2089. <string name="ScriptGuid">{08f3dc9a-fca3-4b1b-8436-e07880f9095e}</string>
  2090. <ProtectedString name="Source"><![CDATA[local version = {
  2091. SdkVersion = "1.4.2"
  2092. }
  2093. return version
  2094. ]]></ProtectedString>
  2095. <BinaryString name="Tags"></BinaryString>
  2096. </Properties>
  2097. </Item>
  2098. <Item class="ModuleScript" referent="RBX8bd464ce27cb4e64ab219c6cb8ccb43d">
  2099. <Properties>
  2100. <Content name="LinkedSource"><null></null></Content>
  2101. <string name="Name">State</string>
  2102. <string name="ScriptGuid"></string>
  2103. <ProtectedString name="Source"><![CDATA[local state = {
  2104. _availableCustomDimensions01 = {},
  2105. _availableCustomDimensions02 = {},
  2106. _availableCustomDimensions03 = {},
  2107. _enableEventSubmission = true,
  2108. Initialized = false,
  2109. ReportErrors = true,
  2110. AutomaticSendBusinessEvents = true
  2111. }
  2112. local validation = require(script.Parent.Validation)
  2113. local logger = require(script.Parent.Logger)
  2114. local http_api = require(script.Parent.HttpApi)
  2115. local store = require(script.Parent.Store)
  2116. local events = require(script.Parent.Events)
  2117. local HTTP = game:GetService("HttpService")
  2118. local GameAnalyticsCommandCenter
  2119. local function getClientTsAdjusted(playerId)
  2120. local PlayerData = store.PlayerCache[playerId]
  2121. if not PlayerData then
  2122. return os.time()
  2123. end
  2124. local clientTs = os.time()
  2125. local clientTsAdjustedInteger = clientTs + PlayerData.ClientServerTimeOffset
  2126. if validation:validateClientTs(clientTsAdjustedInteger) then
  2127. return clientTsAdjustedInteger;
  2128. else
  2129. return clientTs
  2130. end
  2131. end
  2132. local function populateConfigurations(player)
  2133. local PlayerData = store.PlayerCache[player.UserId]
  2134. local sdkConfig = PlayerData.SdkConfig
  2135. if sdkConfig["configurations"] then
  2136. local configurations = sdkConfig["configurations"]
  2137. for _,configuration in pairs(configurations) do
  2138. if configuration then
  2139. local key = configuration["key"] or ""
  2140. local start_ts = configuration["start"] or 0
  2141. local end_ts = configuration["end"] or math.huge
  2142. local client_ts_adjusted = getClientTsAdjusted(player.UserId)
  2143. if #key > 0 and configuration["value"] and client_ts_adjusted > start_ts and client_ts_adjusted < end_ts then
  2144. PlayerData.Configurations[key] = configuration["value"]
  2145. logger:d("configuration added: key=" .. configuration["key"] .. ", value=" .. configuration["value"])
  2146. end
  2147. end
  2148. end
  2149. end
  2150. PlayerData.CommandCenterIsReady = true
  2151. GameAnalyticsCommandCenter = GameAnalyticsCommandCenter or game:GetService("ReplicatedStorage"):WaitForChild("GameAnalyticsCommandCenter")
  2152. GameAnalyticsCommandCenter:FireClient(player, PlayerData.Configurations)
  2153. end
  2154. function state:sessionIsStarted(playerId)
  2155. local PlayerData = store.PlayerCache[playerId]
  2156. if not PlayerData then
  2157. return false
  2158. end
  2159. return PlayerData.SessionStart ~= 0
  2160. end
  2161. function state:isEnabled(playerId)
  2162. local PlayerData = store.PlayerCache[playerId]
  2163. if not PlayerData then
  2164. return false
  2165. elseif PlayerData.SdkConfig and PlayerData.SdkConfig["enabled"] == false then
  2166. return false
  2167. elseif not PlayerData.InitAuthorized then
  2168. return false
  2169. else
  2170. return true
  2171. end
  2172. end
  2173. function state:validateAndFixCurrentDimensions(playerId)
  2174. local PlayerData = store.PlayerCache[playerId]
  2175. -- validate that there are no current dimension01 not in list
  2176. if not validation:validateDimension(self._availableCustomDimensions01, PlayerData.CurrentCustomDimension01) then
  2177. logger:d("Invalid dimension01 found in variable. Setting to nil. Invalid dimension: " .. PlayerData.CurrentCustomDimension01)
  2178. end
  2179. -- validate that there are no current dimension02 not in list
  2180. if not validation:validateDimension(self._availableCustomDimensions02, PlayerData.CurrentCustomDimension02) then
  2181. logger:d("Invalid dimension02 found in variable. Setting to nil. Invalid dimension: " .. PlayerData.CurrentCustomDimension02)
  2182. end
  2183. -- validate that there are no current dimension03 not in list
  2184. if not validation:validateDimension(self._availableCustomDimensions03, PlayerData.CurrentCustomDimension03) then
  2185. logger:d("Invalid dimension03 found in variable. Setting to nil. Invalid dimension: " .. PlayerData.CurrentCustomDimension03)
  2186. end
  2187. end
  2188. function state:setAvailableCustomDimensions01(availableCustomDimensions)
  2189. if not validation:validateCustomDimensions(availableCustomDimensions) then
  2190. return
  2191. end
  2192. self._availableCustomDimensions01 = availableCustomDimensions
  2193. logger:i("Set available custom01 dimension values: (" .. table.concat(availableCustomDimensions, ", ") .. ")")
  2194. end
  2195. function state:setAvailableCustomDimensions02(availableCustomDimensions)
  2196. if not validation:validateCustomDimensions(availableCustomDimensions) then
  2197. return
  2198. end
  2199. self._availableCustomDimensions02 = availableCustomDimensions
  2200. logger:i("Set available custom02 dimension values: (" .. table.concat(availableCustomDimensions, ", ") .. ")")
  2201. end
  2202. function state:setAvailableCustomDimensions03(availableCustomDimensions)
  2203. if not validation:validateCustomDimensions(availableCustomDimensions) then
  2204. return
  2205. end
  2206. self._availableCustomDimensions03 = availableCustomDimensions
  2207. logger:i("Set available custom03 dimension values: (" .. table.concat(availableCustomDimensions, ", ") .. ")")
  2208. end
  2209. function state:setEventSubmission(flag)
  2210. self._enableEventSubmission = flag
  2211. end
  2212. function state:isEventSubmissionEnabled()
  2213. return self._enableEventSubmission
  2214. end
  2215. function state:setCustomDimension01(playerId, dimension)
  2216. local PlayerData = store.PlayerCache[playerId]
  2217. PlayerData.CurrentCustomDimension01 = dimension
  2218. end
  2219. function state:setCustomDimension02(playerId, dimension)
  2220. local PlayerData = store.PlayerCache[playerId]
  2221. PlayerData.CurrentCustomDimension02 = dimension
  2222. end
  2223. function state:setCustomDimension03(playerId, dimension)
  2224. local PlayerData = store.PlayerCache[playerId]
  2225. PlayerData.CurrentCustomDimension03 = dimension
  2226. end
  2227. function state:startNewSession(player, teleportData)
  2228. if state:isEventSubmissionEnabled() then
  2229. logger:i("Starting a new session.")
  2230. end
  2231. local PlayerData = store.PlayerCache[player.UserId]
  2232. -- make sure the current custom dimensions are valid
  2233. state:validateAndFixCurrentDimensions(player.UserId)
  2234. local initResult = http_api:initRequest(events.GameKey, events.SecretKey, PlayerData, player.UserId)
  2235. local statusCode = initResult.statusCode
  2236. local responseBody = initResult.body
  2237. if statusCode == http_api.EGAHTTPApiResponse.Ok and responseBody then
  2238. -- set the time offset - how many seconds the local time is different from servertime
  2239. local timeOffsetSeconds = 0
  2240. local serverTs = responseBody["server_ts"] or -1
  2241. if serverTs > 0 then
  2242. local clientTs = os.time()
  2243. timeOffsetSeconds = serverTs - clientTs
  2244. end
  2245. responseBody["time_offset"] = timeOffsetSeconds
  2246. PlayerData.SdkConfig = responseBody
  2247. PlayerData.InitAuthorized = true
  2248. elseif statusCode == http_api.EGAHTTPApiResponse.Unauthorized then
  2249. logger:w("Initialize SDK failed - Unauthorized")
  2250. PlayerData.InitAuthorized = false
  2251. else
  2252. -- log the status if no connection
  2253. if statusCode == http_api.EGAHTTPApiResponse.NoResponse or statusCode == http_api.EGAHTTPApiResponse.RequestTimeout then
  2254. logger:i("Init call (session start) failed - no response. Could be offline or timeout.")
  2255. elseif statusCode == http_api.EGAHTTPApiResponse.BadResponse or statusCode == http_api.EGAHTTPApiResponse.JsonEncodeFailed or statusCode == http_api.EGAHTTPApiResponse.JsonDecodeFailed then
  2256. logger:i("Init call (session start) failed - bad response. Could be bad response from proxy or GA servers.")
  2257. elseif statusCode == http_api.EGAHTTPApiResponse.BadRequest or statusCode == http_api.EGAHTTPApiResponse.UnknownResponseCode then
  2258. logger:i("Init call (session start) failed - bad request or unknown response.")
  2259. end
  2260. PlayerData.InitAuthorized = true
  2261. end
  2262. -- set offset in state (memory) from current config (config could be from cache etc.)
  2263. PlayerData.ClientServerTimeOffset = PlayerData.SdkConfig["time_offset"] or 0
  2264. -- populate configurations
  2265. populateConfigurations(player)
  2266. if not state:isEnabled(player.UserId) then
  2267. logger:w("Could not start session: SDK is disabled.")
  2268. return
  2269. end
  2270. if teleportData then
  2271. PlayerData.SessionID = teleportData.SessionID
  2272. PlayerData.SessionStart = teleportData.SessionStart
  2273. else
  2274. PlayerData.SessionID = HTTP:GenerateGUID(false):lower()
  2275. PlayerData.SessionStart = getClientTsAdjusted(player.UserId)
  2276. end
  2277. if state:isEventSubmissionEnabled() then
  2278. events:addSessionStartEvent(player.UserId, teleportData)
  2279. end
  2280. end
  2281. function state:endSession(playerId)
  2282. if state.Initialized and state:isEventSubmissionEnabled() then
  2283. logger:i("Ending session.")
  2284. if state:isEnabled(playerId) and state:sessionIsStarted(playerId) then
  2285. events:addSessionEndEvent(playerId)
  2286. store.PlayerCache[playerId] = nil
  2287. end
  2288. end
  2289. end
  2290. function state:getConfigurationStringValue(playerId, key, defaultValue)
  2291. local PlayerData = store.PlayerCache[playerId]
  2292. return PlayerData.Configurations[key] or defaultValue
  2293. end
  2294. function state:isCommandCenterReady(playerId)
  2295. local PlayerData = store.PlayerCache[playerId]
  2296. return PlayerData.CommandCenterIsReady
  2297. end
  2298. function state:getConfigurationsContentAsString(playerId)
  2299. local PlayerData = store.PlayerCache[playerId]
  2300. return HTTP:JSONEncode(PlayerData.Configurations)
  2301. end
  2302. return state
  2303. ]]></ProtectedString>
  2304. <BinaryString name="Tags"></BinaryString>
  2305. </Properties>
  2306. </Item>
  2307. <Item class="ModuleScript" referent="RBX4f67f666894641cf9fa8b191f2a175c9">
  2308. <Properties>
  2309. <Content name="LinkedSource"><null></null></Content>
  2310. <string name="Name">Validation</string>
  2311. <string name="ScriptGuid"></string>
  2312. <ProtectedString name="Source"><![CDATA[local validation = {}
  2313. local logger = require(script.Parent.Logger)
  2314. local utilities = require(script.Parent.Utilities)
  2315. function validation:validateCustomDimensions(customDimensions)
  2316. return validation:validateArrayOfStrings(20, 32, false, "custom dimensions", customDimensions)
  2317. end
  2318. function validation:validateDimension(dimensions, dimension)
  2319. -- allow nil
  2320. if utilities:isStringNullOrEmpty(dimension) then
  2321. return true
  2322. end
  2323. if not utilities:stringArrayContainsString(dimensions, dimension) then
  2324. return false
  2325. end
  2326. return true
  2327. end
  2328. function validation:validateResourceCurrencies(resourceCurrencies)
  2329. if not validation:validateArrayOfStrings(20, 64, false, "resource currencies", resourceCurrencies) then
  2330. return false
  2331. end
  2332. -- validate each string for regex
  2333. for _,resourceCurrency in pairs(resourceCurrencies) do
  2334. if not string.find(resourceCurrency, "^[A-Za-z]+$") then
  2335. logger:w("resource currencies validation failed: a resource currency can only be A-Z, a-z. String was: " .. resourceCurrency)
  2336. return false
  2337. end
  2338. end
  2339. return true
  2340. end
  2341. function validation:validateResourceItemTypes(resourceItemTypes)
  2342. if not validation:validateArrayOfStrings(20, 32, false, "resource item types", resourceItemTypes) then
  2343. return false
  2344. end
  2345. -- validate each string for regex
  2346. for _,resourceItemType in pairs(resourceItemTypes) do
  2347. if not validation:validateEventPartCharacters(resourceItemType) then
  2348. logger:w("resource item types validation failed: a resource item type cannot contain other characters than A-z, 0-9, -_., ()!?. String was: " .. resourceItemType)
  2349. return false
  2350. end
  2351. end
  2352. return true
  2353. end
  2354. function validation:validateEventPartCharacters(eventPart)
  2355. if not string.find(eventPart, "^[A-Za-z0-9%s%-_%.%(%)!%?]+$") then
  2356. return false
  2357. end
  2358. return true
  2359. end
  2360. function validation:validateArrayOfStrings(maxCount, maxStringLength, allowNoValues, logTag, arrayOfStrings)
  2361. local arrayTag = logTag
  2362. if not arrayTag then
  2363. arrayTag = "Array"
  2364. end
  2365. -- use arrayTag to annotate warning log
  2366. if not arrayOfStrings then
  2367. logger:w(arrayTag .. " validation failed: array cannot be nil.")
  2368. return false
  2369. end
  2370. -- check if empty
  2371. if not allowNoValues and #arrayOfStrings == 0 then
  2372. logger:w(arrayTag .. " validation failed: array cannot be empty.")
  2373. return false
  2374. end
  2375. -- check if exceeding max count
  2376. if maxCount > 0 and #arrayOfStrings > maxCount then
  2377. logger:w(arrayTag .. " validation failed: array cannot exceed " .. tostring(maxCount) .. " values. It has " .. #arrayOfStrings .. " values.")
  2378. return false
  2379. end
  2380. -- validate each string
  2381. for _,arrayString in pairs(arrayOfStrings) do
  2382. local stringLength = 0
  2383. if arrayString then
  2384. stringLength = #arrayString
  2385. end
  2386. -- check if empty (not allowed)
  2387. if stringLength == 0 then
  2388. logger:w(arrayTag .. " validation failed: contained an empty string.")
  2389. return false
  2390. end
  2391. -- check if exceeding max length
  2392. if maxStringLength > 0 and stringLength > maxStringLength then
  2393. logger:w(arrayTag .. " validation failed: a string exceeded max allowed length (which is: " .. tostring(maxStringLength) .. "). String was: " .. arrayString)
  2394. return false
  2395. end
  2396. end
  2397. return true
  2398. end
  2399. function validation:validateBuild(build)
  2400. if not validation:validateShortString(build, false) then
  2401. return false
  2402. end
  2403. return true
  2404. end
  2405. function validation:validateShortString(shortString, canBeEmpty)
  2406. -- String is allowed to be empty or nil
  2407. if canBeEmpty and utilities:isStringNullOrEmpty(shortString) then
  2408. return true
  2409. end
  2410. if utilities:isStringNullOrEmpty(shortString) or #shortString > 32 then
  2411. return false
  2412. end
  2413. return true
  2414. end
  2415. function validation:validateKeys(gameKey, secretKey)
  2416. if string.find(gameKey, "^[A-Za-z0-9]+$") and #gameKey == 32 then
  2417. if string.find(secretKey, "^[A-Za-z0-9]+$") and #secretKey == 40 then
  2418. return true
  2419. end
  2420. end
  2421. return false
  2422. end
  2423. function validation:validateAndCleanInitRequestResponse(initResponse)
  2424. -- make sure we have a valid dict
  2425. if not initResponse then
  2426. logger:w("validateInitRequestResponse failed - no response dictionary.")
  2427. return nil
  2428. end
  2429. local validatedDict = {}
  2430. -- validate enabled field
  2431. validatedDict["enabled"] = initResponse["enabled"] or true
  2432. -- validate server_ts
  2433. local serverTsNumber = initResponse["server_ts"] or -1
  2434. if serverTsNumber > 0 then
  2435. validatedDict["server_ts"] = serverTsNumber
  2436. end
  2437. validatedDict["configurations"] = initResponse["configurations"] or {}
  2438. return validatedDict
  2439. end
  2440. function validation:validateClientTs(clientTs)
  2441. if clientTs < 1000000000 or clientTs > 9999999999 then
  2442. return false
  2443. end
  2444. return true
  2445. end
  2446. function validation:validateCurrency(currency)
  2447. if utilities:isStringNullOrEmpty(currency) then
  2448. return false
  2449. end
  2450. if string.find(currency, "^[A-Z]+$") and #currency == 3 then
  2451. return true
  2452. end
  2453. return false
  2454. end
  2455. function validation:validateEventPartLength(eventPart, allowNull)
  2456. if allowNull and utilities:isStringNullOrEmpty(eventPart) then
  2457. return true
  2458. end
  2459. if utilities:isStringNullOrEmpty(eventPart) then
  2460. return false
  2461. end
  2462. if #eventPart == 0 or #eventPart > 64 then
  2463. return false
  2464. end
  2465. return true
  2466. end
  2467. function validation:validateBusinessEvent(currency, amount, cartType, itemType, itemId)
  2468. -- validate currency
  2469. if not validation:validateCurrency(currency) then
  2470. logger:w("Validation fail - business event - currency: Cannot be (null) and need to be A-Z, 3 characters and in the standard at openexchangerates.org. Failed currency: " .. currency)
  2471. return false
  2472. end
  2473. if amount < 0 then
  2474. logger:w("Validation fail - business event - amount: Cannot be less then 0. Failed amount: " .. amount)
  2475. return false
  2476. end
  2477. -- validate cartType
  2478. if not validation:validateShortString(cartType, true) then
  2479. logger:w("Validation fail - business event - cartType. Cannot be above 32 length. String: " .. cartType)
  2480. return false
  2481. end
  2482. -- validate itemType length
  2483. if not validation:validateEventPartLength(itemType, false) then
  2484. logger:w("Validation fail - business event - itemType: Cannot be (null), empty or above 64 characters. String: " .. itemType)
  2485. return false
  2486. end
  2487. -- validate itemType chars
  2488. if not validation:validateEventPartCharacters(itemType) then
  2489. logger:w("Validation fail - business event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. itemType)
  2490. return false
  2491. end
  2492. -- validate itemId
  2493. if not validation:validateEventPartLength(itemId, false) then
  2494. logger:w("Validation fail - business event - itemId. Cannot be (null), empty or above 64 characters. String: " .. itemId)
  2495. return false
  2496. end
  2497. if not validation:validateEventPartCharacters(itemId) then
  2498. logger:w("Validation fail - business event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. itemId)
  2499. return false
  2500. end
  2501. return true
  2502. end
  2503. function validation:validateResourceEvent(flowTypeValues, flowType, currency, amount, itemType, itemId, currencies, itemTypes)
  2504. if flowType ~= flowTypeValues.Source and flowType ~= flowTypeValues.Sink then
  2505. logger:w("Validation fail - resource event - flowType: Invalid flow type " .. tostring(flowType))
  2506. return false
  2507. end
  2508. if utilities:isStringNullOrEmpty(currency) then
  2509. logger:w("Validation fail - resource event - currency: Cannot be (null)")
  2510. return false
  2511. end
  2512. if not utilities:stringArrayContainsString(currencies, currency) then
  2513. logger:w("Validation fail - resource event - currency: Not found in list of pre-defined available resource currencies. String: " .. currency)
  2514. return false
  2515. end
  2516. if not (amount > 0) then
  2517. logger:w("Validation fail - resource event - amount: Float amount cannot be 0 or negative. Value: " .. tostring(amount))
  2518. return false
  2519. end
  2520. if utilities:isStringNullOrEmpty(itemType) then
  2521. logger:w("Validation fail - resource event - itemType: Cannot be (null)")
  2522. return false
  2523. end
  2524. if not validation:validateEventPartLength(itemType, false) then
  2525. logger:w("Validation fail - resource event - itemType: Cannot be (null), empty or above 64 characters. String: " .. itemType)
  2526. return false
  2527. end
  2528. if not validation:validateEventPartCharacters(itemType) then
  2529. logger:w("Validation fail - resource event - itemType: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. itemType)
  2530. return false
  2531. end
  2532. if not utilities:stringArrayContainsString(itemTypes, itemType) then
  2533. logger:w("Validation fail - resource event - itemType: Not found in list of pre-defined available resource itemTypes. String: " .. itemType)
  2534. return false
  2535. end
  2536. if not validation:validateEventPartLength(itemId, false) then
  2537. logger:w("Validation fail - resource event - itemId: Cannot be (null), empty or above 64 characters. String: " .. itemId)
  2538. return false
  2539. end
  2540. if not validation:validateEventPartCharacters(itemId) then
  2541. logger:w("Validation fail - resource event - itemId: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. itemId)
  2542. return false
  2543. end
  2544. return true
  2545. end
  2546. function validation:validateProgressionEvent(progressionStatusValues, progressionStatus, progression01, progression02, progression03)
  2547. if progressionStatus ~= progressionStatusValues.Start and progressionStatus ~= progressionStatusValues.Complete and progressionStatus ~= progressionStatusValues.Fail then
  2548. logger:w("Validation fail - progression event: Invalid progression status " .. tostring(progressionStatus))
  2549. return false
  2550. end
  2551. -- Make sure progressions are defined as either 01, 01+02 or 01+02+03
  2552. if not utilities:isStringNullOrEmpty(progression03) and not (not utilities:isStringNullOrEmpty(progression02) or utilities:isStringNullOrEmpty(progression01)) then
  2553. logger:w("Validation fail - progression event: 03 found but 01+02 are invalid. Progression must be set as either 01, 01+02 or 01+02+03.")
  2554. return false
  2555. elseif not utilities:isStringNullOrEmpty(progression02) and utilities:isStringNullOrEmpty(progression01) then
  2556. logger:w("Validation fail - progression event: 02 found but not 01. Progression must be set as either 01, 01+02 or 01+02+03")
  2557. return false
  2558. elseif utilities:isStringNullOrEmpty(progression01) then
  2559. logger:w("Validation fail - progression event: progression01 not valid. Progressions must be set as either 01, 01+02 or 01+02+03")
  2560. return false
  2561. end
  2562. -- progression01 (required)
  2563. if not validation:validateEventPartLength(progression01, false) then
  2564. logger:w("Validation fail - progression event - progression01: Cannot be (null), empty or above 64 characters. String: " .. progression01)
  2565. return false
  2566. end
  2567. if not validation:validateEventPartCharacters(progression01) then
  2568. logger:w("Validation fail - progression event - progression01: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. progression01)
  2569. return false
  2570. end
  2571. -- progression02
  2572. if not utilities:isStringNullOrEmpty(progression02) then
  2573. if not validation:validateEventPartLength(progression02, false) then
  2574. logger:w("Validation fail - progression event - progression02: Cannot be empty or above 64 characters. String: " .. progression02)
  2575. return false
  2576. end
  2577. if not validation:validateEventPartCharacters(progression02) then
  2578. logger:w("Validation fail - progression event - progression02: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. progression02)
  2579. return false
  2580. end
  2581. end
  2582. -- progression03
  2583. if not utilities:isStringNullOrEmpty(progression03) then
  2584. if not validation:validateEventPartLength(progression03, false) then
  2585. logger:w("Validation fail - progression event - progression03: Cannot be empty or above 64 characters. String: " .. progression03)
  2586. return false
  2587. end
  2588. if not validation:validateEventPartCharacters(progression03) then
  2589. logger:w("Validation fail - progression event - progression03: Cannot contain other characters than A-z, 0-9, -_., ()!?. String: " .. progression03)
  2590. return false
  2591. end
  2592. end
  2593. return true
  2594. end
  2595. function validation:validateEventIdLength(eventId)
  2596. if utilities:isStringNullOrEmpty(eventId) then
  2597. return false
  2598. end
  2599. local count = 0
  2600. for s in string.gmatch(eventId, "([^:]+)") do
  2601. count = count + 1
  2602. if count > 5 then
  2603. return false
  2604. end
  2605. if #s > 64 then
  2606. return false
  2607. end
  2608. end
  2609. return true
  2610. end
  2611. function validation:validateEventIdCharacters(eventId)
  2612. if utilities:isStringNullOrEmpty(eventId) then
  2613. return false
  2614. end
  2615. local count = 0
  2616. for s in string.gmatch(eventId, "([^:]+)") do
  2617. count = count + 1
  2618. if count > 5 then
  2619. return false
  2620. end
  2621. if not string.find(s, "^[A-Za-z0-9%s%-_%.%(%)!%?]+$") then
  2622. return false
  2623. end
  2624. end
  2625. return true
  2626. end
  2627. function validation:validateDesignEvent(eventId)
  2628. if not validation:validateEventIdLength(eventId) then
  2629. logger:w("Validation fail - design event - eventId: Cannot be (null) or empty. Only 5 event parts allowed seperated by :. Each part need to be 32 characters or less. String: " .. eventId)
  2630. return false
  2631. end
  2632. if not validation:validateEventIdCharacters(eventId) then
  2633. logger:w("Validation fail - design event - eventId: Non valid characters. Only allowed A-z, 0-9, -_., ()!?. String: " .. eventId)
  2634. return false
  2635. end
  2636. -- value: allow 0, negative and nil (not required)
  2637. return true
  2638. end
  2639. function validation:validateLongString(longString, canBeEmpty)
  2640. -- String is allowed to be empty
  2641. if canBeEmpty and utilities:isStringNullOrEmpty(longString) then
  2642. return true
  2643. end
  2644. if utilities:isStringNullOrEmpty(longString) or #longString > 8192 then
  2645. return false
  2646. end
  2647. return true
  2648. end
  2649. function validation:validateErrorEvent(severityValues, severity, message)
  2650. if severity ~= severityValues.debug and severity ~= severityValues.info and severity ~= severityValues.warning and severity ~= severityValues.error and severity ~= severityValues.critical then
  2651. logger:w("Validation fail - error event - severity: Severity was unsupported value " .. tostring(severity))
  2652. return false
  2653. end
  2654. if not validation:validateLongString(message, true) then
  2655. logger:w("Validation fail - error event - message: Message cannot be above 8192 characters.")
  2656. return false
  2657. end
  2658. return true
  2659. end
  2660. return validation
  2661. ]]></ProtectedString>
  2662. <BinaryString name="Tags"></BinaryString>
  2663. </Properties>
  2664. </Item>
  2665. <Item class="ModuleScript" referent="RBX14584cc0d6704a73828d545fa96d87a7">
  2666. <Properties>
  2667. <Content name="LinkedSource"><null></null></Content>
  2668. <string name="Name">Threading</string>
  2669. <string name="ScriptGuid"></string>
  2670. <ProtectedString name="Source"><![CDATA[local threading = {
  2671. _canSafelyClose = true,
  2672. _endThread = false,
  2673. _isRunning = false,
  2674. _blocks = {},
  2675. _scheduledBlock = nil,
  2676. _hasScheduledBlockRun = true
  2677. }
  2678. local logger = require(script.Parent.Logger)
  2679. local RunService = game:GetService("RunService")
  2680. local function getScheduledBlock()
  2681. local now = tick()
  2682. if not threading._hasScheduledBlockRun and threading._scheduledBlock ~= nil and threading._scheduledBlock.deadline <= now then
  2683. threading._hasScheduledBlockRun = true
  2684. return threading._scheduledBlock
  2685. else
  2686. return nil
  2687. end
  2688. end
  2689. local function run()
  2690. spawn(function()
  2691. logger:d("Starting GA thread")
  2692. while not threading._endThread do
  2693. threading._canSafelyClose = false
  2694. if #threading._blocks ~= 0 then
  2695. for _,b in pairs(threading._blocks) do
  2696. pcall(function()
  2697. b.block()
  2698. end)
  2699. end
  2700. threading._blocks = {}
  2701. end
  2702. local timedBlock = getScheduledBlock()
  2703. if timedBlock ~= nil then
  2704. pcall(function()
  2705. timedBlock.block()
  2706. end)
  2707. end
  2708. threading._canSafelyClose = true
  2709. wait(1)
  2710. end
  2711. logger:d("GA thread stopped")
  2712. end)
  2713. --Safely Close
  2714. game:BindToClose(function()
  2715. -- waiting bug fix to work inside studio
  2716. if RunService:IsStudio() then
  2717. return
  2718. end
  2719. --Give game.Players.PlayerRemoving time to to its thang
  2720. wait(1)
  2721. --Delay
  2722. if not threading._canSafelyClose then
  2723. repeat
  2724. wait()
  2725. until threading._canSafelyClose
  2726. end
  2727. wait(3)
  2728. end)
  2729. end
  2730. function threading:scheduleTimer(interval, callback)
  2731. if self._endThread then
  2732. return
  2733. end
  2734. if not self._isRunning then
  2735. self._isRunning = true
  2736. run()
  2737. end
  2738. local timedBlock = {
  2739. block = callback,
  2740. deadline = tick() + interval
  2741. }
  2742. if self._hasScheduledBlockRun then
  2743. self._scheduledBlock = timedBlock
  2744. self._hasScheduledBlockRun = false
  2745. end
  2746. end
  2747. function threading:performTaskOnGAThread(callback)
  2748. if self._endThread then
  2749. return
  2750. end
  2751. if not self._isRunning then
  2752. self._isRunning = true
  2753. run()
  2754. end
  2755. local timedBlock = {
  2756. block = callback,
  2757. }
  2758. self._blocks[#self._blocks + 1] = timedBlock
  2759. end
  2760. function threading:stopThread()
  2761. self._endThread = true
  2762. end
  2763. return threading
  2764. ]]></ProtectedString>
  2765. <BinaryString name="Tags"></BinaryString>
  2766. </Properties>
  2767. </Item>
  2768. <Item class="ModuleScript" referent="RBX99cd668c174048349f2ece6db3c8afd4">
  2769. <Properties>
  2770. <Content name="LinkedSource"><null></null></Content>
  2771. <string name="Name">GAErrorSeverity</string>
  2772. <string name="ScriptGuid"></string>
  2773. <ProtectedString name="Source"><![CDATA[local function readonlytable(table)
  2774. return setmetatable({}, {
  2775. __index = table,
  2776. __newindex = function(t, k, v)
  2777. error("Attempt to modify read-only table: " .. t .. ", key=" .. k .. ", value=" .. v)
  2778. end,
  2779. __metatable = false
  2780. });
  2781. end
  2782. return readonlytable({
  2783. debug = "debug";
  2784. info = "info";
  2785. warning = "warning";
  2786. error = "error";
  2787. critical = "critical";
  2788. })
  2789. ]]></ProtectedString>
  2790. <BinaryString name="Tags"></BinaryString>
  2791. </Properties>
  2792. </Item>
  2793. <Item class="ModuleScript" referent="RBX9a762c40155746f09057ee57f778235c">
  2794. <Properties>
  2795. <Content name="LinkedSource"><null></null></Content>
  2796. <string name="Name">GAProgressionStatus</string>
  2797. <string name="ScriptGuid"></string>
  2798. <ProtectedString name="Source"><![CDATA[local function readonlytable(table)
  2799. return setmetatable({}, {
  2800. __index = table,
  2801. __newindex = function(t, k, v)
  2802. error("Attempt to modify read-only table: " .. t .. ", key=" .. k .. ", value=" .. v)
  2803. end,
  2804. __metatable = false
  2805. });
  2806. end
  2807. return readonlytable({
  2808. Start = "Start";
  2809. Complete = "Complete";
  2810. Fail = "Fail";
  2811. })
  2812. ]]></ProtectedString>
  2813. <BinaryString name="Tags"></BinaryString>
  2814. </Properties>
  2815. </Item>
  2816. <Item class="ModuleScript" referent="RBXeaa8109bf36e45c987ef4664347edc33">
  2817. <Properties>
  2818. <Content name="LinkedSource"><null></null></Content>
  2819. <string name="Name">GAResourceFlowType</string>
  2820. <string name="ScriptGuid"></string>
  2821. <ProtectedString name="Source"><![CDATA[local function readonlytable(table)
  2822. return setmetatable({}, {
  2823. __index = table,
  2824. __newindex = function(t, k, v)
  2825. error("Attempt to modify read-only table: " .. t .. ", key=" .. k .. ", value=" .. v)
  2826. end,
  2827. __metatable = false
  2828. });
  2829. end
  2830. return readonlytable({
  2831. Source = "Source";
  2832. Sink = "Sink";
  2833. })
  2834. ]]></ProtectedString>
  2835. <BinaryString name="Tags"></BinaryString>
  2836. </Properties>
  2837. </Item>
  2838. </Item>
  2839. </Item>
  2840. </roblox>