text Untitled 9 views
// 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;
}