// COMPLETE REPLACEMENT: copy this entire file into a new PlayFab CloudScript revision. // Contains the original handlers supplied in this conversation plus OoglyClaimDailyReward. // // REVISION 18 (2026-10-15) = revision 17 with the security lockdown (Phase 0 of // SECURITY-PLAN-2026-10-15.md): // - Removed GrantItemToPlayer, GivePlayerCurrency, RemovePlayerCurrency, ConsoleGiveCurrency, // ConsoleGiveCosmetic and ValidateOculusPurchaseAndGrant. Any logged-in client could call them to // mint currency or items for anyone, or drain anyone. No game code, Rule or scheduled task calls them. // - GrantCatalogItemToCurrentPlayer, GrantEventReward and RepairIAPItems only grant items from the // server tables in the OB SECURITY section at the end. RepairIAPItems and the daily VIP check ask // Meta whether the player really owns the SKU, instead of trusting client-written OwnedIAP. // - The Be The Monster code sits at top level again. In revision 17 it had been pasted inside // obDailyReply after its return, so OoglyBeMonsterRoundReward was never registered. function getWebhookUrl() { var data = server.GetTitleInternalData({ Keys: ["DiscordWebhookURL"] }); return data.Data["DiscordWebhookURL"] || ""; } // LOGIN & VALIDATION handlers.NewLogin = function(args, context) { if (context.playStreamEvent.CustomTags['userProof'] != undefined && context.playStreamEvent.CustomTags['userId'] != undefined) { var url = 'https://graph.oculus.com/user_nonce_validate?nonce=' + context.playStreamEvent.CustomTags['userProof'] + '&user_id=' + context.playStreamEvent.CustomTags['userId'] + '&access_token=OC|6755960357813543|dceda3c54c45b49a8502d52b320b7e6e'; var responseString = http.request(url, 'post', '', 'application/json', {}); if (JSON.parse(responseString).is_valid != undefined) { if (!JSON.parse(responseString).is_valid) { server.BanUsers({ 'DurationInHours': 64, 'PlayFabId': currentPlayerId, 'Reason': 'Validation was unsuccessful', 'IPAddress': context.playStreamEvent.IPV4Address }); return { error: 'Unable To Login' }; } } else { server.BanUsers({ 'DurationInHours': 64, 'PlayFabId': currentPlayerId, 'Reason': 'Custom tags are null', 'IPAddress': context.playStreamEvent.IPV4Address }); return { error: 'Custom tags were null' }; } } else { server.BanUsers({ 'DurationInHours': 64, 'PlayFabId': currentPlayerId, 'Reason': 'Custom tags are null', 'IPAddress': context.playStreamEvent.IPV4Address }); return { error: 'Custom tags were null' }; } }; handlers.BetterAuthenticator = function(args) { var getUserInforesult = server.GetUserAccountInfo({PlayFabId: currentPlayerId}).UserInfo; if (!getUserInforesult.ServerCustomIdInfo) { var contentBody = { "content": "**INVALID PLAYER ATTEMPTING TO AUTHENTICATE**: " + currentPlayerId + "\nINVALID LOGIN TYPE :x:" + "\nCUSTOM ID: N/A" }; var webhookURL = getWebhookUrl(); http.request(webhookURL, "post", JSON.stringify(contentBody), "application/json", {}); server.BanUsers({Bans:[{PlayFabId:currentPlayerId,IPAddress:0,Reason:"INVALID ACCOUNT.",DurationInHours: 672}]}); server.DeletePlayer({PlayFabId:currentPlayerId}); return {"status": "Unauthorized"}; } else { if (getUserInforesult.ServerCustomIdInfo.CustomId.startsWith("OCULUS")) { if (getUserInforesult.ServerCustomIdInfo.CustomId.substring(6).length == 16 || getUserInforesult.ServerCustomIdInfo.CustomId.substring(6).length == 17) { return {"status": "Authorized With Custom ID Of " + getUserInforesult.ServerCustomIdInfo.CustomId}; } else { var contentBody = { "content": "**INVALID PLAYER ATTEMPTING TO AUTHENTICATE**: " + currentPlayerId + "\nINVALID ORG SCOPED ID :x:" + "\nCUSTOM ID: " + getUserInforesult.ServerCustomIdInfo.CustomId }; var webhookURL = getWebhookUrl(); http.request(webhookURL, "post", JSON.stringify(contentBody), "application/json", {}); server.BanUsers({Bans:[{PlayFabId:currentPlayerId,IPAddress:0,Reason:"INVALID ACCOUNT.",DurationInHours: 672}]}); server.DeletePlayer({PlayFabId:currentPlayerId}); } } else { return {"status": "Invalid Custom Id Type"}; } } }; // BAN MANAGEMENT handlers.banPlayer = function(args) { var duration = args.duration; var reason = args.reason; server.BanUsers({Bans:[{DurationInHours:duration,PlayFabId:currentPlayerId,Reason:reason}]}); }; handlers.IncrementBanCount = function(args, context) { var dateKey = "BanCount_" + (new Date()).toISOString().slice(0,10); var dataResult = server.GetTitleInternalData({ Keys: [dateKey] }); var currentCountStr = dataResult.Data[dateKey]; var count = currentCountStr ? parseInt(currentCountStr) : 0; count++; server.SetTitleInternalData({ Key: dateKey, Value: count.toString() }); return { newCount: count }; }; handlers.GetBanCount = function(args, context) { var dateKey = "BanCount" + (new Date()).toISOString().slice(0, 10); var dataResult = server.GetTitleInternalData({ Keys: [dateKey] }); var count = dataResult.Data[dateKey] ? parseInt(dataResult.Data[dateKey]) : 0; return { count: count }; }; // ANTI CHEAT handlers.AntiLemonLoader = function(args, context) { var result = server.GetUserAccountInfo({ PlayFabId: currentPlayerId }); var id = context.playerProfile.PlayerId; var cidd = result.UserInfo.CustomIdInfo ? result.UserInfo.CustomIdInfo.CustomId : "N/A"; var playerData = server.GetUserReadOnlyData({ PlayFabId: currentPlayerId, Keys: ["androidDataPath", "PlayerName"] }); var androidDataPath = playerData.Data["androidDataPath"] ? playerData.Data["androidDataPath"].Value : null; var lemonloader = ["/villanstudios.com/files/mods", "/villanstudios.com/files/Mods"]; var banReason = "Mods"; var deletePlayer = false; if (androidDataPath && lemonloader.includes(androidDataPath)) { banReason = "CHEATING. USING A PRIVATE APPLAB."; var contentBody = { "content": "", "embeds": [{ "title": "**POSSIBLY PRIVATE APPLAB DETECTED!**", "color": 16711680, "fields": [{ "name": "PLAYER DETAILS", "value": "\nCUSTOM ID: " + cidd + "\nPLAYER ID: " + id }] }], "attachments": [] }; http.request(getWebhookUrl(), "post", JSON.stringify(contentBody), "application/json", {}); deletePlayer = true; } if (deletePlayer) { server.BanUsers({ Bans: [{DurationInHours: 24, IPAddress: 0, PlayFabId: currentPlayerId, Reason: banReason}] }); } else { log.debug("Valid package name: " + androidDataPath); } }; handlers.AntiDll = function(args) { var cheatPaths = [ "/storage/emulated/0/MelonLoader/villanstudios.com/files/mods", "/storage/emulated/0/Android/data/villanstudios.com/files/Mods", "/sdcard/Android/data/villanstudios.com/files/Mods" ]; var playerData = server.GetUserAccountInfo({ PlayFabId: currentPlayerId }).UserInfo; var customId = playerData.CustomIdInfo && playerData.CustomIdInfo.CustomId; var readOnlyData = server.GetUserReadOnlyData({PlayFabId: currentPlayerId, Keys: ["androidDataPath"]}); var androidDataPath = readOnlyData.Data["androidDataPath"] ? readOnlyData.Data["androidDataPath"].Value : null; var hasCheatFolder = androidDataPath && cheatPaths.indexOf(androidDataPath) !== -1; if (hasCheatFolder) { server.BanUsers({ Bans: [{PlayFabId: currentPlayerId, DurationInHours: 1385, Reason: "CHEATING - Mod folder detected"}] }); var contentBody = { "content": null, "embeds": [{ "title": "Modding Detected", "description": "**Custom ID:** " + customId + "\n**Player ID:** " + currentPlayerId + "\n**Path:** " + androidDataPath, "color": 16711680, "author": {"name": "Anti-Cheat"} }], "attachments": [] }; http.request(getWebhookUrl(), "POST", JSON.stringify(contentBody), "application/json", {}); return { message: "User banned for 1385 hours due to cheating." }; } else { return { message: "Player is clean." }; } }; handlers.ValidatePlayerAction = function(args, context) { var playerId = context.playerId; if (args.cheatToolDetected) { var logMessage = "Player " + playerId + " reported using cheat tool: " + args.cheatToolDetected; server.WriteLog(logMessage); return { success: false, message: "Cheat tool detected. Action denied." }; } if (args.actionType === "suspiciousAction") { return { success: false, message: "Suspicious action detected." }; } return { success: true, message: "Action validated." }; }; // IAP & CURRENCY // GrantItemToPlayer, GivePlayerCurrency and RemovePlayerCurrency were removed in revision 18 (see top). handlers.CompleteIAPPurchase = function(args, context) { var url = 'https://graph.oculus.com/5409297455797322/consume_entitlement?nonce=' + args.UserProof + '&user_id=' + args.MetaId + '&sku=' + args.SKU + '&access_token=OC|6755960357813543|dceda3c54c45b49a8502d52b320b7e6e'; var responseString = http.request(url, 'post', '', 'application/json', {}); if (JSON.parse(responseString).success != undefined) { if (JSON.parse(responseString).success) { var recordTransactionRequest = { PlayFabId: currentPlayerId, TitleId: titleId, PurchaseId: args.SKU, PurchaseAmount: args.PurchaseAmount }; server.UpdateUserInternalData(recordTransactionRequest); return true; } } return false; }; // ValidateOculusPurchaseAndGrant was removed in revision 18: it granted whatever itemId the client // named for any receipt, and nothing in the game calls it. // PHOTON handlers.GetPhotonAuth = function(args, context) { var authData = "446a5b3e-3d7c-4a99-9eae-21e7b90d8a91"; return authData; }; handlers.GrantCatalogItemToCurrentPlayer = function (args, context) { log.info("GrantCatalogItemToCurrentPlayer called."); if (!args) throw "Missing args"; if (!args.ItemId || args.ItemId === "") throw "Missing ItemId"; if (!args.CatalogVersion || args.CatalogVersion === "") throw "Missing CatalogVersion"; var itemId = args.ItemId; var catalogVersion = args.CatalogVersion; // Only the free-claim items listed on the server; anything else used to be a free item of choice. if (!obIsFreeClaim(catalogVersion, itemId)) { log.error("[GrantCatalogItemToCurrentPlayer] refused, not a free-claim item: " + catalogVersion + "/" + itemId); return { success: false, alreadyOwned: false, granted: false, message: "Item is not claimable.", itemId: itemId, playFabId: currentPlayerId }; } log.info("CurrentPlayerId: " + currentPlayerId); log.info("CatalogVersion: " + catalogVersion); log.info("ItemId: " + itemId); var inventoryResult = server.GetUserInventory({PlayFabId: currentPlayerId}); if (inventoryResult && inventoryResult.Inventory) { for (var i = 0; i < inventoryResult.Inventory.length; i++) { if (inventoryResult.Inventory[i].ItemId == itemId) { log.info("Player already owns item: " + itemId); return { success: true, alreadyOwned: true, granted: false, message: "Player already owns this item.", itemId: itemId, playFabId: currentPlayerId }; } } } var grantResult = server.GrantItemsToUser({ PlayFabId: currentPlayerId, CatalogVersion: catalogVersion, ItemIds: [itemId], Annotation: "Claimed from Unity button" }); log.info("GrantItemsToUser completed."); return { success: true, alreadyOwned: false, granted: true, message: "Item granted successfully.", itemId: itemId, playFabId: currentPlayerId, grantResult: grantResult }; }; handlers.GrantEventReward = function (args, context) { var eventCode = args && args.eventCode ? args.eventCode : null; if (!eventCode) throw "Missing eventCode"; // The reward list comes from the server table, never from the client's args. var cosmeticItemIds = obEventRewards(eventCode); if (!cosmeticItemIds || cosmeticItemIds.length === 0) throw "No reward configured for event: " + eventCode; var dataResult = server.GetUserData({PlayFabId: currentPlayerId, Keys: [eventCode]}); var progress = null; if (dataResult.Data && dataResult.Data[eventCode] && dataResult.Data[eventCode].Value) { progress = JSON.parse(dataResult.Data[eventCode].Value); } else { throw "No event progress found for key: " + eventCode; } if (!progress.collected || progress.collected.length !== 9) throw "Collected array is invalid"; var allCollected = true; for (var i = 0; i < progress.collected.length; i++) { if (!progress.collected[i]) { allCollected = false; break; } } if (!allCollected) { return {success: false, message: "Not all collectibles are collected yet.", eventCode: eventCode}; } if (progress.rewardClaimed === true) { return {success: true, message: "Reward already claimed.", eventCode: eventCode, rewardClaimed: true}; } var inventory = server.GetUserInventory({PlayFabId: currentPlayerId}); var ownedMap = {}; if (inventory && inventory.Inventory) { for (var j = 0; j < inventory.Inventory.length; j++) ownedMap[inventory.Inventory[j].ItemId] = true; } var grantedItems = []; for (var k = 0; k < cosmeticItemIds.length; k++) { var itemId = cosmeticItemIds[k]; if (!itemId) continue; if (!ownedMap[itemId]) { server.GrantItemsToUser({PlayFabId: currentPlayerId, ItemIds: [itemId], CatalogVersion: "Johnsons"}); grantedItems.push(itemId); } } progress.rewardClaimed = true; server.UpdateUserData({PlayFabId: currentPlayerId, Data: {[eventCode]: JSON.stringify(progress)}, Permission: "Public"}); return {success: true, eventCode: eventCode, rewardClaimed: true, grantedItems: grantedItems}; }; // ConsoleGiveCurrency and ConsoleGiveCosmetic were removed in revision 18. They checked IsMod in // normal player data, which any client can write, so anyone could grant anything to anyone; the // console script that called them (OoglyConsoleCloudScript) is not used by the game. If a mod console // comes back, read IsMod with server.GetUserReadOnlyData, which only the server or an admin can set. handlers.RepairIAPItems = function (args, context) { var playFabId = currentPlayerId; try { log.info("[RepairIAPItems] START PlayFabId=" + playFabId); log.info("[RepairIAPItems] Raw args=" + JSON.stringify(args)); if (!args) return Fail("Missing args."); if (!args.sku) return Fail("Missing sku."); var sku = String(args.sku).trim(); // The bundle's catalog and items come from the server table (OB_IAP_BUNDLES), never from the // client's args: the client could otherwise name any item. var bundle = obIapBundle(sku); if (!bundle) return Fail("Unknown sku."); var catalogVersion = bundle.catalog; var itemNames = CleanItemList(bundle.items); log.info("[RepairIAPItems] sku=[" + sku + "]"); log.info("[RepairIAPItems] catalogVersion=[" + catalogVersion + "]"); log.info("[RepairIAPItems] itemNames=[" + itemNames.join(",") + "]"); if (sku.length === 0) return Fail("Sku was empty."); if (catalogVersion.length === 0) return Fail("CatalogVersion was empty."); if (itemNames.length === 0) return Fail("No valid item names were sent."); var keysToCheck = ["Owned IAP", "OwnedIAP", "OwenedIAP", "Owened IAP"]; var normalData = server.GetUserData({PlayFabId: playFabId, Keys: keysToCheck}); var readOnlyData = server.GetUserReadOnlyData({PlayFabId: playFabId, Keys: keysToCheck}); var foundKey = ""; var foundSource = ""; var ownedIAPRaw = ""; for (var k = 0; k < keysToCheck.length; k++) { var normalValue = GetDataValue(normalData, keysToCheck[k]); var readOnlyValue = GetDataValue(readOnlyData, keysToCheck[k]); log.info("[RepairIAPItems] NormalData key=[" + keysToCheck[k] + "] value=[" + normalValue + "]"); log.info("[RepairIAPItems] ReadOnlyData key=[" + keysToCheck[k] + "] value=[" + readOnlyValue + "]"); if (ownedIAPRaw.length === 0 && normalValue.length > 0) { ownedIAPRaw = normalValue; foundKey = keysToCheck[k]; foundSource = "PlayerData"; } if (ownedIAPRaw.length === 0 && readOnlyValue.length > 0) { ownedIAPRaw = readOnlyValue; foundKey = keysToCheck[k]; foundSource = "ReadOnlyData"; } } var ownedIAPs = ParseCsv(ownedIAPRaw); // OwnedIAP is written by the client, so it only decides whether Meta is asked at all (every // shop stand runs this at each login). Ownership itself is Meta's answer. var claimsIAP = ContainsCaseInsensitive(ownedIAPs, sku); var ownsIAP = claimsIAP && obOwnsSku(sku); log.info("[RepairIAPItems] foundSource=[" + foundSource + "]"); log.info("[RepairIAPItems] foundKey=[" + foundKey + "]"); log.info("[RepairIAPItems] ownedIAPRaw=[" + ownedIAPRaw + "]"); log.info("[RepairIAPItems] parsedOwnedIAPs=[" + ownedIAPs.join(",") + "]"); log.info("[RepairIAPItems] claimsIAP=[" + claimsIAP + "] ownsIAP=[" + ownsIAP + "]"); if (!ownsIAP) { return { success: true, error: "", stoppedReason: claimsIAP ? "Meta does not confirm the player owns this sku." : "Player does not own sku in any checked IAP key.", ownsIAP: false, sku: sku, catalogVersion: catalogVersion, foundSource: foundSource, foundKey: foundKey, ownedIAPRaw: ownedIAPRaw, parsedOwnedIAPs: ownedIAPs, requiredItems: itemNames, ownedItems: [], missingItems: [], grantedItems: [] }; } var inventory = server.GetUserInventory({PlayFabId: playFabId}); var ownedItems = []; if (inventory.Inventory) { for (var i = 0; i < inventory.Inventory.length; i++) { if (inventory.Inventory[i] && inventory.Inventory[i].ItemId) ownedItems.push(inventory.Inventory[i].ItemId); } } log.info("[RepairIAPItems] inventoryCount=[" + ownedItems.length + "]"); log.info("[RepairIAPItems] ownedItems=[" + ownedItems.join(",") + "]"); var missingItems = []; for (var r = 0; r < itemNames.length; r++) { if (!ContainsExact(ownedItems, itemNames[r])) missingItems.push(itemNames[r]); } log.info("[RepairIAPItems] missingCount=[" + missingItems.length + "]"); log.info("[RepairIAPItems] missingItems=[" + missingItems.join(",") + "]"); var grantedItems = []; var grantResultRaw = ""; if (missingItems.length > 0) { log.info("[RepairIAPItems] Calling GrantItemsToUser. catalogVersion=[" + catalogVersion + "] items=[" + missingItems.join(",") + "]"); var grantResult = server.GrantItemsToUser({ PlayFabId: playFabId, CatalogVersion: catalogVersion, ItemIds: missingItems, Annotation: "IAP repair grant for " + sku }); grantResultRaw = JSON.stringify(grantResult); log.info("[RepairIAPItems] grantResult=[" + grantResultRaw + "]"); if (grantResult && grantResult.ItemGrantResults) { for (var g = 0; g < grantResult.ItemGrantResults.length; g++) { if (grantResult.ItemGrantResults[g] && grantResult.ItemGrantResults[g].ItemId) grantedItems.push(grantResult.ItemGrantResults[g].ItemId); } } } log.info("[RepairIAPItems] grantedCount=[" + grantedItems.length + "]"); log.info("[RepairIAPItems] grantedItems=[" + grantedItems.join(",") + "]"); log.info("[RepairIAPItems] END SUCCESS"); return { success: true, error: "", stoppedReason: "", ownsIAP: true, sku: sku, catalogVersion: catalogVersion, foundSource: foundSource, foundKey: foundKey, ownedIAPRaw: ownedIAPRaw, parsedOwnedIAPs: ownedIAPs, requiredItems: itemNames, ownedItems: ownedItems, missingItems: missingItems, grantedItems: grantedItems, grantResultRaw: grantResultRaw }; } catch (e) { var message = e && e.message ? e.message : String(e); log.error("[RepairIAPItems] EXCEPTION [" + message + "]"); return { success: false, error: message, exception: true, ownsIAP: false, requiredItems: [], ownedItems: [], missingItems: [], grantedItems: [] }; } }; function Fail(error) { log.error("[RepairIAPItems] FAIL [" + error + "]"); return {success: false, error: error, stoppedReason: error, ownsIAP: false, requiredItems: [], ownedItems: [], missingItems: [], grantedItems: []}; } function GetDataValue(result, key) { if (!result || !result.Data || !result.Data[key] || result.Data[key].Value === null || result.Data[key].Value === undefined) return ""; return String(result.Data[key].Value).trim(); } function CleanItemList(rawList) { var result = []; for (var i = 0; i < rawList.length; i++) { if (rawList[i] === null || rawList[i] === undefined) continue; var cleaned = String(rawList[i]).trim(); if (cleaned.length <= 0) continue; if (!ContainsExact(result, cleaned)) result.push(cleaned); } return result; } function ParseCsv(value) { var result = []; if (!value) return result; var split = String(value).split(","); for (var i = 0; i < split.length; i++) { var cleaned = String(split[i]).trim(); if (cleaned.length > 0) result.push(cleaned); } return result; } function ContainsExact(list, value) { for (var i = 0; i < list.length; i++) { if (String(list[i]) === String(value)) return true; } return false; } function ContainsCaseInsensitive(list, value) { var wanted = String(value).trim().toLowerCase(); for (var i = 0; i < list.length; i++) { if (String(list[i]).trim().toLowerCase() === wanted) return true; } return false; }