// 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;
}